$max_bytes ) { return false; } wp_mkdir_p( dirname( $target ) ); self::protect_storage(); $zip = new \ZipArchive(); $root = basename( $source ); if ( true !== $zip->open( $target, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) ) { return false; } foreach ( $files as $file ) { $zip->addFile( $file, $root . '/' . ltrim( str_replace( '\\', '/', substr( $file, strlen( $source ) ) ), '/' ) ); } if ( ! $zip->close() ) { wp_delete_file( $target ); return false; } self::prune( $component, $key ); return $target; } /** * Archived versions of a component, newest first. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @return array Version => archive path. */ public static function versions( $component, $key ) { $dir = self::component_dir( $component, $key ); if ( ! $dir || ! is_dir( $dir ) ) { return array(); } $found = array(); foreach ( (array) glob( $dir . '/*.zip' ) as $file ) { $found[ basename( $file, '.zip' ) ] = $file; } uksort( $found, static function ( $a, $b ) { return version_compare( $b, $a ); } ); return $found; } /** * Whether a version is archived and can be restored. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @param string $version Version. * @return bool */ public static function has( $component, $key, $version ) { $path = self::path( $component, $key, $version ); return $path && file_exists( $path ); } /** * Reinstalls an archived version over the current one. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @param string $version Version to restore. * @return true|\WP_Error */ public static function restore( $component, $key, $version ) { if ( ! self::has( $component, $key, $version ) ) { return new \WP_Error( 'missing', __( 'That version is no longer archived.', 'maintained-log' ) ); } require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/plugin.php'; if ( ! WP_Filesystem() ) { return new \WP_Error( 'filesystem', __( 'WordPress cannot write files directly on this server, so the previous version cannot be reinstalled automatically.', 'maintained-log' ) ); } // The version being replaced is archived too, so the rollback can itself be undone. $current = self::installed_version( $component, $key ); if ( $current && $current !== $version && Settings::get( 'rollback_enabled' ) ) { self::archive( $component, $key ); } $skin = new \WP_Ajax_Upgrader_Skin(); $upgrader = 'plugin' === $component ? new \Plugin_Upgrader( $skin ) : new \Theme_Upgrader( $skin ); $result = $upgrader->install( self::path( $component, $key, $version ), array( 'overwrite_package' => true ) ); if ( is_wp_error( $result ) ) { return $result; } if ( ! $result ) { $errors = $skin->get_errors(); return $errors->has_errors() ? $errors : new \WP_Error( 'install_failed', __( 'The previous version could not be installed.', 'maintained-log' ) ); } return true; } /** * Archive path for a version. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @param string $version Version. * @return string Empty if the component cannot be archived. */ public static function path( $component, $key, $version ) { $dir = self::component_dir( $component, $key ); return $dir ? $dir . '/' . sanitize_file_name( $version ) . '.zip' : ''; } /** * Base storage directory. The random suffix keeps archives (which may * include premium plugin code) from being guessed on servers that ignore * .htaccess. * * @return string */ public static function storage_dir() { $uploads = wp_upload_dir( null, false ); return trailingslashit( $uploads['basedir'] ) . 'maintained-log-rollback-' . substr( wp_hash( 'maintained-log-rollback' ), 0, 12 ); } /** * Folder of a plugin or theme, or false if it cannot be archived. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @return string|false */ private static function source_dir( $component, $key ) { if ( 'plugin' === $component ) { $folder = dirname( $key ); // Single-file plugins live directly in the plugins folder: nothing to isolate. if ( '.' === $folder || '' === $folder ) { return false; } $dir = WP_PLUGIN_DIR . '/' . $folder; } else { $theme = wp_get_theme( $key ); if ( ! $theme->exists() ) { return false; } $dir = $theme->get_stylesheet_directory(); } return is_dir( $dir ) ? untrailingslashit( wp_normalize_path( $dir ) ) : false; } /** * Installed version of a plugin or theme. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @return string */ private static function installed_version( $component, $key ) { if ( 'plugin' === $component ) { $file = WP_PLUGIN_DIR . '/' . $key; if ( ! file_exists( $file ) ) { return ''; } if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $data = get_plugin_data( $file, false, false ); return (string) $data['Version']; } $theme = wp_get_theme( $key ); return $theme->exists() ? (string) $theme->get( 'Version' ) : ''; } /** * Storage folder for one component. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. * @return string|false */ private static function component_dir( $component, $key ) { if ( ! in_array( $component, array( 'plugin', 'theme' ), true ) ) { return false; } $slug = 'plugin' === $component ? dirname( $key ) : $key; $slug = sanitize_file_name( $slug ); if ( '' === $slug || '.' === $slug ) { return false; } return self::storage_dir() . '/' . $component . 's/' . $slug; } /** * All files under a folder. * * @param string $dir Folder. * @return string[] */ private static function list_files( $dir ) { $files = array(); $items = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ) ); foreach ( $items as $item ) { if ( $item->isFile() ) { $files[] = wp_normalize_path( $item->getPathname() ); } } return $files; } /** * Keeps only the configured number of archives per component. * * @param string $component plugin|theme. * @param string $key Plugin file or theme slug. */ private static function prune( $component, $key ) { $keep = max( 1, (int) Settings::get( 'rollback_keep' ) ); $versions = array_values( self::versions( $component, $key ) ); foreach ( array_slice( $versions, $keep ) as $file ) { wp_delete_file( $file ); } } /** * Blocks direct web access to the archives where the server allows it. */ private static function protect_storage() { $base = self::storage_dir(); if ( ! file_exists( $base . '/index.php' ) ) { file_put_contents( $base . '/index.php', "\nDeny from all\n\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents } } }