diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 8a682f94..f5de75de 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -25,12 +25,10 @@ function bootstrap() { return; } + // Plugins. add_filter( 'install_plugins_tabs', __NAMESPACE__ . '\\add_direct_tab' ); add_filter( 'plugins_api', __NAMESPACE__ . '\\handle_did_during_ajax', 10, 3 ); add_filter( 'plugins_api', 'FAIR\\Packages\\search_by_did', 10, 3 ); - add_filter( 'upgrader_package_options', 'FAIR\\Packages\\cache_did_for_install', 10, 1 ); - add_action( 'upgrader_post_install', 'FAIR\\Packages\\delete_cached_did_for_install', 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 ); @@ -46,6 +44,19 @@ function bootstrap() { add_action( 'install_plugins_featured', __NAMESPACE__ . '\\replace_featured_message' ); add_action( 'admin_init', fn() => remove_action( 'install_plugins_featured', 'install_dashboard' ) ); } + + // Themes. + add_filter( 'themes_api', __NAMESPACE__ . '\\handle_did_during_ajax', 10, 3 ); + add_filter( 'themes_api', 'FAIR\\Packages\\search_by_did', 10, 3 ); + add_filter( 'themes_api_result', __NAMESPACE__ . '\\alter_slugs', 10, 3 ); + add_action( 'load-themes.php', __NAMESPACE__ . '\\set_stylesheet_to_hashed_on_theme_activation' ); + add_filter( 'clean_url', __NAMESPACE__ . '\\set_theme_to_hashed_for_customize', 10, 3 ); + add_filter( 'wp_prepare_themes_for_js', __NAMESPACE__ . '\\maybe_add_data_to_theme_description', 10, 1 ); + + // Common. + add_filter( 'upgrader_package_options', 'FAIR\\Packages\\cache_did_for_install', 10, 1 ); + add_action( 'upgrader_post_install', 'FAIR\\Packages\\delete_cached_did_for_install', 10, 3 ); + add_filter( 'upgrader_pre_download', 'FAIR\\Packages\\upgrader_pre_download', 10, 1 ); } /** @@ -104,13 +115,13 @@ function replace_featured_message() { /** * Handles the AJAX request for plugin information when a DID is present. * - * @param mixed $result The result of the plugins_api call. + * @param mixed $result The result of the API call. * @param string $action The action being performed. - * @param object $args The arguments passed to the plugins_api call. + * @param object $args The arguments passed to the API call. * @return mixed */ function handle_did_during_ajax( $result, $action, $args ) { - if ( ! wp_doing_ajax() || 'plugin_information' !== $action || ! isset( $args->slug ) ) { + if ( ! wp_doing_ajax() || ! isset( $args->slug ) || ( 'plugin_information' !== $action && 'theme_information' !== $action ) ) { return $result; } @@ -299,7 +310,76 @@ function set_slug_to_hashed() : void { } /** - * Check if this is a FAIR plugin, for legacy data. + * Set the stylesheet to the hashed version on theme activation. + * + * After installing a theme via AJAX, the activation button's link + * includes the escaped DID, not the hash of the DID. + * + * The stylesheet parameter needs to be in the slug-didhash format + * so that the theme can be found. + * + * The nonce also needs to be regenerated as the action includes + * the stylesheet. + * + * @return void + */ +function set_stylesheet_to_hashed_on_theme_activation() { + // phpcs:ignore HM.PHP.Isset.MultipleArguments + if ( ! isset( $_GET['action'], $_GET['stylesheet'] ) || $_GET['action'] !== 'activate' ) { + return; + } + + $stylesheet = sanitize_text_field( wp_unslash( $_GET['stylesheet'] ) ); + check_admin_referer( 'switch-theme_' . $stylesheet ); + + if ( ! str_contains( $stylesheet, '-did--' ) ) { + return; + } + + $did = 'did:' . explode( '-did:', str_replace( '--', ':', $stylesheet ), 2 )[1]; + if ( ! preg_match( '/^did:plc:.+$/', $did ) ) { + return; + } + + $hashed_stylesheet = explode( '-did--', $stylesheet, 2 )[0] . '-' . Packages\get_did_hash( $did ); + $_GET['stylesheet'] = $hashed_stylesheet; + $_REQUEST['stylesheet'] = $hashed_stylesheet; + $new_nonce = wp_create_nonce( 'switch-theme_' . $hashed_stylesheet ); + $_GET['_wpnonce'] = $new_nonce; + $_REQUEST['_wpnonce'] = $new_nonce; +} + +/** + * Set the theme to the hashed version in the customizer. + * + * Immediately after installation, the "Live Preview" button + * includes the escaped DID, not the hash of the DID. + * + * The theme parameter needs to be in the slug-didhash format + * so that the theme can be found. + * + * @param string $url The URL to filter. + * @return string + */ +function set_theme_to_hashed_for_customize( $url ) { + if ( str_contains( $url, 'customize.php?theme=' ) ) { + $theme = explode( 'theme=', $url )[1]; + + if ( str_contains( $theme, '-did--' ) ) { + $did = 'did:' . explode( '-did:', str_replace( '--', ':', $theme ), 2 )[1]; + + if ( preg_match( '/^did:plc:.+$/', $did ) ) { + $hashed_theme = explode( '-did--', $theme, 2 )[0] . '-' . Packages\get_did_hash( $did ); + $url = str_replace( $theme, $hashed_theme, $url ); + } + } + } + + return $url; +} + +/** + * Check if this is a FAIR package, for legacy data. * * FAIR data is bridged into legacy data via the _fair property, and needs * to have a valid DID. We can use this to enhance our existing metadata. @@ -307,7 +387,7 @@ function set_slug_to_hashed() : void { * @param array|stdClass $api_data Legacy dotorg-formatted data to check. * @return bool */ -function is_fair_plugin( $api_data ) : bool { +function is_fair_package( $api_data ) : bool { $api = (array) $api_data; if ( empty( $api['_fair'] ) ) { return false; @@ -382,7 +462,7 @@ function maybe_hijack_legacy_plugin_info() { } // Is this a FAIR plugin, actually? - if ( ! is_fair_plugin( $api ) ) { + if ( ! is_fair_package( $api ) ) { return; } @@ -401,31 +481,58 @@ function maybe_hijack_legacy_plugin_info() { } /** - * Filters the Plugin Installation API response results. + * Filters the Installation API response results. * * @since 2.7.0 * * @param object|WP_Error $res Response object or WP_Error. - * @param string $action The type of information being requested from the Plugin Installation API. - * @param object $args Plugin API arguments. + * @param string $action The type of information being requested from the Installation API. + * @param object $args API arguments. */ function alter_slugs( $res, $action, $args ) { - if ( 'query_plugins' !== $action ) { + if ( 'query_plugins' !== $action && 'query_themes' !== $action ) { return $res; } - if ( empty( $res->plugins ) ) { + $type = explode( '_', $action )[1]; + + if ( + ( $type === 'plugin' && empty( $res->plugins ) ) + || ( $type === 'theme' && empty( $res->themes ) ) + ) { return $res; } + $items = $type === 'plugin' ? $res->plugins : $res->themes; + // Alter the slugs to our globally unique version. - foreach ( $res->plugins as &$plugin ) { - if ( ! is_fair_plugin( $plugin ) ) { + foreach ( $items as &$item ) { + if ( ! is_fair_package( $item ) ) { continue; } - $did = $plugin['_fair']['id']; - $plugin['slug'] = esc_attr( $plugin['slug'] . '-' . str_replace( ':', '--', $did ) ); + if ( $type === 'plugin' ) { + $did = $item['_fair']['id']; + $item['slug'] = esc_attr( $item['slug'] . '-' . str_replace( ':', '--', $did ) ); + } else { + // Installed themes need to have the slug-didhash format + // so their activation status can be determined. + $did_hash = Packages\get_did_hash( $item->_fair['id'] ); + $slug = $item->slug; + if ( ! str_ends_with( $slug, '-' . $did_hash ) ) { + $slug = $item->slug . '-' . $did_hash; + } + $theme = wp_get_theme( $slug ); + if ( $theme->exists() ) { + $item->slug = esc_attr( $slug ); + continue; + } + + // Themes that aren't installed need the slug--escaped-did format + // so their metadata can be retrieved. + $did = $item->_fair['id']; + $item->slug = esc_attr( $item->slug . '-' . str_replace( ':', '--', $did ) ); + } } return $res; @@ -472,7 +579,7 @@ function sort_sections_in_api( $res ) { * @return array Altered actions. */ function maybe_hijack_plugin_install_button( $links, $plugin ) { - if ( ! is_fair_plugin( $plugin ) || ! str_contains( $plugin['slug'], '-did--' ) ) { + if ( ! is_fair_package( $plugin ) || ! str_contains( $plugin['slug'], '-did--' ) ) { return $links; } @@ -513,7 +620,7 @@ function maybe_hijack_plugin_install_button( $links, $plugin ) { * @return string Plugin card description. */ function maybe_add_data_to_description( $description, $plugin ) { - if ( ! is_fair_plugin( $plugin ) ) { + if ( ! is_fair_package( $plugin ) ) { return $description; } @@ -527,3 +634,34 @@ function maybe_add_data_to_description( $description, $plugin ) { $description .= '
' . $additional_description; + } else { + $theme['description'] .= $additional_description . ''; + } + } + unset( $theme ); + + return $themes; +} diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 3b95fd5c..a97a05a7 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -637,6 +637,13 @@ function get_package_data( $did ) { ]; if ( 'theme' === $type ) { $response['theme_uri'] = $response['url']; + $response['preview_url'] = $metadata->url ?? ''; + $response['author'] = [ + 'display_name' => $metadata->authors[0]->name, + ]; + } else { + $response['author'] = $metadata->authors[0]->name; + $response['author_uri'] = $metadata->authors[0]->url; } return $response; @@ -717,7 +724,10 @@ function rename_source_selection( string $source, string $remote_source, WP_Upgr } // Sanity check. - if ( $upgrader->new_plugin_data['Name'] !== $metadata->name ) { + if ( + ( $upgrader instanceof Plugin_Upgrader && $upgrader->new_plugin_data['Name'] !== $metadata->name ) + || ( $upgrader instanceof Theme_Upgrader && $upgrader->new_theme_data['Name'] !== $metadata->name ) + ) { return $source; } @@ -895,13 +905,13 @@ function fetch_and_validate_package_alias( DIDDocument $did ) { /** * Enable searching by DID. * - * @param mixed $result The result of the plugins_api call. + * @param mixed $result The result of the API call. * @param string $action The action being performed. - * @param stdClass $args The arguments passed to the plugins_api call. + * @param stdClass $args The arguments passed to the API call. * @return mixed The search result for the DID. */ function search_by_did( $result, $action, $args ) { - if ( 'query_plugins' !== $action || empty( $args->search ) ) { + if ( empty( $args->search ) || ( 'query_plugins' !== $action && 'query_themes' !== $action ) ) { return $result; } @@ -916,8 +926,9 @@ function search_by_did( $result, $action, $args ) { return $result; } + $type = explode( '_', $action )[1]; $result = [ - 'plugins' => [ $api_data ], + $type => [ $type === 'plugin' ? $api_data : (object) $api_data ], 'info' => [ 'page' => 1, 'pages' => 1, diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index aa6f415b..d640f1e6 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -271,6 +271,11 @@ public function customize_theme_update_html( $prepared_themes ) { return $prepared_themes; } + $did_hash = Packages\get_did_hash( $this->did ); + if ( ! str_ends_with( $theme->slug, '-' . $did_hash ) ) { + $theme->slug = $theme->slug . '-' . $did_hash; + } + if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) { $prepared_themes[ $theme->slug ]['update'] = $this->append_theme_actions_content( $theme ); } else {