Overview

Packages

  • AlfredBundler
  • None

Classes

  • AlfredBundler
  • AlfredBundlerIcon
  • AlfredBundlerInternalClass
  • AlfredBundlerLogger
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: 
   3: /**
   4:  * Alfred Bundler PHP API file
   5:  *
   6:  * Main PHP interface for the Alfred Dependency Bundler.
   7:  *
   8:  * This file is part of the Alfred Bundler, released under the MIT licence.
   9:  * Copyright (c) 2014 The Alfred Bundler Team
  10:  * See https://github.com/shawnrice/alfred-bundler for more information
  11:  *
  12:  * @copyright  The Alfred Bundler Team 2014
  13:  * @license    http://opensource.org/licenses/MIT  MIT
  14:  * @version    Taurus 1
  15:  * @link       http://shawnrice.github.io/alfred-bundler
  16:  * @package    AlfredBundler
  17:  * @since      File available since Taurus 1
  18:  */
  19: 
  20: require_once( __DIR__ . '/includes/php-classes/AlfredBundlerLogger.php' );
  21: require_once( __DIR__ . '/includes/php-classes/AlfredBundlerIcon.php' );
  22: 
  23: // We'll add this in for testing
  24: if ( ! class_exists( 'AlfredBundlerInternalClass' ) ) :
  25: /**
  26:  * Internal API Class for Alfred Bundler
  27:  *
  28:  * This class is the only one that you should interact with. The rest of the
  29:  * magic that the bundler performs happens under the hood. Also, the backend
  30:  * of the bundler (here the 'AlfredBundlerInternalClass') may change; however,
  31:  * this wrapper will continue to work with the bundler API for the remainder of
  32:  * this major version.
  33:  *
  34:  * @since     Class available since Taurus 1
  35:  * @package   AlfredBundler
  36:  *
  37:  */
  38: class AlfredBundlerInternalClass {
  39: 
  40:   /**
  41:    * A filepath to the bundler directory
  42:    *
  43:    * @access public
  44:    * @var string
  45:    */
  46:   public   $data;
  47: 
  48:   /**
  49:    * A filepath to the bundler cache directory
  50:    *
  51:    * @access public
  52:    * @var string
  53:    */
  54:   public   $cache;
  55: 
  56:   /**
  57:    * The MAJOR version of the bundler (which API to use)
  58:    *
  59:    * @access private
  60:    * @var string
  61:    */
  62:   public   $major_version;
  63: 
  64:   /**
  65:    * The MINOR version of the bundler
  66:    *
  67:    * @access private
  68:    * @var string
  69:    */
  70:   public   $minor_version;
  71: 
  72:   /**
  73:    * Filepath to an Alfred info.plist file
  74:    *
  75:    * @access private
  76:    * @var string
  77:    */
  78:   private   $plist;
  79: 
  80:   /**
  81:    * The Bundle ID of the workflow using the bundler
  82:    *
  83:    * @access private
  84:    * @var string
  85:    */
  86:   public   $bundle;
  87: 
  88:   /**
  89:    * The name of the workflow using the bundler
  90:    *
  91:    * @access private
  92:    * @var string
  93:    */
  94:   public   $name;
  95: 
  96:   /**
  97:    * The data directory of the workflow using the bundler
  98:    *
  99:    * @var  string
 100:    */
 101:   public   $workflowData;
 102: 
 103:   /**
 104:    * The background 'color' of the user's current theme in Alfred (light or dark)
 105:    *
 106:    * @access private
 107:    * @var string
 108:    */
 109:   public   $background;
 110: 
 111:   // Just a resource to check on a fileinfo thingie.
 112:   public $finfo;
 113: 
 114:   //
 115:   public $alfredVersion;
 116: 
 117:   /**
 118:    * [$caches description]
 119:    *
 120:    * @access private
 121:    * @var    array
 122:    */
 123:   private $caches;
 124: 
 125:   /**
 126:    * Desc
 127:    *
 128:    * @access public
 129:    * @var    object
 130:    */
 131:   public $log;
 132: 
 133:   /**
 134:    * Desc
 135:    *
 136:    * @access public
 137:    * @var    object
 138:    */
 139:   public $userLog;
 140: 
 141:   /**
 142:    * Whether or not enviromental variables are present
 143:    *
 144:    * @access public
 145:    * @var    bool
 146:    */
 147:   public $env;
 148: 
 149: 
 150:   /**
 151:    * The class constructor
 152:    *
 153:    * Sets necessary variables.
 154:    *
 155:    * @access public
 156:    * @param string $options a list of options to configure the instance
 157:    * @return bool          Return 'true' regardless
 158:    */
 159:   public function __construct( $options = [] ) {
 160: 
 161:     if ( isset( $_ENV['AB_BRANCH'] ) ) {
 162:       $this->major_version = $_ENV['AB_BRANCH'];
 163:     } else {
 164:       $this->major_version = file_get_contents(  __DIR__ . '/meta/version_major' );
 165:     }
 166: 
 167:     $this->minor_version   = file_get_contents(  __DIR__ . '/meta/version_minor' );
 168: 
 169:     $this->data  = trim( "{$_SERVER[ 'HOME' ]}/Library/Application Support/Alfred 2/Workflow Data/alfred.bundler-{$this->major_version}" );
 170:     $this->cache = trim( "{$_SERVER[ 'HOME' ]}/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/alfred.bundler-{$this->major_version}" );
 171: 
 172:     $this->setup();
 173: 
 174:     $this->log = new AlfredBundlerLogger( "{$this->data}/data/logs/bundler-{$this->major_version}" );
 175: 
 176:     if ( isset( $options[ 'log' ] ) )
 177:       $log = $options[ 'log' ];
 178:     else
 179:       $log = 'file';
 180: 
 181:     $this->userLog = new AlfredBundlerLogger( "{$this->workflowData}/{$this->name}", $log );
 182: 
 183:     // Let's just return something
 184:     return TRUE;
 185:   }
 186: 
 187:   /**
 188:    * General setup function for variables and directories
 189:    *
 190:    */
 191:   private function setup() {
 192: 
 193:     // This should be taken care of by the bundlet, but for redundnacy...
 194:     if ( ! file_exists( 'info.plist' ) )
 195:           throw new Exception( 'Using the Alfred Bundler requires a valid info.plist file to be found; in other words, it needs to be used in a workflow. ');
 196: 
 197:     if ( isset( $_ENV[ 'alfred_version' ] ) )
 198:       $this->setupModern();
 199:     else
 200:       $this->setupDeprecated();
 201: 
 202:     $this->setupDirStructure();
 203: 
 204:   }
 205: 
 206:   /**
 207:    * Sets variables for Alfred v2.4:277+
 208:    */
 209:   private function setupModern() {
 210: 
 211:     $this->bundle       = $_ENV[ 'alfred_workflow_bundleid' ];
 212:     $this->name         = $_ENV[ 'alfred_workflow_name' ];
 213:     $this->workflowData = $_ENV[ 'alfred_workflow_data' ];
 214:     $this->env          = TRUE;
 215: 
 216:   }
 217: 
 218:   /**
 219:    * Sets variables for versions of Alfred pre-2.4:277
 220:    */
 221:   private function setupDeprecated() {
 222: 
 223:     $this->bundle       = $this->readPlist( 'info.plist', 'bundleid' );
 224:     $this->name         = $this->readPlist( 'info.plist', 'name' );
 225:     $this->workflowData = $_SERVER[ 'HOME' ] .
 226:       "/Library/Application Support/Alfred 2/Workflow Data/" . $this->bundle;
 227:     $this->env          = FALSE;
 228: 
 229:   }
 230: 
 231:   /**
 232:    * Creates the directories for the bundler
 233:    */
 234:   private function setupDirStructure() {
 235: 
 236:     // This list is a bit redundant for the logic below, but that's fine.
 237:     $directories = array(
 238:       "{$this->data}/data",
 239:       "{$this->cache}",
 240:       "{$this->cache}/color",
 241:       "{$this->cache}/misc",
 242:       "{$this->cache}/php",
 243:       "{$this->cache}/ruby",
 244:       "{$this->cache}/python",
 245:       "{$this->cache}/utilities",
 246:     );
 247: 
 248:     foreach ( $directories as $dir ) :
 249:       // @TODO add in better error handling here (for redundancy)
 250:       if ( ! file_exists( $dir ) )
 251:         mkdir( $dir, 0775, TRUE );
 252:     endforeach;
 253:   }
 254: 
 255:   /**
 256:    * Load an asset using a generic function
 257:    *
 258:    *
 259:    * @since  Taurus 1
 260:    * @access public
 261:    *
 262:    * @param string $type    Type of asset
 263:    * @param string $name    Name of asset
 264:    * @param string $version Version of asset to load
 265:    * @param string $json    = '' Path to json file
 266:    * @return mixed             Returns path to utility on success, FALSE on failure
 267:    */
 268:   public function load( $type, $name, $version, $json = '' ) {
 269: 
 270:     if ( empty( $json ) ) {
 271:       if ( file_exists( __DIR__ . "/meta/defaults/{$name}.json" ) ) {
 272:         $json_path = __DIR__ . "/meta/defaults/{$name}.json";
 273:       } else {
 274:         // JSON File cannot be found
 275:         $error = TRUE;
 276:       }
 277:     } else if ( file_exists( $json ) ) { $line = __LINE__;
 278:         $json_path = $json;
 279:       } else {
 280:       // JSON File cannot be found
 281:       $error = TRUE;
 282:     }
 283: 
 284:     // Check to see if the JSON is valid
 285:     if ( ! json_decode( file_get_contents( $json_path ) ) ) { $line = __LINE__;
 286:       // JSON file not valid
 287:       $error = TRUE;
 288:     }
 289: 
 290:     // If we've encountered an error, then write the error and exit
 291:     if ( isset( $error ) && ( $error === TRUE ) ) {
 292:       // There is an error with the JSON file.
 293:       // Output the error to STDERR.
 294: 
 295:       $this->log->log( "There is a problem with the __implementation__ of " .
 296:         "the Alfred Bundler when trying to load '{$name}'. Please " .
 297:         "let the workflow author know.", 'CRITICAL', 'console' );
 298:       return FALSE;
 299:     }
 300: 
 301:     $json = json_decode( file_get_contents( $json_path ), TRUE );
 302: 
 303:     // See if the file is installed
 304:     if ( ! file_exists(
 305:         "{$this->data}/data/assets/{$type}/{$name}/{$version}/invoke" ) ) {
 306: 
 307:       if ( ! $this->installAsset(
 308:           "{$this->data}/bundler/meta/defaults/{$name}.json", $version ) ) {
 309:         return FALSE;
 310:       }
 311:     }
 312: 
 313:     // Register the asset. We don't need to worry about the return.
 314:     $this->register( $name, $version ); $line = __LINE__;
 315:     $this->log->log( "Registering assset '{$name}'",
 316:       'INFO', 'console' );
 317: 
 318:     // The file should exist now, but we'll try anyway
 319:     if ( ! file_exists(
 320:         "{$this->data}/data/assets/{$type}/{$name}/{$version}/invoke" ) ) {
 321: 
 322:       return FALSE;
 323:     }
 324: 
 325:     if ( $type != 'utility' ) {
 326:       // We don't have to worry about gatekeeper
 327:       $this->log->log( "Loaded '{$name}' version {$version} of type " .
 328:         "'{$type}'", 'INFO', 'console' );
 329: 
 330:       return "{$this->data}/data/assets/{$type}/{$name}/{$version}/"
 331:         . trim( file_get_contents( "{$this->data}/data/assets/{$type}/{$name}/{$version}/invoke" ) );
 332:     } else {
 333:       // It's a utility, so let's see if we need gatekeeper
 334:       if ( ! ( isset( $json[ 'gatekeeper' ] )
 335:           && ( $json[ 'gatekeeper' ] == TRUE ) ) ) {
 336: 
 337:         // We don't have to worry about gatekeeper
 338:         $this->log->log( "Loaded '{$name}' version {$version} of type " .
 339:           "'{$type}'", 'INFO', 'console' );
 340: 
 341:         return "{$this->data}/data/assets/{$type}/{$name}/{$version}/"
 342:           . trim( file_get_contents( "{$this->data}/data/assets/{$type}/{$name}/{$version}/invoke" ) );
 343:       }
 344:     }
 345: 
 346:     // At this point, we need to check in with gatekeeper
 347: 
 348:     // Find the icon to pass to the Gatekeeper script
 349:     if ( file_exists( 'icon.png' ) )
 350:       $icon = realpath( 'icon.png' );
 351:     else
 352:       $icon = 'default';
 353: 
 354:     // Create the path variable
 355:     $path = "{$this->data}/data/assets/{$type}/{$name}/{$version}/"
 356:       . $json[ 'versions' ][ $version ][ 'invoke' ];
 357: 
 358:     // Set the message for the Gatekeeper script (if there is one)
 359:     if ( isset( $json[ 'message' ] ) )
 360:       $message = $json[ 'message' ];
 361:     else
 362:       $message = '';
 363: 
 364:     // Double check with the gatekeeper function (doesn't necessarily
 365:     // run the gatekeeper script).
 366:     if ( $this->gatekeeper( $name, $path, $message, $icon ) ) {
 367:       // The utility has been whitelisted, so return the path
 368:       $this->log->log( "Loaded '{$name}' v{$version} of type '{$type}'.",
 369:         'INFO', 'console' );
 370:       return $path;
 371:     } else {
 372:       // The utility has not been whitelisted, so return an error.
 373:       // The gatekeeper function already wrote the error to STDERR.
 374:       return FALSE;
 375:     }
 376: 
 377:     // We shouldn't get here. If we have, then it's a malformed request.
 378:     // Output the error to STDERR.
 379:     $this->log->log( "There is a problem with the __implementation__ of " .
 380:       "the Alfred Bundler when trying to load '{$name}'. Please let the " .
 381:       "workflow author know.", 'ERROR', 'console' );
 382: 
 383:     return FALSE;
 384:   }
 385: 
 386: 
 387: 
 388:   public function icon( $font, $name, $color = '000000', $alter = FALSE ) {
 389:     if ( ! isset( $this->icon ) )
 390:       $this->icon = new AlfredBundlerIcon( $this );
 391: 
 392:     return $this->icon->icon([ 'font' => $font, 'name' => $name, 'color' => $color, 'alter' => $alter ]);
 393:   }
 394: 
 395:   /**
 396:    * Loads a utility
 397:    *
 398:    * @since  Taurus 1
 399:    * @access public
 400:    *
 401:    * @param string $name    Name of utility
 402:    * @param string $version = 'latest' Version of utility
 403:    * @param string $json    = ''        File path to json
 404:    * @return mixed                       Path to utility on success, FALSE on failure
 405:    */
 406:   public function utility( $name, $version = 'latest', $json = '' ) {
 407:     if ( empty( $json ) ) {
 408:       return $this->load( 'utility', $name, $version );
 409:     } else {
 410:       if ( file_exists( $json ) ) {
 411:         return $this->load( 'utility', $name, $version, $json );
 412:       }
 413:     }
 414:     return FALSE;
 415:   }
 416: 
 417:   /**
 418:    * Loads / requires a library
 419:    *
 420:    * @since  Taurus 1
 421:    * @access public
 422:    *
 423:    * @param string $name    Name of library
 424:    * @param string $version = 'latest' Version of library
 425:    * @param string $json    = ''        File path to json
 426:    * @return bool                        TRUE on success, FALSE on failure
 427:    */
 428:   public function library( $name, $version = 'latest', $json = '' ) {
 429:     $dir = "{$this->data}/data/assets/php/{$name}/{$version}";
 430:     if ( file_exists( "{$dir}/invoke" ) ) {
 431:       require_once "{$dir}/" . trim( file_get_contents( "{$dir}/invoke" ) );
 432:       return TRUE;
 433:     } else {
 434:       if ( $this->load( 'php', $name, $version, $json ) ) {
 435:         require_once "{$dir}/" . trim( file_get_contents( "{$dir}/invoke" ) );
 436:         return TRUE;
 437:       } else {
 438:         return FALSE;
 439:       }
 440:     }
 441:   }
 442: 
 443:   /**
 444:    * Loads a wrapper object
 445:    *
 446:    * @param   string   $wrapper  name of wrapper to load
 447:    * @param   boolean  $debug    turn debugging off / on
 448:    *
 449:    * @return  object             a wrapper object
 450:    */
 451:   public function wrapper( $wrapper, $debug = FALSE ) {
 452:     $wrapperPointer = [
 453:         'cocoadialog'=>'cocoaDialog',
 454:         'terminalnotifier'=>'terminal-notifier'
 455:     ];
 456:     $wrappersDir = "{$this->data}/bundler/includes/wrappers/php";
 457:     if ( file_exists( "{$wrappersDir}/{$wrapper}.php" ) ) {
 458:       require_once "{$wrappersDir}/{$wrapper}.php";
 459:       $this->log->log( "Loaded '{$wrapper}' bindings", 'INFO', 'console' );
 460:       return new $wrapper( $this->utility( $wrapperPointer[strtolower( $wrapper )] ), $debug );
 461:     } else {
 462:       $this->log->log( "'{$wrapper}' not found.", 'ERROR', 'console' );
 463:       return 10;
 464:     }
 465:   }
 466: 
 467:   /**
 468:    * Loads / requires composer packages
 469:    *
 470:    * @todo refactor into a composer class
 471:    *
 472:    * @param array $packages An array of packages to load in composer
 473:    * @return bool            True on success, false on failure
 474:    */
 475:   public function composer( $packages ) {
 476:     $composerDir = "{$this->data}/data/assets/php/composer";
 477:     if ( ! file_exists( $composerDir ) )
 478:       mkdir( $composerDir, 0755, TRUE );
 479: 
 480:     if ( ! file_exists( "{$composerDir}/composer.phar" ) ) {
 481:       $this->download( "https://getcomposer.org/composer.phar", "{$composerDir}/composer.phar" ); $line = __LINE__;
 482:       exec( "'{$composerDir}/composer.phar'", $output, $status );
 483:       if ( $status !== 0 ) {
 484:         $this->log->log( "Composer.phar is corrupt. Deleting...", 'CRITICAL', 'console' );
 485:       } else {
 486:         $this->log->log( "Installing Composer.phar to `{$composerDir}`", 'INFO', 'both' );
 487:       }
 488:       // Add check to make sure the that file is complete above...
 489:     }
 490: 
 491:     $install = FALSE;
 492: 
 493:     if ( file_exists( "{$composerDir}/bundles/{$this->bundle}/autoload.php" ) ) {
 494:       if ( file_exists( "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/composer.json" ) ) {
 495:         $installDir = "{$this->cache}/{$this->bundle}/composer";
 496:         if ( ! file_exists( $installDir ) )
 497:           mkdir( "{$installDir}", 0775, TRUE );
 498:         $json = json_encode( array( "require" => $packages ) );
 499:         $json = str_replace( '\/', '/', $json ); // Make sure that the json is valid for composer.
 500:         file_put_contents( "{$installDir}/composer.json", $json );
 501: 
 502:         if ( hash_file( 'md5', "{$installDir}/composer.json" )
 503:           == hash_file( 'md5', "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/composer.json" ) ) {
 504:           $this->log->log( "Loaded Composer packages for {$this->bundle}.",
 505:             'INFO', 'console' );
 506:           require_once(
 507:             "{$composerDir}/bundles/{$this->bundle}/autoload.php" );
 508: 
 509:           return TRUE;
 510:         } else {
 511:           $install = TRUE;
 512:           if ( file_exists( "{$composerDir}/bundles/{$this->bundle}" ) ) {
 513:             $this->rrmdir( "{$composerDir}/bundles/{$this->bundle}" );
 514:           }
 515:         }
 516:       }
 517:     } else {
 518:       $install = TRUE;
 519:     }
 520: 
 521:     if ( $install == TRUE ) {
 522:       if ( is_dir( "{$composerDir}/bundles/{$this->bundle}" ) ) {
 523:         $this->rrmdir( "{$composerDir}/bundles/{$this->bundle}" );
 524:       }
 525:       if ( $this->installComposerPackage( $packages ) === TRUE ) {
 526:         $this->log->log( "Loaded Composer packages for {$this->bundle}.",
 527:           'INFO', 'console' );
 528:         require_once "{$composerDir}/bundles/{$this->bundle}/autoload.php";
 529:         return TRUE;
 530:       } else {
 531:         $this->log->log( "ERROR: failed to install packages for {$this->bundle}", 'ERROR', 'both' );
 532:         return FALSE;
 533:       }
 534:     }
 535:   }
 536: 
 537: 
 538: 
 539:   /**
 540:    * Creates an icns file out of the workflow's 'icon.png' file
 541:    *
 542:    * @return string Path to generated icns file
 543:    */
 544:   public function icns() {
 545: 
 546:     if ( ! file_exists( $this->plist ) )
 547:       return FALSE;
 548:     if ( ( ! isset( $this->bundle ) ) || empty( $this->bundle ) )
 549:       return FALSE;
 550:     if ( ! file_exists(
 551:         realpath( dirname( "{$this->plist}" ) . "/icon.png" ) ) ) {
 552:       return FALSE;
 553:     }
 554: 
 555:     if ( file_exists( "{$this->cache}/icns/{$this->bundle}.icns" ) ) {
 556:       return "{$this->cache}/icns/{$this->bundle}.icns";
 557:     } else {
 558:       $script = realpath( "'" . __DIR__ . '/includes/png_to_icns.sh' . "'" );
 559:       $icon   = realpath( dirname( "{$this->plist}" ) . "/icon.png" );
 560:       exec( "bash '{$script}' '{$icon}' '{$this->bundle}.icns'" );
 561:       if ( file_exists( "{$this->cache}/icns/{$this->bundle}.icns" ) )
 562:         return "{$this->cache}/icns/{$this->bundle}.icns";
 563:       else
 564:         return FALSE;
 565:     }
 566:   }
 567: 
 568:   /******************************************************************************
 569:  * BEGIN INSTALL FUNCTIONS
 570:  *****************************************************************************/
 571: 
 572:   /**
 573:    * Installs an asset based on JSON information
 574:    *
 575:    * @param string $json    File path to json
 576:    * @param string $version = 'latest' Version of asset to install
 577:    * @return bool                        TRUE on success, FALSE on failure
 578:    */
 579:   public function installAsset( $json, $version = 'latest' ) {
 580:     if ( ! file_exists( $json ) ) {
 581:       $this->log->log( "Cannot install asset because the JSON file ('{$json}') is not present.", 'ERROR', 'console' );
 582:       return FALSE;
 583:     }
 584: 
 585:     $json = json_decode( file_get_contents( $json ), TRUE );
 586: 
 587:     // Check to make sure that the JSON is valid.
 588:     if ( $json == null ) {
 589:       $this->log->log( "Cannot install asset because the JSON file ('{$json}') is not valid.", 'ERROR', 'console' );
 590:       return FALSE;
 591:     }
 592: 
 593:     $installDir = "{$this->data}/data/assets/{$json[ 'type' ]}/{$json[ 'name' ]}/{$version}";
 594:     // Make the installation directory if it doesn't exist
 595:     if ( ! file_exists( $installDir ) )
 596:       mkdir( $installDir, 0775, TRUE );
 597: 
 598:     // Make the temporary directory
 599:     $tmpDir = "{$this->cache}/installers";
 600:     if ( ! file_exists( $tmpDir ) )
 601:       mkdir( $tmpDir, 0775, TRUE );
 602: 
 603:     $name = $json[ 'name' ];
 604:     $type = $json[ 'type' ];
 605: 
 606:     // Check to see if the version asked for is in the json; else, fallback to
 607:     // default if exists; if not, throw error.
 608:     if ( ! isset( $json[ 'versions' ][ $version ] ) ) {
 609:       if ( ! isset( $json[ 'versions' ][ 'latest' ] ) ) {
 610:         $this->log->log( "Cannot install {$name} because no version found and cannot fallback to 'latest'.", 'ERROR', 'both');
 611:         return FALSE;
 612:       } else {
 613:         $version = 'latest';
 614:       }
 615:     }
 616:     $invoke  = $json[ 'versions' ][ $version ][ 'invoke' ];
 617:     $install = $json[ 'versions' ][ $version ][ 'install' ];
 618: 
 619:     // Download the file(s).
 620:     foreach ( $json[ 'versions' ][ $version ][ 'files' ] as $url ) {
 621:       $file = pathinfo( parse_url( $url[ 'url' ], PHP_URL_PATH ) );
 622:       $file = $file[ 'basename' ];
 623:       if ( ! $this->download( $url[ 'url' ], "{$tmpDir}/{$file}" ) ) {
 624:         $this->log->log( "Cannot download {$name} at {$url['url']}.", 'ERROR', 'console' );
 625:         return FALSE; // The download failed, for some reason.
 626:       }
 627: 
 628:       // @TODO : Convert these to native PHP functions
 629:       if ( $url[ 'method' ] == 'zip' ) {
 630:         // Unzip the file into the cache directory, silently.
 631:         exec( "unzip -qo '{$tmpDir}/{$file}' -d '{$tmpDir}'" );
 632:       } else if ( $url[ 'method' ] == 'tgz' || $url[ 'method' ] == 'tar.gz' ) {
 633:           // Untar the file into the cache directory, silently.
 634:           exec( "tar xzf '{$tmpDir}/{$file}' -C '{$tmpDir}'" );
 635:         }
 636:     }
 637:     if ( is_array( $install ) ) {
 638:       foreach ( $install as $i ) {
 639:         // Replace the strings in the INSTALL json with the proper values.
 640:         $i = str_replace( "__FILE__" , "{$tmpDir}/$file", $i );
 641:         $i = str_replace( "__CACHE__", "{$tmpDir}/",      $i );
 642:         $i = str_replace( "__DATA__" , "{$installDir}/",  $i );
 643:         exec( $i );
 644:       }
 645:     }
 646:     // Add in the invoke file
 647:     file_put_contents( "{$installDir}/invoke", $invoke );
 648:     $this->log->log( "Installed '{$type}': '{$name}' -- version '{$version}'.", 'INFO', 'both' );
 649:     $this->rrmdir( "{$tmpDir}" );
 650:     return TRUE;
 651:   }
 652: 
 653:   /**
 654:    * Installs composer packages
 655:    *
 656:    * @todo refactor into a composer class
 657:    *
 658:    * @param array $packages List of composer ready packages with versions
 659:    * @return bool            TRUE on success, FALSE on failure
 660:    */
 661:   private function installComposerPackage( $packages ) {
 662:     if ( ! is_array( $packages ) ) {
 663:       // The packages variable needs to be an array
 664:       $this->log->log( "An array must be passed to install Composer assets.", 'ERROR', 'console' );
 665:       return FALSE;
 666:     }
 667: 
 668:     $installDir = "{$this->cache}/{$this->bundle}/composer";
 669: 
 670:     if ( ! file_exists( $installDir ) )
 671:       mkdir( "{$installDir}", 0775, TRUE );
 672: 
 673:     $json = json_encode( array( "require" => $packages ) );
 674:     $json = str_replace( '\/', '/', $json ); // Make sure that the json is valid for composer.
 675:     file_put_contents( "{$installDir}/composer.json", $json );
 676: 
 677:     $cmd = "php '{$this->data}/data/assets/php/composer/composer.phar' install -q -d '{$installDir}'";
 678:     exec( $cmd );
 679:     // Add in error checking to make sure that it worked.
 680: 
 681:     $packages = json_decode( file_get_contents( "{$installDir}/vendor/composer/installed.json" ), TRUE );
 682: 
 683:     // Files to be changed
 684:     $files = array( 'autoload_psr4.php', 'autoload_namespaces.php', 'autoload_files.php', 'autoload_classmap.php' );
 685:     $destination = "{$this->data}/data/assets/php/composer/vendor";
 686:     $installed = array();
 687: 
 688:     foreach ( $packages as $package ) :
 689: 
 690:     $name        = explode( '/', $package[ 'name' ] ); // As: vendor/package
 691:     $vendor      = $name[0];                         // vendor
 692:     $name        = $name[1];                           // package name
 693:     $version     = $package[ 'version' ];           // version installed
 694:     $installed[] = array( 'name' => $name,
 695:       'vendor' => $vendor,
 696:       'version' => $version );
 697: 
 698:     foreach ( $files as $file ) :
 699:       if ( file_exists( "{$installDir}/vendor/composer/{$file}" ) ) {
 700:         $f = file( "{$installDir}/vendor/composer/{$file}" );
 701:         foreach ( $f as $num => $line ) :
 702:           $line = str_replace( '$vendorDir = dirname(dirname(__FILE__));',  "\$vendorDir = '{$this->data}/data/assets/php/composer/vendor';", $line );
 703:           $line = str_replace( '$baseDir = dirname($vendorDir);', "\$baseDir = '{$this->data}/data/assets/php/composer';", $line );
 704:           $line = str_replace( 'array($vendorDir . \'/' . $vendor . '/' . $name, 'array($vendorDir . \'/' . $vendor . '/' . $name . '-' . $version, $line );
 705:           $f[ $num ] = $line;
 706:         endforeach;
 707:         file_put_contents( "{$installDir}/vendor/composer/{$file}",
 708:           implode( '', $f ) );
 709:       }
 710:     endforeach;
 711:     $this->log->log( "Rewrote Composer autoload file for workflow.", 'INFO', 'console' );
 712: 
 713:     if ( ! file_exists( "{$destination}/{$vendor}/{$name}-{$version}" ) ) {
 714:       if ( ! file_exists( "{$destination}/{$vendor}" ) )
 715:         mkdir( "{$destination}/{$vendor}", 0775, TRUE ); // Make the vendor dir if necessary
 716:       rename( "{$installDir}/vendor/{$vendor}/{$name}", "{$destination}/{$vendor}/{$name}-{$version}" );
 717:     }
 718:     endforeach;
 719:     if ( ! file_exists( "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}" ) )
 720:       mkdir( "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}", 0775, TRUE );
 721: 
 722:     if ( ! file_exists( "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/packages.json" ) ) {
 723:       $data = str_replace( '\/', '/', json_encode( $installed ) );
 724:       file_put_contents( "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/packages.json", $data );
 725:     }
 726: 
 727:     rename( "{$installDir}/vendor/composer", "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/composer" );
 728:     rename( "{$installDir}/vendor/autoload.php", "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/autoload.php" );
 729:     file_put_contents( "{$this->data}/data/assets/php/composer/bundles/{$this->bundle}/composer.json", $json );
 730: 
 731:     $this->log->log( "Successfully installed composer packages. Cleaning up....", 'INFO', 'console' );
 732: 
 733:     $this->rrmdir( $installDir );
 734: 
 735:     return TRUE;
 736:   }
 737: 
 738:   /******************************************************************************
 739:  * END INSTALL FUNCTIONS
 740:  *****************************************************************************/
 741: 
 742: 
 743: 
 744:   /*******************************************************************************
 745:  * BEGIN HELPER FUNCTIONS
 746:  ******************************************************************************/
 747: 
 748:   /**
 749:    * Returns bundle id of workflow using bundler
 750:    *
 751:    * @return string Bundle id
 752:    */
 753:   public function bundle() {
 754:     return $this->bundle;
 755:   }
 756: 
 757:   /**
 758:    * Reads a plist value using PlistBuddy
 759:    *
 760:    * @param string $plist File path to plist
 761:    * @param string $key   Key of plist to read
 762:    * @return mixed         FALSE if plist doesn't exist, else value of key
 763:    */
 764:   private function readPlist( $plist, $key ) {
 765:     if ( ! file_exists( $plist ) )
 766:       return FALSE;
 767: 
 768:     return exec( "/usr/libexec/PlistBuddy -c 'Print :{$key}' '{$plist}'" );
 769:   }
 770: 
 771:   /**
 772:    * Wraps a cURL function to download files
 773:    *
 774:    * @param string $url     A URL to the file
 775:    * @param string $file    The destination file
 776:    * @param int   $timeout =  '3' A timeout variable (in seconds)
 777:    * @return bool                   True on success and error code / false on failure
 778:    *
 779:    * @access public
 780:    * @since  Taurus 1
 781:    */
 782:   public function download( $url, $file, $timeout = '5' ) {
 783:     // Check the URL here
 784: 
 785:     // Make sure that the download directory exists
 786:     if ( ! ( file_exists( dirname( $file ) ) && is_dir( dirname( $file ) ) ) )
 787:       return FALSE;
 788: 
 789:     $ch = curl_init( $url );
 790:     $fp = fopen( $file , "w" );
 791: 
 792:     curl_setopt_array( $ch, array(
 793:         CURLOPT_FILE => $fp,
 794:         CURLOPT_HEADER => FALSE,
 795:         CURLOPT_FOLLOWLOCATION => TRUE,
 796:         CURLOPT_CONNECTTIMEOUT => $timeout
 797:       ) );
 798: 
 799: 
 800:     // Curl error codes: http://curl.haxx.se/libcurl/c/libcurl-errors.html
 801:     if ( curl_exec( $ch ) === FALSE ) {
 802:       curl_close( $ch );
 803:       fclose( $fp );
 804:       return curl_error( $ch ); // Under some circumstances, if the file cannot
 805:       // be downloaded, then this will return an error
 806:       // stating that $ch is not a valid cURL resource
 807:     }
 808: 
 809:     $this->log->log( "Downloading `{$url}` ...", 'INFO', 'console' );
 810: 
 811:     curl_close( $ch );
 812:     fclose( $fp );
 813:     return TRUE;
 814:   }
 815: 
 816: 
 817:   /**
 818:    * Log function for the user to employ
 819:    *
 820:    * @param   string  $message        message to log
 821:    * @param   mixed   $level='INFO'   level of the log message
 822:    * @param   string  $destination='' destination ( file, console, both )
 823:    *                                  an empty argument will use the default
 824:    *                                  destination set at instantiation
 825:    *
 826:    * @since Taurus 1
 827:    * @see AlfredBundlerLogger:log
 828:    */
 829:   public function log( $message, $level = 'INFO', $destination = '' ) {
 830:     $this->userLog->log( $message, $level, $destination, 3 );
 831:   }
 832: 
 833:   /**
 834:    * Wraps around 'log' to level 'DEBUG'
 835:    *
 836:    * @param   string  $message        message to log
 837:    * @param   string  $destination='' destination ( file, console, both )
 838:    *                                  an empty argument will use the default
 839:    *                                  destination set at instantiation
 840:    *
 841:    * @since Taurus 1
 842:    * @see AlfredBundlerLogger:log
 843:    * @see AlfredBundlerInternalClass:log
 844:    */
 845:   public function debug( $message, $destination = '' ) {
 846:     $this->userLog->log( $message, 'DEBUG', $destination, 3 );
 847:   }
 848: 
 849:   /**
 850:    * Wraps around 'log' to level 'INFO'
 851:    *
 852:    * @param   string  $message        message to log
 853:    * @param   string  $destination='' destination ( file, console, both )
 854:    *                                  an empty argument will use the default
 855:    *                                  destination set at instantiation
 856:    *
 857:    * @since Taurus 1
 858:    * @see AlfredBundlerLogger:log
 859:    * @see AlfredBundlerInternalClass:log
 860:    */
 861:   public function info( $message, $destination = '' ) {
 862:     $this->userLog->log( $message, 'INFO', $destination, 3 );
 863:   }
 864: 
 865:   /**
 866:    * Wraps around 'log' to level 'WARNING'
 867:    *
 868:    * @param   string  $message        message to log
 869:    * @param   string  $destination='' destination ( file, console, both )
 870:    *                                  an empty argument will use the default
 871:    *                                  destination set at instantiation
 872:    *
 873:    * @since Taurus 1
 874:    * @see AlfredBundlerLogger:log
 875:    * @see AlfredBundlerInternalClass:log
 876:    */
 877:   public function warning( $message, $destination = '' ) {
 878:     $this->userLog->log( $message, 'WARNING', $destination, 3 );
 879:   }
 880: 
 881:   /**
 882:    * Wraps around 'log' to level 'ERROR'
 883:    *
 884:    * @param   string  $message        message to log
 885:    * @param   string  $destination='' destination ( file, console, both )
 886:    *                                  an empty argument will use the default
 887:    *                                  destination set at instantiation
 888:    *
 889:    * @since Taurus 1
 890:    * @see AlfredBundlerLogger:log
 891:    * @see AlfredBundlerInternalClass:log
 892:    */
 893:   public function error( $message, $destination = '' ) {
 894:     $this->userLog->log( $message, 'ERROR', $destination, 3 );
 895:   }
 896: 
 897:   /**
 898:    * Wraps around 'log' to level 'CRITICAL'
 899:    *
 900:    * @param   string  $message        message to log
 901:    * @param   string  $destination='' destination ( file, console, both )
 902:    *                                  an empty argument will use the default
 903:    *                                  destination set at instantiation
 904:    *
 905:    * @since Taurus 1
 906:    * @see AlfredBundlerLogger:log
 907:    * @see AlfredBundlerInternalClass:log
 908:    */
 909:   public function critical( $message, $destination = '' ) {
 910:     $this->userLog->log( $message, 'CRITICAL', $destination, 3 );
 911:   }
 912: 
 913:   /**
 914:    * Wraps around 'log' to send to 'console'
 915:    *
 916:    * @param   string  $message  message to log
 917:    * @param   mixed   $level    level of log
 918:    *
 919:    * @since Taurus 1
 920:    * @see AlfredBundlerLogger:log
 921:    * @see AlfredBundlerInternalClass:log
 922:    */
 923:   public function console( $message, $level = 'INFO' ) {
 924:     $this->userLog->log( $message, $level, 'console', 3 );
 925:   }
 926: 
 927:   /**
 928:    * Recursively removes a folder along with all its files and directories
 929:    *
 930:    * @link http://php.net/manual/en/function.rmdir.php#110489
 931:    *
 932:    * @access public
 933:    * @since  Taurus 1
 934:    *
 935:    * @param string $path Path to directory to remove
 936:    */
 937:   public function rrmdir( $path ) {
 938:     // Open the source directory to read in files
 939:     $files = array_diff( scandir( $path ), array( '.', '..' ) );
 940:     foreach ( $files as $file ) {
 941:       if ( is_dir( "{$path}/{$file}" ) )
 942:         $this->rrmdir( "{$path}/{$file}" );
 943:       else if ( file_exists( "{$path}/{$file}" ) )
 944:         unlink( "{$path}/{$file}" );
 945:       else {
 946:         echo "Removing dir error... uh..." . PHP_EOL; // add in proper logging
 947:       }
 948:     }
 949:     return rmdir( $path );
 950:   }
 951: 
 952:   /**
 953:    * Queries Gatekeeper to whitelist apps
 954:    *
 955:    * Invokes the Gatekeeper script if the path has not already been called. The
 956:    * call, if successful, is cached. If the cache file is present, then return
 957:    * the path from there instead of calling the cache again.
 958:    *
 959:    * @access public
 960:    * @since  Taurus 1
 961:    *
 962:    * @param string $name    The name of the utility
 963:    * @param string $path    The fullpath to the utility
 964:    * @param string $message The "permissions" message from the JSON
 965:    * @param string $icon    The workflow icon file (if exists)
 966:    * @return mixed           FALSE on failure, path to utility on success
 967:    */
 968:   public function gatekeeper( $name, $path, $message = '', $icon = '' ) {
 969: 
 970:     $assetCache = "{$this->cache}/utilities";
 971: 
 972:     // Make sure the directory exists
 973:     if ( ! ( ( file_exists( $assetCache ) && is_dir( $assetCache ) ) ) )
 974:       mkdir( $assetCache, 0775, TRUE );
 975: 
 976:     // Cache path for this call
 977:     $key       = md5( "{$name}-{$version}-{$type}-{$json}" );
 978:     $cachePath =      "{$assetCache}/{$key}";
 979: 
 980:     if ( file_exists( "$cachePath" ) ) {
 981:       $path = file_get_contents( $cachePath );
 982:       if ( file_exists( $path ) ) {
 983:         // The cache has been found, and we have the asset installed already.
 984:         return $path;
 985:       }
 986:     }
 987: 
 988:     // If we're here, then we need to run the Gatekeeper script
 989: 
 990:     // Path to gatekeeper script
 991:     $gatekeeper = realpath( __DIR__ ) . '/includes/gatekeeper.sh';
 992:     // Execute the Gatekeeper script
 993:     exec( "bash '{$gatekeeper}' '{$name}' '{$path}' '{$message}' '{$icon}' '{$this->bundle}'", $output, $status );
 994: 
 995:     // If the previous call returns a successful status code, then cache
 996:     // the path and return it. Else, move to failure.
 997:     if ( $status == 0 ) {
 998:       file_put_contents( $cachePath, $path );
 999:       return $path;
1000:     }
1001: 
1002:     // There was an error with the Gatekeeper script (exited with a non-zero
1003:     // status).
1004: 
1005:     // Output the error to STDERR.
1006:     $this->log->log( "Bundler Error: '{$name}' is needed to properly run " .
1007:       "this workflow, and it must be whitelisted for Gatekeeper. You " .
1008:       "either denied the request, or another error occured with " .
1009:       " the Gatekeeper script.", 'ERROR', 'both' );
1010: 
1011:     // So return FALSE as failure.
1012:     return FALSE;
1013:   }
1014: 
1015:   /**
1016:    * Registers an asset
1017:    *
1018:    * The Bundler keeps a registry of which workflows use which assets.
1019:    *
1020:    * @access public
1021:    * @since  Taurus 1
1022:    *
1023:    * @param string $asset   Name of the asset to be registered
1024:    * @param string $version Version of asset to use
1025:    * @return bool             Returns TRUE on success, FALSE on failure
1026:    */
1027:   public function register( $asset, $version ) {
1028: 
1029:     // We need the bundle to be set if we are to register the asset
1030:     if ( ( ! isset( $this->bundle ) ) || empty( $this->bundle ) )
1031:       return FALSE;
1032: 
1033:     // Load the registry data
1034:     if ( ! file_exists( "{$this->data}/data/registry.json" ) ) {
1035:       $registry = array();
1036:     } else {
1037:       if ( ! ( json_decode( file_get_contents( "{$this->data}/data/registry.json" ) ) ) ) {
1038:         // The JSON file is bad -- start over.
1039:         $registry = array();
1040:       } else {
1041:         $registry = json_decode( file_get_contents( "{$this->data}/data/registry.json" ), TRUE );
1042:       }
1043:     }
1044: 
1045:     if ( isset( $registry[ $asset ] ) ) {
1046:       if ( ! array_key_exists( $version , $registry[ $asset ] ) ) {
1047:         $registry[ $asset ][ $version ] = array();
1048:         $update = TRUE;
1049:       }
1050:       if ( ! is_array( $registry[ $asset ][ $version] ) ) {
1051:         $registry[ $asset ][ $version ] = array();
1052:       }
1053:       if ( ! in_array( $this->bundle , $registry[ $asset ][ $version ] ) ) {
1054:         $registry[ $asset ][ $version ][] = $this->bundle;
1055:         $update = TRUE;
1056:       }
1057:     } else {
1058:       $registry[ $asset ] = array( $version => $this->bundle );
1059:       $update = TRUE;
1060:     }
1061: 
1062:     if ( $update ) file_put_contents( "{$this->data}/data/registry.json" , utf8_encode( json_encode( $registry ) ) );
1063: 
1064:     return TRUE;
1065:   }
1066: 
1067:   /****************************************************************************
1068:    * END HELPER FUNCTIONS
1069:    ***************************************************************************/
1070: 
1071:   /****************************************************************************
1072:    * BEGIN BONUS FUNCTIONS
1073:    ***************************************************************************/
1074: 
1075:   /**
1076:    * Uses CocoaDialog to display a notification
1077:    *
1078:    * @param string $title   Title for notification
1079:    * @param string $message Message of notification
1080:    * @param string $icon = null An array of options that Terminal Notifer takes
1081:    * @return bool                      TRUE on success, FALSE on failure
1082:    */
1083:   public function notify( $title, $message, $icon = null ) {
1084:     if ( gettype( $title ) !== 'string' || gettype( $message ) !== 'string' )
1085:       return false;
1086: 
1087:     $client = $this->wrapper( 'CocoaDialog' );
1088:     $icon_type = 'icon';
1089:     if ( ( ! is_null( $icon ) ) && ( gettype( $icon ) === 'string' ) ) {
1090:       if ( ! file_exists( $icon ) ) {
1091:         if ( ! in_array( $icon, $client->global_icons ) ) {
1092:           $icon_type = null;
1093:         }
1094:       } else {
1095:         $icon_type = 'icon_file';
1096:       }
1097:     } else {
1098:       $icon_type = null;
1099:     }
1100:     $notification = [
1101:       'title'=>$title,
1102:       'description'=>$message,
1103:       'alpha'=>1,
1104:       'background_top'=>'ffffff',
1105:       'background_bottom'=>'ffffff',
1106:       'border_color'=>'ffffff',
1107:       'text_color'=>'000000',
1108:       'no_growl'=>true,
1109:     ];
1110:     if ( ! is_null( $icon_type ) ) {
1111:       $notification[$icon_type] = $icon;
1112:     }
1113:     $client->notify( $notification );
1114:     return true;
1115: 
1116:    }
1117: 
1118:   /****************************************************************************
1119:    * END BONUS FUNCTIONS
1120:    ***************************************************************************/
1121: 
1122: }
1123: endif;
1124: 
1125: 
Alfred Bundler API documentation generated by ApiGen 2.8.0