root/trunk/wp-admin/includes/plugin.php

Revision 1522, 32.0 kB (checked in by donncha, 3 days ago)

WP Merge with revision 9730

Line 
1 <?php
2 /**
3  * WordPress Plugin Administration API
4  *
5  * @package WordPress
6  * @subpackage Administration
7  */
8
9 /**
10  * Parse the plugin contents to retrieve plugin's metadata.
11  *
12  * The metadata of the plugin's data searches for the following in the plugin's
13  * header. All plugin data must be on its own line. For plugin description, it
14  * must not have any newlines or only parts of the description will be displayed
15  * and the same goes for the plugin data. The below is formatted for printing.
16  *
17  * <code>
18  * /*
19  * Plugin Name: Name of Plugin
20  * Plugin URI: Link to plugin information
21  * Description: Plugin Description
22  * Author: Plugin author's name
23  * Author URI: Link to the author's web site
24  * Version: Must be set in the plugin for WordPress 2.3+
25  * Text Domain: Optional. Unique identifier, should be same as the one used in
26  *        plugin_text_domain()
27  * Domain Path: Optional. Only useful if the translations are located in a
28  *        folder above the plugin's base path. For example, if .mo files are
29  *        located in the locale folder then Domain Path will be "/locale/" and
30  *        must have the first slash. Defaults to the base folder the plugin is
31  *        located in.
32  *  * / # Remove the space to close comment
33  * </code>
34  *
35  * Plugin data returned array contains the following:
36  *        'Name' - Name of the plugin, must be unique.
37  *        'Title' - Title of the plugin and the link to the plugin's web site.
38  *        'Description' - Description of what the plugin does and/or notes
39  *        from the author.
40  *        'Author' - The author's name
41  *        'AuthorURI' - The authors web site address.
42  *        'Version' - The plugin version number.
43  *        'PluginURI' - Plugin web site address.
44  *        'TextDomain' - Plugin's text domain for localization.
45  *        'DomainPath' - Plugin's relative directory path to .mo files.
46  *
47  * Some users have issues with opening large files and manipulating the contents
48  * for want is usually the first 1kiB or 2kiB. This function stops pulling in
49  * the plugin contents when it has all of the required plugin data.
50  *
51  * The first 8kiB of the file will be pulled in and if the plugin data is not
52  * within that first 8kiB, then the plugin author should correct their plugin
53  * and move the plugin data headers to the top.
54  *
55  * The plugin file is assumed to have permissions to allow for scripts to read
56  * the file. This is not checked however and the file is only opened for
57  * reading.
58  *
59  * @link http://trac.wordpress.org/ticket/5651 Previous Optimizations.
60  * @link http://trac.wordpress.org/ticket/7372 Further and better Optimizations.
61  * @since 1.5.0
62  *
63  * @param string $plugin_file Path to the plugin file
64  * @param bool $markup If the returned data should have HTML markup applied
65  * @param bool $translate If the returned data should be translated
66  * @return array See above for description.
67  */
68 function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
69     // We don't need to write to the file, so just open for reading.
70     $fp = fopen($plugin_file, 'r');
71
72     // Pull only the first 8kiB of the file in.
73     $plugin_data = fread( $fp, 8192 );
74
75     // PHP will close file handle, but we are good citizens.
76     fclose($fp);
77
78     preg_match( '|Plugin Name:(.*)$|mi', $plugin_data, $name );
79     preg_match( '|Plugin URI:(.*)$|mi', $plugin_data, $uri );
80     preg_match( '|Version:(.*)|i', $plugin_data, $version );
81     preg_match( '|Description:(.*)$|mi', $plugin_data, $description );
82     preg_match( '|Author:(.*)$|mi', $plugin_data, $author_name );
83     preg_match( '|Author URI:(.*)$|mi', $plugin_data, $author_uri );
84     preg_match( '|Text Domain:(.*)$|mi', $plugin_data, $text_domain );
85     preg_match( '|Domain Path:(.*)$|mi', $plugin_data, $domain_path );
86
87     foreach ( array( 'name', 'uri', 'version', 'description', 'author_name', 'author_uri', 'text_domain', 'domain_path' ) as $field ) {
88         if ( !empty( ${$field} ) )
89             ${$field} = trim(${$field}[1]);
90         else
91             ${$field} = '';
92     }
93
94     $plugin_data = array(
95                 'Name' => $name, 'Title' => $name, 'PluginURI' => $uri, 'Description' => $description,
96                 'Author' => $author_name, 'AuthorURI' => $author_uri, 'Version' => $version,
97                 'TextDomain' => $text_domain, 'DomainPath' => $domain_path
98                 );
99     if ( $markup || $translate )
100         $plugin_data = _get_plugin_data_markup_translate($plugin_data, $markup, $translate);
101     return $plugin_data;
102 }
103
104 function _get_plugin_data_markup_translate($plugin_data, $markup = true, $translate = true) {
105
106     //Translate fields
107     if( $translate && ! empty($plugin_data['TextDomain']) ) {
108         if( ! empty( $plugin_data['DomainPath'] ) )
109             load_plugin_textdomain($plugin_data['TextDomain'], dirname($plugin_file). $plugin_data['DomainPath']);
110         else
111             load_plugin_textdomain($plugin_data['TextDomain'], dirname($plugin_file));
112
113         foreach ( array('Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version') as $field )
114             $plugin_data[ $field ] = translate($plugin_data[ $field ], $plugin_data['TextDomain']);
115     }
116
117     //Apply Markup
118     if ( $markup ) {
119         if ( ! empty($plugin_data['PluginURI']) && ! empty($plugin_data['Name']) )
120             $plugin_data['Title'] = '<a href="' . $plugin_data['PluginURI'] . '" title="' . __( 'Visit plugin homepage' ) . '">' . $plugin_data['Name'] . '</a>';
121         else
122             $plugin_data['Title'] = $plugin_data['Name'];
123
124         if ( ! empty($plugin_data['AuthorURI']) )
125             $plugin_data['Author'] = '<a href="' . $plugin_data['AuthorURI'] . '" title="' . __( 'Visit author homepage' ) . '">' . $plugin_data['Author'] . '</a>';
126
127         $plugin_data['Description'] = wptexturize( $plugin_data['Description'] );
128         if( ! empty($plugin_data['Author']) )
129             $plugin_data['Description'] .= ' <cite>' . sprintf( __('By %s'), $plugin_data['Author'] ) . '.</cite>';
130     }
131
132     $plugins_allowedtags = array('a' => array('href' => array(),'title' => array()),'abbr' => array('title' => array()),'acronym' => array('title' => array()),'code' => array(),'em' => array(),'strong' => array());
133
134     // Sanitize all displayed data
135     $plugin_data['Title']       = wp_kses($plugin_data['Title'], $plugins_allowedtags);
136     $plugin_data['Version']     = wp_kses($plugin_data['Version'], $plugins_allowedtags);
137     $plugin_data['Description'] = wp_kses($plugin_data['Description'], $plugins_allowedtags);
138     $plugin_data['Author']      = wp_kses($plugin_data['Author'], $plugins_allowedtags);
139
140     return $plugin_data;
141 }
142
143 /**
144  * Check the plugins directory and retrieve all plugin files with plugin data.
145  *
146  * WordPress only supports plugin files in the base plugins directory
147  * (wp-content/plugins) and in one directory above the plugins directory
148  * (wp-content/plugins/my-plugin). The file it looks for has the plugin data and
149  * must be found in those two locations. It is recommended that do keep your
150  * plugin files in directories.
151  *
152  * The file with the plugin data is the file that will be included and therefore
153  * needs to have the main execution for the plugin. This does not mean
154  * everything must be contained in the file and it is recommended that the file
155  * be split for maintainability. Keep everything in one file for extreme
156  * optimization purposes.
157  *
158  * @since unknown
159  *
160  * @param string $plugin_folder Optional. Relative path to single plugin folder.
161  * @return array Key is the plugin file path and the value is an array of the plugin data.
162  */
163 function get_plugins($plugin_folder = '') {
164
165     if ( ! $cache_plugins = wp_cache_get('plugins', 'plugins') )
166         $cache_plugins = array();
167
168     if ( isset($cache_plugins[ $plugin_folder ]) )
169         return $cache_plugins[ $plugin_folder ];
170
171     $wp_plugins = array ();
172     $plugin_root = WP_PLUGIN_DIR;
173     if( !empty($plugin_folder) )
174         $plugin_root .= $plugin_folder;
175
176     // Files in wp-content/plugins directory
177     $plugins_dir = @ opendir( $plugin_root);
178     if ( $plugins_dir ) {
179         while (($file = readdir( $plugins_dir ) ) !== false ) {
180             if ( substr($file, 0, 1) == '.' )
181                 continue;
182             if ( is_dir( $plugin_root.'/'.$file ) ) {
183                 $plugins_subdir = @ opendir( $plugin_root.'/'.$file );
184                 if ( $plugins_subdir ) {
185                     while (($subfile = readdir( $plugins_subdir ) ) !== false ) {
186                         if ( substr($subfile, 0, 1) == '.' )
187                             continue;
188                         if ( substr($subfile, -4) == '.php' )
189                             $plugin_files[] = "$file/$subfile";
190                     }
191                 }
192             } else {
193                 if ( substr($file, -4) == '.php' )
194                     $plugin_files[] = $file;
195             }
196         }
197     }
198     @closedir( $plugins_dir );
199     @closedir( $plugins_subdir );
200
201     if ( !$plugins_dir || !$plugin_files )
202         return $wp_plugins;
203
204     foreach ( $plugin_files as $plugin_file ) {
205         if ( !is_readable( "$plugin_root/$plugin_file" ) )
206             continue;
207
208         $plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false ); //Do not apply markup/translate as it'll be cached.
209
210         if ( empty ( $plugin_data['Name'] ) )
211             continue;
212
213         $wp_plugins[plugin_basename( $plugin_file )] = $plugin_data;
214     }
215
216     uasort( $wp_plugins, create_function( '$a, $b', 'return strnatcasecmp( $a["Name"], $b["Name"] );' ));
217
218     $cache_plugins[ $plugin_folder ] = $wp_plugins;
219     wp_cache_set('plugins', $cache_plugins, 'plugins');
220
221     return $wp_plugins;
222 }
223
224 /**
225  * Check whether the plugin is active by checking the active_plugins list.
226  *
227  * @since 2.5.0
228  *
229  * @param string $plugin Base plugin path from plugins directory.
230  * @return bool True, if in the active plugins list. False, not in the list.
231  */
232 function is_plugin_active($plugin) {
233     return in_array($plugin, get_option('active_plugins'));
234 }
235
236 /**
237  * Attempts activation of plugin in a "sandbox" and redirects on success.
238  *
239  * A plugin that is already activated will not attempt to be activated again.
240  *
241  * The way it works is by setting the redirection to the error before trying to
242  * include the plugin file. If the plugin fails, then the redirection will not
243  * be overwritten with the success message. Also, the options will not be
244  * updated and the activation hook will not be called on plugin error.
245  *
246  * It should be noted that in no way the below code will actually prevent errors
247  * within the file. The code should not be used elsewhere to replicate the
248  * "sandbox", which uses redirection to work.
249  * {@source 13 1}
250  *
251  * If any errors are found or text is outputted, then it will be captured to
252  * ensure that the success redirection will update the error redirection.
253  *
254  * @since unknown
255  *
256  * @param string $plugin Plugin path to main plugin file with plugin data.
257  * @param string $redirect Optional. URL to redirect to.
258  * @return WP_Error|null WP_Error on invalid file or null on success.
259  */
260 function activate_plugin($plugin, $redirect = '') {
261     $current = get_option('active_plugins');
262     $plugin = plugin_basename(trim($plugin));
263
264     $valid = validate_plugin($plugin);
265     if ( is_wp_error($valid) )
266         return $valid;
267
268     if ( !in_array($plugin, $current) ) {
269         if ( !empty($redirect) )
270             wp_redirect(add_query_arg('_error_nonce', wp_create_nonce('plugin-activation-error_' . $plugin), $redirect)); // we'll override this later if the plugin can be included without fatal error
271         ob_start();
272         @include(WP_PLUGIN_DIR . '/' . $plugin);
273         $current[] = $plugin;
274         sort($current);
275         update_option('active_plugins', $current);
276         do_action('activate_' . $plugin);
277         ob_end_clean();
278     }
279
280     return null;
281 }
282
283 /**
284  * Deactivate a single plugin or multiple plugins.
285  *
286  * The deactivation hook is disabled by the plugin upgrader by using the $silent
287  * parameter.
288  *
289  * @since unknown
290  *
291  * @param string|array $plugins Single plugin or list of plugins to deactivate.
292  * @param bool $silent Optional, default is false. Prevent calling deactivate hook.
293  */
294 function deactivate_plugins($plugins, $silent= false) {
295     $current = get_option('active_plugins');
296
297     if ( !is_array($plugins) )
298         $plugins = array($plugins);
299
300     foreach ( $plugins as $plugin ) {
301         $plugin = plugin_basename($plugin);
302         if( ! is_plugin_active($plugin) )
303             continue;
304         array_splice($current, array_search( $plugin, $current), 1 ); // Fixed Array-fu!
305         if ( ! $silent ) //Used by Plugin updater to internally deactivate plugin, however, not to notify plugins of the fact to prevent plugin output.
306             do_action('deactivate_' . trim( $plugin ));
307     }
308
309     update_option('active_plugins', $current);
310 }
311
312 /**
313  * Activate multiple plugins.
314  *
315  * When WP_Error is returned, it does not mean that one of the plugins had
316  * errors. It means that one or more of the plugins file path was invalid.
317  *
318  * The execution will be halted as soon as one of the plugins has an error.
319  *
320  * @since unknown
321  *
322  * @param string|array $plugins
323  * @param string $redirect Redirect to page after successful activation.
324  * @return bool|WP_Error True when finished or WP_Error if there were errors during a plugin activation.
325  */
326 function activate_plugins($plugins, $redirect = '') {
327     if ( !is_array($plugins) )
328         $plugins = array($plugins);
329
330     $errors = array();
331     foreach ( (array) $plugins as $plugin ) {
332         if ( !empty($redirect) )
333             $redirect = add_query_arg('plugin', $plugin, $redirect);
334         $result = activate_plugin($plugin, $redirect);
335         if ( is_wp_error($result) )
336             $errors[$plugin] = $result;
337     }
338
339     if ( !empty($errors) )
340         return new WP_Error('plugins_invalid', __('One of the plugins is invalid.'), $errors);
341
342     return true;
343 }
344
345 /**
346  * Remove directory and files of a plugin for a single or list of plugin(s).
347  *
348  * If the plugins parameter list is empty, false will be returned. True when
349  * completed.
350  *
351  * @since unknown
352  *
353  * @param array $plugins List of plugin
354  * @param string $redirect Redirect to page when complete.
355  * @return mixed
356  */
357 function delete_plugins($plugins, $redirect = '' ) {
358     global $wp_filesystem;
359
360     if( empty($plugins) )
361         return false;
362
363     $checked = array();
364     foreach( $plugins as $plugin )
365         $checked[] = 'checked[]=' . $plugin;
366
367     ob_start();
368     $url = wp_nonce_url('plugins.php?action=delete-selected&verify-delete=1&' . implode('&', $checked), 'bulk-manage-plugins');
369     if ( false === ($credentials = request_filesystem_credentials($url)) ) {
370         $data = ob_get_contents();
371         ob_end_clean();
372         if( ! empty($data) ){
373             include_once( ABSPATH . 'wp-admin/admin-header.php');
374             echo $data;
375             include( ABSPATH . 'wp-admin/admin-footer.php');
376             exit;
377         }
378         return;
379     }
380
381     if ( ! WP_Filesystem($credentials) ) {
382         request_filesystem_credentials($url, '', true); //Failed to connect, Error and request again
383         $data = ob_get_contents();
384         ob_end_clean();
385         if( ! empty($data) ){
386             include_once( ABSPATH . 'wp-admin/admin-header.php');
387             echo $data;
388             include( ABSPATH . 'wp-admin/admin-footer.php');
389             exit;
390         }
391         return;
392     }
393
394     if ( $wp_filesystem->errors->get_error_code() ) {
395         return $wp_filesystem->errors;
396     }
397
398     if ( ! is_object($wp_filesystem) )
399         return new WP_Error('fs_unavailable', __('Could not access filesystem.'));
400
401     if ( $wp_filesystem->errors->get_error_code() )
402         return new WP_Error('fs_error', __('Filesystem error'), $wp_filesystem->errors);
403
404     //Get the base plugin folder
405     $plugins_dir = $wp_filesystem->wp_plugins_dir();
406     if ( empty($plugins_dir) )
407         return new WP_Error('fs_no_plugins_dir', __('Unable to locate WordPress Plugin directory.'));
408
409     $plugins_dir = trailingslashit( $plugins_dir );
410
411     $errors = array();
412
413     foreach( $plugins as $plugin_file ) {
414         // Run Uninstall hook
415         if ( is_uninstallable_plugin( $plugin_file ) )
416             uninstall_plugin($plugin_file);
417
418         $this_plugin_dir = trailingslashit( dirname($plugins_dir . $plugin_file) );
419         // If plugin is in its own directory, recursively delete the directory.
420         if ( strpos($plugin_file, '/') && $this_plugin_dir != $plugins_dir ) //base check on if plugin includes directory seperator AND that its not the root plugin folder
421             $deleted = $wp_filesystem->delete($this_plugin_dir, true);
422         else
423             $deleted = $wp_filesystem->delete($plugins_dir . $plugin_file);
424
425         if ( ! $deleted )
426             $errors[] = $plugin_file;
427     }
428
429     if( ! empty($errors) )
430         return new WP_Error('could_not_remove_plugin', sprintf(__('Could not fully remove the plugin(s) %s'), implode(', ', $errors)) );
431
432     return true;
433 }
434
435 function validate_active_plugins() {
436     $check_plugins = get_option('active_plugins');
437
438     // Sanity check.  If the active plugin list is not an array, make it an
439     // empty array.
440     if ( !is_array($check_plugins) ) {
441         update_option('active_plugins', array());
442         return;
443     }
444
445     //Invalid is any plugin that is deactivated due to error.
446     $invalid = array();
447
448     // If a plugin file does not exist, remove it from the list of active
449     // plugins.
450     foreach ( $check_plugins as $check_plugin ) {
451         $result = validate_plugin($check_plugin);
452         if ( is_wp_error( $result ) ) {
453             $invalid[$check_plugin] = $result;
454             deactivate_plugins( $check_plugin, true);
455         }
456     }
457     return $invalid;
458 }
459
460 /**
461  * Validate the plugin path.
462  *
463  * Checks that the file exists and {@link validate_file() is valid file}.
464  *
465  * @since unknown
466  *
467  * @param string $plugin Plugin Path
468  * @return WP_Error|int 0 on success, WP_Error on failure.
469  */
470 function validate_plugin($plugin) {
471     if ( validate_file($plugin) )
472         return new WP_Error('plugin_invalid', __('Invalid plugin path.'));
473     if ( ! file_exists(WP_PLUGIN_DIR . '/' . $plugin) )
474         return new WP_Error('plugin_not_found', __('Plugin file does not exist.'));
475
476     return 0;
477 }
478
479 /**
480  * Whether the plugin can be uninstalled.
481  *
482  * @since 2.7.0
483  *
484  * @param string $plugin Plugin path to check.
485  * @return bool Whether plugin can be uninstalled.
486  */
487 function is_uninstallable_plugin($plugin) {
488     $file = plugin_basename($plugin);
489
490     $uninstallable_plugins = (array) get_option('uninstall_plugins');
491     if ( isset( $uninstallable_plugins[$file] ) || file_exists( WP_PLUGIN_DIR . '/' . dirname($file) . '/uninstall.php' ) )
492         return true;
493
494     return false;
495 }
496
497 /**
498  * Uninstall a single plugin.
499  *
500  * Calls the uninstall hook, if it is available.
501  *
502  * @since 2.7.0
503  *
504  * @param string $plugin Relative plugin path from Plugin Directory.
505  */
506 function uninstall_plugin($plugin) {
507     $file = plugin_basename($plugin);
508
509     $uninstallable_plugins = (array) get_option('uninstall_plugins');
510     if ( file_exists( WP_PLUGIN_DIR . '/' . dirname($file) . '/uninstall.php' ) ) {
511         if ( isset( $uninstallable_plugins[$file] ) ) {
512             unset($uninstallable_plugins[$file]);
513             update_option('uninstall_plugins', $uninstallable_plugins);
514         }
515         unset($uninstallable_plugins);
516
517         define('WP_UNINSTALL_PLUGIN', $file);
518         include WP_PLUGIN_DIR . '/' . dirname($file) . '/uninstall.php';
519
520         return true;
521     }
522
523     if ( isset( $uninstallable_plugins[$file] ) ) {
524         $callable = $uninstallable_plugins[$file];
525         unset($uninstallable_plugins[$file]);
526         update_option('uninstall_plugins', $uninstallable_plugins);
527         unset($uninstallable_plugins);
528
529         include WP_PLUGIN_DIR . '/' . $file;
530
531         add_action( 'uninstall_' . $file, $callable );
532         do_action( 'uninstall_' . $file );
533     }
534 }
535
536 //
537 // Menu
538 //
539
540 function add_menu_page( $page_title, $menu_title, $access_level, $file, $function = '', $icon_url = '' ) {
541     global $menu, $admin_page_hooks;
542
543     $file = plugin_basename( $file );
544
545     $admin_page_hooks[$file] = sanitize_title( $menu_title );
546
547     $hookname = get_plugin_page_hookname( $file, '' );
548     if (!empty ( $function ) && !empty ( $hookname ))
549         add_action( $hookname, $function );
550
551     if ( empty($icon_url) )
552         $icon_url = 'images/generic.png';
553     
554     $menu[] = array ( $menu_title, $access_level, $file, $page_title, 'menu-top ' . $hookname, $hookname, $icon_url );
555
556