├── CHANGES.md ├── faster-updates.php ├── LICENSE ├── testing_instructions.md ├── readme.txt ├── modules ├── themes │ ├── class-main.php │ └── class-upgrader.php └── plugins │ ├── class-main.php │ └── class-upgrader.php └── functions └── move.php /CHANGES.md: -------------------------------------------------------------------------------- 1 | [unreleased] 2 | * initial pass 3 | * add generic hooks for overriding update processing for `update-core.php` 4 | * add `wp_opcache_invalidate_directory()` 5 | * add fixes for VirtualBox issues 6 | * removed unused hooks from `move_dir()` 7 | -------------------------------------------------------------------------------- /faster-updates.php: -------------------------------------------------------------------------------- 1 | 7 | * @license MIT 8 | */ 9 | 10 | /** 11 | * Plugin Name: Faster Updates 12 | * Author: WP Core Contributors 13 | * Description: Speeds up plugin/theme updates by moving directories rather than recursively copying files. Only for updating from 'update-core.php'. 14 | * Version: 0.5.5 15 | * Network: true 16 | * License: MIT 17 | * Text Domain: faster-updates 18 | * Requires PHP: 5.6 19 | * Requires at least: 6.0 20 | * GitHub Plugin URI: https://github.com/afragen/faster-updates 21 | * Primary Branch: main 22 | */ 23 | 24 | namespace Faster_Updates; 25 | 26 | /* 27 | * Exit if called directly. 28 | * PHP version check and exit. 29 | */ 30 | if ( ! defined( 'WPINC' ) ) { 31 | die; 32 | } 33 | 34 | require_once __DIR__ . '/functions/move.php'; 35 | require_once __DIR__ . '/modules/plugins/class-main.php'; 36 | require_once __DIR__ . '/modules/themes/class-main.php'; 37 | 38 | new \Faster_Updates\Modules\Plugins\Main(); 39 | new \Faster_Updates\Modules\Themes\Main(); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andy Fragen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /testing_instructions.md: -------------------------------------------------------------------------------- 1 | # Testing Instructions 2 | 3 | 1. In `wp-config.php`, set `WP_DEBUG` and `WP_DEBUG_LOG` to true, and `WP_DEBUG_DISPLAY` to false. You can also install the WP Debugging plugin from the plugin repository to set these more easily. 😉 4 | 1. Install and activate older versions of some simple and complex plugins such as akismet jetpack mailpoet woocommerce wordpress-seo wpforms-lite. You can download older versions from the plugins repository by navigating to Development > Advanced, then scroll to the bottom and download an older version. WP-CLI users can pass a --version parameter with the version number to install and activate. You may also simply decrease the version number in the main plugin file locally. 5 | 1. Navigate to Dashboard > Updates. 6 | 1. Update one plugin. 7 | 1. Update two plugins. 8 | 1. Update the remaining plugins. 9 | 1. Check for errors in the admin screens and frontend. 10 | 1. Check for errors in `wp-content/debug.log`. 11 | 1. Check the plugin directories in `wp-content/plugins`. Ensure that the main directory, and subdirectories have files in them. 12 | 1. Report any errors you encounter, or let us know if you don't encounter any errors. 13 | 1. Feel free to do the same for a few themes. 14 | 15 | If you use VirtualBox in your development environment, commonly used in Chassis and VVV, please help. 16 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | # Faster Updates 2 | 3 | Plugin Name: Faster Updates 4 | Contributors: afragen, costdev, pbiron 5 | License: MIT 6 | Requires PHP: 5.6 7 | Requires at least: 6.0 8 | Tested up to: 6.2 9 | Stable Tag: x.x.x 10 | 11 | Speeds up plugin/theme updates by moving files rather than copying them. 12 | 13 | ## Description 14 | 15 | For testing only. Only works when updating from update-core.php page. [Testing instructions](https://github.com/afragen/faster-updates/blob/main/testing_instructions.md) 16 | 17 | Speeds up plugin/theme updates by moving files rather than copying them. Reduces the chance of running out of diskspace during updates. Lower memory usage reduces the chance of timeouts during updates. 18 | 19 | Substitution of move_dir for copy_dir adding more efficiency to the plugin/theme update process. This could improve the efficiency and performance for 99+% of users who opt-in and will likely fix #53832, #54166, and #34676. 20 | 21 | ### VirtualBox 22 | 23 | VirtualBox is being tested. If you encounter any problems while using VirtualBox please let us know. 24 | 25 | ## Changelog 26 | 27 | #### [unreleased] 28 | * initial pass 29 | * add generic hooks for overriding update processing for `update-core.php` 30 | * add `wp_opcache_invalidate_directory()` 31 | * add fixes for VirtualBox issues 32 | * removed unused hooks from `move_dir()` 33 | -------------------------------------------------------------------------------- /modules/themes/class-main.php: -------------------------------------------------------------------------------- 1 | 87 |
88 |

