1 <?php
2 /**
3 * Contains wrapper class for Alphred, the main entry point to use the library
4 *
5 * PHP version 5
6 *
7 * @package Alphred
8 * @copyright Shawn Patrick Rice 2014
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 /**
20 * Wrapper Class.
21 *
22 * This provides a simple wrapper for all of the important parts of the Alphred library.
23 * It also simplifies the usage of some of the internal components, so calls to this class
24 * do not always mirror calls to the internal components.
25 *
26 */
27 class Alphred {
28
29 /**
30 * Initializes the wrapper object
31 *
32 * @param array $options options that can be configured
33 * currently, only two options are available:
34 * 1. error_on_empty - displays a script filter item when empty
35 * 2. no_filter - initializes object without a script filter
36 * 3. no_config - creates without a config item
37 * 4. config_filename - sets filename for the config (default: `config`)
38 * 5. config_handler - sets the handler for the config (default: `ini`)
39 * @param array|boolean $plugins plugins to be run at load
40 */
41 public function __construct( $options = [ 'error_on_empty' => true ] ) {
42
43 // We're going to parse the ini file (if it exists) and just merge what we find there
44 // with the $options array. The original $options array will override the workflow.ini file.
45 $options = array_merge( $options, $this->parse_ini_file() );
46
47 // Create a script filter object unless explicitly turned off
48 if ( ! isset( $options[ 'no_filter' ] ) || true !== $options[ 'no_filter' ] ) {
49 $this->filter = new Alphred\ScriptFilter( $options );
50 }
51
52 if ( ! isset( $options[ 'no_config' ] ) || true !== $options[ 'no_config' ] ) {
53 // Use `ini` as the default handler and `config` as the default filename
54 $handler = ( isset( $options['config_handler'] ) ) ? $options['config_handler'] : 'ini';
55 $filename = ( isset( $options['config_filename'] ) ) ? $options['config_filename'] : 'config';
56 // Create the config object
57 $this->config = new Alphred\Config( $handler, $filename );
58 }
59 }
60
61 /**
62 * Reads the `workflow.ini` file if it exists
63 *
64 * @return array an array of config values
65 */
66 private function parse_ini_file() {
67 // If the file does not exist, then exit early with an empty array
68 if ( ! file_exists( 'workflow.ini' ) ) {
69 return [];
70 }
71
72 // Read the ini file
73 $ini = Alphred\Ini::read_ini( 'workflow.ini' );
74
75 // Just return the alphred bit
76 return $ini['alphred'];
77
78 }
79
80 /**
81 * Execute a php script in the background
82 *
83 * @todo Work on argument escaping
84 *
85 * @param string $script path to php script
86 * @param mixed $args args to pass to the script
87 */
88 public function background( $script, $args = false ) {
89 // Make sure that the script
90 if ( ! file_exists( $script ) ) {
91 // File does not exist, so throw an exception
92 throw new Alphred\FileDoesNotExist( "Script `{$script}` does not exist.", 4 );
93 }
94 if ( $args ) {
95 if ( is_array( $args ) ) {
96 // Turn $args into a string if we were passed an array
97 $args = implode( "' '", $args );
98 // prepend and append the extra quotation marks... everything *should* be quoted now
99 $args = "'{$args}'";
100 } else {
101 // Quote args if it is a string
102 $args = "'{$args}'";
103 }
104 // Let's escape double-quotes
105 $args = str_replace( '"', '\"', $args );
106 }
107 // Set a variable to let us know that we're in the background, and execute the script
108 exec( "ALPHRED_IN_BACKGROUND=1 /usr/bin/nohup php '{$script}' {$args} >/dev/null 2>&1 &", $output, $return );
109 }
110
111 /**
112 * Tells you whether or not a script is running in the background
113 *
114 * @since 1.0.0
115 *
116 * @return boolean true if in the background; false if not
117 */
118 public function is_background() {
119 return Alphred\Globals::is_background();
120 }
121
122 /**
123 * Calls an Alfred External Trigger
124 *
125 * Single and double-quotes in the argument might break this method, so make sure that you
126 * escape them appropriately.
127 *
128 * @since 1.0.0
129 * @uses Alphred\Alfred::call_external_trigger()
130 *
131 * @param string $bundle the bundle id of the workflow to trigger
132 * @param string $trigger the name of the trigger
133 * @param string|boolean $argument an argument to pass
134 */
135 public function call_external_trigger( $bundle, $trigger, $argument = false ) {
136 Alphred\Alfred::call_external_trigger( $bundle, $trigger, $argument );
137 }
138
139 /**
140 * Tells you if the current theme is `light` or `dark`
141 *
142 * @uses Alphred\Alfred::light_or_dark()
143 * @return string either 'light' or 'dark'
144 */
145 public function theme_background() {
146 return Alphred\Alfred::light_or_dark();
147 }
148
149
150 /**
151 * Filters an array based on a query
152 *
153 * Passing an empty query ($needle) to this method will simply return the initial array.
154 * If you have `fold` on, then this will fail on characters that cannot be translitered
155 * into regular ASCII, so most Asian languages.
156 *
157 * The options to be set are:
158 * * max_results -- the maximum number of results to return (default: false)
159 * * min_score -- the minimum score to return (0-100) (default: false)
160 * * return_score -- whether or not to return the score along with the results (default: false)
161 * * fold -- whether or not to fold diacritical marks, thus making
162 * `über` into `uber`. (default: true)
163 * * match_type -- the type of filters to run. (default: MATCH_ALL)
164 *
165 * The match_type is defined as constants, and so you can call them by the flags or by
166 * the integer value. Options:
167 * Match items that start with the query
168 * 1: MATCH_STARTSWITH
169 * Match items whose capital letters start with ``query``
170 * 2: MATCH_CAPITALS
171 * Match items with a component "word" that matches ``query``
172 * 4: MATCH_ATOM
173 * Match items whose initials (based on atoms) start with ``query``
174 * 8: MATCH_INITIALS_STARTSWITH
175 * Match items whose initials (based on atoms) contain ``query``
176 * 16: MATCH_INITIALS_CONTAIN
177 * Combination of MATCH_INITIALS_STARTSWITH and MATCH_INITIALS_CONTAIN
178 * 24: MATCH_INITIALS
179 * Match items if ``query`` is a substring
180 * 32: MATCH_SUBSTRING
181 * Match items if all characters in ``query`` appear in the item in order
182 * 64: MATCH_ALLCHARS
183 * Combination of all other ``MATCH_*`` constants
184 * 127: MATCH_ALL
185 *
186 * @param array $haystack the array of items to filter
187 * @param string $needle the search query to filter against
188 * @param string|boolean $key the name of the key to filter on if array is associative
189 * @param array $options a list of options to configure the filter
190 * @return array an array of filtered items
191 */
192 public function filter( $haystack, $needle, $key = false, $options = [] ) {
193 return Alphred\Filter::Filter( $haystack, $needle, $key, $options );
194 }
195
196
197
198 /*****************************************************************************
199 * Wrapper methods for script filters
200 ****************************************************************************/
201
202 /**
203 * Adds a result to the script filter
204 *
205 * @since 1.0.0
206 *
207 * @param array $item an array of values to parse that construct an Alphred\Result object
208 */
209 public function add_result( $item ) {
210 return $this->filter->add_result( new \Alphred\Result( $item ) );
211 }
212
213 /**
214 * Prints the script filter XML
215 *
216 * @since 1.0.0
217 *
218 * @return mixed
219 */
220 public function to_xml() {
221 $this->filter->to_xml();
222 }
223
224 /*****************************************************************************
225 * Wrapper methods for requests ( GET / POST )
226 ****************************************************************************/
227
228 /**
229 * Makes a `GET` Request
230 *
231 * This method is good for simple `GET` requests. By default, requests are cached for
232 * 600 seconds (ten minutes), and all options are passed via the `$options` array. Here
233 * are the options:
234 * params (array as $key => $value)
235 * auth (array as [ username, password ] )
236 * user_agent (string)
237 * headers (array as list of headers to add)
238 *
239 * Set only the options that you need.
240 *
241 * To turn caching off, just set `$cache_ttl` to 0.
242 *
243 * The `$cache_bin` is the subfolder within the workflow's cache folder. If set to `true`,
244 * then the cache bin will be named after the hostname of the URL. So, if you are requesting
245 * something from `http://api.github.com/v3/shawnrice/repos`, the `cache bin` would be
246 * `api.github.com`. If you were requesting `http://www.google.com`, then the `cache bin`
247 * would be `www.google.com`.
248 *
249 * @uses Alphred\Request
250 *
251 * @param string $url the URL
252 * @param array|boolean $options an array of options for the request
253 * @param integer $cache_ttl cache time to live in seconds
254 * @param string|boolean $cache_bin cache bin
255 * @return string the results
256 */
257 public function get( $url, $options = false, $cache_ttl = 600, $cache_bin = true ) {
258 $request = $this->create_request( $url, $options, $cache_ttl, $cache_bin, 'get' );
259 return $request->execute();
260 }
261
262 /**
263 * Makes a `POST` request
264 *
265 * @see Alphred::get() See `get()` for details. The method is basically the same.
266 *
267 * @uses Alphred\Request
268 *
269 * @param string $url [description]
270 * @param array|boolean $options an array of options for the request
271 * @param integer $cache_ttl cache time to live in seconds
272 * @param string|boolean $cache_bin cache bin
273 * @return string the results
274 */
275 public function post( $url, $options = false, $cache_ttl = 600, $cache_bin = true ) {
276 $request = $this->create_request( $url, $options, $cache_ttl, $cache_bin, 'post' );
277 return $request->execute();
278 }
279
280 /**
281 * Creates a request object
282 *
283 * @param string $url the URL
284 * @param array|boolean $options an array of options for the request
285 * @param integer $cache_ttl cache time to live in seconds
286 * @param string|boolean $cache_bin cache bin
287 * @param string $type either `get` or `post`
288 * @return Alphred\Request the request object
289 */
290 private function create_request( $url, $options, $cache_ttl, $cache_bin, $type ) {
291
292 if ( $cache_ttl > 0 ) {
293 // Create an object with caching on
294 $request = new Alphred\Request( $url, [ 'cache' => true,
295 'cache_ttl' => $cache_ttl,
296 'cache_bin' => $cache_bin ] );
297 } else {
298 // Create an object with caching off
299 $request = new Alphred\Request( $url, [ 'cache' => false ] );
300 }
301 // Set it to `POST` if that's what they want
302 if ( 'post' == $type ) {
303 $request->use_post();
304 }
305 // If there are options, then go through them and set everything
306 if ( $options ) {
307 if ( isset( $options['params'] ) ) {
308 if ( ! is_array( $options['params'] ) ) {
309 throw new Alphred\Exception( 'Parameters must be passed as an array', 4 );
310 }
311 // Add the parameters
312 $request->add_parameters( $options['params'] );
313 }
314 // For basic http authentication
315 if ( isset( $options['auth'] ) ) {
316 // Make sure that there are two options in the auth array
317 if ( ! is_array( $options['auth'] ) || ( 2 !== count( $options['auth'] ) ) ) {
318 throw new Alphred\Exception( 'You need two arguments in the auth array.', 4 );
319 }
320 // Set the options
321 $request->set_auth( $options['auth'][0], $options['auth'][1] );
322 }
323 // If we need a user agent
324 if ( isset( $options['user_agent'] ) ) {
325 // Make sure that the user agent is a string
326 if ( ! is_string( $options['user_agent'] ) ) {
327 // It's not, so throw an exception
328 throw new Alphred\Exception( 'The user agent must be a string', 4 );
329 }
330 // Set the user agent
331 $request->set_user_agent( $options['user_agent'] );
332 }
333 // If we need to add headers
334 if ( isset( $options['headers'] ) ) {
335 if ( ! is_array( $options['headers'] ) ) {
336 throw new Alphred\Exception( 'Headers must be passed as an array', 4 );
337 } else {
338 $request->set_headers( $options['headers'] );
339 }
340 }
341 }
342 return $request;
343 }
344
345 /**
346 * Clears a cache bin
347 *
348 * Clears a cache bin. If you send it with no argument (i.e.: `$bin = false`), then
349 * it will attempt to clear the workflow's cache directory. Note: this will throw an
350 * exception if it encounters a sub-directory. While it would be easy to make this
351 * function clear sub-directories, it shouldn't. If you are storing data other than responses
352 * in your cache directory, then use a cache-bin with the requests.
353 *
354 * @since 1.0.0
355 * @throws Alphred\Exception when encountering a sub-directory
356 * @uses Alphred\Request::clear_cache()
357 *
358 * @param string|boolean $bin the cache bin to clear
359 * @return null
360 */
361 public function clear_cache( $bin = false ) {
362 return Alphred\Request::clear_cache( $bin );
363 }
364
365
366 /*****************************************************************************
367 * Config functionality
368 ****************************************************************************/
369
370 /**
371 * Reads a configuration value
372 *
373 * @param string $key name of key
374 * @return mixed the value of the key or null if not set
375 */
376 public function config_read( $key ) {
377 try {
378 // Try to read it, and catch the exception if it is not set
379 return $this->config->read( $key );
380 } catch ( Alphred\ConfigKeyNotSet $e ) {
381 // There is nothing, so return null
382 return null;
383 }
384 }
385
386 /**
387 * Sets a configuration value
388 *
389 * @param string $key the name of the key
390 * @param mixed $value the value for the key
391 */
392 public function config_set( $key, $value ) {
393 $this->config->set( $key, $value );
394 }
395
396 /**
397 * Deletes a config value
398 *
399 * @param string $key name of the key
400 */
401 public function config_delete( $key ) {
402 $this->config->delete( $key );
403 }
404
405 /**
406 * Sends a system notification
407 *
408 * Use this for async notifications or when running code in the background. If you want
409 * regular "end-of-workflow" notifications, then use Alfred's built-in set.
410 *
411 * Since this uses AppleScript notifications, all of them will, unfortunately, have the
412 * icon for Script Editor in them, and this is not replaceable. If you want more control
413 * over your notifications, then use something like CocoaDialog or Terminal-Notifier.
414 *
415 * @since 1.0.0
416 * @uses Alphred\Notification::notify()
417 * @todo Check that return value is correct
418 * @see Alphred\Notification::notify() For more information on how to call with the correct options.
419 *
420 * @param array $options the list of options to construct the notification
421 * @return boolean success
422 */
423 public function send_notification( $options ) {
424 return \Alphred\Notification::notify( $options );
425 }
426
427
428 /*****************************************************************************
429 * Keychain Wrapper functions
430 ****************************************************************************/
431
432 /**
433 * Gets a password from the keychain
434 *
435 * @uses \Alphred\Keychain::find_password()
436 *
437 * @param string $account the name of the account (key) for the password
438 * @return string|boolean the password or false if not found
439 */
440 public function get_password( $account ) {
441 // \Alphred\Keychain::find_password throws an exception when the password does not
442 // exist. This wrapper returns false if the password has not been found.
443 try {
444 return \Alphred\Keychain::find_password( $account, null );
445 } catch ( \Alphred\PasswordNotFound $e ) {
446 \Alphred\Log::console( "No password for account `{$account}` was found. Returning false.", 2 );
447 return false;
448 }
449 }
450
451 /**
452 * Deletes a password from the keychain
453 *
454 * @uses \Alphred\Keychain::delete_password()
455 *
456 * @param string $account the name of the account (key) for the password
457 * @return boolean true if it existed and was deleted, false if it didn't exist
458 */
459 public function delete_password( $account ) {
460 return \Alphred\Keychain::delete_password( $account, null );
461 }
462
463 /**
464 * Saves a password to the keychain
465 *
466 * @uses \Alphred\Keychain::save_password()
467 *
468 * @param string $account the name of the account (key) for the password
469 * @param string $password the password
470 * @return boolean
471 */
472 public function save_password( $account, $password ) {
473 return \Alphred\Keychain::save_password( $account, $password, true, null );
474 }
475
476 /**
477 * Creates an AppleScript dialog to enter a password securely
478 *
479 * Note: this will return 'canceled' if the user presses the 'cancel' button
480 *
481 * @uses Alphred\Dialog
482 *
483 * @param string|boolean $text the text for the dialog
484 * @param string|boolean $title the title of the dialog; defaults to the workflow name
485 * @param string|boolean $icon An icon to use with the dialog box
486 * @return string the result of the user-input
487 */
488 public function get_password_dialog( $text = false, $title = false, $icon = false ) {
489 // Set the default text
490 if ( ! $text ) {
491 $text = 'Please enter the password.';
492 }
493 // Set the default title to be that of the workflow's name
494 if ( ! $title ) {
495 $title = \Alphred\Globals::get( 'alfred_workflow_name' );
496 }
497 // Create hidden answer AppleScript dialog
498 $dialog = new \Alphred\Dialog([
499 'text' => $text,
500 'title' => $title,
501 'default_answer' => '',
502 'hidden_answer' => true
503 ]);
504 // If there was an icon, then set it
505 if ( $icon ) {
506 $dialog->set_icon( $icon );
507 }
508 // Execute the dialog and return the result
509 return $dialog->execute();
510 }
511
512 /*****************************************************************************
513 * Logging Functions
514 ****************************************************************************/
515
516 /**
517 * Sends a log message to the console
518 *
519 * If the log level is set higher than the level that this function is called with,
520 * then nothing will happen.
521 *
522 * @see \Alphred\Log::console() More information on the console log
523 * @uses \Alphred\Log
524 *
525 * @param string $message the message to log
526 * @param string|integer $level the log level
527 * @param integer|boolean $trace how far to go in the stacktrace. Defaults to the last level.
528 * @return mixed default returns nothing
529 */
530 public function console( $message, $level = 'INFO', $trace = false ) {
531 \Alphred\Log::console( $message, $level, $trace );
532 }
533
534
535 /**
536 * Writes a log message to a log file
537 *
538 * @see \Alphred\Log::file() More information on the file log
539 * @uses \Alphred\Log
540 *
541 * @param string $message message to log
542 * @param string|int $level log level
543 * @param string $filename filename with no extension
544 * @param boolean|int $trace how far back to trace
545 */
546 public function log( $message, $level = 'INFO', $filename = 'workflow', $trace = false ) {
547 \Alphred\Log::file( $message, $level, $filename, $trace );
548 }
549
550 /*****************************************************************************
551 * Text Processing Filters
552 ****************************************************************************/
553
554 /**
555 * Takes a unix epoch time and renders it as a string
556 *
557 * This also works for future times. If you set `$words` to `true`, then you will
558 * get "one" instead of "1". Past times are appended with "ago"; future times are
559 * prepended with "in ".
560 *
561 * @param integer $seconds unix epoch time value
562 * @param boolean $words whether to use words or numerals
563 * @return string
564 */
565 public function time_ago( $seconds, $words = false ) {
566 return Alphred\Date::ago( $seconds, $words );
567 }
568
569 /**
570 * Takes a time and gives you a fuzzy description of when it is/was relative to now
571 *
572 * So, something like "5 days, 16 hours, and 34 minutes ago" turns into "almost a week ago";
573 * Something like "16 hours from now" turns into "yesterday"; and something like "1 month from now"
574 * turns into "in a month"; it's fuzzy. Also, the first strings need to be a unix epoch time,
575 * so the number of seconds since 1 Jan, 1970 12:00AM.
576 *
577 * @param int $seconds a unix epoch time
578 * @return string a string that represents an approximate time
579 */
580 public function fuzzy_time_diff( $seconds ) {
581 return Alphred\Date::fuzzy_ago( $seconds );
582 }
583
584 /**
585 * Implodes an array into a string with commas (uses an Oxford comma)
586 *
587 * If you set `$suffix` to `true`, then the function expects an associative array
588 * as 'suffix' => 'word', so an array like:
589 * ````php
590 * $list = [ 'penny' => 'one', 'quarters' => 'three', 'dollars' => 'five' ];
591 * ````
592 * will render as: "one penny, three quarters, and five dollars"
593 *
594 * @param array $list the array to add commas to
595 * @param boolean $suffix whether or not there is a suffix
596 * @return string the array, but as a string with commas
597 */
598 public function add_commas( $list, $suffix = false ) {
599 return \Alphred\Text::add_commas_to_list( $list, $suffix );
600 }
601
602 /*****************************************************************************
603 * AppleScript Actions
604 ****************************************************************************/
605
606 /**
607 * Activates an application
608 *
609 * Brings an application to the front, launching it if necessary
610 *
611 * @param string $application the name of the application
612 */
613 public function activate( $application ) {
614 Alphred\AppleScript::activate( $application );
615 }
616
617 /**
618 * Gets the active window
619 *
620 * @return array an array of [ 'app_name' => $name, 'window_name' => $name ]
621 */
622 public function get_active_window() {
623 return Alphred\AppleScript::get_front();
624 }
625
626 /**
627 * Brings an application to the front
628 *
629 * This is like `activate`, but it does not open the application if it is
630 * not already open.
631 *
632 * @param string $application the name of an application
633 */
634 public function bring_to_front( $application ) {
635 Alphred\AppleScript::bring_to_front( $application );
636 }
637
638
639
640 }