Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
df6f4fd
Rename `is_fair_plugin()` to `is_fair_package()`.
costdev Oct 4, 2025
a19f617
Organize plugin and theme hooks.
costdev Oct 4, 2025
e5c2a2e
Add AJAX DID support.
costdev Oct 4, 2025
7caee11
Add "Search by DID" support.
costdev Oct 4, 2025
00cea11
Alter slugs for theme cards too.
costdev Oct 4, 2025
8613628
After an AJAX installation, use the theme's installed slug for AJAX a…
costdev Oct 4, 2025
08bba9c
Handle differences in plugin/theme author shape in update data.
costdev Oct 4, 2025
5c6ceed
Add `preview_url` to a theme's update data.
costdev Oct 4, 2025
9c38458
Set the theme's slug to `slug-didhash` when preparing it for JS.
costdev Oct 4, 2025
272b915
If available, add the hostname to the theme's description.
costdev Oct 4, 2025
8572a63
Support the "Live Preview" link immediately after installation.
costdev Oct 4, 2025
fff6a88
Swap from hooking `plugins_loaded` to hooking `clean_url` instead.
costdev Oct 4, 2025
2064dbe
Fix the docblock for `set_theme_to_hashed_for_customize()`.
costdev Oct 4, 2025
94b6dea
Add a space before the `return` statement.
costdev Oct 4, 2025
7e153dc
Use plurals for the type checks.
costdev Nov 7, 2025
21c7311
Don't use plurals for the type checks.
costdev Nov 7, 2025
7f1b875
$item is an object
afragen Nov 7, 2025
bd623f6
fix for some theme issues
afragen Nov 7, 2025
2c00a5f
Most applies to both plugins/themes
afragen Nov 7, 2025
56bd51d
restructure a bit
afragen Nov 7, 2025
f2e9355
During theme_information, return the theme's author as a string.
costdev Nov 8, 2025
4acf843
change to slug-did-hash
afragen Nov 8, 2025
d904201
remove commented code
afragen Nov 10, 2025
5c4aa61
change slug to slug_didhash for installed theme
afragen Nov 10, 2025
6ebe114
shorten syntax for single text replacement
afragen Nov 10, 2025
0b0714a
fix theme slug depending upon directory name
afragen Nov 10, 2025
53ff77d
already set as default
afragen Nov 17, 2025
3e5f865
Add back plugin author and author_uri after rebase mistake.
costdev Mar 3, 2026
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
176 changes: 153 additions & 23 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 @@ -41,6 +39,19 @@ 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 );
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 );

// Needed for pre WordPress 6.9 compatibility.
if ( ! is_wp_version_compatible( '6.9' ) ) {
add_action( 'install_plugins_featured', __NAMESPACE__ . '\\replace_featured_message' );
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,30 +481,49 @@ 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 = rtrim( explode( '_', $action )[1], 's' );

if (
( $type === 'plugin' && empty( $res->plugins ) )
|| ( $type === 'theme' && empty( $res->themes ) )
) {
return $res;
}

// 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;
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'];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting for myself when I'm next at my PC:

  • Previously, we were setting this to $plugin['_fair']['id'].
  • Now we're setting this to $item->_fair['id'].

Are we sure that $item is an object for both plugins and themes?

If no issues exist, why did the previous array syntax work for plugins without issue - or did it?

$did_hash = Packages\get_did_hash( $item->_fair['id'] );
$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_did_hash );
if ( $theme->exists() ) {
$item->slug = esc_attr( $slug_did_hash );
}
}
Packages\add_package_to_release_cache( $did );
}

Expand Down Expand Up @@ -472,7 +571,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 +612,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 @@ -523,7 +622,38 @@ function maybe_add_data_to_description( $description, $plugin ) {
return $description;
}

/* translators: %1$s: repository hostname */
$description .= '</p><p class="authors"><em>' . sprintf( __( 'Hosted on %1$s', 'fair' ), esc_html( $repo_host ) ) . '</em>';
/* translators: %s: repository hostname */
$description .= '</p><p class="authors"><em>' . sprintf( __( 'Hosted on %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: %s: repository hostname */
$additional_description = '<p class="authors"><em>' . sprintf( __( 'Hosted on %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;
}
18 changes: 12 additions & 6 deletions inc/packages/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -695,6 +693,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 @@ -1020,13 +1025,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 @@ -1041,8 +1046,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
24 changes: 19 additions & 5 deletions inc/updater/class-updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,13 @@ 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'];
$package_data->slug = $package_data->slug_didhash;
}

return $package_data;
}

/**
Expand All @@ -236,12 +242,16 @@ 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'];
// 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 +275,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 = array_key_exists( $theme->slug, $prepared_themes ) ? $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