89 | 90 |
91 | wp_get_update_data(), 98 | ) 99 | ); 100 | 101 | require_once ABSPATH . 'wp-admin/admin-footer.php'; 102 | } 103 | 104 | /** 105 | * Bulk Theme Upgrade: Uses a custom theme upgrader class. 106 | * 107 | * Also replaces the 'action' GET parameter of 'update.php' 108 | * with 'update-selected-themes-faster'. 109 | * 110 | * @since 1.0.0 111 | * 112 | * @return void 113 | */ 114 | public function bulk_update() { 115 | if ( ! current_user_can( 'update_themes' ) ) { 116 | wp_die( __( 'Sorry, you are not allowed to update themes for this site.' ) ); 117 | } 118 | 119 | check_admin_referer( 'bulk-update-themes' ); 120 | 121 | if ( isset( $_GET['themes'] ) ) { 122 | $themes = explode( ',', stripslashes( $_GET['themes'] ) ); 123 | } elseif ( isset( $_POST['checked'] ) ) { 124 | $themes = (array) $_POST['checked']; 125 | } else { 126 | $themes = array(); 127 | } 128 | 129 | $themes = array_map( 'urldecode', $themes ); 130 | 131 | $url = 'update.php?action=update-selected-themes-faster&themes=' . urlencode( implode( ',', $themes ) ); 132 | $nonce = 'bulk-update-themes'; 133 | 134 | wp_enqueue_script( 'updates' ); 135 | iframe_header(); 136 | 137 | $upgrader = new Upgrader( new \Bulk_Theme_Upgrader_Skin( compact( 'nonce', 'url' ) ) ); 138 | $upgrader->bulk_upgrade( $themes ); 139 | 140 | iframe_footer(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /modules/plugins/class-main.php: -------------------------------------------------------------------------------- 1 | 87 |
88 |

89 | 90 |
91 | wp_get_update_data(), 98 | ) 99 | ); 100 | 101 | require_once ABSPATH . 'wp-admin/admin-footer.php'; 102 | } 103 | 104 | /** 105 | * Bulk Plugin Upgrade: Uses a custom plugin upgrader class. 106 | * 107 | * Also replaces the 'action' GET parameter of 'update.php' 108 | * with 'update-selected-plugins-faster'. 109 | * 110 | * @since 1.0.0 111 | * 112 | * @return void 113 | */ 114 | public function bulk_update() { 115 | if ( ! current_user_can( 'update_plugins' ) ) { 116 | wp_die( __( 'Sorry, you are not allowed to update plugins for this site.' ) ); 117 | } 118 | 119 | check_admin_referer( 'bulk-update-plugins' ); 120 | 121 | if ( isset( $_GET['plugins'] ) ) { 122 | $plugins = explode( ',', stripslashes( $_GET['plugins'] ) ); 123 | } elseif ( isset( $_POST['checked'] ) ) { 124 | $plugins = (array) $_POST['checked']; 125 | } else { 126 | $plugins = array(); 127 | } 128 | 129 | $plugins = array_map( 'urldecode', $plugins ); 130 | 131 | $url = 'update.php?action=update-selected-plugins-faster&plugins=' . urlencode( implode( ',', $plugins ) ); 132 | $nonce = 'bulk-update-plugins'; 133 | 134 | wp_enqueue_script( 'updates' ); 135 | iframe_header(); 136 | 137 | $upgrader = new Upgrader( new \Bulk_Plugin_Upgrader_Skin( compact( 'nonce', 'url' ) ) ); 138 | $upgrader->bulk_upgrade( $plugins ); 139 | 140 | iframe_footer(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /functions/move.php: -------------------------------------------------------------------------------- 1 | exists( $to ) ) { 47 | return new WP_Error( 48 | 'to_directory_already_exists_move_dir', 49 | sprintf( 50 | /* translators: %s: The '$to' argument name. */ 51 | __( '%s already exists.' ), 52 | '$to' 53 | ) 54 | ); 55 | } 56 | 57 | if ( trailingslashit( $from ) === trailingslashit( $to ) ) { 58 | return false; 59 | } 60 | 61 | $result = false; 62 | 63 | if ( 'direct' === $wp_filesystem->method ) { 64 | if ( $wp_filesystem->delete( $to, true ) ) { 65 | $result = @rename( $from, $to ); 66 | } 67 | } else { 68 | // Non-direct filesystems use some version of rename without a fallback. 69 | $result = $wp_filesystem->move( $from, $to, $overwrite ); 70 | } 71 | 72 | if ( $result ) { 73 | /* 74 | * When using an environment with shared folders, 75 | * there is a delay in updating the filesystem's cache. 76 | * 77 | * This is a known issue in environments with a VirtualBox provider. 78 | * 79 | * A 200ms delay gives time for the filesystem to update its cache, 80 | * prevents "Operation not permitted", and "No such file or directory" warnings. 81 | * 82 | * This delay is used in other projects, including Composer. 83 | * @link https://github.com/composer/composer/blob/main/src/Composer/Util/Platform.php#L228-L233 84 | */ 85 | usleep( 200000 ); 86 | wp_opcache_invalidate_directory( $to ); 87 | } 88 | 89 | if ( ! $result ) { 90 | if ( ! $wp_filesystem->is_dir( $to ) ) { 91 | if ( ! $wp_filesystem->mkdir( $to, FS_CHMOD_DIR ) ) { 92 | return new \WP_Error( 'mkdir_failed_move_dir', __( 'Could not create directory.' ), $to ); 93 | } 94 | } 95 | 96 | $result = copy_dir( $from, $to, array( basename( $to ) ) ); 97 | 98 | // Clear the source directory. 99 | if ( ! is_wp_error( $result ) ) { 100 | $wp_filesystem->delete( $from, true ); 101 | } 102 | } 103 | 104 | return $result; 105 | } 106 | 107 | /** 108 | * Invalidate OPcache of directory of files. 109 | * 110 | * @since 6.2.0 111 | * 112 | * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 113 | * 114 | * @param string $dir The path to invalidate. 115 | * @return void 116 | */ 117 | function wp_opcache_invalidate_directory( $dir ) { 118 | global $wp_filesystem; 119 | 120 | if ( ! is_string( $dir ) || '' === trim( $dir ) ) { 121 | if ( WP_DEBUG ) { 122 | $error_message = sprintf( 123 | /* translators: %s: The function name. */ 124 | __( '%s expects a non-empty string.' ), 125 | 'wp_opcache_invalidate_directory()' 126 | ); 127 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error 128 | trigger_error( $error_message ); 129 | } 130 | return; 131 | } 132 | 133 | $dirlist = $wp_filesystem->dirlist( $dir, false, true ); 134 | 135 | if ( empty( $dirlist ) ) { 136 | return; 137 | } 138 | 139 | /* 140 | * Recursively invalidate opcache of files in a directory. 141 | * 142 | * WP_Filesystem_*::dirlist() returns an array of file and directory information. 143 | * 144 | * This does not include a path to the file or directory. 145 | * To invalidate files within sub-directories, recursion is needed 146 | * to prepend an absolute path containing the sub-directory's name. 147 | * 148 | * @param array $dirlist Array of file/directory information from WP_Filesystem_Base::dirlist(), 149 | * with sub-directories represented as nested arrays. 150 | * @param string $path Absolute path to the directory. 151 | */ 152 | $invalidate_directory = function( $dirlist, $path ) use ( &$invalidate_directory ) { 153 | $path = trailingslashit( $path ); 154 | 155 | foreach ( $dirlist as $name => $details ) { 156 | if ( 'f' === $details['type'] ) { 157 | wp_opcache_invalidate( $path . $name, true ); 158 | continue; 159 | } 160 | 161 | if ( is_array( $details['files'] ) && ! empty( $details['files'] ) ) { 162 | $invalidate_directory( $details['files'], $path . $name ); 163 | } 164 | } 165 | }; 166 | 167 | $invalidate_directory( $dirlist, $dir ); 168 | } 169 | -------------------------------------------------------------------------------- /modules/themes/class-upgrader.php: -------------------------------------------------------------------------------- 1 | '', // Please always pass this. 64 | 'destination' => '', // ...and this. 65 | 'clear_destination' => false, 66 | 'clear_working' => false, 67 | 'abort_if_destination_exists' => true, 68 | 'hook_extra' => array(), 69 | ); 70 | 71 | $args = wp_parse_args( $args, $defaults ); 72 | 73 | // These were previously extract()'d. 74 | $source = $args['source']; 75 | $destination = $args['destination']; 76 | $clear_destination = $args['clear_destination']; 77 | 78 | set_time_limit( 300 ); 79 | 80 | if ( empty( $source ) || empty( $destination ) ) { 81 | return new \WP_Error( 'bad_request', $this->strings['bad_request'] ); 82 | } 83 | $this->skin->feedback( 'installing_package' ); 84 | 85 | /** 86 | * Filters the installation response before the installation has started. 87 | * 88 | * Returning a value that could be evaluated as a `WP_Error` will effectively 89 | * short-circuit the installation, returning that value instead. 90 | * 91 | * @since 2.8.0 92 | * 93 | * @param bool|WP_Error $response Installation response. 94 | * @param array $hook_extra Extra arguments passed to hooked filters. 95 | */ 96 | $res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] ); 97 | 98 | if ( is_wp_error( $res ) ) { 99 | return $res; 100 | } 101 | 102 | // Retain the original source and destinations. 103 | $remote_source = $args['source']; 104 | $local_destination = $destination; 105 | 106 | $source_files = array_keys( $wp_filesystem->dirlist( $remote_source ) ); 107 | $remote_destination = $wp_filesystem->find_folder( $local_destination ); 108 | 109 | // Locate which directory to copy to the new folder. This is based on the actual folder holding the files. 110 | if ( 1 === count( $source_files ) && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) { 111 | // Only one folder? Then we want its contents. 112 | $source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] ); 113 | } elseif ( 0 === count( $source_files ) ) { 114 | // There are no files? 115 | return new \WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] ); 116 | } else { 117 | // It's only a single file, the upgrader will use the folder name of this file as the destination folder. 118 | // Folder name is based on zip filename. 119 | $source = trailingslashit( $args['source'] ); 120 | } 121 | 122 | /** 123 | * Filters the source file location for the upgrade package. 124 | * 125 | * @since 2.8.0 126 | * @since 4.4.0 The $hook_extra parameter became available. 127 | * 128 | * @param string $source File source location. 129 | * @param string $remote_source Remote file source location. 130 | * @param WP_Upgrader $upgrader WP_Upgrader instance. 131 | * @param array $hook_extra Extra arguments passed to hooked filters. 132 | */ 133 | $source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] ); 134 | 135 | if ( is_wp_error( $source ) ) { 136 | return $source; 137 | } 138 | 139 | // Has the source location changed? If so, we need a new source_files list. 140 | if ( $source !== $remote_source ) { 141 | $source_files = array_keys( $wp_filesystem->dirlist( $source ) ); 142 | } 143 | 144 | /* 145 | * Protection against deleting files in any important base directories. 146 | * Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the 147 | * destination directory (WP_PLUGIN_DIR / wp-content/themes) intending 148 | * to copy the directory into the directory, whilst they pass the source 149 | * as the actual files to copy. 150 | */ 151 | $protected_directories = array( ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' ); 152 | 153 | if ( is_array( $wp_theme_directories ) ) { 154 | $protected_directories = array_merge( $protected_directories, $wp_theme_directories ); 155 | } 156 | 157 | if ( in_array( $destination, $protected_directories, true ) ) { 158 | $remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) ); 159 | $destination = trailingslashit( $destination ) . trailingslashit( basename( $source ) ); 160 | } 161 | 162 | if ( $clear_destination ) { 163 | // We're going to clear the destination if there's something there. 164 | $this->skin->feedback( 'remove_old' ); 165 | 166 | $removed = $this->clear_destination( $remote_destination ); 167 | 168 | /** 169 | * Filters whether the upgrader cleared the destination. 170 | * 171 | * @since 2.8.0 172 | * 173 | * @param true|WP_Error $removed Whether the destination was cleared. 174 | * True upon success, WP_Error on failure. 175 | * @param string $local_destination The local package destination. 176 | * @param string $remote_destination The remote package destination. 177 | * @param array $hook_extra Extra arguments passed to hooked filters. 178 | */ 179 | $removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] ); 180 | 181 | if ( is_wp_error( $removed ) ) { 182 | return $removed; 183 | } 184 | } elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) { 185 | // If we're not clearing the destination folder and something exists there already, bail. 186 | // But first check to see if there are actually any files in the folder. 187 | $_files = $wp_filesystem->dirlist( $remote_destination ); 188 | if ( ! empty( $_files ) ) { 189 | $wp_filesystem->delete( $remote_source, true ); // Clear out the source files. 190 | return new \WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination ); 191 | } 192 | } 193 | 194 | // Create destination if needed. 195 | if ( ! $wp_filesystem->exists( $remote_destination ) ) { 196 | if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) { 197 | return new \WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination ); 198 | } 199 | } 200 | 201 | $result = \Faster_Updates\Functions\move_dir( $source, $remote_destination, true ); 202 | 203 | // Clear the working folder? 204 | if ( $args['clear_working'] ) { 205 | $wp_filesystem->delete( $remote_source, true ); 206 | } 207 | 208 | if ( is_wp_error( $result ) ) { 209 | return $result; 210 | } 211 | 212 | $destination_name = basename( str_replace( $local_destination, '', $destination ) ); 213 | if ( '.' === $destination_name ) { 214 | $destination_name = ''; 215 | } 216 | 217 | $this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' ); 218 | 219 | /** 220 | * Filters the installation response after the installation has finished. 221 | * 222 | * @since 2.8.0 223 | * 224 | * @param bool $response Installation response. 225 | * @param array $hook_extra Extra arguments passed to hooked filters. 226 | * @param array $result Installation result data. 227 | */ 228 | $res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result ); 229 | 230 | if ( is_wp_error( $res ) ) { 231 | $this->result = $res; 232 | return $res; 233 | } 234 | 235 | // Bombard the calling function will all the info which we've just used. 236 | return $this->result; 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /modules/plugins/class-upgrader.php: -------------------------------------------------------------------------------- 1 | '', // Please always pass this. 64 | 'destination' => '', // ...and this. 65 | 'clear_destination' => false, 66 | 'clear_working' => false, 67 | 'abort_if_destination_exists' => true, 68 | 'hook_extra' => array(), 69 | ); 70 | 71 | $args = wp_parse_args( $args, $defaults ); 72 | 73 | // These were previously extract()'d. 74 | $source = $args['source']; 75 | $destination = $args['destination']; 76 | $clear_destination = $args['clear_destination']; 77 | 78 | set_time_limit( 300 ); 79 | 80 | if ( empty( $source ) || empty( $destination ) ) { 81 | return new \WP_Error( 'bad_request', $this->strings['bad_request'] ); 82 | } 83 | $this->skin->feedback( 'installing_package' ); 84 | 85 | /** 86 | * Filters the installation response before the installation has started. 87 | * 88 | * Returning a value that could be evaluated as a `WP_Error` will effectively 89 | * short-circuit the installation, returning that value instead. 90 | * 91 | * @since 2.8.0 92 | * 93 | * @param bool|WP_Error $response Installation response. 94 | * @param array $hook_extra Extra arguments passed to hooked filters. 95 | */ 96 | $res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] ); 97 | 98 | if ( is_wp_error( $res ) ) { 99 | return $res; 100 | } 101 | 102 | // Retain the original source and destinations. 103 | $remote_source = $args['source']; 104 | $local_destination = $destination; 105 | 106 | $source_files = array_keys( $wp_filesystem->dirlist( $remote_source ) ); 107 | $remote_destination = $wp_filesystem->find_folder( $local_destination ); 108 | 109 | // Locate which directory to copy to the new folder. This is based on the actual folder holding the files. 110 | if ( 1 === count( $source_files ) && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) { 111 | // Only one folder? Then we want its contents. 112 | $source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] ); 113 | } elseif ( 0 === count( $source_files ) ) { 114 | // There are no files? 115 | return new \WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] ); 116 | } else { 117 | // It's only a single file, the upgrader will use the folder name of this file as the destination folder. 118 | // Folder name is based on zip filename. 119 | $source = trailingslashit( $args['source'] ); 120 | } 121 | 122 | /** 123 | * Filters the source file location for the upgrade package. 124 | * 125 | * @since 2.8.0 126 | * @since 4.4.0 The $hook_extra parameter became available. 127 | * 128 | * @param string $source File source location. 129 | * @param string $remote_source Remote file source location. 130 | * @param WP_Upgrader $upgrader WP_Upgrader instance. 131 | * @param array $hook_extra Extra arguments passed to hooked filters. 132 | */ 133 | $source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] ); 134 | 135 | if ( is_wp_error( $source ) ) { 136 | return $source; 137 | } 138 | 139 | // Has the source location changed? If so, we need a new source_files list. 140 | if ( $source !== $remote_source ) { 141 | $source_files = array_keys( $wp_filesystem->dirlist( $source ) ); 142 | } 143 | 144 | /* 145 | * Protection against deleting files in any important base directories. 146 | * Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the 147 | * destination directory (WP_PLUGIN_DIR / wp-content/themes) intending 148 | * to copy the directory into the directory, whilst they pass the source 149 | * as the actual files to copy. 150 | */ 151 | $protected_directories = array( ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' ); 152 | 153 | if ( is_array( $wp_theme_directories ) ) { 154 | $protected_directories = array_merge( $protected_directories, $wp_theme_directories ); 155 | } 156 | 157 | if ( in_array( $destination, $protected_directories, true ) ) { 158 | $remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) ); 159 | $destination = trailingslashit( $destination ) . trailingslashit( basename( $source ) ); 160 | } 161 | 162 | if ( $clear_destination ) { 163 | // We're going to clear the destination if there's something there. 164 | $this->skin->feedback( 'remove_old' ); 165 | 166 | $removed = $this->clear_destination( $remote_destination ); 167 | 168 | /** 169 | * Filters whether the upgrader cleared the destination. 170 | * 171 | * @since 2.8.0 172 | * 173 | * @param true|WP_Error $removed Whether the destination was cleared. 174 | * True upon success, WP_Error on failure. 175 | * @param string $local_destination The local package destination. 176 | * @param string $remote_destination The remote package destination. 177 | * @param array $hook_extra Extra arguments passed to hooked filters. 178 | */ 179 | $removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] ); 180 | 181 | if ( is_wp_error( $removed ) ) { 182 | return $removed; 183 | } 184 | } elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) { 185 | // If we're not clearing the destination folder and something exists there already, bail. 186 | // But first check to see if there are actually any files in the folder. 187 | $_files = $wp_filesystem->dirlist( $remote_destination ); 188 | if ( ! empty( $_files ) ) { 189 | $wp_filesystem->delete( $remote_source, true ); // Clear out the source files. 190 | return new \WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination ); 191 | } 192 | } 193 | 194 | // Create destination if needed. 195 | if ( ! $wp_filesystem->exists( $remote_destination ) ) { 196 | if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) { 197 | return new \WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination ); 198 | } 199 | } 200 | 201 | $result = \Faster_Updates\Functions\move_dir( $source, $remote_destination, true ); 202 | 203 | // Clear the working folder? 204 | if ( $args['clear_working'] ) { 205 | $wp_filesystem->delete( $remote_source, true ); 206 | } 207 | 208 | if ( is_wp_error( $result ) ) { 209 | return $result; 210 | } 211 | 212 | $destination_name = basename( str_replace( $local_destination, '', $destination ) ); 213 | if ( '.' === $destination_name ) { 214 | $destination_name = ''; 215 | } 216 | 217 | $this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' ); 218 | 219 | /** 220 | * Filters the installation response after the installation has finished. 221 | * 222 | * @since 2.8.0 223 | * 224 | * @param bool $response Installation response. 225 | * @param array $hook_extra Extra arguments passed to hooked filters. 226 | * @param array $result Installation result data. 227 | */ 228 | $res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result ); 229 | 230 | if ( is_wp_error( $res ) ) { 231 | $this->result = $res; 232 | return $res; 233 | } 234 | 235 | // Bombard the calling function will all the info which we've just used. 236 | return $this->result; 237 | } 238 | 239 | } 240 | --------------------------------------------------------------------------------