Plugins: Add support for `Update URI` header.

This allows third-party plugins to avoid accidentally being overwritten with an update of a plugin of a similar name from the WordPress.org Plugin Directory.

Additionally, introduce the `update_plugins_{$hostname}` filter, which third-party plugins can use to offer updates for a given hostname.

If set, the `Update URI` header field should be a URI and have a unique hostname.

Some examples include:

* `https://wordpress.org/plugins/example-plugin/`
* `https://example.com/my-plugin/`
* `my-custom-plugin-name`

`Update URI: false` also works, and unless there is code handling the `false` hostname, the plugin will never get an update notification.

If the header is present, the WordPress.org API will currently only return updates for the plugin if it matches the following format:

* `https://wordpress.org/plugins/{$slug}/`
* `w.org/plugin/{$slug}`

If the header has any other value, the API will not return a result and will ignore the plugin for update purposes.

Props dd32, DavidAnderson, meloniq, markjaquith, DrewAPicture, mweichert, design_dolphin, filosofo, sean212, nhuja, JeroenReumkens, infolu, dingdang, joyously, earnjam, williampatton, grapplerulrich, markparnell, apedog, afragen, miqrogroove, rmccue, crazycoders, jdgrimes, damonganto, joostdevalk, jorbin, georgestephanis, khromov, GeekStreetWP, jb510, Rarst, juliobox, Ipstenu, mikejolley, Otto42, gMagicScott, TJNowell, GaryJ, knutsp, mordauk, nvartolomei, aspexi, chriscct7, benoitchantre, ryno267, lev0, gregorlove, dougwollison, SergeyBiryukov.
See #14179, #23318, #32101.
Built from https://develop.svn.wordpress.org/trunk@50921


git-svn-id: http://core.svn.wordpress.org/trunk@50530 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Sergey Biryukov 2021-05-17 17:04:01 +00:00
parent ff46fda58d
commit 9fbc705dc0
5 changed files with 136 additions and 33 deletions

View File

@ -47,15 +47,19 @@ class WP_Plugin_Install_List_Table extends WP_List_Table {
$plugin_info = get_site_transient( 'update_plugins' ); $plugin_info = get_site_transient( 'update_plugins' );
if ( isset( $plugin_info->no_update ) ) { if ( isset( $plugin_info->no_update ) ) {
foreach ( $plugin_info->no_update as $plugin ) { foreach ( $plugin_info->no_update as $plugin ) {
$plugin->upgrade = false; if ( isset( $plugin->slug ) ) {
$plugins[ $plugin->slug ] = $plugin; $plugin->upgrade = false;
$plugins[ $plugin->slug ] = $plugin;
}
} }
} }
if ( isset( $plugin_info->response ) ) { if ( isset( $plugin_info->response ) ) {
foreach ( $plugin_info->response as $plugin ) { foreach ( $plugin_info->response as $plugin ) {
$plugin->upgrade = true; if ( isset( $plugin->slug ) ) {
$plugins[ $plugin->slug ] = $plugin; $plugin->upgrade = true;
$plugins[ $plugin->slug ] = $plugin;
}
} }
} }

View File

@ -44,6 +44,7 @@
* *
* @since 1.5.0 * @since 1.5.0
* @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers. * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers.
* @since 5.8.0 Added support for `Update URI` header.
* *
* @param string $plugin_file Absolute path to the main plugin file. * @param string $plugin_file Absolute path to the main plugin file.
* @param bool $markup Optional. If the returned data should have HTML markup applied. * @param bool $markup Optional. If the returned data should have HTML markup applied.
@ -63,6 +64,7 @@
* @type bool $Network Whether the plugin can only be activated network-wide. * @type bool $Network Whether the plugin can only be activated network-wide.
* @type string $RequiresWP Minimum required version of WordPress. * @type string $RequiresWP Minimum required version of WordPress.
* @type string $RequiresPHP Minimum required version of PHP. * @type string $RequiresPHP Minimum required version of PHP.
* @type string $UpdateURI ID of the plugin for update purposes, should be a URI.
* } * }
*/ */
function get_plugin_data( $plugin_file, $markup = true, $translate = true ) { function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
@ -79,6 +81,7 @@ function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
'Network' => 'Network', 'Network' => 'Network',
'RequiresWP' => 'Requires at least', 'RequiresWP' => 'Requires at least',
'RequiresPHP' => 'Requires PHP', 'RequiresPHP' => 'Requires PHP',
'UpdateURI' => 'Update URI',
// Site Wide Only is deprecated in favor of Network. // Site Wide Only is deprecated in favor of Network.
'_sitewide' => 'Site Wide Only', '_sitewide' => 'Site Wide Only',
); );

