|
1 <?php |
|
2 /** |
|
3 * Upgrade API: Plugin_Upgrader class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Upgrader |
|
7 * @since 4.6.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Core class used for upgrading/installing plugins. |
|
12 * |
|
13 * It is designed to upgrade/install plugins from a local zip, remote zip URL, |
|
14 * or uploaded zip file. |
|
15 * |
|
16 * @since 2.8.0 |
|
17 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php. |
|
18 * |
|
19 * @see WP_Upgrader |
|
20 */ |
|
21 class Plugin_Upgrader extends WP_Upgrader { |
|
22 |
|
23 /** |
|
24 * Plugin upgrade result. |
|
25 * |
|
26 * @since 2.8.0 |
|
27 * @var array|WP_Error $result |
|
28 * |
|
29 * @see WP_Upgrader::$result |
|
30 */ |
|
31 public $result; |
|
32 |
|
33 /** |
|
34 * Whether a bulk upgrade/installation is being performed. |
|
35 * |
|
36 * @since 2.9.0 |
|
37 * @var bool $bulk |
|
38 */ |
|
39 public $bulk = false; |
|
40 |
|
41 /** |
|
42 * Initialize the upgrade strings. |
|
43 * |
|
44 * @since 2.8.0 |
|
45 */ |
|
46 public function upgrade_strings() { |
|
47 $this->strings['up_to_date'] = __('The plugin is at the latest version.'); |
|
48 $this->strings['no_package'] = __('Update package not available.'); |
|
49 /* translators: %s: package URL */ |
|
50 $this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s…' ), '<span class="code">%s</span>' ); |
|
51 $this->strings['unpack_package'] = __('Unpacking the update…'); |
|
52 $this->strings['remove_old'] = __('Removing the old version of the plugin…'); |
|
53 $this->strings['remove_old_failed'] = __('Could not remove the old plugin.'); |
|
54 $this->strings['process_failed'] = __('Plugin update failed.'); |
|
55 $this->strings['process_success'] = __('Plugin updated successfully.'); |
|
56 $this->strings['process_bulk_success'] = __('Plugins updated successfully.'); |
|
57 } |
|
58 |
|
59 /** |
|
60 * Initialize the installation strings. |
|
61 * |
|
62 * @since 2.8.0 |
|
63 */ |
|
64 public function install_strings() { |
|
65 $this->strings['no_package'] = __('Installation package not available.'); |
|
66 /* translators: %s: package URL */ |
|
67 $this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s…' ), '<span class="code">%s</span>' ); |
|
68 $this->strings['unpack_package'] = __('Unpacking the package…'); |
|
69 $this->strings['installing_package'] = __('Installing the plugin…'); |
|
70 $this->strings['no_files'] = __('The plugin contains no files.'); |
|
71 $this->strings['process_failed'] = __('Plugin installation failed.'); |
|
72 $this->strings['process_success'] = __('Plugin installed successfully.'); |
|
73 } |
|
74 |
|
75 /** |
|
76 * Install a plugin package. |
|
77 * |
|
78 * @since 2.8.0 |
|
79 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. |
|
80 * |
|
81 * @param string $package The full local path or URI of the package. |
|
82 * @param array $args { |
|
83 * Optional. Other arguments for installing a plugin package. Default empty array. |
|
84 * |
|
85 * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. |
|
86 * Default true. |
|
87 * } |
|
88 * @return bool|WP_Error True if the installation was successful, false or a WP_Error otherwise. |
|
89 */ |
|
90 public function install( $package, $args = array() ) { |
|
91 |
|
92 $defaults = array( |
|
93 'clear_update_cache' => true, |
|
94 ); |
|
95 $parsed_args = wp_parse_args( $args, $defaults ); |
|
96 |
|
97 $this->init(); |
|
98 $this->install_strings(); |
|
99 |
|
100 add_filter('upgrader_source_selection', array($this, 'check_package') ); |
|
101 if ( $parsed_args['clear_update_cache'] ) { |
|
102 // Clear cache so wp_update_plugins() knows about the new plugin. |
|
103 add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 ); |
|
104 } |
|
105 |
|
106 $this->run( array( |
|
107 'package' => $package, |
|
108 'destination' => WP_PLUGIN_DIR, |
|
109 'clear_destination' => false, // Do not overwrite files. |
|
110 'clear_working' => true, |
|
111 'hook_extra' => array( |
|
112 'type' => 'plugin', |
|
113 'action' => 'install', |
|
114 ) |
|
115 ) ); |
|
116 |
|
117 remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 ); |
|
118 remove_filter('upgrader_source_selection', array($this, 'check_package') ); |
|
119 |
|
120 if ( ! $this->result || is_wp_error($this->result) ) |
|
121 return $this->result; |
|
122 |
|
123 // Force refresh of plugin update information |
|
124 wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); |
|
125 |
|
126 return true; |
|
127 } |
|
128 |
|
129 /** |
|
130 * Upgrade a plugin. |
|
131 * |
|
132 * @since 2.8.0 |
|
133 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. |
|
134 * |
|
135 * @param string $plugin The basename path to the main plugin file. |
|
136 * @param array $args { |
|
137 * Optional. Other arguments for upgrading a plugin package. Default empty array. |
|
138 * |
|
139 * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. |
|
140 * Default true. |
|
141 * } |
|
142 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise. |
|
143 */ |
|
144 public function upgrade( $plugin, $args = array() ) { |
|
145 |
|
146 $defaults = array( |
|
147 'clear_update_cache' => true, |
|
148 ); |
|
149 $parsed_args = wp_parse_args( $args, $defaults ); |
|
150 |
|
151 $this->init(); |
|
152 $this->upgrade_strings(); |
|
153 |
|
154 $current = get_site_transient( 'update_plugins' ); |
|
155 if ( !isset( $current->response[ $plugin ] ) ) { |
|
156 $this->skin->before(); |
|
157 $this->skin->set_result(false); |
|
158 $this->skin->error('up_to_date'); |
|
159 $this->skin->after(); |
|
160 return false; |
|
161 } |
|
162 |
|
163 // Get the URL to the zip file |
|
164 $r = $current->response[ $plugin ]; |
|
165 |
|
166 add_filter('upgrader_pre_install', array($this, 'deactivate_plugin_before_upgrade'), 10, 2); |
|
167 add_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'), 10, 4); |
|
168 //'source_selection' => array($this, 'source_selection'), //there's a trac ticket to move up the directory for zip's which are made a bit differently, useful for non-.org plugins. |
|
169 if ( $parsed_args['clear_update_cache'] ) { |
|
170 // Clear cache so wp_update_plugins() knows about the new plugin. |
|
171 add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 ); |
|
172 } |
|
173 |
|
174 $this->run( array( |
|
175 'package' => $r->package, |
|
176 'destination' => WP_PLUGIN_DIR, |
|
177 'clear_destination' => true, |
|
178 'clear_working' => true, |
|
179 'hook_extra' => array( |
|
180 'plugin' => $plugin, |
|
181 'type' => 'plugin', |
|
182 'action' => 'update', |
|
183 ), |
|
184 ) ); |
|
185 |
|
186 // Cleanup our hooks, in case something else does a upgrade on this connection. |
|
187 remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 ); |
|
188 remove_filter('upgrader_pre_install', array($this, 'deactivate_plugin_before_upgrade')); |
|
189 remove_filter('upgrader_clear_destination', array($this, 'delete_old_plugin')); |
|
190 |
|
191 if ( ! $this->result || is_wp_error($this->result) ) |
|
192 return $this->result; |
|
193 |
|
194 // Force refresh of plugin update information |
|
195 wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); |
|
196 |
|
197 return true; |
|
198 } |
|
199 |
|
200 /** |
|
201 * Bulk upgrade several plugins at once. |
|
202 * |
|
203 * @since 2.8.0 |
|
204 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional. |
|
205 * |
|
206 * @param array $plugins Array of the basename paths of the plugins' main files. |
|
207 * @param array $args { |
|
208 * Optional. Other arguments for upgrading several plugins at once. Default empty array. |
|
209 * |
|
210 * @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. |
|
211 * Default true. |
|
212 * } |
|
213 * @return array|false An array of results indexed by plugin file, or false if unable to connect to the filesystem. |
|
214 */ |
|
215 public function bulk_upgrade( $plugins, $args = array() ) { |
|
216 |
|
217 $defaults = array( |
|
218 'clear_update_cache' => true, |
|
219 ); |
|
220 $parsed_args = wp_parse_args( $args, $defaults ); |
|
221 |
|
222 $this->init(); |
|
223 $this->bulk = true; |
|
224 $this->upgrade_strings(); |
|
225 |
|
226 $current = get_site_transient( 'update_plugins' ); |
|
227 |
|
228 add_filter('upgrader_clear_destination', array($this, 'delete_old_plugin'), 10, 4); |
|
229 |
|
230 $this->skin->header(); |
|
231 |
|
232 // Connect to the Filesystem first. |
|
233 $res = $this->fs_connect( array(WP_CONTENT_DIR, WP_PLUGIN_DIR) ); |
|
234 if ( ! $res ) { |
|
235 $this->skin->footer(); |
|
236 return false; |
|
237 } |
|
238 |
|
239 $this->skin->bulk_header(); |
|
240 |
|
241 /* |
|
242 * Only start maintenance mode if: |
|
243 * - running Multisite and there are one or more plugins specified, OR |
|
244 * - a plugin with an update available is currently active. |
|
245 * @TODO: For multisite, maintenance mode should only kick in for individual sites if at all possible. |
|
246 */ |
|
247 $maintenance = ( is_multisite() && ! empty( $plugins ) ); |
|
248 foreach ( $plugins as $plugin ) |
|
249 $maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin] ) ); |
|
250 if ( $maintenance ) |
|
251 $this->maintenance_mode(true); |
|
252 |
|
253 $results = array(); |
|
254 |
|
255 $this->update_count = count($plugins); |
|
256 $this->update_current = 0; |
|
257 foreach ( $plugins as $plugin ) { |
|
258 $this->update_current++; |
|
259 $this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true); |
|
260 |
|
261 if ( !isset( $current->response[ $plugin ] ) ) { |
|
262 $this->skin->set_result('up_to_date'); |
|
263 $this->skin->before(); |
|
264 $this->skin->feedback('up_to_date'); |
|
265 $this->skin->after(); |
|
266 $results[$plugin] = true; |
|
267 continue; |
|
268 } |
|
269 |
|
270 // Get the URL to the zip file. |
|
271 $r = $current->response[ $plugin ]; |
|
272 |
|
273 $this->skin->plugin_active = is_plugin_active($plugin); |
|
274 |
|
275 $result = $this->run( array( |
|
276 'package' => $r->package, |
|
277 'destination' => WP_PLUGIN_DIR, |
|
278 'clear_destination' => true, |
|
279 'clear_working' => true, |
|
280 'is_multi' => true, |
|
281 'hook_extra' => array( |
|
282 'plugin' => $plugin |
|
283 ) |
|
284 ) ); |
|
285 |
|
286 $results[$plugin] = $this->result; |
|
287 |
|
288 // Prevent credentials auth screen from displaying multiple times |
|
289 if ( false === $result ) |
|
290 break; |
|
291 } //end foreach $plugins |
|
292 |
|
293 $this->maintenance_mode(false); |
|
294 |
|
295 // Force refresh of plugin update information. |
|
296 wp_clean_plugins_cache( $parsed_args['clear_update_cache'] ); |
|
297 |
|
298 /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ |
|
299 do_action( 'upgrader_process_complete', $this, array( |
|
300 'action' => 'update', |
|
301 'type' => 'plugin', |
|
302 'bulk' => true, |
|
303 'plugins' => $plugins, |
|
304 ) ); |
|
305 |
|
306 $this->skin->bulk_footer(); |
|
307 |
|
308 $this->skin->footer(); |
|
309 |
|
310 // Cleanup our hooks, in case something else does a upgrade on this connection. |
|
311 remove_filter('upgrader_clear_destination', array($this, 'delete_old_plugin')); |
|
312 |
|
313 return $results; |
|
314 } |
|
315 |
|
316 /** |
|
317 * Check a source package to be sure it contains a plugin. |
|
318 * |
|
319 * This function is added to the {@see 'upgrader_source_selection'} filter by |
|
320 * Plugin_Upgrader::install(). |
|
321 * |
|
322 * @since 3.3.0 |
|
323 * |
|
324 * @global WP_Filesystem_Base $wp_filesystem Subclass |
|
325 * |
|
326 * @param string $source The path to the downloaded package source. |
|
327 * @return string|WP_Error The source as passed, or a WP_Error object |
|
328 * if no plugins were found. |
|
329 */ |
|
330 public function check_package($source) { |
|
331 global $wp_filesystem; |
|
332 |
|
333 if ( is_wp_error($source) ) |
|
334 return $source; |
|
335 |
|
336 $working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit(WP_CONTENT_DIR), $source); |
|
337 if ( ! is_dir($working_directory) ) // Sanity check, if the above fails, let's not prevent installation. |
|
338 return $source; |
|
339 |
|
340 // Check the folder contains at least 1 valid plugin. |
|
341 $plugins_found = false; |
|
342 $files = glob( $working_directory . '*.php' ); |
|
343 if ( $files ) { |
|
344 foreach ( $files as $file ) { |
|
345 $info = get_plugin_data( $file, false, false ); |
|
346 if ( ! empty( $info['Name'] ) ) { |
|
347 $plugins_found = true; |
|
348 break; |
|
349 } |
|
350 } |
|
351 } |
|
352 |
|
353 if ( ! $plugins_found ) |
|
354 return new WP_Error( 'incompatible_archive_no_plugins', $this->strings['incompatible_archive'], __( 'No valid plugins were found.' ) ); |
|
355 |
|
356 return $source; |
|
357 } |
|
358 |
|
359 /** |
|
360 * Retrieve the path to the file that contains the plugin info. |
|
361 * |
|
362 * This isn't used internally in the class, but is called by the skins. |
|
363 * |
|
364 * @since 2.8.0 |
|
365 * |
|
366 * @return string|false The full path to the main plugin file, or false. |
|
367 */ |
|
368 public function plugin_info() { |
|
369 if ( ! is_array($this->result) ) |
|
370 return false; |
|
371 if ( empty($this->result['destination_name']) ) |
|
372 return false; |
|
373 |
|
374 $plugin = get_plugins('/' . $this->result['destination_name']); //Ensure to pass with leading slash |
|
375 if ( empty($plugin) ) |
|
376 return false; |
|
377 |
|
378 $pluginfiles = array_keys($plugin); //Assume the requested plugin is the first in the list |
|
379 |
|
380 return $this->result['destination_name'] . '/' . $pluginfiles[0]; |
|
381 } |
|
382 |
|
383 /** |
|
384 * Deactivates a plugin before it is upgraded. |
|
385 * |
|
386 * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade(). |
|
387 * |
|
388 * @since 2.8.0 |
|
389 * @since 4.1.0 Added a return value. |
|
390 * |
|
391 * @param bool|WP_Error $return Upgrade offer return. |
|
392 * @param array $plugin Plugin package arguments. |
|
393 * @return bool|WP_Error The passed in $return param or WP_Error. |
|
394 */ |
|
395 public function deactivate_plugin_before_upgrade($return, $plugin) { |
|
396 |
|
397 if ( is_wp_error($return) ) //Bypass. |
|
398 return $return; |
|
399 |
|
400 // When in cron (background updates) don't deactivate the plugin, as we require a browser to reactivate it |
|
401 if ( wp_doing_cron() ) |
|
402 return $return; |
|
403 |
|
404 $plugin = isset($plugin['plugin']) ? $plugin['plugin'] : ''; |
|
405 if ( empty($plugin) ) |
|
406 return new WP_Error('bad_request', $this->strings['bad_request']); |
|
407 |
|
408 if ( is_plugin_active($plugin) ) { |
|
409 //Deactivate the plugin silently, Prevent deactivation hooks from running. |
|
410 deactivate_plugins($plugin, true); |
|
411 } |
|
412 |
|
413 return $return; |
|
414 } |
|
415 |
|
416 /** |
|
417 * Delete the old plugin during an upgrade. |
|
418 * |
|
419 * Hooked to the {@see 'upgrader_clear_destination'} filter by |
|
420 * Plugin_Upgrader::upgrade() and Plugin_Upgrader::bulk_upgrade(). |
|
421 * |
|
422 * @since 2.8.0 |
|
423 * |
|
424 * @global WP_Filesystem_Base $wp_filesystem Subclass |
|
425 * |
|
426 * @param bool|WP_Error $removed |
|
427 * @param string $local_destination |
|
428 * @param string $remote_destination |
|
429 * @param array $plugin |
|
430 * @return WP_Error|bool |
|
431 */ |
|
432 public function delete_old_plugin($removed, $local_destination, $remote_destination, $plugin) { |
|
433 global $wp_filesystem; |
|
434 |
|
435 if ( is_wp_error($removed) ) |
|
436 return $removed; //Pass errors through. |
|
437 |
|
438 $plugin = isset($plugin['plugin']) ? $plugin['plugin'] : ''; |
|
439 if ( empty($plugin) ) |
|
440 return new WP_Error('bad_request', $this->strings['bad_request']); |
|
441 |
|
442 $plugins_dir = $wp_filesystem->wp_plugins_dir(); |
|
443 $this_plugin_dir = trailingslashit( dirname($plugins_dir . $plugin) ); |
|
444 |
|
445 if ( ! $wp_filesystem->exists($this_plugin_dir) ) //If it's already vanished. |
|
446 return $removed; |
|
447 |
|
448 // If plugin is in its own directory, recursively delete the directory. |
|
449 if ( strpos($plugin, '/') && $this_plugin_dir != $plugins_dir ) //base check on if plugin includes directory separator AND that it's not the root plugin folder |
|
450 $deleted = $wp_filesystem->delete($this_plugin_dir, true); |
|
451 else |
|
452 $deleted = $wp_filesystem->delete($plugins_dir . $plugin); |
|
453 |
|
454 if ( ! $deleted ) |
|
455 return new WP_Error('remove_old_failed', $this->strings['remove_old_failed']); |
|
456 |
|
457 return true; |
|
458 } |
|
459 } |