Alphred
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  • Download

Namespaces

  • Alphred
  • None

Classes

  • Alphred
  1 <?php
  2 /**
  3  * Contains the Request library for Alphred
  4  *
  5  * PHP version 5
  6  *
  7  * @package      Alphred
  8  * @copyright  Shawn Patrick Rice 2014-2015
  9  * @license    http://opensource.org/licenses/MIT  MIT
 10  * @version    1.0.0
 11  * @author     Shawn Patrick Rice <rice@shawnrice.org>
 12  * @link       http://www.github.com/shawnrice/alphred
 13  * @link       http://shawnrice.github.io/alphred
 14  * @since      File available since Release 1.0.0
 15  *
 16  */
 17 
 18 
 19 namespace Alphred;
 20 
 21 /**
 22  * Generic, light-weight, low-functionality wrapper around PHP's cURL library
 23  *
 24  * This Requests library should be good enough for most requests, as long as you
 25  * aren't doing anything special or crazy. If you outgrow it, then you should either
 26  * (1) use Guzzle, or (2) write your own requests library that has better coverage.
 27  *
 28  * Granted, this should handle MOST use cases. I don't know if it handles file uploads.
 29  * Theoretically, it does, but I wouldn't bank on it, and, if it doesn't, I will not
 30  * expand the functionality to cover file uploads.
 31  *
 32  * With this, you can easily make GET or POST requests. Set extra headers. Easily set
 33  * a user-agent. Set parameters. And cache the data for later retrieval.
 34  *
 35  *
 36  */
 37 class Request {
 38 
 39     /**
 40      * The internal cURL handler
 41      * @var Resource
 42      */
 43     private $handler;
 44 
 45     /**
 46      * An internal structuring of the request object for cache creation
 47      * @var array
 48      */
 49     private $object;
 50 
 51     /**
 52      * Creates a request object
 53      *
 54      * Currently, all the options apply to caching. So the three that are understood are:
 55      * 1. `cache`,
 56      * 2. `cache_life`, and
 57      * 3. `cache_bin`.
 58      *
 59      * `cache` is a boolean that can turn on/off caching. It is recommended that you turn it on.
 60      * `cache_life` is how long the cache will live. In other words, no attempts to get new data
 61      * will be made until the data saved is older than the cache life (in seconds). It defaults to
 62      * `3600` (one hour).
 63      * `cache_bin` is the sub-directory in the workflow's cache folder where the results are saved.
 64      * If `cache_bin` is set to `false` while caching is turned on, then all the results will be saved
 65      * directly into the workflow's cache directory.
 66      *
 67      * Cache files are saved as md5 hashes of the request object. So, if you change anything about the
 68      * request, then it will be considered a new cache file. Data is saved to the cache _only_ if
 69      * we receive an HTTP response code less than 400.
 70      *
 71      * My advice is not to touch these options and let the cache work with its default behavior.
 72      *
 73      * A further note on `cache_bin`: the 'cache_bin' option, if `true`, will create a cache_bin
 74      * that is a directory in the cache directory named after the hostname. So if the url is
 75      * `http://api.github.com/api....` then the `cache_bin` will be `api.github.com`, and all
 76      * cached data will be saved in that directory. Otherwise, if you pass a string, then that will
 77      * become the directory it will be saved under.
 78      *
 79      *
 80      * @param string $url     the URL to request
 81      * @param array   $options [description]
 82      */
 83     public function __construct(
 84       $url,
 85       $options = array( 'cache' => true, 'cache_ttl' => 600, 'cache_bin' => true )
 86     ) {
 87 
 88         // Create the cURL handler
 89         $this->handler = curl_init();
 90         // Default to `GET`, which is, (not) coincidentally, the cURL default
 91         $this->object['request_type'] = 'get';
 92 
 93         // Empty parameters array
 94         $this->object['parameters'] = [];
 95         // Empty headers array
 96         $this->headers = [];
 97 
 98         // If the request object was initialized with a URL, then set the URL
 99         $this->set_url( $url );
100 
101         // Set the cache options
102         $this->set_caches( $options );
103 
104         // Set some reasonable defaults
105         curl_setopt_array( $this->handler, [ CURLOPT_RETURNTRANSFER => 1, CURLOPT_FAILONERROR => 1 ]);
106 
107     }
108 
109     /**
110      * Sets the cache options
111      *
112      * @param array $options an array of cache options
113      */
114     private function set_caches( $options ) {
115         if ( ! isset( $options['cache_bin' ] ) ) {
116             // exit early if no cache bin is set
117             return;
118         }
119         // Here we can automatically set the cache bin to the URL hostname
120         if ( true === $options[ 'cache_bin' ] ) {
121             $options[ 'cache_bin' ] = parse_url( $this->object['url'], PHP_URL_HOST );
122         }
123 
124         if ( isset( $options['cache'] ) && $options['cache'] ) {
125             if ( isset( $options['cache_ttl'] ) ) {
126                 $this->cache_life = $options['cache_ttl'];
127             }
128             if ( isset( $options['cache_bin'] ) ) {
129                 $this->cache_bin = $options['cache_bin'];
130             }
131         }
132     }
133 
134 
135     /**
136      * Executes the cURL request
137      *
138      * If you set `$code` to `true`, then this function will return an associative array as:
139      * ````php
140      * [ 'code' => HTTP_RESPONSE_CODE,
141      *   'data' => RESPONSE_DATA
142      * ];
143      *````
144      * If you get cached data, then the code will be "faked" as a 302, which is appropriate.
145      *
146      * If there is an error, then the code will be 0. So, if you manage to get expired cache
147      * data, then the code will be 0 and there will be data. If there is no expired cache data,
148      * then you will receive an array of `[ 0, false ]`.
149      *
150      * This method does not cache data unless the response code is less than 400. If you need
151      * better data integrity than that, use Guzzle or write your own request library. Or improve
152      * this one by putting in a pull request on the Github repo.
153      *
154      * @param  boolean $code whether or not to return an HTTP response code
155      * @return string|array        the response data, or an array with the code
156      */
157     public function execute( $code = false ) {
158 
159         // Set a preliminary HTTP response code of 0 (not defined)
160         $this->code = 0;
161 
162         // Build the fields
163         $this->build_fields();
164 
165         // By now, the cURL request should be entirely built, so let's see proceed. First, we'll look to see if there
166         // is valid, cached data. If so, return that. If not, try to get new data. If that fails, try to return expired
167         // cache data. If that fails, then fail.
168 
169         if ( isset( $this->cache_life ) ) {
170             if ( $data = $this->get_cached_data() ) {
171                 // Debug-level log message
172                 Log::console( "Getting the data from cache, aged " . $this->get_cache_age() . " seconds.", 0 );
173 
174                 // Close the cURL handler for good measure; we don't need it anymore
175                 curl_close( $this->handler );
176 
177                 if ( false === $code ) {
178                     // Just return the data
179                     return $data;
180                 } else {
181                     // They wanted an HTTP code, and we don't have a real one for them because we're getting this
182                     // from the internal cache, so we'll just fake a 302 response code
183                     return [ 'code' => 302, 'data' => $data ];
184                 }
185             }
186         }
187 
188         // Well, we need to actually ask the server for some data, so let us go ahead and make the request
189         $this->results = curl_exec( $this->handler );
190 
191         // This is the error message.
192         $error = curl_error( $this->handler );
193         // This is the error number; anything greater than 0 means something went wrong
194         $errno = curl_errno( $this->handler );
195 
196         // Let's do some error checking on the request now, and then try to execute some fallbacks if there
197         // actually was a problem. First, check to make sure that the errno (cURL error code) is 0, which
198         // indicates success.
199         if ( $errno === 0 ) {
200             // The cURL request was successful, so log a debug message of "success"
201             Log::console( "cURL query successful.", 0 );
202         } else if ( $data = $this->get_cached_data_anyway() ) {
203             // Try to get expired cached results....
204             // This could work with error code 6 (cannot resolve) because that _could_
205             // indicate that we just don't have an internet connection right now.
206 
207             // Let the console know we're using old data, with a level of `WARNING`
208             Log::console( 'Could not complete request, but, instead, using expired cache data.', 2 );
209             // Close the handler
210             curl_close( $this->handler );
211 
212             if ( false === $code ) {
213             // Just return the results
214                 return $data;
215             } else {
216                 // The requested the code as well, so we'll return an array with the code:
217                 return [ 'code' => 0, 'data' => $data ];
218             }
219         } else {
220             // Let them know what debuggin information follows
221             Log::console( 'Request completely failed, and no cached data exists. cURL debug information follows:', 3 );
222             // Log the error number (if that helps them)
223             Log::console( "cURL error number: {$errno}", 3 );
224             // Log the error message
225             Log::console( "cURL error message: `{$error}`.", 3 );
226             // We might as well close the handler
227             curl_close( $this->handler );
228             if ( false === $code ) {
229                 // And let's just return false to get out of this failed function. Alas...
230                 return false;
231             } else {
232                 // But they also wanted the code, so we'll return 0 for the code
233                 return [ 'code' => 0, 'data' => false ];
234             }
235         }
236 
237         // Get the information about the last request
238         $info = curl_getinfo( $this->handler );
239         // This might bug out if connection failed.
240         $this->code = $info['http_code'];
241 
242         // Close the cURL handler
243         curl_close( $this->handler );
244 
245         // Cache the data if the cache life is greater than 0 seconds ...AND... the HTTP code is less than 400
246         if ( isset( $this->cache_life ) && ( $this->cache_life > 0 ) && ( $this->code < 400 ) ) {
247             $this->save_cache_data( $this->results );
248         }
249 
250         if ( false === $code ) {
251         // Just return the results
252             return $this->results;
253         } else {
254             // The requested the code as well, so we'll return an array with the code:
255             return [ 'code' => $this->code, 'data' => $this->results ];
256         }
257     }
258 
259     /**
260      * Builds the post fields array
261      *
262      * @since 1.0.0
263      */
264     private function build_post_fields() {
265         if ( count( $this->object['parameters'] ) > 0 ) {
266             curl_setopt( $this->handler, CURLOPT_POSTFIELDS, http_build_query( $this->object['parameters'] ) );
267         }
268     }
269 
270     /**
271      * Builds the post fields array
272      *
273      * @since 1.0.0
274      */
275     private function build_get_fields() {
276         // If parameters are set, then append them
277         if ( count( $this->object['parameters'] ) > 0 ) {
278             // Add the ? that is needed for an appropriate `GET` request, and append the params
279             $url = $this->object['url'] . '?' . http_build_query( $this->object['parameters'] );
280             // Set the new URL that will include the parameters.
281             curl_setopt( $this->handler, CURLOPT_URL, $url );
282         }
283     }
284 
285     /**
286      * Builds the fields out of parameters array
287      *
288      * @since 1.0.0
289      */
290     private function build_fields() {
291         // If the method is `GET`, then we need to append the parameters to
292         // the URL to make sure that it goes alright. Post parameters are included
293         // separately and should already be set.
294         if ( 'get' == $this->object['request_type'] ) {
295             $this->build_get_fields();
296         } else if ( 'post' == $this->object['request_type'] ) {
297             $this->build_post_fields();
298         } else {
299             // This should never happen. I mean it. There is no way that I can think of that this exception will be
300             // thrown. If it is, then please report it, and send me the code you used to make it happen.
301             throw new Exception( "You should never see this, but somehow you are making an unsupported request", 4 );
302         }
303     }
304 
305     /**
306      * Sets basic authorization for a cURL request
307      *
308      * If you need more advanced authorization methods, and if you cannot make them happen with
309      * headers, then use a different library. I recommend Guzzle.
310      *
311      * @since 1.0.0
312      *
313      * @param string $username a username
314      * @param string $password a password
315      */
316     public function set_auth( $username, $password ) {
317         curl_setopt( $this->handler, CURLOPT_HTTPAUTH, CURLAUTH_BASIC );
318         curl_setopt( $this->handler, CURLOPT_USERPWD, "{$username}:{$password}" );
319         $this->object['username'] = $username;
320     }
321 
322     /**
323      * Sets the URL for the cURL request
324      *
325      * @todo        Add in custom exception
326      * @since 1.0.0
327      *
328      * @throws  Exception when $url is not a valid URL
329      * @param   string $url a valid URL
330      */
331     public function set_url( $url ) {
332         // Validate the URL to make sure that it is one
333         if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
334             throw new Exception("The url provided ({$url}) is not valid.");
335         }
336         curl_setopt( $this->handler, CURLOPT_URL, filter_var( $url, FILTER_SANITIZE_URL ) );
337         $this->object['url'] = $url;
338     }
339 
340     /**
341      * Sets the `user agent` for the cURL request
342      *
343      * @since 1.0.0
344      *
345      * @param string $agent a user agent
346      */
347     public function set_user_agent( $agent ) {
348         curl_setopt( $this->handler, CURLOPT_USERAGENT, $agent );
349         $this->object['agent'] = $agent;
350     }
351 
352     /**
353      * Sets the headers on a cURL request
354      *
355      * @since 1.0.0
356      *
357      * @param string|array $headers sets extra headers for the cURL request
358      */
359     public function set_headers( $headers ) {
360         if ( ! is_array( $headers ) ) {
361             // Just transform it into an array
362             $headers = [ $headers ];
363         }
364         curl_setopt( $this->handler, CURLOPT_HTTPHEADER, $headers );
365         $this->object['headers'] = $headers;
366 
367     }
368 
369     /**
370      * Adds a header into the headers array
371      *
372      * You can actually pass multiple headers with an array, or just pass a single
373      * header with a string.
374      *
375      * @since 1.0.0
376      *
377      * @param string|array $header the header to add
378      */
379     public function add_header( $header ) {
380         // Check the variable. We expect string, but let's be sure.
381         if ( is_string( $header ) ) {
382             // Since it's a string, just push it into the headers array.
383             array_push( $this->headers, $header );
384         } else if ( is_array( $header ) ) {
385             // Well, they sent an array, so let's just assume that they want to set
386             // multiple headers here.
387             foreach( $header as $head ) :
388                 if ( is_string( $head ) ) {
389                     // Push each header into the headers array. We can't really check
390                     // to make sure that these headers are okay or fine. So we'll just
391                     // have to deal with failure later if they aren't.
392                     array_push( $this->headers, $head );
393                 } else {
394                     // We're going to assume it's an array, so we'll push them together
395                     // for the error message.
396                     $head = implode( "|", $head );
397                     // Bad header. Throw an exception.
398                     throw new Exception( "You can't push these headers ({$h}).", 4 );
399                 }
400             endforeach;
401         } else {
402             // Bad header. Throw an exception.
403             throw new Exception( "You can't push this header: `{$h}`", 4 );
404         }
405         // Set the headers in the cURL array.
406         $this->set_headers( $this->headers );
407     }
408 
409   /**
410    * Sets the request to use `POST` rather than `GET`
411    *
412      * @since 1.0.0
413    */
414     public function use_post() {
415         // Update the curl handler to use post
416         curl_setopt( $this->handler, CURLOPT_POST, 1 );
417         // Update the internal object to use post
418         $this->object['request_type'] = 'post';
419     }
420 
421     /**
422      * Adds a parameter to the parameters array
423      *
424      * @since 1.0.0
425      *
426      * @param string $key   the name of the parameter
427      * @param string $value the value of the parameter
428      */
429     public function add_parameter( $key, $value ) {
430         $this->object['parameters'][$key] = $value;
431     }
432 
433     /**
434      * Adds parameters to the request
435      *
436      * @since 1.0.0
437      *
438      * @throws Exception when passed something other than an array
439      * @param array $params an array of parameters
440      */
441     public function add_parameters( $params ) {
442         if ( ! is_array( $params ) ) {
443             throw new Exception( 'Parameters must be defined as an array', 4 );
444         }
445         foreach( $params as $key => $value ) :
446             $this->add_parameter( $key, $value );
447         endforeach;
448     }
449 
450     /**
451      * Gets cached data
452      *
453      * This method first checks if the cache file exists. If `$ignore_life` is true,
454      * then it will return the data without checking the life. Otherwise, we'll check
455      * to make sure that the `$cache_life` is set. Next, we check the age of the cache.
456      * If any of these fail, then we return false, which indicates we should get new
457      * data. Otherwise, we retrieve the cache.
458      *
459      * @since 1.0.0
460      *
461      * @return string|boolean the data saved in the cache or false
462      */
463     private function get_cached_data( $ignore_life = false ) {
464 
465         // Does the cache file exist?
466         if ( ! file_exists( $this->get_cache_file() ) ) {
467             return false;
468         }
469 
470         // We don't care if the cache is expired. If there is data, give it
471         // to us anyway (this is good when there is no internet connection, but
472         // we really want the data).
473         if ( $ignore_life ) {
474             return file_get_contents( $this->get_cache_file() );
475         }
476 
477         // Is the cache life set?
478         if ( ! isset( $this->cache_life ) ) {
479             return false;
480         }
481 
482         // Has the has expired?
483         if ( $this->cache_life < $this->get_cache_age() ) {
484             return false;
485         }
486 
487         // Return the contents of the cached file
488         return file_get_contents( $this->get_cache_file() );
489     }
490 
491     /**
492      * Retrieves cached data regardless of cache life
493      *
494      * @since 1.0.0
495      *
496      * @return string|boolean returns the cached data or `false` if none exists
497      */
498     private function get_cached_data_anyway() {
499         return $this->get_cached_data( true );
500     }
501 
502     /**
503      * Saves data to a cache file
504      *
505      * @since 1.0.0
506      *
507      * @param  string $data the data to save to the cache
508      */
509     private function save_cache_data( $data ) {
510         // Make sure that the cache directory exists
511         $this->create_cache_dir();
512 
513         // Debug-level log message
514         \Alphred\Log::log( "Saving cached data to `" . $this->get_cache_file() . "`", 0, 'debug' );
515 
516         // Save the data
517         file_put_contents( $this->get_cache_file(), $data );
518     }
519 
520     /**
521      * Creates a cache key based on the request object
522      *
523      * @since 1.0.0
524      *
525      * @return string   a cache key
526      */
527     private function get_cache_key() {
528         return md5( json_encode( $this->object ) );
529     }
530 
531     /**
532      * Returns the file cache
533      *
534      * @since 1.0.0
535      *
536      * @return string the full path to the cache file
537      */
538     private function get_cache_file() {
539         return $this->get_cache_dir() . '/' . $this->get_cache_key();
540     }
541 
542     /**
543      * Returns the directory for the cache
544      *
545      * @since 1.0.0
546      *
547      * @return string full path to cache directory
548      */
549     private function get_cache_dir() {
550         $path = \Alphred\Globals::get( 'alfred_workflow_cache' );
551         if ( isset( $this->cache_bin ) && $this->cache_bin ) {
552             $path .= '/' . $this->cache_bin;
553         }
554         return $path;
555     }
556 
557     /**
558      * Creates a cache directory if it does not exist
559      *
560      * @since 1.0.0
561      *
562      * @throws \Alphred\RunningOutsideOfAlfred when environmental variables are not set
563      * @return boolean success or failure if directory has been made
564      */
565     private function create_cache_dir() {
566         if ( ! \Alphred\Globals::get( 'alfred_workflow_cache' ) ) {
567             throw new \Alphred\RunningOutsideOfAlfred( 'Cache directory unknown', 4 );
568         }
569         if ( ! file_exists( $this->get_cache_dir() ) ) {
570             // Debug-level log message
571             \Alphred\Log::log( "Creating cache dir `" . $this->get_cache_dir() . "`", 0, 'debug' );
572             return mkdir( $this->get_cache_dir(), 0775, true );
573         }
574     }
575 
576     /**
577      * Gets the age of a cache file
578      *
579      * @since 1.0.0
580      *
581      * @return integer the age of a file in seconds
582      */
583     private function get_cache_age() {
584         if ( ! file_exists( $this->get_cache_file() ) ) {
585             // Cache does not exist
586             return false;
587         }
588         return time() - filemtime( $this->get_cache_file() );
589     }
590 
591     /**
592      * Clears a cache bin
593      *
594      * Call the file with no arguments if you aren't using a cache bin; however, this
595      * will choke on sub-directories.
596      *
597      * @throws Exception when encountering a sub-directory
598      *
599      * @param  string|boolean $bin the name of the cache bin (or a URL if you're setting them automatically)
600      * @return null
601      */
602     public function clear_cache( $bin = false ) {
603         // Get the cache directory
604         $dir = \Alphred\Globals::cache();
605         if ( ! $bin ) {
606             return self::clear_directory( $dir );
607         }
608         if ( filter_var( $bin, FILTER_VALIDATE_URL ) ) {
609             $dir = $dir . '/' . parse_url( $bin, PHP_URL_HOST );
610         } else {
611             $dir = "{$dir}/{$bin}";
612         }
613         // Clear the directory
614         return self::clear_directory( $dir );
615     }
616 
617     /**
618      * Clears all the files out of a directory
619      *
620      * @since 1.0.0
621      * @throws Exception when encountering a sub-directory
622      *
623      * @param  string $dir a path to a directory
624      */
625     private function clear_directory( $dir ) {
626         if ( ! file_exists( $dir ) || ! is_dir( $dir ) || '.' === $dir ) {
627             // Throw an exception because this is a bad request to clear the cache
628             throw new Exception( "Cannot clear directory: `{$dir}`", 3 );
629         }
630 
631         \Alphred\Log::console( "Clearing cache directory `{$dir}`.", 1 );
632 
633         $files = array_diff( scandir( $dir ), [ '.', '..' ] );
634         foreach( $files as $file ) :
635             // Do not delete sub-directories
636             if ( is_dir( $file ) ) {
637                 // We could expand this to support deleting sub-directories by just calling this method
638                 // recursively, but it is better just to use the cache_bin and keep the caches separate.
639                 throw new Exception( "Cannot delete subdirectory `{$file}` in `{$dir}`", 3 );
640             } else {
641                 // Delete the file
642                 unlink( "{$dir}/{$file}" );
643             }
644 
645         endforeach;
646     }
647 
648 
649     /**********
650      * Old functions
651      **********/
652 
653     // public function simple_download( $url, $destination = '', $mkdir = false ) {
654     //  // Function to download a URL easily.
655     //  $url = filter_var( $url, FILTER_SANITIZE_URL );
656     //  if ( empty( $destination ) ) {
657     //      return file_get_contents( $url ); }
658     //  else {
659     //      if ( file_exists( $destination ) && is_dir( $destination ) ) {
660     //          $destination = $destination . '/' . basename( parse_url( $url, PHP_URL_PATH ) );
661     //      }
662     //      file_put_contents( $destination, file_get_contents( $url ) );
663     //  }
664     //  return $destination;
665     // }
666 
667     // public function get_favicon( $url, $destination = '', $cache = true, $ttl = 604800 ) {
668     //  $url = parse_url( $url );
669     //  $domain = $url['host'];
670     //  if ( $cache && $file = $this->cache( "{$domain}.png", 'favicons', $ttl ) ) {
671     //      return $file;
672     //  }
673     //  $favicon = file_get_contents( "https://www.google.com/s2/favicons?domain={$domain}" );
674     //  if ( empty( $destination ) ) {
675     //      $destination = Globals::get( 'alfred_workflow_cache' ) . '/favicons';
676     //  }
677     //  if ( ! file_exists( $destination ) && substr( $destination, -4 ) !== '.png' ) {
678     //      mkdir( $destination, 0755, true );
679     //  }
680     //  if ( file_exists( $destination ) && is_dir( $desintation ) ) {
681     //      $destination .= "/{$domain}.png";
682     //  }
683 
684     //  file_put_contents( $destination, $favicon );
685     //  return $destination;
686     // }
687 
688 
689 }
Alphred API documentation generated by ApiGen