View File

@ -435,7 +435,24 @@ function wp_plugin_update_row( $file, $plugin_data ) {
); );
$plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags ); $plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags );
$details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $response->slug . '&section=changelog&TB_iframe=true&width=600&height=800' ); $plugin_slug = isset( $response->slug ) ? $response->slug : $response->id;
if ( isset( $response->slug ) ) {
$details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $plugin_slug . '&section=changelog' );
} elseif ( isset( $response->url ) ) {
$details_url = $response->url;
} else {
$details_url = $plugin_data['PluginURI'];
}
$details_url = add_query_arg(
array(
'TB_iframe' => 'true',
'width' => 600,
'height' => 800,
),
$details_url
);
/** @var WP_Plugins_List_Table $wp_list_table */ /** @var WP_Plugins_List_Table $wp_list_table */
$wp_list_table = _get_list_table( $wp_list_table = _get_list_table(
@ -461,8 +478,8 @@ function wp_plugin_update_row( $file, $plugin_data ) {
'<td colspan="%s" class="plugin-update colspanchange">' . '<td colspan="%s" class="plugin-update colspanchange">' .
'<div class="update-message notice inline %s notice-alt"><p>', '<div class="update-message notice inline %s notice-alt"><p>',
$active_class, $active_class,
esc_attr( $response->slug . '-update' ), esc_attr( $plugin_slug . '-update' ),
esc_attr( $response->slug ), esc_attr( $plugin_slug ),
esc_attr( $file ), esc_attr( $file ),
esc_attr( $wp_list_table->get_column_count() ), esc_attr( $wp_list_table->get_column_count() ),
$notice_type $notice_type

View File

@ -296,8 +296,11 @@ function wp_update_plugins( $extra_stats = array() ) {
$current = new stdClass; $current = new stdClass;
} }
$new_option = new stdClass; $updates = new stdClass;
$new_option->last_checked = time(); $updates->last_checked = time();
$updates->response = array();
$updates->translations = array();
$updates->no_update = array();
$doing_cron = wp_doing_cron(); $doing_cron = wp_doing_cron();
@ -327,7 +330,7 @@ function wp_update_plugins( $extra_stats = array() ) {
$plugin_changed = false; $plugin_changed = false;
foreach ( $plugins as $file => $p ) { foreach ( $plugins as $file => $p ) {
$new_option->checked[ $file ] = $p['Version']; $updates->checked[ $file ] = $p['Version'];
if ( ! isset( $current->checked[ $file ] ) || (string) $current->checked[ $file ] !== (string) $p['Version'] ) { if ( ! isset( $current->checked[ $file ] ) || (string) $current->checked[ $file ] !== (string) $p['Version'] ) {
$plugin_changed = true; $plugin_changed = true;
@ -418,38 +421,114 @@ function wp_update_plugins( $extra_stats = array() ) {
$response = json_decode( wp_remote_retrieve_body( $raw_response ), true ); $response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
foreach ( $response['plugins'] as &$plugin ) { if ( $response && is_array( $response ) ) {
$plugin = (object) $plugin; $updates->response = $response['plugins'];
$updates->translations = $response['translations'];
$updates->no_update = $response['no_update'];
}
if ( isset( $plugin->compatibility ) ) { // Support updates for any plugins using the `Update URI` header field.
$plugin->compatibility = (object) $plugin->compatibility; foreach ( $plugins as $plugin_file => $plugin_data ) {
if ( ! $plugin_data['UpdateURI'] || isset( $updates->response[ $plugin_file ] ) ) {
continue;
}
foreach ( $plugin->compatibility as &$data ) { $hostname = wp_parse_url( esc_url_raw( $plugin_data['UpdateURI'] ), PHP_URL_HOST );
$data = (object) $data;
/**
* Filters the update response for a given plugin hostname.
*
* The dynamic portion of the hook name, `$hostname`, refers to the hostname
* of the URI specified in the `Update URI` header field.
*
* @since 5.8.0
*
* @param array|false $update {
* The plugin update data with the latest details. Default false.
*
* @type string $id Optional. ID of the plugin for update purposes, should be a URI
* specified in the `Update URI` header field.
* @type string $slug Slug of the plugin.
* @type string $version The version of the plugin.
* @type string $url The URL for details of the plugin.
* @type string $package Optional. The update ZIP for the plugin.
* @type string $tested Optional. The version of WordPress the plugin is tested against.
* @type string $requires_php Optional. The version of PHP which the plugin requires.
* @type bool $autoupdate Optional. Whether the plugin should automatically update.
* @type array $icons Optional. Array of plugin icons.
* @type array $banners Optional. Array of plugin banners.
* @type array $banners_rtl Optional. Array of plugin RTL banners.
* @type array $translations {
* Optional. List of translation updates for the plugin.
*
* @type string $language The language the translation update is for.
* @type string $version The version of the plugin this translation is for.
* This is not the version of the language file.
* @type string $updated The update timestamp of the translation file.
* Should be a date in the `YYYY-MM-DD HH:MM:SS` format.
* @type string $package The ZIP location containing the translation update.
* @type string $autoupdate Whether the translation should be automatically installed.
* }
* }
* @param array $plugin_data Plugin headers.
* @param string $plugin_file Plugin filename.
* @param array $locales Installed locales to look translations for.
*/
$update = apply_filters( "update_plugins_{$hostname}", false, $plugin_data, $plugin_file, $locales );
if ( ! $update ) {
continue;
}
$update = (object) $update;
// Is it valid? We require at least a version.
if ( ! isset( $update->version ) ) {
continue;
}
// These should remain constant.
$update->id = $plugin_data['UpdateURI'];
$update->plugin = $plugin_file;
// WordPress needs the version field specified as 'new_version'.
if ( ! isset( $update->new_version ) ) {
$update->new_version = $update->version;
}
// Handle any translation updates.
if ( ! empty( $update->translations ) ) {
foreach ( $update->translations as $translation ) {
if ( isset( $translation['language'], $translation['package'] ) ) {
$translation['type'] = 'plugin';
$translation['slug'] = isset( $update->slug ) ? $update->slug : $update->id;
$updates->translations[] = $translation;
}
} }
} }
unset( $updates->no_update[ $plugin_file ], $updates->response[ $plugin_file ] );
if ( version_compare( $update->new_version, $plugin_data['Version'], '>' ) ) {
$updates->response[ $plugin_file ] = $update;
} else {
$updates->no_update[ $plugin_file ] = $update;
}
} }
unset( $plugin, $data ); $sanitize_plugin_update_payload = function( &$item ) {
$item = (object) $item;
foreach ( $response['no_update'] as &$plugin ) { unset( $item->translations, $item->compatibility );
$plugin = (object) $plugin;
}
unset( $plugin ); return $item;
};
if ( is_array( $response ) ) { array_walk( $updates->response, $sanitize_plugin_update_payload );
$new_option->response = $response['plugins']; array_walk( $updates->no_update, $sanitize_plugin_update_payload );
$new_option->translations = $response['translations'];
// TODO: Perhaps better to store no_update in a separate transient with an expiry?
$new_option->no_update = $response['no_update'];
} else {
$new_option->response = array();
$new_option->translations = array();
$new_option->no_update = array();
}
set_site_transient( 'update_plugins', $new_option ); set_site_transient( 'update_plugins', $updates );
} }
/** /**

View File

@ -13,7 +13,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.8-alpha-50920'; $wp_version = '5.8-alpha-50921';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.