Overview

Packages

  • AlfredBundler
  • None

Classes

  • AlfredBundler
  • AlfredBundlerIcon
  • AlfredBundlerInternalClass
  • AlfredBundlerLogger
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: 
  3: /**
  4:  * PHP implementation of the Alfred Bundler
  5:  *
  6:  * Main PHP interface for the Alfred Dependency Bundler. This file should be
  7:  * the only one from the bundler that is distributed with your workflow.
  8:  * You can use this file and the class contained within it to lazy load a
  9:  * variety of assets that can be used in your workflows.
 10:  *
 11:  * This file is part of the Alfred Bundler, released under the MIT licence.
 12:  * Copyright (c) 2014 The Alfred Bundler Team
 13:  * See https://shawnrice.github.io/alfred-bundler for more information.
 14:  *
 15:  * @copyright  The Alfred Bundler Team 2014
 16:  * @license    http://opensource.org/licenses/MIT  MIT
 17:  * @version    Taurus 1
 18:  * @link       http://shawnrice.github.io/alfred-bundler
 19:  * @package    AlfredBundler
 20:  * @since      File available since Aries 1
 21:  */
 22: 
 23: 
 24: /**
 25:  * The PHP interface for the Alfred Bundler
 26:  *
 27:  * This class is the only one that you should interact with. The rest of the
 28:  * magic that the bundler performs happens under the hood. Also, the backend
 29:  * of the bundler (here the 'AlfredBundlerInternalClass') may change; however,
 30:  * this bundlet will continue to work with the bundler API for the remainder of
 31:  * this major version (Taurus). Limited documentation is below, for more
 32:  * detailed documentation, see http://shawnrice.github.io/alfred-bundler.
 33:  *
 34:  * Example usage:
 35:  * <code>
 36:  * require_once( 'alfred.bundler.php' );
 37:  * $b = new AlfredBundler;
 38:  *
 39:  * // Downloads and requires David Ferguson's PHP Workflows library
 40:  * $b->library( 'Workflows' );
 41:  *
 42:  * // Download icons
 43:  * $icon1 = $b->icon( 'elusive', 'dashboard', 'ab332c', TRUE );
 44:  * $icon2 = $b->icon( 'fontawesome', 'adjust', 'aabbcc', '998877');
 45:  * $icon3 = $b->icon( 'fontawesome, 'bug' );
 46:  * $icon4 = $b->icon( 'system', 'Accounts' );
 47:  *
 48:  * // Send a message to the console (shows up in Alfred's debugger)
 49:  * $b->log( 'Loaded icons', 'INFO', 'console' );
 50:  * // Log a message to a logfile found in the workflow's data directory
 51:  * $b->log( 'Initial bootstrap complete, check the log file', 'DEBUG', 'log' );
 52:  * // Send the same message to the console and the log file
 53:  * $b->log( 'Bootstrap completed.', 'INFO', 'both' );
 54:  *
 55:  *
 56:  * // Get Pashua to use later
 57:  * $pashua = $b->utility( 'Pashua' );
 58:  *
 59:  * // Get an asset not included in the "defaults"
 60:  * $myAsset = $b->load( 'utility', 'myAsset', 'latest', '/path/to/json' );
 61:  *
 62:  * // Load 'cocoadialog' with the bundler's wrappers
 63:  * $cocoadialog = $b->wrapper( 'cocoadialog' );
 64:  *
 65:  * // Load/install composer packages
 66:  * $bundler->composer( array(
 67:  *   "monolog/monolog" => "1.10.*@dev"
 68:  * ));
 69:  *
 70:  * </code>
 71:  *
 72:  * @see       AlfredBundlerInternalClass
 73:  * @since     Class available since Taurus 1
 74:  *
 75:  */
 76: class AlfredBundler {
 77: 
 78:   /**
 79:    * An internal object that interfaces with the PHP Bundler API
 80:    *
 81:    * @access protected
 82:    * @var object
 83:    */
 84:   protected $bundler;
 85: 
 86:   /**
 87:    * A filepath to the bundler directory
 88:    *
 89:    * We're using this name so it doesn't clash with the internal $data
 90:    * variable.
 91:    *
 92:    * @access public
 93:    * @var string
 94:    */
 95:   private   $_data;
 96: 
 97:   /**
 98:    * A filepath to the bundler cache directory
 99:    *
100:    * We're using this name so it doesn't clash with the internal $data
101:    * variable.
102:    *
103:    * @access private
104:    * @var string
105:    */
106:   private   $_cache;
107: 
108:   /**
109:    * The MAJOR version of the bundler (which API to use)
110:    *
111:    * @access private
112:    * @var string
113:    */
114:   private   $_major_version;
115: 
116:   /**
117:    * The class constructor
118:    *
119:    * @return bool Returns successful/failed instantiation
120:    */
121:   public function __construct() {
122: 
123:     if ( ! file_exists( 'info.plist' ) ) {
124:       throw new Exception('The Alfred Bundler cannot be used without an `info.plist` file present.');
125:       return FALSE;
126:     }
127: 
128:     if ( isset( $_ENV[ 'AB_BRANCH' ] ) && ! empty( $_ENV[ 'AB_BRANCH' ] ) ) {
129:       $this->_major_version = $_ENV[ 'AB_BRANCH' ];
130:     } else {
131:       $this->_major_version = 'devel';
132:     }
133: 
134:     // Set date/time to avoid warnings/errors.
135:     if ( ! ini_get( 'date.timezone' ) ) {
136:       $tz = exec( 'tz=`ls -l /etc/localtime` && echo ${tz#*/zoneinfo/}' );
137:       ini_set( 'date.timezone', $tz );
138:     }
139: 
140:     $this->_data  = "{$_SERVER['HOME']}/Library/Application Support/Alfred 2/" .
141:       "Workflow Data/alfred.bundler-{$this->_major_version}";
142:     $this->_cache = "{$_SERVER['HOME']}/Library/Caches/" .
143:       "com.runningwithcrayons.Alfred-2/Workflow Data/" .
144:       "alfred.bundler-{$this->_major_version}";
145: 
146:     if ( file_exists( "{$this->_data}/bundler/AlfredBundler.php" ) ) {
147:       require_once ( "{$this->_data}/bundler/AlfredBundler.php" );
148:       $this->bundler = new AlfredBundlerInternalClass();
149:     } else {
150:       if ( $this->installBundler() === FALSE ) {
151:         // The bundler could not install itself, so throw an exception.
152:         throw new Exception('The Alfred Bundler could not be installed.');
153:         return FALSE;
154:       } else {
155:         chmod( "{$this->_data}/bundler/includes/LightOrDark", 0755 );
156:         // The bundler is now in place, so require the actual PHP Bundler file
157:         require_once "{$this->_data}/bundler/AlfredBundler.php";
158:         // Create the internal class object
159:         $this->bundler = new AlfredBundlerInternalClass();
160:         $this->bundler->notify(
161:         'Alfred Bundler',
162:         'Installation successful. Thank you for waiting.',
163:         "{$this->_data}/bundler/meta/icons/bundle.png"
164:       );
165:       }
166:     }
167: 
168:     // Call the wrapper to update itself, processed is forked for speed
169:     exec( "bash '{$this->_data}/bundler/meta/update-wrapper.sh'" );
170: 
171:     return TRUE;
172:   }
173: 
174:   /**
175:    * Logs output to the console
176:    *
177:    * This method provides very limited console logging functionality. It is
178:    * employed only by this bundlet only when it is installing the PHP
179:    * implementation of the Alfred Bundler. A much more robust logging
180:    * functionality is the the backend, so those methods will be used at all
181:    * other times.
182:    *
183:    * @see     AlfredBundlerInternalClass::log
184:    * @see     AlfredBundlerLogger::log
185:    * @see     AlfredBundlerLogger::logFile
186:    * @see     AlfredBundlerLogger::logConsole
187:    *
188:    * @param   string  $message  message to be logged
189:    * @param   mixed   $level    log level to be recorded
190:    *
191:    * @since   Taurus 1
192:    *
193:    */
194:   private function report( $message, $level ) {
195: 
196:     // These are the appropriate log levels
197:     $logLevels = array( 0 => 'DEBUG',
198:       1 => 'INFO',
199:       2 => 'WARNING',
200:       3 => 'ERROR',
201:       4 => 'CRITICAL',
202:     );
203: 
204:     $date = date( 'H:i:s', time() );
205:     file_put_contents( 'php://stderr', "[{$date}] [{$level}] {$message}" . PHP_EOL );
206: 
207:   }
208: 
209:   /**
210:    * Passes calls to the internal object
211:    *
212:    * Wrapper function that passes all other calls to the internal object when
213:    * the method has not been found in this class
214:    *
215:    * @param string $method Name of method
216:    * @param array $args   An array of args passed
217:    * @return mixed          Whatever the internal function sends back
218:    *
219:    * @since  Taurus 1
220:    */
221:   public function __call( $method, $args ) {
222: 
223:     // Make sure that the bundler installation was not refused
224:     if ( isset( $_ENV[ 'ALFRED_BUNDLER_INSTALL_REFUSED'] ) ) {
225:       $bt = array_unshift( debug_backtrace() );
226:       $date = date( 'H:i:s' );
227:       $trace = basename( $bt[ 'file' ] ) . ':' . $bt[ 'line' ];
228:       $message = "Trying to call an Alfred Bundler method ({$method}), but user refused to install the bundler.";
229:       $this->report( "[{$date}] [{$trace}] {$message}", 'CRITICAL' );
230:       return FALSE;
231:     }
232: 
233: 
234:     // Check to make sure that the method exists in the
235:     // 'AlfredBundlerInternalClass' class
236:     if ( ! method_exists( $this->bundler, $method ) ) {
237:       // Whoops. We called a non-existent method
238:       $this->bundler->log( "Could not find method [$method] in class 'AlfredBundler'.",
239:         'ERROR', 'console' );
240:       return FALSE;
241:     }
242: 
243:     // The method exists, so call it and return the output
244:     return call_user_func_array( array( $this->bundler, $method ), $args );
245:   }
246: 
247:   /**
248:    * Gets variables from the AlfredBundlerInternalClass
249:    *
250:    * @param   string  $name  name of variable to get
251:    * @return  mixed         the variable from the internal class object
252:    *
253:    * @access public
254:    * @since  Taurus 1
255:    */
256:   public function &__get( $name ) {
257:     if ( isset( $this->bundler ) && is_object( $this->bundler ) ) {
258:       if ( isset( $this->bundler->$name ) )
259:         return $this->bundler->$name;
260:     }
261:   }
262: 
263: /******************************************************************************
264:  *** Begin Installation Functions
265:  *****************************************************************************/
266: 
267: /**
268:  * Validates and checks variables for installation
269:  *
270:  * @return  bool returns false on failure
271:  * @since   Taurus 1
272:  */
273: private function prepareInstallation() {
274:   if ( ! file_exists( 'info.plist' ) ) {
275:     $this->report( "You need a valid `info.plist` to use the Alfred Bundler.", 'CRITICAL' );
276:     return FALSE;
277:   }
278:   if ( isset( $_ENV[ 'alfred_version' ] ) )
279:     $this->prepareModern();
280:   else
281:     $this->prepareDeprecated();
282: 
283: }
284: 
285: /**
286:  * Sets the name of the workflow
287:  * Method used for Alfredv2.4:277+
288:  *
289:  * @since Taurus 1
290:  */
291: private function prepareModern() {
292:   $this->name = $_ENV[ 'alfred_workflow_name' ];
293: }
294: 
295: /**
296:  * Sets the name of the workflow
297:  * This method is used only for version < Alfredv2.4:277
298:  *
299:  * @since Taurus 1
300:  */
301: private function prepareDeprecated() {
302:   $this->name = exec( "/usr/libexec/PlistBuddy -c 'Print :name' 'info.plist'" );
303: }
304: 
305: /**
306:  * Prepares the text for the installation confirmation AS dialog
307:  *
308:  * @since Taurus 1
309:  */
310: private function prepareASDialog() {
311: 
312:   if ( file_exists( 'icon.png' ) ) {
313:     $icon = realpath( 'icon.png' );
314:     $icon = str_replace('/', ':', 'icon');
315:     $icon = substr( $icon, 1, strlen( $icon ) - 1 );
316:   } else {
317:     $icon = "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:SideBarDownloadsFolder.icns";
318:   }
319:   // Text for the dialog message.
320:   $text = "{$this->name} needs to install additional components, which will be placed in the Alfred storage directory and will not interfere with your system.
321: 
322: You may be asked to allow some components to run, depending on your security settings.
323: 
324: You can decline this installation, but {$this->name} may not work without them. There will be a slight delay after accepting.";
325: 
326:   $this->script = "display dialog \"$text\" " .
327:     "buttons {\"More Info\",\"Cancel\",\"Proceed\"} default button 3 " .
328:     "with title \"{$this->name} Setup\" with icon file \"$icon\"";
329: 
330: }
331: 
332: /**
333:  * Executes AppleScript dialog confirmation and handles return value
334:  *
335:  * @todo decide and implement proper exit behavior for failure
336:  * @return  bool  confirmation / refusal to install the bundler
337:  *
338:  * @since Taurus 1
339:  */
340: private function processASDialog() {
341:   $info = "https://github.com/shawnrice/alfred-bundler/wiki/What-is-the-Alfred-Bundler";
342:   $confirm = str_replace( 'button returned:', '', exec( "osascript -e '{$this->script}'" ) );
343:   if ( $confirm == 'More Info' ) {
344:     exec( "open {$info}" );
345:     die(); // Stop the workflow. If it's a script filter, then this will happen anyway.
346:   } else if ( $confirm == 'Proceed' ) {
347:     return TRUE;
348:   } else {
349:     $this->report( "User canceled installation of Alfred Bundler. Unknown " .
350:       "and possibly catastrophic effects to follow.",
351:       'CRITICAL' );
352:     $_ENV[ 'ALFRED_BUNDLER_INSTALL_REFUSED' ] = TRUE;
353:     return FALSE;
354:   }
355:   return TRUE;
356: }
357: 
358:   /**
359:    * Makes the data and cache directories for the Bundler
360:    *
361:    * @TODO: add in error handling for failed permissions (should be _very_ rare)
362:    * @since  Taurus 1
363:    */
364:   private function prepareDirectories() {
365:     // Make the bundler cache directory
366:     if ( ! file_exists( $this->_cache ) )
367:       mkdir( $this->_cache, 0755, TRUE );
368:     // Make the bundler data directory
369:     if ( ! file_exists( $this->_data ) )
370:       mkdir( $this->_data, 0755, TRUE );
371:   }
372: 
373:   private function userCanceledInstallation() {
374:     throw new Exception('The user canceled the installation of the Alfred Bundler.');
375:   }
376: 
377:   /**
378:    * Installs the Alfred Bundler
379:    *
380:    * @return bool Success or failure of installation
381:    *
382:    * @since  Taurus 1
383:    */
384:   private function installBundler() {
385: 
386:     $this->prepareInstallation();
387:     $this->prepareASDialog();
388: 
389:     if ( ! $this->processASDialog() ) {
390:       $this->userCanceledInstallation(); // Throw an exception.
391:       return FALSE;
392:     }
393:     $this->prepareDirectories();
394: 
395:     // This is a list of mirrors that host the bundler. A current list is in
396:     // bundler/meta/bundler_servers, but that file should not exist on the
397:     // machine -- yet -- because this is the function that installs that file.
398:     // The 'latest' tag is the current release.
399:     $suffix = "-latest.zip";
400:     if ( isset( $_ENV[ 'AB_BRANCH' ] ) &&
401:       ( ! empty( $_ENV[ 'AB_BRANCH' ] ) ) ) {
402:       $suffix = ".zip";
403:     }
404:     $bundler_servers = array(
405:       "https://github.com/shawnrice/alfred-bundler/archive/{$this->_major_version}{$suffix}",
406:       "https://bitbucket.org/shawnrice/alfred-bundler/get/{$this->_major_version}{$suffix}"
407:     );
408: 
409:     // Cycle through the servers until we find one that is up.
410:     foreach ( $bundler_servers as $server ) :
411:       $success = $this->dl( $server, "{$this->_cache}/bundler.zip" );
412:       if ( $success === TRUE ) {
413:         $this->report( "Downloaded Bundler Installation from... {$server}", 'INFO' );
414:         break; // We found one, so break
415:       }
416:     endforeach;
417: 
418:     // If success is true, then we downloaded a copy of the bundler
419:     if ( $success !== TRUE ) {
420:       $this->report( "Could not reach server to install Alfred Bundler.", 'CRITICAL' );
421:       unlink( "{$this->_cache}/bundler.zip" );
422:       return FALSE;
423:     }
424: 
425:     // Unzip the bundler archive
426:     $zip = new ZipArchive;
427:     $resource = $zip->open( "{$this->_cache}/bundler.zip" );
428:     if ( $resource !== TRUE ) {
429:       $this->report( "Bundler install zip file corrupt.", 'CRITICAL' );
430:       if ( file_exists( "{$this->_cache}/bundler.zip" ) ) {
431:         unlink( "{$this->_cache}/bundler.zip" );
432:       }
433:       return FALSE;
434:     } else {
435:       $zip->extractTo( "{$this->_cache}" );
436:       $zip->close();
437:     }
438: 
439:     if ( file_exists( "{$this->_data}/bundler" ) ) {
440:       $this->report( "Bundler already installed. Exiting install script.", 'WARNING' );
441:       return FALSE; // Add in error reporting
442:     }
443: 
444:     // Move the bundler into place
445:     $directoryHandle = opendir( $this->_cache );
446:     while ( FALSE !== ( $file = readdir( $directoryHandle ) ) ) {
447:         if ( is_dir( "{$this->_cache}/{$file}" ) && (strpos( $file, "alfred-bundler-" ) === 0 ) ) {
448:           $bundlerFolder = "{$this->_cache}/{$file}";
449:           closedir( $directoryHandle );
450:           break;
451:         }
452:     }
453: 
454:     if ( ( ! isset( $bundlerFolder ) ) || ( empty( $bundlerFolder ) ) ) {
455:       $this->report( "Could not find Alfred Bundler folder in installation zip.", 'CRITICAL' );
456:       return FALSE;
457:     }
458: 
459:     rename( "{$bundlerFolder}/bundler", "{$this->_data}/bundler" );
460: 
461:     $this->report( 'Alfred Bundler successfully installed, cleaning up...', 'INFO');
462:     unlink( "{$this->_cache}/bundler.zip" );
463:     // We'll do a cheat here to remove the leftover installation files
464:     exec( "rm -fR '{$bundlerFolder}'" );
465:     return TRUE; // The bundler should be in place now
466:   }
467: 
468:   /**
469:    * Wraps a cURL function to download files
470:    *
471:    * This method should be used only by the bundlet to download the
472:    * bundler from the server
473:    *
474:    * @param string $url     A URL to the file
475:    * @param string $file    The destination file
476:    * @param int    $timeout = '5' A timeout variable (in seconds)
477:    * @return bool            True on success and error code / false on failure
478:    *
479:    * @since  Taurus 1
480:    */
481:   private function dl( $url, $file, $timeout = '5' ) {
482:     // Check the URL here
483: 
484:     // Make sure that the download directory exists
485:     if ( ! file_exists( realpath( dirname( $file ) ) ) ) {
486:       $this->report( "Bundler install directory could not be created.", 'CRITICAL' );
487:       return FALSE;
488:     }
489: 
490:     // Create the cURL object
491:     $ch = curl_init( $url );
492:     // Open the file that we'll write to
493:     $fp = fopen( $file , "w" );
494: 
495:     // Set standard cURL options
496:     curl_setopt_array( $ch, array(
497:         CURLOPT_FILE => $fp,
498:         CURLOPT_HEADER => FALSE,
499:         CURLOPT_FOLLOWLOCATION => TRUE,
500:         CURLOPT_CONNECTTIMEOUT => $timeout
501:       ) );
502: 
503: 
504:     // Execute the cURL request and check for errors
505:     if ( curl_exec( $ch ) === FALSE ) {
506:       // We've run into an error, so, let's handle the error as best as possible
507:       // Close the connection
508:       curl_close( $ch );
509:       // Close the file
510:       fclose( $fp );
511: 
512:       // If the file has been written (partially), then delete it
513:       if ( file_exists( $file ) )
514:         unlink( $file );
515: 
516:       // Return the cURL error
517:       // Curl error codes: http://curl.haxx.se/libcurl/c/libcurl-errors.html
518:       return curl_error( $ch ) ;
519:     }
520: 
521:     // File downloaded without error, so close the connection
522:     curl_close( $ch );
523:     // Close the file
524:     fclose( $fp );
525: 
526:     // Return success
527:     return TRUE;
528:   }
529: 
530: /******************************************************************************
531:  *** End Installation Functions
532:  *****************************************************************************/
533: 
534: }
535: 
536: 
537: // Update logic for the bundler
538: // ----------------------------
539: // Unfortunately, Apple doesn't let us have the pcntl functions natively, so
540: // we'll take a different route and spoof this sort of thing. Here is a not
541: // very well developed implementation of forking the update process.
542: //
543: // if ( ! function_exists('pcntl_fork') )
544: //   die('PCNTL functions not available on this PHP installation');
545: // else {
546: //   $pid = pcntl_fork();
547: //
548: //   switch($pid) {
549: //       case -1:
550: //           print "Could not fork!\n";
551: //           exit;
552: //       case 0:
553: //           // Check for bundler minor update
554: //           $cmd = "sh '$__data/bundler/meta/update.sh' > /dev/null 2>&1";
555: //           exec( $cmd );
556: //           break;
557: //       default:
558: //           print "In parent!\n";
559: //   }
560: // }
Alfred Bundler API documentation generated by ApiGen 2.8.0