diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 6cdf5b19..f5ec4810 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -18,6 +18,7 @@ const TAB_DIRECT = 'fair_direct'; const ACTION_INSTALL = 'fair-install-plugin'; const ACTION_INSTALL_NONCE = 'fair-install-plugin'; +const ACTION_INSTALL_DID = 'fair-install-did'; /** * Bootstrap. @@ -29,10 +30,12 @@ function bootstrap() { add_filter( 'install_plugins_tabs', __NAMESPACE__ . '\\add_direct_tab' ); add_filter( 'plugins_api', __NAMESPACE__ . '\\handle_did_during_ajax', 10, 3 ); + add_filter( 'upgrader_pre_download', 'FAIR\\Packages\\upgrader_pre_download', 10, 1 ); add_action( 'install_plugins_' . TAB_DIRECT, __NAMESPACE__ . '\\render_tab_direct' ); add_action( 'load-plugin-install.php', __NAMESPACE__ . '\\load_plugin_install' ); add_action( 'install_plugins_pre_plugin-information', __NAMESPACE__ . '\\maybe_hijack_plugin_info', 0 ); add_action( 'update-custom_' . ACTION_INSTALL, __NAMESPACE__ . '\\handle_direct_install' ); + add_action( 'wp_ajax_check_plugin_dependencies', __NAMESPACE__ . '\\set_slug_to_hashed' ); } /** @@ -51,7 +54,7 @@ function add_direct_tab( $tabs ) { * * @param mixed $result The result of the plugins_api call. * @param string $action The action being performed. - * @param array $args The arguments passed to the plugins_api call. + * @param object $args The arguments passed to the plugins_api call. * @return mixed */ function handle_did_during_ajax( $result, $action, $args ) { @@ -69,14 +72,11 @@ function handle_did_during_ajax( $result, $action, $args ) { return $result; } - $release = Updater\get_latest_release_from_did( $did ); - if ( is_wp_error( $release ) ) { - return $release; - } + wp_cache_set( ACTION_INSTALL_DID, $did ); + Updater\add_package_to_release_cache( $did ); + add_filter( 'http_request_args', 'FAIR\\Updater\\maybe_add_accept_header', 20, 2 ); - return (object) [ - 'download_link' => $release->artifacts->package[0]->url, - ]; + return (object) Packages\get_update_data( $did ); } /** @@ -235,11 +235,34 @@ function handle_direct_install() { } $skin = new WP_Upgrader_Skin(); - $res = Packages\install_plugin( $id, $skin, $version ); - var_dump( $res ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_dump + Packages\install_plugin( $id, $skin, $version ); exit; } +/** + * Set slug to hashed slug from escaped slug-did. + * + * Needed for check_plugin_dependencies_during_ajax(). + * + * @return void + */ +function set_slug_to_hashed() : void { + // phpcs:disable HM.Security.NonceVerification.Missing + if ( ! isset( $_POST['slug'] ) ) { + return; + } + + $escaped_slug = sanitize_text_field( wp_unslash( $_POST['slug'] ) ); + $did = 'did:' . explode( '-did:', str_replace( '--', ':', $escaped_slug ), 2 )[1]; + if ( ! preg_match( '/^did:(web|plc):.+$/', $did ) ) { + return; + } + + // Reset to proper hashed slug. + $_POST['slug'] = explode( '-did--', $escaped_slug, 2 )[0] . '-' . Packages\get_did_hash( $did ); + // phpcs:enable +} + /** * Hijack embedded info page. * diff --git a/inc/packages/class-metadatadocument.php b/inc/packages/class-metadatadocument.php index 8ff3cf30..26aa6952 100644 --- a/inc/packages/class-metadatadocument.php +++ b/inc/packages/class-metadatadocument.php @@ -42,6 +42,13 @@ class MetadataDocument { */ public $slug; + /** + * File name. + * + * @var string + */ + public $filename; + /** * License. * @@ -124,6 +131,7 @@ public static function from_data( stdClass $data ) { $optional = [ 'name', 'slug', + 'filename', 'description', 'keywords', 'sections', diff --git a/inc/packages/class-upgrader.php b/inc/packages/class-upgrader.php index 171ed84d..4da565a0 100644 --- a/inc/packages/class-upgrader.php +++ b/inc/packages/class-upgrader.php @@ -244,7 +244,6 @@ protected function run_install( string $destination, $options ) { $artifact = pick_artifact_by_lang( $this->release->artifacts->package ); add_package_to_release_cache( $this->package->id ); - add_filter( 'upgrader_pre_download', 'FAIR\\Updater\\upgrader_pre_download', 10, 1 ); // Download the package. $path = $this->download_package( $artifact->url, false, $options['hook_extra'] ); @@ -442,7 +441,7 @@ public function install_theme( $clear_cache, $overwrite ) { */ public function install( MetadataDocument $package, ReleaseDocument $release, $clear_cache = true, $overwrite = false ) { $this->init(); - // $this->install_strings(); + $this->install_strings(); $this->package = $package; $this->release = $release; @@ -458,6 +457,50 @@ public function install( MetadataDocument $package, ReleaseDocument $release, $c return new WP_Error( 'fair.packages.upgrader.install.invalid_type', 'Invalid package type.' ); } } + + /** + * Retrieves the hashed path to the file that contains the plugin info. + * + * This isn't used internally in the class, but is called by the skins. + * + * @since WordPress 2.8.0 + * + * @return string|false The full path to the main plugin file, or false. + */ + public function plugin_info() { + if ( ! isset( $this->package ) ) { + return false; + } + + return get_hashed_filename( $this->package ); + } + + /** + * Gets the WP_Theme object for a theme. + * + * @since WordPress 2.8.0 + * @since WordPress 3.0.0 The `$theme` argument was added. + * + * @param string $theme The directory name of the theme. This is optional, and if not supplied, + * the directory name from the last result will be used. + * @return WP_Theme|false The theme's info object, or false `$theme` is not supplied + * and the last result isn't set. + */ + public function theme_info( $theme = null ) { + if ( empty( $theme ) ) { + if ( ! empty( $this->result['destination_name'] ) ) { + $theme = $this->result['destination_name']; + } else { + return false; + } + } + + $theme = wp_get_theme( $theme ); + $theme->cache_delete(); + + return $theme; + } + /** * Checks that the source package contains a valid plugin. * @@ -663,29 +706,4 @@ protected function validate_theme( $dir ) { $this->new_theme_data = $info; } - /** - * Renames a package's directory when it doesn't match the slug. - * - * This is commonly required for packages from Git hosts. - * - * @param string $source Path of $source. - * @param string $remote_source Path of $remote_source. - * - * @return string - */ - public function rename_source_selection( string $source, string $remote_source ) { - global $wp_filesystem; - - if ( str_contains( $source, get_did_hash( $this->package->id ) ) && basename( $source ) === $this->package->slug ) { - return $source; - } - - $new_source = trailingslashit( $remote_source ) . $this->package->slug . '-' . get_did_hash( $this->package->id ); - - if ( trailingslashit( strtolower( $source ) ) !== trailingslashit( strtolower( $new_source ) ) ) { - $wp_filesystem->move( $source, $new_source, true ); - } - - return trailingslashit( $new_source ); - } } diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 7b26463c..99269501 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -9,8 +9,9 @@ use FAIR\Packages\DID\PLC; use FAIR\Packages\DID\Web; -use function FAIR\Updater\get_packages; +use FAIR\Updater; use WP_Error; +use WP_Upgrader; use WP_Upgrader_Skin; const SERVICE_ID = 'FairPackageManagementRepo'; @@ -138,20 +139,13 @@ function install_plugin( string $id, WP_Upgrader_Skin $skin, ?string $version = return $document; } - // Fetch data from the repository. - $service = $document->get_service( SERVICE_ID ); - if ( empty( $service ) ) { - return new WP_Error( 'fair.packages.install_plugin.no_service', __( 'DID is not a valid package to install.', 'fair' ) ); - } - $repo_url = $service->serviceEndpoint; - // Filter to valid keys for signing. $valid_keys = $document->get_fair_signing_keys(); if ( empty( $valid_keys ) ) { return new WP_Error( 'fair.packages.install_plugin.no_signing_keys', __( 'DID does not contain valid signing keys.', 'fair' ) ); } - $metadata = fetch_metadata_doc( $repo_url ); + $metadata = fetch_package_metadata( $id ); if ( is_wp_error( $metadata ) ) { return $metadata; } @@ -162,7 +156,8 @@ function install_plugin( string $id, WP_Upgrader_Skin $skin, ?string $version = return new WP_Error( 'fair.packages.install_plugin.no_releases', __( 'No releases found in the repository.', 'fair' ) ); } - $upgrader = new Upgrader( $skin ); + $skin_class = ucwords( str_replace( 'wp-', '', $metadata->type ) ) . '_Installer_Skin'; + $upgrader = new Upgrader( new $skin_class() ); return $upgrader->install( $metadata, $release ); } @@ -214,6 +209,37 @@ function pick_release( array $releases, ?string $version = null ) : ?ReleaseDocu return array_find( $releases, fn ( $release ) => $release->version === $version ); } +/** + * Get the latest release for a DID. + * + * @param string $id DID. + * + * @return ReleaseDocument|WP_Error The latest release, or a WP_Error object on failure. + */ +function get_latest_release_from_did( $id ) { + $document = get_did_document( $id ); + if ( is_wp_error( $document ) ) { + return $document; + } + + $valid_keys = $document->get_fair_signing_keys(); + if ( empty( $valid_keys ) ) { + return new WP_Error( 'fair.packages.install_plugin.no_signing_keys', __( 'DID does not contain valid signing keys.', 'fair' ) ); + } + + $metadata = fetch_package_metadata( $id ); + if ( is_wp_error( $metadata ) ) { + return $metadata; + } + + $release = pick_release( $metadata->releases ); + if ( empty( $release ) ) { + return new WP_Error( 'fair.packages.install_plugin.no_releases', __( 'No releases found in the repository.', 'fair' ) ); + } + + return $release; +} + /** * Get viable languages for a given locale. * @@ -438,7 +464,7 @@ function check_requirements( ReleaseDocument $release ) { */ function get_installed_version( string $id, string $type ) { $type .= 's'; - $packages = get_packages(); + $packages = Updater\get_packages(); if ( empty( $packages[ $type ][ $id ] ) ) { // Not installed. @@ -448,4 +474,191 @@ function get_installed_version( string $id, string $type ) { return get_file_data( $packages[ $type ][ $id ], [ 'Version' => 'Version' ] )['Version']; } +/** + * Get icons. + * + * @param array $icons Array of icon data. + * + * @return array + */ +function get_icons( $icons ) : array { + if ( empty( $icons ) ) { + return []; + } + + $icons_arr = []; + $regular = array_find( $icons, fn ( $icon ) => $icon->width === 772 && $icon->height === 250 ); + $high_res = array_find( $icons, fn ( $icon ) => $icon->width === 1544 && $icon->height === 500 ); + $svg = array_find( $icons, fn ( $icon ) => str_contains( $icon->{'content-type'}, 'svg+xml' ) ); + + if ( empty( $regular ) && empty( $high_res ) && empty( $svg ) ) { + return []; + } + + $icons_arr['1x'] = $regular->url ?? ''; + $icons_arr['2x'] = $high_res->url ?? ''; + if ( str_contains( $svg->url, 's.w.org/plugins' ) ) { + $icons_arr['default'] = $svg->url; + } else { + $icons_arr['svg'] = $svg->url ?? ''; + } + + return $icons_arr; +} + +/** + * Get banners. + * + * @param array $banners Array of banner data. + * + * @return array + */ +function get_banners( $banners ) : array { + if ( empty( $banners ) ) { + return []; + } + + $banners_arr = []; + $regular = array_find( $banners, fn ( $banner ) => $banner->width === 772 && $banner->height === 250 ); + $high_res = array_find( $banners, fn ( $banner ) => $banner->width === 1544 && $banner->height === 500 ); + + if ( empty( $regular ) && empty( $high_res ) ) { + return []; + } + + $banners_arr['low'] = $regular->url; + $banners_arr['high'] = $high_res->url; + + return $banners_arr; +} + +/** + * Get hashed file name from MetadataDocument. + * + * @param MetadataDocument $metadata MetadataDocument. + * + * @return string + */ +function get_hashed_filename( $metadata ) : string { + $filename = $metadata->filename; + $type = str_replace( 'wp-', '', $metadata->type ); + $did_hash = '-' . get_did_hash( $metadata->id ); + + list( $slug, $file ) = explode( '/', $filename, 2 ); + if ( 'plugin' === $type ) { + if ( ! str_contains( $slug, $did_hash ) ) { + $slug .= $did_hash; + } + $filename = $slug . '/' . $file; + } else { + $filename = $slug . $did_hash; + } + + return $filename; +} + +/** + * Get update data for use with transient and API responses. + * + * @param string $did DID. + * @return array|WP_Error + */ +function get_update_data( $did ) { + $metadata = fetch_package_metadata( $did ); + if ( is_wp_error( $metadata ) ) { + return $metadata; + } + + $release = get_latest_release_from_did( $did ); + if ( is_wp_error( $release ) ) { + return $release; + } + + $required_versions = version_requirements( $release ); + $filename = get_hashed_filename( $metadata ); + $type = str_replace( 'wp-', '', $metadata->type ); + + $response = [ + 'name' => $metadata->name, + 'author' => $metadata->authors[0]->name, + 'author_uri' => $metadata->authors[0]->url, + 'slug' => $metadata->slug . '-' . get_did_hash( $did ), + $type => $filename, + 'file' => $filename, + 'url' => $metadata->url ?? $metadata->slug, + 'sections' => (array) $metadata->sections, + 'icons' => isset( $release->artifacts->icon ) ? get_icons( $release->artifacts->icon ) : [], + 'banners' => isset( $release->artifacts->banner ) ? get_banners( $release->artifacts->banner ) : [], + 'update-supported' => true, + 'requires' => $required_versions['requires_wp'], + 'requires_php' => $required_versions['requires_php'], + 'new_version' => $release->version, + 'version' => $release->version, + 'remote_version' => $release->version, + 'package' => $release->artifacts->package[0]->url, + 'download_link' => $release->artifacts->package[0]->url, + 'tested' => $required_versions['tested_to'], + 'external' => 'xxx', + ]; + if ( 'theme' === $type ) { + $response['theme_uri'] = $response['url']; + } + + return $response; +} + +/** + * Send upgrader_pre_download filter to hook `upgrader_source_selection` during AJAX. + * + * @param bool $false Whether to bail without returning the package. + * Default false. + * @return bool + */ +function upgrader_pre_download( $false ) : bool { + add_filter( 'upgrader_source_selection', __NAMESPACE__ . '\\rename_source_selection', 10, 3 ); + return $false; +} + + /** + * Renames a package's directory when it doesn't match the slug. + * + * This is commonly required for packages from Git hosts. + * + * @param string $source Path of $source. + * @param string $remote_source Path of $remote_source. + * @param WP_Upgrader $upgrader An Upgrader object. + * + * @return string + */ +function rename_source_selection( string $source, string $remote_source, WP_Upgrader $upgrader ) { + global $wp_filesystem; + + $did = wp_cache_get( Admin\ACTION_INSTALL_DID ); + + if ( ! $did ) { + return $source; + } + + $metadata = fetch_package_metadata( $did ); + if ( is_wp_error( $metadata ) ) { + return $metadata; + } + + // Sanity check. + if ( $upgrader->new_plugin_data['Name'] !== $metadata->name ) { + return $source; + } + + if ( str_contains( $source, get_did_hash( $did ) ) && basename( $source ) === $metadata->slug ) { + return $source; + } + + $new_source = trailingslashit( $remote_source ) . $metadata->slug . '-' . get_did_hash( $did ); + + if ( trailingslashit( strtolower( $source ) ) !== trailingslashit( strtolower( $new_source ) ) ) { + $wp_filesystem->move( $source, $new_source, true ); + } + + return trailingslashit( $new_source ); +} // phpcs:enable diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index 70cda097..38455438 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -8,9 +8,6 @@ namespace FAIR\Updater; use FAIR\Packages; -use function FAIR\Packages\fetch_package_metadata; -use function FAIR\Packages\get_did_hash; - use Plugin_Upgrader; use stdClass; use Theme_Upgrader; @@ -106,11 +103,11 @@ public function run() { return; } - $this->metadata = fetch_package_metadata( $this->did ); + $this->metadata = Packages\fetch_package_metadata( $this->did ); if ( is_wp_error( $this->metadata ) ) { return $this->metadata; } - $this->release = get_latest_release_from_did( $this->did ); + $this->release = Packages\get_latest_release_from_did( $this->did ); if ( is_wp_error( $this->release ) ) { return $this->release; } @@ -133,7 +130,6 @@ public function load_hooks() { } add_package_to_release_cache( $this->did ); - add_filter( 'upgrader_pre_download', __NAMESPACE__ . '\\upgrader_pre_download', 10, 1 ); } /** @@ -204,12 +200,12 @@ public function repo_api_details( $result, string $action, stdClass $response ) } // Exit if not our repo. - $slug_arr = [ $this->metadata->slug, $this->metadata->slug . '-' . get_did_hash( $this->metadata->id ) ]; + $slug_arr = [ $this->metadata->slug, $this->metadata->slug . '-' . Packages\get_did_hash( $this->did ) ]; if ( ! in_array( $response->slug, $slug_arr, true ) ) { return $result; } - return (object) $this->get_update_data(); + return (object) Packages\get_update_data( $this->did ); } /** @@ -227,7 +223,10 @@ public function update_site_transient( $transient ) { $rel_path = plugin_basename( $this->filepath ); $rel_path = 'theme' === $this->type ? dirname( $rel_path ) : $rel_path; - $response = $this->get_update_data(); + $response = Packages\get_update_data( $this->did ); + if ( is_wp_error( $response ) ) { + return $transient; + } $response = 'plugin' === $this->type ? (object) $response : $response; $is_compatible = Packages\check_requirements( $this->release ); @@ -241,52 +240,6 @@ public function update_site_transient( $transient ) { return $transient; } - /** - * Get update data for use with transient and API responses. - * - * @return array - */ - public function get_update_data() { - $required_versions = Packages\version_requirements( $this->release ); - if ( 'plugin' === $this->type ) { - list( $slug, $file ) = explode( '/', plugin_basename( $this->filepath ), 2 ); - if ( ! str_contains( $slug, '-' . get_did_hash( $this->metadata->id ) ) ) { - $slug .= '-' . get_did_hash( $this->metadata->id ); - } - $filename = $slug . '/' . $file; - } else { - $filename = $this->metadata->slug . '-' . get_did_hash( $this->metadata->id ); - } - - $response = [ - 'name' => $this->metadata->name, - 'author' => $this->metadata->authors[0]->name, - 'author_uri' => $this->metadata->authors[0]->url, - 'slug' => $this->metadata->slug . '-' . get_did_hash( $this->metadata->id ), - $this->type => $filename, - 'file' => $filename, - 'url' => $this->metadata->url ?? $this->metadata->slug, - 'sections' => (array) $this->metadata->sections, - 'icons' => isset( $this->release->artifacts->icon ) ? get_icons( $this->release->artifacts->icon ) : [], - 'banners' => isset( $this->release->artifacts->banner ) ? get_banners( $this->release->artifacts->banner ) : [], - 'update-supported' => true, - 'requires' => $required_versions['requires_wp'], - 'requires_php' => $required_versions['requires_php'], - 'new_version' => $this->release->version, - 'version' => $this->release->version, - 'remote_version' => $this->release->version, - 'package' => $this->release->artifacts->package[0]->url, - 'download_link' => $this->release->artifacts->package[0]->url, - 'tested' => $required_versions['tested_to'], - 'external' => 'xxx', - ]; - if ( 'theme' === $this->type ) { - $response['theme_uri'] = $response['url']; - } - - return $response; - } - /** * Call theme messaging for single site installation. * diff --git a/inc/updater/namespace.php b/inc/updater/namespace.php index dde9f968..a943dd03 100644 --- a/inc/updater/namespace.php +++ b/inc/updater/namespace.php @@ -7,11 +7,7 @@ namespace FAIR\Updater; -use function FAIR\Packages\fetch_package_metadata; -use function FAIR\Packages\get_did_document; -use function FAIR\Packages\pick_release; - -use WP_Error; +use FAIR\Packages; const RELEASE_PACKAGES_CACHE_KEY = 'fair-release-packages'; @@ -20,6 +16,7 @@ */ function bootstrap() { add_action( 'init', __NAMESPACE__ . '\\run' ); + add_filter( 'upgrader_pre_download', __NAMESPACE__ . '\\upgrader_pre_download', 10, 1 ); } /** @@ -33,12 +30,12 @@ function add_package_to_release_cache( string $did ) : void { return; } $releases = wp_cache_get( RELEASE_PACKAGES_CACHE_KEY ) ?: []; - $releases[ $did ] = get_latest_release_from_did( $did ); + $releases[ $did ] = Packages\get_latest_release_from_did( $did ); wp_cache_set( RELEASE_PACKAGES_CACHE_KEY, $releases ); } /** - * Send upgrader_pre_download filter to add_accept_header(). + * Send upgrader_pre_download filter to maybe_add_accept_header(). * * @param bool $false Whether to bail without returning the package. * Default false. @@ -80,37 +77,6 @@ function maybe_add_accept_header( $args, $url ) : array { return $args; } -/** - * Get the latest release for a DID. - * - * @param string $id DID. - * - * @return ReleaseDocument|WP_Error The latest release, or a WP_Error object on failure. - */ -function get_latest_release_from_did( $id ) { - $document = get_did_document( $id ); - if ( is_wp_error( $document ) ) { - return $document; - } - - $valid_keys = $document->get_fair_signing_keys(); - if ( empty( $valid_keys ) ) { - return new WP_Error( 'fair.packages.install_plugin.no_signing_keys', __( 'DID does not contain valid signing keys.', 'fair' ) ); - } - - $metadata = fetch_package_metadata( $id ); - if ( is_wp_error( $metadata ) ) { - return $metadata; - } - - $release = pick_release( $metadata->releases ); - if ( empty( $release ) ) { - return new WP_Error( 'fair.packages.install_plugin.no_releases', __( 'No releases found in the repository.', 'fair' ) ); - } - - return $release; -} - /** * Gather all plugins/themes with data in Update URI and DID header. * @@ -145,64 +111,6 @@ function get_packages() : array { return $packages; } -/** - * Get icons. - * - * @param array $icons Array of icon data. - * - * @return array - */ -function get_icons( $icons ) : array { - if ( empty( $icons ) ) { - return []; - } - - $icons_arr = []; - $regular = array_find( $icons, fn ( $icon ) => $icon->width === 772 && $icon->height === 250 ); - $high_res = array_find( $icons, fn ( $icon ) => $icon->width === 1544 && $icon->height === 500 ); - $svg = array_find( $icons, fn ( $icon ) => str_contains( $icon->{'content-type'}, 'svg+xml' ) ); - - if ( empty( $regular ) && empty( $high_res ) && empty( $svg ) ) { - return []; - } - - $icons_arr['1x'] = $regular->url ?? ''; - $icons_arr['2x'] = $high_res->url ?? ''; - if ( str_contains( $svg->url, 's.w.org/plugins' ) ) { - $icons_arr['default'] = $svg->url; - } else { - $icons_arr['svg'] = $svg->url ?? ''; - } - - return $icons_arr; -} - -/** - * Get banners. - * - * @param array $banners Array of banner data. - * - * @return array - */ -function get_banners( $banners ) : array { - if ( empty( $banners ) ) { - return []; - } - - $banners_arr = []; - $regular = array_find( $banners, fn ( $banner ) => $banner->width === 772 && $banner->height === 250 ); - $high_res = array_find( $banners, fn ( $banner ) => $banner->width === 1544 && $banner->height === 500 ); - - if ( empty( $regular ) && empty( $high_res ) ) { - return []; - } - - $banners_arr['low'] = $regular->url; - $banners_arr['high'] = $high_res->url; - - return $banners_arr; -} - /** * Run FAIR\Updater\Updater for potential packages. *