]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/functions-plugins.php
Add plain API format
[Github/YOURLS.git] / includes / functions-plugins.php
1 <?php
2
3 /**
4  * The filter/plugin API is located in this file, which allows for creating filters
5  * and hooking functions, and methods. The functions or methods will be run when
6  * the filter is called.
7  *
8  * Any of the syntaxes explained in the PHP documentation for the
9  * {@link http://us2.php.net/manual/en/language.pseudo-types.php#language.types.callback 'callback'}
10  * type are valid.
11  *
12  * This API is heavily inspired by the one I implemented in Zenphoto 1.3, which was heavily inspired by the one used in WordPress.
13  *
14  * @author Ozh
15  * @since 1.5
16  */
17
18 if( !isset( $yourls_filters ) )
19     $yourls_filters = array();
20 /* This global var will collect filters with the following structure:
21  * $yourls_filters['hook']['array of priorities']['serialized function names']['array of ['array (functions, accepted_args, filter or action)]']
22  */
23
24 if( !isset( $yourls_actions ) )
25     $yourls_actions = array();
26 /* This global var will collect 'done' actions with the following structure:
27  * $yourls_actions['hook'] => number of time this action was done
28  */
29
30 /**
31  * Registers a filtering function
32  * 
33  * Typical use:
34  *              yourls_add_filter('some_hook', 'function_handler_for_hook');
35  *
36  * @global array $yourls_filters Storage for all of the filters
37  * @param string $hook the name of the YOURLS element to be filtered or YOURLS action to be triggered
38  * @param callback $function_name the name of the function that is to be called.
39  * @param integer $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default=10, lower=earlier execution, and functions with the same priority are executed in the order in which they were added to the filter)
40  * @param int $accepted_args optional. The number of arguments the function accept (default is the number provided).
41  * @param string $type
42  */
43 function yourls_add_filter( $hook, $function_name, $priority = 10, $accepted_args = NULL, $type = 'filter' ) {
44         global $yourls_filters;
45         // At this point, we cannot check if the function exists, as it may well be defined later (which is OK)
46         $id = yourls_filter_unique_id( $hook, $function_name, $priority );
47         
48         $yourls_filters[ $hook ][ $priority ][ $id ] = array(
49                 'function'      => $function_name,
50                 'accepted_args' => $accepted_args,
51                 'type'          => $type,
52         );
53 }
54
55 /**
56  * Hooks a function on to a specific action.
57  *
58  * Actions are the hooks that YOURLS launches at specific points
59  * during execution, or when specific events occur. Plugins can specify that
60  * one or more of its PHP functions are executed at these points, using the
61  * Action API.
62  *
63  * @param string $hook The name of the action to which the $function_to_add is hooked.
64  * @param callback $function_name The name of the function you wish to be called.
65  * @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
66  * @param int $accepted_args optional. The number of arguments the function accept (default 1).
67  */
68 function yourls_add_action( $hook, $function_name, $priority = 10, $accepted_args = 1 ) {
69         return yourls_add_filter( $hook, $function_name, $priority, $accepted_args, 'action' );
70 }
71
72
73
74 /**
75  * Build Unique ID for storage and retrieval.
76  *
77  * Simply using a function name is not enough, as several functions can have the same name when they are enclosed in classes.
78  *
79  * @global array $yourls_filters storage for all of the filters
80  * @param string $hook hook to which the function is attached
81  * @param string|array $function used for creating unique id
82  * @param int|bool $priority used in counting how many hooks were applied.  If === false and $function is an object reference, we return the unique id only if it already has one, false otherwise.
83  * @return string unique ID for usage as array key
84  */
85 function yourls_filter_unique_id( $hook, $function, $priority ) {
86         global $yourls_filters;
87
88         // If function then just skip all of the tests and not overwrite the following.
89         if ( is_string( $function ) ) {
90                 return $function;
91     }
92     
93         if( is_object($function) ) {
94                 // Closures are currently implemented as objects
95                 $function = array( $function, '' );
96         } else {
97                 $function = (array) $function;
98         }
99
100         // Object Class Calling
101         if ( is_object( $function[0] ) ) {
102         return spl_object_hash( $function[0] ) . $function[1];
103         }
104     
105     // Static Calling
106     if ( is_string( $function[0] ) ) {
107                 return $function[0]. '::' .$function[1];
108     }
109
110 }
111
112 /**
113  * Performs a filtering operation on a YOURLS element or event.
114  *
115  * Typical use:
116  *
117  *              1) Modify a variable if a function is attached to hook 'yourls_hook'
118  *              $yourls_var = "default value";
119  *              $yourls_var = yourls_apply_filter( 'yourls_hook', $yourls_var );
120  *
121  *              2) Trigger functions is attached to event 'yourls_event'
122  *              yourls_apply_filter( 'yourls_event' );
123  *      (see yourls_do_action() )
124  * 
125  * Returns an element which may have been filtered by a filter.
126  *
127  * @global array $yourls_filters storage for all of the filters
128  * @param string $hook the name of the YOURLS element or action
129  * @param mixed $value the value of the element before filtering
130  * @return mixed
131  */
132 function yourls_apply_filter( $hook, $value = '' ) {
133         global $yourls_filters;
134         if ( !isset( $yourls_filters[ $hook ] ) )
135                 return $value;
136         
137         $args = func_get_args();
138         
139         // Sort filters by priority
140         ksort( $yourls_filters[ $hook ] );
141         
142         // Loops through each filter
143         reset( $yourls_filters[ $hook ] );
144         do {
145                 foreach( (array) current( $yourls_filters[ $hook ] ) as $the_ ) {
146                         if ( !is_null( $the_['function'] ) ){
147                                 $args[1] = $value;
148                                 $count = $the_['accepted_args'];
149                                 if ( is_null( $count ) ) {
150                                         $_value = call_user_func_array( $the_['function'], array_slice( $args, 1 ) );
151                                 } else {
152                                         $_value = call_user_func_array( $the_['function'], array_slice( $args, 1, (int) $count ) );
153                                 }
154                         }
155                         if( $the_['type'] == 'filter' )
156                                 $value = $_value;
157                 }
158
159         } while ( next( $yourls_filters[ $hook ] ) !== false );
160         
161         if( $the_['type'] == 'filter' )
162                 return $value;
163 }
164
165 /**
166  * Performs an action triggered by a YOURLS event.
167
168  * @param string $hook the name of the YOURLS action
169  * @param mixed $arg action arguments
170  */
171 function yourls_do_action( $hook, $arg = '' ) {
172         global $yourls_actions;
173         
174         // Keep track of actions that are "done"
175         if ( !isset( $yourls_actions ) )
176                 $yourls_actions = array();
177         if ( !isset( $yourls_actions[ $hook ] ) ) {
178                 $yourls_actions[ $hook ] = 1;
179     } else {
180                 ++$yourls_actions[ $hook ];
181     }
182
183         $args = array();
184         if ( is_array( $arg ) && 1 == count( $arg ) && isset( $arg[0] ) && is_object( $arg[0] ) ) // array(&$this)
185                 $args[] =& $arg[0];
186         else
187                 $args[] = $arg;
188         for ( $a = 2; $a < func_num_args(); $a++ )
189                 $args[] = func_get_arg( $a );
190         
191         yourls_apply_filter( $hook, $args );
192 }
193
194 /**
195 * Retrieve the number times an action is fired.
196 *
197 * @param string $hook Name of the action hook.
198 * @return int The number of times action hook <tt>$hook</tt> is fired
199 */
200 function yourls_did_action( $hook ) {
201         global $yourls_actions;
202         if ( !isset( $yourls_actions ) || !isset( $yourls_actions[ $hook ] ) )
203                 return 0;
204         return $yourls_actions[ $hook ];
205 }
206
207 /**
208  * Removes a function from a specified filter hook.
209  *
210  * This function removes a function attached to a specified filter hook. This
211  * method can be used to remove default functions attached to a specific filter
212  * hook and possibly replace them with a substitute.
213  *
214  * To remove a hook, the $function_to_remove and $priority arguments must match
215  * when the hook was added.
216  *
217  * @global array $yourls_filters storage for all of the filters
218  * @param string $hook The filter hook to which the function to be removed is hooked.
219  * @param callback $function_to_remove The name of the function which should be removed.
220  * @param int $priority optional. The priority of the function (default: 10).
221  * @return boolean Whether the function was registered as a filter before it was removed.
222  */
223 function yourls_remove_filter( $hook, $function_to_remove, $priority = 10 ) {
224         global $yourls_filters;
225         
226         $function_to_remove = yourls_filter_unique_id( $hook, $function_to_remove, $priority );
227
228         $remove = isset( $yourls_filters[ $hook ][ $priority ][ $function_to_remove ] );
229
230         if ( $remove === true ) {
231                 unset ( $yourls_filters[$hook][$priority][$function_to_remove] );
232                 if ( empty( $yourls_filters[$hook][$priority] ) )
233                         unset( $yourls_filters[$hook] );
234         }
235         return $remove;
236 }
237
238 /**
239  * Removes a function from a specified action hook.
240  *
241  * @see yourls_remove_filter()
242  *
243  * @param string $hook The action hook to which the function to be removed is hooked.
244  * @param callback $function_to_remove The name of the function which should be removed.
245  * @param int $priority optional. The priority of the function (default: 10).
246  * @return boolean Whether the function was registered as an action before it was removed.
247  */
248
249 function yourls_remove_action( $hook, $function_to_remove, $priority = 10 ) {
250     return yourls_remove_filter( $hook, $function_to_remove, $priority );
251 }
252
253 /**
254  * Removes all functions from a specified action hook.
255  *
256  * @see yourls_remove_all_filters()
257  * @since 1.7.1
258  *
259  * @param string $hook The action to remove hooks from
260  * @param int $priority optional. The priority of the functions to remove
261  * @return boolean true when it's finished
262  */
263
264 function yourls_remove_all_actions( $hook, $priority = false ) {
265     return yourls_remove_all_filters( $hook, $priority );
266 }
267
268 /**
269  * Removes all functions from a specified filter hook.
270  *
271  * @since 1.7.1
272  *
273  * @param string $hook The filter to remove hooks from
274  * @param int $priority optional. The priority of the functions to remove
275  * @return boolean true when it's finished
276  */
277
278 function yourls_remove_all_filters( $hook, $priority = false ) {
279     global $yourls_filters;
280     
281     if( isset( $yourls_filters[ $hook ] ) ) {
282         if( $priority === false ) {
283             unset( $yourls_filters[ $hook ] );
284         } else if ( isset( $yourls_filters[ $hook ][ $priority ] ) ) {
285             unset( $yourls_filters[ $hook ][ $priority ] );
286         }
287     }
288     
289     return true;
290 }
291
292 /**
293  * Check if any filter has been registered for a hook.
294  *
295  * @global array $yourls_filters storage for all of the filters
296  * @param string $hook The name of the filter hook.
297  * @param callback $function_to_check optional.  If specified, return the priority of that function on this hook or false if not attached.
298  * @return int|boolean Optionally returns the priority on that hook for the specified function.
299  */
300 function yourls_has_filter( $hook, $function_to_check = false ) {
301         global $yourls_filters;
302
303         $has = !empty( $yourls_filters[ $hook ] );
304         if ( false === $function_to_check || false == $has ) {
305                 return $has;
306         }
307     
308         if ( !$idx = yourls_filter_unique_id( $hook, $function_to_check, false ) )
309                 return false;
310
311         foreach ( (array) array_keys( $yourls_filters[ $hook ] ) as $priority ) {
312                 if ( isset( $yourls_filters[ $hook ][ $priority ][ $idx ] ) )
313                         return $priority;
314         }
315         return false;
316 }
317
318 function yourls_has_action( $hook, $function_to_check = false ) {
319         return yourls_has_filter( $hook, $function_to_check );
320 }
321
322 /**
323  * Return number of active plugins
324  *
325  * @return integer Number of activated plugins
326  */
327 function yourls_has_active_plugins( ) {
328         global $ydb;
329         
330         if( !property_exists( $ydb, 'plugins' ) || !$ydb->plugins )
331                 $ydb->plugins = array();
332                 
333         return count( $ydb->plugins );
334 }
335
336
337 /**
338  * List plugins in /user/plugins
339  *
340  * @return array Array of [/plugindir/plugin.php]=>array('Name'=>'Ozh', 'Title'=>'Hello', )
341  */
342 function yourls_get_plugins( ) {
343         $plugins = (array) glob( YOURLS_PLUGINDIR .'/*/plugin.php');
344         
345         if( !$plugins )
346                 return array();
347         
348         foreach( $plugins as $key => $plugin ) {
349                 $_plugin = yourls_plugin_basename( $plugin );
350                 $plugins[ $_plugin ] = yourls_get_plugin_data( $plugin );
351                 unset( $plugins[ $key ] );
352         }
353         
354         return $plugins;
355 }
356
357 /**
358  * Check if a plugin is active
359  *
360  * @param string $plugin Physical path to plugin file
361  * @return bool
362  */
363 function yourls_is_active_plugin( $plugin ) {
364         if( !yourls_has_active_plugins( ) )
365                 return false;
366         
367         global $ydb;
368         $plugin = yourls_plugin_basename( $plugin );
369         
370         return in_array( $plugin, $ydb->plugins );
371
372 }
373
374 /**
375  * Parse a plugin header
376  *
377  * @param string $file Physical path to plugin file
378  * @return array Array of 'Field'=>'Value' from plugin comment header lines of the form "Field: Value"
379  */
380 function yourls_get_plugin_data( $file ) {
381         $fp = fopen( $file, 'r' ); // assuming $file is readable, since yourls_load_plugins() filters this
382         $data = fread( $fp, 8192 ); // get first 8kb
383         fclose( $fp );
384         
385         // Capture all the header within first comment block
386         if( !preg_match( '!.*?/\*(.*?)\*/!ms', $data, $matches ) )
387                 return array();
388         
389         // Capture each line with "Something: some text"
390         unset( $data );
391         $lines = preg_split( "[\n|\r]", $matches[1] );
392         unset( $matches );
393
394         $plugin_data = array();
395         foreach( $lines as $line ) {
396                 if( !preg_match( '!(.*?):\s+(.*)!', $line, $matches ) )
397                         continue;
398                 
399                 list( $null, $field, $value ) = array_map( 'trim', $matches);
400                 $plugin_data[ $field ] = $value;
401         }
402         
403         return $plugin_data;
404 }
405
406 // Include active plugins
407 function yourls_load_plugins() {
408         // Don't load plugins when installing or updating
409         if( yourls_is_installing() OR yourls_is_upgrading() )
410                 return;
411         
412         $active_plugins = yourls_get_option( 'active_plugins' );
413         if( false === $active_plugins )
414                 return;
415         
416         global $ydb;
417         $ydb->plugins = array();
418         
419         foreach( (array)$active_plugins as $key=>$plugin ) {
420                 if( yourls_validate_plugin_file( YOURLS_PLUGINDIR.'/'.$plugin ) ) {
421                         include_once( YOURLS_PLUGINDIR.'/'.$plugin );
422                         $ydb->plugins[] = $plugin;
423                         unset( $active_plugins[$key] );
424                 }
425         }
426         
427         // $active_plugins should be empty now, if not, a plugin could not be find: remove it
428         if( count( $active_plugins ) ) {
429                 yourls_update_option( 'active_plugins', $ydb->plugins );
430                 $message = yourls_n( 'Could not find and deactivated plugin :', 'Could not find and deactivated plugins :', count( $active_plugins ) );
431                 $missing = '<strong>'.join( '</strong>, <strong>', $active_plugins ).'</strong>';
432                 yourls_add_notice( $message .' '. $missing );
433         }
434 }
435
436 /**
437  * Check if a file is safe for inclusion (well, "safe", no guarantee)
438  *
439  * @param string $file Full pathname to a file
440  * @return bool
441  */
442 function yourls_validate_plugin_file( $file ) {
443         if (
444                 false !== strpos( $file, '..' )
445                 OR
446                 false !== strpos( $file, './' )
447                 OR
448                 'plugin.php' !== substr( $file, -10 )   // a plugin must be named 'plugin.php'
449                 OR
450                 !is_readable( $file )
451         )
452                 return false;
453                 
454         return true;
455 }
456
457 /**
458  * Activate a plugin
459  *
460  * @param string $plugin Plugin filename (full or relative to plugins directory)
461  * @return mixed string if error or true if success
462  */
463 function yourls_activate_plugin( $plugin ) {
464         // validate file
465         $plugin = yourls_plugin_basename( $plugin );
466         $plugindir = yourls_sanitize_filename( YOURLS_PLUGINDIR );
467         if( !yourls_validate_plugin_file( $plugindir.'/'.$plugin ) )
468                 return yourls__( 'Not a valid plugin file' );
469                 
470         // check not activated already
471         global $ydb;
472         if( yourls_has_active_plugins() && in_array( $plugin, $ydb->plugins ) )
473                 return yourls__( 'Plugin already activated' );
474         
475         // attempt activation. TODO: uber cool fail proof sandbox like in WP.
476         ob_start();
477         include_once( YOURLS_PLUGINDIR.'/'.$plugin );
478         if ( ob_get_length() > 0 ) {
479                 // there was some output: error
480         // @codeCoverageIgnoreStart
481                 $output = ob_get_clean();
482                 return yourls_s( 'Plugin generated unexpected output. Error was: <br/><pre>%s</pre>', $output );
483         // @codeCoverageIgnoreEnd
484         }
485     ob_end_clean();
486
487         // so far, so good: update active plugin list
488         $ydb->plugins[] = $plugin;
489         yourls_update_option( 'active_plugins', $ydb->plugins );
490         yourls_do_action( 'activated_plugin', $plugin );
491         yourls_do_action( 'activated_' . $plugin );
492         
493         return true;
494 }
495
496 /**
497  * Deactivate a plugin
498  *
499  * @param string $plugin Plugin filename (full relative to plugins directory)
500  * @return mixed string if error or true if success
501  */
502 function yourls_deactivate_plugin( $plugin ) {
503         $plugin = yourls_plugin_basename( $plugin );
504
505         // Check plugin is active
506         if( !yourls_is_active_plugin( $plugin ) )
507                 return yourls__( 'Plugin not active' );
508         
509         // Deactivate the plugin
510         global $ydb;
511         $key = array_search( $plugin, $ydb->plugins );
512         if( $key !== false ) {
513                 array_splice( $ydb->plugins, $key, 1 );
514         }
515         
516         yourls_update_option( 'active_plugins', $ydb->plugins );
517         yourls_do_action( 'deactivated_plugin', $plugin );
518         yourls_do_action( 'deactivated_' . $plugin );
519         
520         return true;
521 }
522
523 /**
524  * Return the path of a plugin file, relative to the plugins directory
525  */
526 function yourls_plugin_basename( $file ) {
527         $file = yourls_sanitize_filename( $file );
528         $plugindir = yourls_sanitize_filename( YOURLS_PLUGINDIR );
529         $file = str_replace( $plugindir, '', $file );
530         return trim( $file, '/' );
531 }
532
533 /**
534  * Return the URL of the directory a plugin
535  */
536 function yourls_plugin_url( $file ) {
537         $url = YOURLS_PLUGINURL . '/' . yourls_plugin_basename( $file );
538         if( yourls_is_ssl() or yourls_needs_ssl() )
539                 $url = str_replace( 'http://', 'https://', $url );
540         return yourls_apply_filter( 'plugin_url', $url, $file );
541 }
542
543 /**
544  * Display list of links to plugin admin pages, if any
545  */
546 function yourls_list_plugin_admin_pages() {
547         global $ydb;
548         
549         if( !property_exists( $ydb, 'plugin_pages' ) || !$ydb->plugin_pages )
550                 return;
551         
552         $plugin_links = array();
553         foreach( (array)$ydb->plugin_pages as $plugin => $page ) {
554                 $plugin_links[ $plugin ] = array(
555                         'url'    => yourls_admin_url( 'plugins.php?page='.$page['slug'] ),
556                         'anchor' => $page['title'],
557                 );
558         }
559         return $plugin_links;
560 }
561
562 /**
563  * Register a plugin administration page
564  */
565 function yourls_register_plugin_page( $slug, $title, $function ) {
566         global $ydb;
567         
568         if( !property_exists( $ydb, 'plugin_pages' ) || !$ydb->plugin_pages )
569                 $ydb->plugin_pages = array();
570
571         $ydb->plugin_pages[ $slug ] = array(
572                 'slug'     => $slug,
573                 'title'    => $title,
574                 'function' => $function,
575         );
576 }
577
578 /**
579  * Handle plugin administration page
580  *
581  */
582 function yourls_plugin_admin_page( $plugin_page ) {
583         global $ydb;
584
585         // Check the plugin page is actually registered
586         if( !isset( $ydb->plugin_pages[$plugin_page] ) ) {
587                 yourls_die( yourls__( 'This page does not exist. Maybe a plugin you thought was activated is inactive?' ), yourls__( 'Invalid link' ) );
588         }
589         
590         // Draw the page itself
591         yourls_do_action( 'load-' . $plugin_page);
592         yourls_html_head( 'plugin_page_' . $plugin_page, $ydb->plugin_pages[$plugin_page]['title'] );
593         yourls_html_logo();
594         yourls_html_menu();
595         
596         call_user_func( $ydb->plugin_pages[$plugin_page]['function'] );
597         
598         yourls_html_footer();
599 }
600
601
602 /**
603  * Callback function: Sort plugins 
604  *
605  * @link http://php.net/uasort
606  * @codeCoverageIgnore
607  *
608  * @param array $plugin_a
609  * @param array $plugin_b
610  * @return int 0, 1 or -1, see uasort()
611  */
612 function yourls_plugins_sort_callback( $plugin_a, $plugin_b ) {
613         $orderby = yourls_apply_filter( 'plugins_sort_callback', 'Plugin Name' );
614         $order   = yourls_apply_filter( 'plugins_sort_callback', 'ASC' );
615
616         $a = isset( $plugin_a[ $orderby ] ) ? $plugin_a[ $orderby ] : '';
617         $b = isset( $plugin_b[ $orderby ] ) ? $plugin_b[ $orderby ] : '';
618
619         if ( $a == $b )
620                 return 0;
621
622         if ( 'DESC' == $order )
623                 return ( $a < $b ) ? 1 : -1;
624         else
625                 return ( $a < $b ) ? -1 : 1;            
626 }