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