Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
78658e5
Rename `is_fair_plugin()` to `is_fair_package()`.
costdev Oct 4, 2025
fd2f6a2
Organize plugin and theme hooks.
costdev Oct 4, 2025
38ad9b7
Add AJAX DID support.
costdev Oct 4, 2025
2032ce9
Add "Search by DID" support.
costdev Oct 4, 2025
7bd96b8
Alter slugs for theme cards too.
costdev Oct 4, 2025
4f22be5
After an AJAX installation, use the theme's installed slug for AJAX a…
costdev Oct 4, 2025
6fe02ca
Handle differences in plugin/theme author shape in update data.
costdev Oct 4, 2025
30b6d19
Add `preview_url` to a theme's update data.
costdev Oct 4, 2025
43a87ab
Use the correct upgrader property when matching a name for source sel…
costdev Oct 4, 2025
b79d67f
Set the theme's slug to `slug-didhash` when preparing it for JS.
costdev Oct 4, 2025
5de452c
If available, add the hostname to the theme's description.
costdev Oct 4, 2025
9df8aa3
Support the "Live Preview" link immediately after installation.
costdev Oct 4, 2025
1376fc7
Swap from hooking `plugins_loaded` to hooking `clean_url` instead.
costdev Oct 4, 2025
f18293d
Fix the docblock for `set_theme_to_hashed_for_customize()`.
costdev Oct 4, 2025
5fa83b8
Add a space before the `return` statement.
costdev Oct 4, 2025
fe8c155
Merge branch 'main' into fix_theme_support_for_admin
afragen Oct 15, 2025
7f08c96
Merge branch 'main' into fix_theme_support_for_admin
costdev Nov 7, 2025
e5f4ef7
Use plurals for the type checks.
costdev Nov 7, 2025
a9252ec
Don't use plurals for the type checks.
costdev Nov 7, 2025
0409faa
$item is an object
afragen Nov 7, 2025
17a3c03
fix for some theme issues
afragen Nov 7, 2025
855f892
Most applies to both plugins/themes
afragen Nov 7, 2025
77bd924
restructure a bit
afragen Nov 7, 2025
c12549e
During theme_information, return the theme's author as a string.
costdev Nov 8, 2025
d189a63
change to slug-did-hash
afragen Nov 8, 2025
8a1ba32
remove commented code
afragen Nov 10, 2025
99802ce
change slug to slug_didhash for installed theme
afragen Nov 10, 2025
f717ec6
shorten syntax for single text replacement
afragen Nov 10, 2025
600b26a
fix theme slug depending upon directory name
afragen Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 152 additions & 20 deletions inc/packages/admin/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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 );
}

/**
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -299,15 +310,84 @@ 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.
*
* @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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -401,31 +481,52 @@ 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 = rtrim( explode( '_', $action )[1], 's' );

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 ) );
$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;
}
$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 );
if ( $theme->exists() ) {
$item->slug = esc_attr( $slug );
}
}
}

return $res;
Expand Down Expand Up @@ -472,7 +573,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;
}

Expand Down Expand Up @@ -513,7 +614,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;
}

Expand All @@ -527,3 +628,34 @@ function maybe_add_data_to_description( $description, $plugin ) {
$description .= '</p><p class="authors"><em>' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . '</em>';
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 = '<p class="authors"><em>' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . '</em>';
if ( empty( $theme->description ) ) {
$theme['description'] .= '</p>' . $additional_description;
} else {
$theme['description'] .= $additional_description . '</p>';
}
}
unset( $theme );

return $themes;
}
21 changes: 16 additions & 5 deletions inc/packages/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions inc/updater/class-updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

Expand All @@ -265,12 +270,16 @@ 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;
}

if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) {
$prepared_themes[ $theme->slug ]['update'] = $this->append_theme_actions_content( $theme );
} else {
Expand Down