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