From df6f4fd9de92ea8f25fc8472f97dfb74eab059cf Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:08:24 +0100 Subject: [PATCH 01/28] Rename `is_fair_plugin()` to `is_fair_package()`. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 287bbc06..ac8fb555 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -299,7 +299,7 @@ function set_slug_to_hashed() : void { } /** - * Check if this is a FAIR plugin, for legacy data. + * 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 +307,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 +382,7 @@ function maybe_hijack_legacy_plugin_info() { } // Is this a FAIR plugin, actually? - if ( ! is_fair_plugin( $api ) ) { + if ( ! is_fair_package( $api ) ) { return; } @@ -472,7 +472,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 +513,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; } From a19f617a86980b5e3b88e0c6c0771a980318dc16 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:09:06 +0100 Subject: [PATCH 02/28] Organize plugin and theme hooks. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index ac8fb555..06573419 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 ); @@ -41,6 +39,15 @@ function bootstrap() { add_action( 'wp_ajax_check_plugin_dependencies', __NAMESPACE__ . '\\set_slug_to_hashed' ); add_filter( 'wp_list_table_class_name', __NAMESPACE__ . '\\maybe_override_list_table' ); + // Themes. + add_filter( 'themes_api', __NAMESPACE__ . '\\handle_did_during_ajax', 10, 3 ); + add_filter( 'themes_api', 'FAIR\\Packages\\search_by_did', 10, 3 ); + + // 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 ); + // Needed for pre WordPress 6.9 compatibility. if ( ! is_wp_version_compatible( '6.9' ) ) { add_action( 'install_plugins_featured', __NAMESPACE__ . '\\replace_featured_message' ); From e5c2a2ec60124f79d56fb76c5f6ce581573f0157 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:09:43 +0100 Subject: [PATCH 03/28] Add AJAX DID support. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 06573419..3aa97437 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -111,13 +111,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; } From 7caee11a1784e7e47bb7cb44b8fb4f359f553959 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:10:55 +0100 Subject: [PATCH 04/28] Add "Search by DID" support. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/namespace.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 9733ae4f..9de07e76 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -1020,13 +1020,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; } @@ -1041,8 +1041,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, From 00cea1130b93f2db8addae3ff8fdd98dbb981374 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:12:18 +0100 Subject: [PATCH 05/28] Alter slugs for theme cards too. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 49 +++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 3aa97437..574ba6c0 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -42,6 +42,7 @@ function bootstrap() { // 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 ); // Common. add_filter( 'upgrader_package_options', 'FAIR\\Packages\\cache_did_for_install', 10, 1 ); @@ -408,30 +409,58 @@ function maybe_hijack_legacy_plugin_info() { } /** - * Filters the Plugin Installation API response results. + * Filters the Installation API response results. * * @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. - * @return object|WP_Error + * @param string $action The type of information being requested from the Installation API. + * @param object $args API arguments. */ function handle_did_in_search_results( $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; } +<<<<<<< HEAD // Alter the slugs to our globally unique version and populate release cache. - foreach ( $res->plugins as &$plugin ) { - if ( ! is_fair_plugin( $plugin ) ) { + $items = $type === 'plugin' ? $res->plugins : $res->themes; + + // Alter the slugs to our globally unique version. + 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 ) ); + } Packages\add_package_to_release_cache( $did ); } From 8613628c0568f648e9d7868e1a29c3a8366b54f3 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:16:20 +0100 Subject: [PATCH 06/28] After an AJAX installation, use the theme's installed slug for AJAX activation. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 574ba6c0..0ec0c92d 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -43,6 +43,7 @@ function bootstrap() { 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' ); // Common. add_filter( 'upgrader_package_options', 'FAIR\\Packages\\cache_did_for_install', 10, 1 ); @@ -306,6 +307,46 @@ function set_slug_to_hashed() : void { $_POST['slug'] = explode( '-did--', $escaped_slug, 2 )[0] . '-' . Packages\get_did_hash( $did ); } +/** + * 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; +} + /** * Check if this is a FAIR package, for legacy data. * From 08bba9ce860df7e93d39374bba56bd235925c36d Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:17:07 +0100 Subject: [PATCH 07/28] Handle differences in plugin/theme author shape in update data. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/namespace.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 9de07e76..1c21ee33 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -665,8 +665,6 @@ function get_package_data( $did ) { $response = [ 'name' => $metadata->name, - 'author' => $metadata->authors[0]->name, - 'author_uri' => $metadata->authors[0]->url, 'slug' => $metadata->slug, 'slug_didhash' => $metadata->slug . '-' . get_did_hash( $did ), $type => $filename, @@ -695,6 +693,12 @@ function get_package_data( $did ) { ]; if ( 'theme' === $type ) { $response['theme_uri'] = $response['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; From 5c6ceedee31d325b04a7ee2164a7862947229650 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:17:23 +0100 Subject: [PATCH 08/28] Add `preview_url` to a theme's update data. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/namespace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 1c21ee33..4166c30a 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -693,6 +693,7 @@ 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, ]; From 9c384582b09b12051ead71164138d8764ebbb84b Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:19:38 +0100 Subject: [PATCH 09/28] Set the theme's slug to `slug-didhash` when preparing it for JS. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/updater/class-updater.php | 5 +++++ 1 file changed, 5 insertions(+) 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 { From 272b915b7ce6de25e03711d07828b8bed040e502 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 05:05:01 +0100 Subject: [PATCH 10/28] If available, add the hostname to the theme's description. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 0ec0c92d..5c2f3900 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -44,6 +44,7 @@ function bootstrap() { 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( '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 ); @@ -604,3 +605,33 @@ function maybe_add_data_to_description( $description, $plugin ) { $description .= '

' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . ''; return $description; } + +/** + * Filters the theme description when preparing themes for JS. + * + * @param array $themes Array of themes prepared for JS. + * @return array Array of themes with possible modifications. + */ +function maybe_add_data_to_theme_description( $themes ) { + foreach ( $themes as &$theme ) { + $did = get_file_data( get_stylesheet_directory() . '/style.css', [ 'ThemeID' => 'Theme ID' ] )['ThemeID']; + if ( empty( $did ) || ! str_starts_with( $did, 'did:plc:' ) ) { + continue; + } + + $repo_host = Info\get_repository_hostname( $did ); + if ( empty( $repo_host ) ) { + continue; + } + + /* translators: %1$s: repository hostname */ + $additional_description = '

' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . ''; + if ( empty( $theme->description ) ) { + $theme['description'] .= '

' . $additional_description; + } else { + $theme['description'] .= $additional_description . '

'; + } + } + unset( $theme ); + return $themes; +} From 8572a63edf9dbbe752e31a929342911ab639a993 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 06:29:38 +0100 Subject: [PATCH 11/28] Support the "Live Preview" link immediately after installation. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 5c2f3900..4c6ae508 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -44,6 +44,7 @@ function bootstrap() { 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_action( 'plugins_loaded', __NAMESPACE__ . '\\set_theme_to_hashed_for_customize', 9 ); add_filter( 'wp_prepare_themes_for_js', __NAMESPACE__ . '\\maybe_add_data_to_theme_description', 10, 1 ); // Common. @@ -348,6 +349,44 @@ function set_stylesheet_to_hashed_on_theme_activation() { $_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. + * + * @return void + */ +function set_theme_to_hashed_for_customize() { + // phpcs:ignore HM.PHP.Isset.MultipleArguments + if ( ! isset( $_GET['theme'] ) || ! isset( $_GET['return'] ) ) { + return; + } + + $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ); + if ( ! str_contains( $request_uri, 'customize.php' ) ) { + return; + } + + + $theme = sanitize_text_field( wp_unslash( $_GET['theme'] ) ); + if ( ! str_contains( $theme, '-did--' ) ) { + return; + } + + $did = 'did:' . explode( '-did:', str_replace( '--', ':', $theme ), 2 )[1]; + if ( ! preg_match( '/^did:plc:.+$/', $did ) ) { + return; + } + + $hashed_theme = explode( '-did--', $theme, 2 )[0] . '-' . Packages\get_did_hash( $did ); + $_GET['theme'] = $hashed_theme; + $_REQUEST['theme'] = $hashed_theme; +} + /** * Check if this is a FAIR package, for legacy data. * From fff6a88a13e4b35757f4c2cbd6c34fa70cd14f48 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 06:41:21 +0100 Subject: [PATCH 12/28] Swap from hooking `plugins_loaded` to hooking `clean_url` instead. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 34 +++++++++++--------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 4c6ae508..b68c8fa1 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -44,7 +44,7 @@ function bootstrap() { 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_action( 'plugins_loaded', __NAMESPACE__ . '\\set_theme_to_hashed_for_customize', 9 ); + 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. @@ -360,31 +360,21 @@ function set_stylesheet_to_hashed_on_theme_activation() { * * @return void */ -function set_theme_to_hashed_for_customize() { - // phpcs:ignore HM.PHP.Isset.MultipleArguments - if ( ! isset( $_GET['theme'] ) || ! isset( $_GET['return'] ) ) { - return; - } - - $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ); - if ( ! str_contains( $request_uri, 'customize.php' ) ) { - return; - } +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]; - $theme = sanitize_text_field( wp_unslash( $_GET['theme'] ) ); - if ( ! str_contains( $theme, '-did--' ) ) { - return; - } - - $did = 'did:' . explode( '-did:', str_replace( '--', ':', $theme ), 2 )[1]; - if ( ! preg_match( '/^did:plc:.+$/', $did ) ) { - return; + 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 ); + } + } } - $hashed_theme = explode( '-did--', $theme, 2 )[0] . '-' . Packages\get_did_hash( $did ); - $_GET['theme'] = $hashed_theme; - $_REQUEST['theme'] = $hashed_theme; + return $url; } /** From 2064dbe315bcbc0ba6dbca3c9346be342749ee1a Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 06:43:13 +0100 Subject: [PATCH 13/28] Fix the docblock for `set_theme_to_hashed_for_customize()`. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index b68c8fa1..f240eb5f 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -358,7 +358,8 @@ function set_stylesheet_to_hashed_on_theme_activation() { * The theme parameter needs to be in the slug-didhash format * so that the theme can be found. * - * @return void + * @param string $url The URL to filter. + * @return string */ function set_theme_to_hashed_for_customize( $url ) { if ( str_contains( $url, 'customize.php?theme=' ) ) { From 94b6deafa27bfd8ddd31b51e2a908adde049c5ea Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 4 Oct 2025 20:37:36 +0100 Subject: [PATCH 14/28] Add a space before the `return` statement. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index f240eb5f..d24f28a2 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -663,5 +663,6 @@ function maybe_add_data_to_theme_description( $themes ) { } } unset( $theme ); + return $themes; } From 7e153dc62b5b5a7fb9512e7790153ee0eb077cbd Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:17:56 +0000 Subject: [PATCH 15/28] Use plurals for the type checks. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index d24f28a2..a6c2e7ee 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -495,23 +495,20 @@ function handle_did_in_search_results( $res, $action, $args ) { $type = explode( '_', $action )[1]; if ( - ( $type === 'plugin' && empty( $res->plugins ) ) - || ( $type === 'theme' && empty( $res->themes ) ) + ( $type === 'plugins' && empty( $res->plugins ) ) + || ( $type === 'themes' && empty( $res->themes ) ) ) { return $res; } -<<<<<<< HEAD // Alter the slugs to our globally unique version and populate release cache. - $items = $type === 'plugin' ? $res->plugins : $res->themes; - - // Alter the slugs to our globally unique version. + $items = $type === 'plugins' ? $res->plugins : $res->themes; foreach ( $items as &$item ) { if ( ! is_fair_package( $item ) ) { continue; } - if ( $type === 'plugin' ) { + if ( $type === 'plugins' ) { $did = $item['_fair']['id']; $item['slug'] = esc_attr( $item['slug'] . '-' . str_replace( ':', '--', $did ) ); } else { From 21c73116378a94d992a73c82addd5aa0b6b59256 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:20:19 +0000 Subject: [PATCH 16/28] Don't use plurals for the type checks. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/admin/namespace.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index a6c2e7ee..91e68b9b 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -492,23 +492,23 @@ function handle_did_in_search_results( $res, $action, $args ) { return $res; } - $type = explode( '_', $action )[1]; + $type = rtrim( explode( '_', $action )[1], 's' ); if ( - ( $type === 'plugins' && empty( $res->plugins ) ) - || ( $type === 'themes' && empty( $res->themes ) ) + ( $type === 'plugin' && empty( $res->plugins ) ) + || ( $type === 'theme' && empty( $res->themes ) ) ) { return $res; } // Alter the slugs to our globally unique version and populate release cache. - $items = $type === 'plugins' ? $res->plugins : $res->themes; + $items = $type === 'plugin' ? $res->plugins : $res->themes; foreach ( $items as &$item ) { if ( ! is_fair_package( $item ) ) { continue; } - if ( $type === 'plugins' ) { + if ( $type === 'plugin' ) { $did = $item['_fair']['id']; $item['slug'] = esc_attr( $item['slug'] . '-' . str_replace( ':', '--', $did ) ); } else { From 7f1b875a823d8e447d9b99efa26cf3c4f0567dc9 Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Fri, 7 Nov 2025 11:47:36 -0800 Subject: [PATCH 17/28] $item is an object Signed-off-by: Andy Fragen --- inc/packages/admin/namespace.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 91e68b9b..b6a8f48e 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -509,8 +509,8 @@ function handle_did_in_search_results( $res, $action, $args ) { } if ( $type === 'plugin' ) { - $did = $item['_fair']['id']; - $item['slug'] = esc_attr( $item['slug'] . '-' . str_replace( ':', '--', $did ) ); + $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. From bd623f647a6d99d646ddf90e746cdd52014e3384 Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Fri, 7 Nov 2025 11:47:59 -0800 Subject: [PATCH 18/28] fix for some theme issues Signed-off-by: Andy Fragen --- inc/updater/class-updater.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index d640f1e6..b1fa1dd8 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -236,12 +236,17 @@ public function update_site_transient( $transient ) { } $rel_path = plugin_basename( $this->filepath ); - $rel_path = 'theme' === $this->type ? dirname( $rel_path ) : $rel_path; + $rel_path = 'theme' === $this->type ? basename( dirname( $rel_path ) ) : $rel_path; $response = Packages\get_package_data( $this->did ); if ( is_wp_error( $response ) ) { return $transient; } - $response['slug'] = $response['slug_didhash']; + // $response['slug'] = $response['slug_didhash']; + // Delete any existing update for this package if non-hashed slug. + // Avoids duplicate update theme entries. + if ( 'theme' === $this->type && $response['file'] === $rel_path ) { + unset( $transient->response[ $response['slug'] ] ); + } $response = 'plugin' === $this->type ? (object) $response : $response; $is_compatible = Packages\check_requirements( $this->release ); @@ -265,13 +270,12 @@ public function update_site_transient( $transient ) { * @return array */ public function customize_theme_update_html( $prepared_themes ) { - $theme = $this->metadata; - if ( 'theme' !== $this->type ) { return $prepared_themes; } $did_hash = Packages\get_did_hash( $this->did ); + $theme = (object) Packages\get_package_data( $this->did ); if ( ! str_ends_with( $theme->slug, '-' . $did_hash ) ) { $theme->slug = $theme->slug . '-' . $did_hash; } From 2c00a5f872bb8e760b673c94495ffccd4e28304c Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Fri, 7 Nov 2025 12:28:28 -0800 Subject: [PATCH 19/28] Most applies to both plugins/themes Signed-off-by: Andy Fragen --- inc/packages/admin/namespace.php | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index b6a8f48e..43d19a49 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -508,29 +508,25 @@ function handle_did_in_search_results( $res, $action, $args ) { continue; } - 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; - } + $did = $item->_fair['id']; + $did_hash = Packages\get_did_hash( $item->_fair['id'] ); + $slug = $item->slug; + if ( ! str_ends_with( $slug, '-' . $did_hash ) ) { + $slug = $item->slug . '-' . $did_hash; + } + + // Installed themes need the slug-didhash format + // so their activation status can be determined. + if ( 'theme' === $type ) { $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 ) ); } Packages\add_package_to_release_cache( $did ); + + $item->slug = esc_attr( $item->slug . '-' . str_replace( ':', '--', $did ) ); } return $res; From 56bd51dca92a5ef716dacfc81ed6adb71a03aa45 Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Fri, 7 Nov 2025 12:39:19 -0800 Subject: [PATCH 20/28] restructure a bit Signed-off-by: Andy Fragen --- inc/packages/admin/namespace.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 43d19a49..9fbba334 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -514,6 +514,7 @@ function handle_did_in_search_results( $res, $action, $args ) { if ( ! str_ends_with( $slug, '-' . $did_hash ) ) { $slug = $item->slug . '-' . $did_hash; } + $item->slug = esc_attr( $item->slug . '-' . str_replace( ':', '--', $did ) ); // Installed themes need the slug-didhash format // so their activation status can be determined. @@ -521,12 +522,9 @@ function handle_did_in_search_results( $res, $action, $args ) { $theme = wp_get_theme( $slug ); if ( $theme->exists() ) { $item->slug = esc_attr( $slug ); - continue; } } Packages\add_package_to_release_cache( $did ); - - $item->slug = esc_attr( $item->slug . '-' . str_replace( ':', '--', $did ) ); } return $res; From f2e93553e169e14c159be0f9667de566b3fecf91 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Sat, 8 Nov 2025 05:53:46 +0000 Subject: [PATCH 21/28] During theme_information, return the theme's author as a string. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/updater/class-updater.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index b1fa1dd8..01935984 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -219,7 +219,12 @@ public function repo_api_details( $result, string $action, stdClass $response ) return $result; } - return (object) Packages\get_package_data( $this->did ); + $package_data = (object) Packages\get_package_data( $this->did ); + if ( $this->type === 'theme' ) { + $package_data->author = $package_data->author['display_name']; + } + + return $package_data; } /** From 4acf84377be7f035a7e95af0cd4d50d0299bbdab Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Sat, 8 Nov 2025 07:28:45 -0800 Subject: [PATCH 22/28] change to slug-did-hash Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> Signed-off-by: Andy Fragen --- inc/packages/admin/namespace.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index 9fbba334..ad67373d 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -510,18 +510,18 @@ function handle_did_in_search_results( $res, $action, $args ) { $did = $item->_fair['id']; $did_hash = Packages\get_did_hash( $item->_fair['id'] ); - $slug = $item->slug; - if ( ! str_ends_with( $slug, '-' . $did_hash ) ) { - $slug = $item->slug . '-' . $did_hash; + $slug_did_hash = $item->slug; + if ( ! str_ends_with( $slug_did_hash, '-' . $did_hash ) ) { + $slug_did_hash .= '-' . $did_hash; } $item->slug = esc_attr( $item->slug . '-' . str_replace( ':', '--', $did ) ); // Installed themes need the slug-didhash format // so their activation status can be determined. if ( 'theme' === $type ) { - $theme = wp_get_theme( $slug ); + $theme = wp_get_theme( $slug_did_hash ); if ( $theme->exists() ) { - $item->slug = esc_attr( $slug ); + $item->slug = esc_attr( $slug_did_hash ); } } Packages\add_package_to_release_cache( $did ); From d904201b5e86348c2d272f311669c05606c3d391 Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Mon, 10 Nov 2025 10:18:59 -0800 Subject: [PATCH 23/28] remove commented code Signed-off-by: Andy Fragen --- inc/updater/class-updater.php | 1 - 1 file changed, 1 deletion(-) diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index 01935984..4298b8f3 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -246,7 +246,6 @@ public function update_site_transient( $transient ) { if ( is_wp_error( $response ) ) { return $transient; } - // $response['slug'] = $response['slug_didhash']; // Delete any existing update for this package if non-hashed slug. // Avoids duplicate update theme entries. if ( 'theme' === $this->type && $response['file'] === $rel_path ) { From 5c4aa61222b2444b3a1dbab556fb6fea6efe8ccf Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Mon, 10 Nov 2025 10:38:38 -0800 Subject: [PATCH 24/28] change slug to slug_didhash for installed theme otherwise $theme->exists() fails Signed-off-by: Andy Fragen --- inc/updater/class-updater.php | 1 + 1 file changed, 1 insertion(+) diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index 4298b8f3..f089157b 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -222,6 +222,7 @@ public function repo_api_details( $result, string $action, stdClass $response ) $package_data = (object) Packages\get_package_data( $this->did ); if ( $this->type === 'theme' ) { $package_data->author = $package_data->author['display_name']; + $package_data->slug = $package_data->slug_didhash; } return $package_data; From 6ebe114dc360c896ec15409520e9eaf8c2f6d4a7 Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Mon, 10 Nov 2025 10:42:59 -0800 Subject: [PATCH 25/28] shorten syntax for single text replacement Signed-off-by: Andy Fragen --- inc/packages/admin/namespace.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inc/packages/admin/namespace.php b/inc/packages/admin/namespace.php index ad67373d..413ecdf6 100644 --- a/inc/packages/admin/namespace.php +++ b/inc/packages/admin/namespace.php @@ -622,8 +622,8 @@ function maybe_add_data_to_description( $description, $plugin ) { return $description; } - /* translators: %1$s: repository hostname */ - $description .= '

' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . ''; + /* translators: %s: repository hostname */ + $description .= '

' . sprintf( __( 'Hosted on %s', 'fair' ), esc_html( $repo_host ) ) . ''; return $description; } @@ -645,8 +645,8 @@ function maybe_add_data_to_theme_description( $themes ) { continue; } - /* translators: %1$s: repository hostname */ - $additional_description = '

' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . ''; + /* translators: %s: repository hostname */ + $additional_description = '

' . sprintf( __( 'Hosted on %s', 'fair' ), esc_html( $repo_host ) ) . ''; if ( empty( $theme->description ) ) { $theme['description'] .= '

' . $additional_description; } else { From 0b0714acb6d94657824da081c7f35b3c8093b0cc Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Mon, 10 Nov 2025 11:16:24 -0800 Subject: [PATCH 26/28] fix theme slug depending upon directory name Signed-off-by: Andy Fragen --- inc/updater/class-updater.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/updater/class-updater.php b/inc/updater/class-updater.php index f089157b..0f4ba7ae 100644 --- a/inc/updater/class-updater.php +++ b/inc/updater/class-updater.php @@ -282,7 +282,7 @@ public function customize_theme_update_html( $prepared_themes ) { $did_hash = Packages\get_did_hash( $this->did ); $theme = (object) Packages\get_package_data( $this->did ); if ( ! str_ends_with( $theme->slug, '-' . $did_hash ) ) { - $theme->slug = $theme->slug . '-' . $did_hash; + $theme->slug = array_key_exists( $theme->slug, $prepared_themes ) ? $theme->slug : $theme->slug . '-' . $did_hash; } if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) { From 53ff77db099241e9a9101e8a0f8fd7ca0c2f488e Mon Sep 17 00:00:00 2001 From: Andy Fragen Date: Mon, 17 Nov 2025 09:12:04 -0800 Subject: [PATCH 27/28] already set as default Signed-off-by: Andy Fragen --- inc/packages/namespace.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 4166c30a..43d2b17d 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -697,9 +697,6 @@ function get_package_data( $did ) { $response['author'] = [ 'display_name' => $metadata->authors[0]->name, ]; - } else { - $response['author'] = $metadata->authors[0]->name; - $response['author_uri'] = $metadata->authors[0]->url; } return $response; From 3e5f865b5c95ae1e06730ab0a5fd9c01e4a944b9 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:16:58 +0000 Subject: [PATCH 28/28] Add back plugin author and author_uri after rebase mistake. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- inc/packages/namespace.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inc/packages/namespace.php b/inc/packages/namespace.php index 43d2b17d..4166c30a 100644 --- a/inc/packages/namespace.php +++ b/inc/packages/namespace.php @@ -697,6 +697,9 @@ function get_package_data( $did ) { $response['author'] = [ 'display_name' => $metadata->authors[0]->name, ]; + } else { + $response['author'] = $metadata->authors[0]->name; + $response['author_uri'] = $metadata->authors[0]->url; } return $response;