diff --git a/wp-admin/includes/class-wp-plugin-install-list-table.php b/wp-admin/includes/class-wp-plugin-install-list-table.php index 3067f97674..9c5f382e9d 100644 --- a/wp-admin/includes/class-wp-plugin-install-list-table.php +++ b/wp-admin/includes/class-wp-plugin-install-list-table.php @@ -47,15 +47,19 @@ class WP_Plugin_Install_List_Table extends WP_List_Table { $plugin_info = get_site_transient( 'update_plugins' ); if ( isset( $plugin_info->no_update ) ) { foreach ( $plugin_info->no_update as $plugin ) { - $plugin->upgrade = false; - $plugins[ $plugin->slug ] = $plugin; + if ( isset( $plugin->slug ) ) { + $plugin->upgrade = false; + $plugins[ $plugin->slug ] = $plugin; + } } } if ( isset( $plugin_info->response ) ) { foreach ( $plugin_info->response as $plugin ) { - $plugin->upgrade = true; - $plugins[ $plugin->slug ] = $plugin; + if ( isset( $plugin->slug ) ) { + $plugin->upgrade = true; + $plugins[ $plugin->slug ] = $plugin; + } } } diff --git a/wp-admin/includes/plugin.php b/wp-admin/includes/plugin.php index 3a4b488127..55d57e31e2 100644 --- a/wp-admin/includes/plugin.php +++ b/wp-admin/includes/plugin.php @@ -44,6 +44,7 @@ * * @since 1.5.0 * @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 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 string $RequiresWP Minimum required version of WordPress. * @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 ) { @@ -79,6 +81,7 @@ function get_plugin_data( $plugin_file, $markup = true, $translate = true ) { 'Network' => 'Network', 'RequiresWP' => 'Requires at least', 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', // Site Wide Only is deprecated in favor of Network. '_sitewide' => 'Site Wide Only', ); diff --git a/wp-admin/includes/update.php b/wp-admin/includes/update.php index 956396f4c8..aeccc3e049 100644 --- a/wp-admin/includes/update.php +++ b/wp-admin/includes/update.php @@ -435,7 +435,24 @@ function wp_plugin_update_row( $file, $plugin_data ) { ); $plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags ); - $details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $response->slug . '§ion=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 . '§ion=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 */ $wp_list_table = _get_list_table( @@ -461,8 +478,8 @@ function wp_plugin_update_row( $file, $plugin_data ) { '' . '

', $active_class, - esc_attr( $response->slug . '-update' ), - esc_attr( $response->slug ), + esc_attr( $plugin_slug . '-update' ), + esc_attr( $plugin_slug ), esc_attr( $file ), esc_attr( $wp_list_table->get_column_count() ), $notice_type diff --git a/wp-includes/update.php b/wp-includes/update.php index d4b538e29d..79e3b3ea33 100644 --- a/wp-includes/update.php +++ b/wp-includes/update.php @@ -296,8 +296,11 @@ function wp_update_plugins( $extra_stats = array() ) { $current = new stdClass; } - $new_option = new stdClass; - $new_option->last_checked = time(); + $updates = new stdClass; + $updates->last_checked = time(); + $updates->response = array(); + $updates->translations = array(); + $updates->no_update = array(); $doing_cron = wp_doing_cron(); @@ -327,7 +330,7 @@ function wp_update_plugins( $extra_stats = array() ) { $plugin_changed = false; 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'] ) { $plugin_changed = true; @@ -418,38 +421,114 @@ function wp_update_plugins( $extra_stats = array() ) { $response = json_decode( wp_remote_retrieve_body( $raw_response ), true ); - foreach ( $response['plugins'] as &$plugin ) { - $plugin = (object) $plugin; + if ( $response && is_array( $response ) ) { + $updates->response = $response['plugins']; + $updates->translations = $response['translations']; + $updates->no_update = $response['no_update']; + } - if ( isset( $plugin->compatibility ) ) { - $plugin->compatibility = (object) $plugin->compatibility; + // Support updates for any plugins using the `Update URI` header field. + foreach ( $plugins as $plugin_file => $plugin_data ) { + if ( ! $plugin_data['UpdateURI'] || isset( $updates->response[ $plugin_file ] ) ) { + continue; + } - foreach ( $plugin->compatibility as &$data ) { - $data = (object) $data; + $hostname = wp_parse_url( esc_url_raw( $plugin_data['UpdateURI'] ), PHP_URL_HOST ); + + /** + * 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 ) { - $plugin = (object) $plugin; - } + unset( $item->translations, $item->compatibility ); - unset( $plugin ); + return $item; + }; - if ( is_array( $response ) ) { - $new_option->response = $response['plugins']; - $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(); - } + array_walk( $updates->response, $sanitize_plugin_update_payload ); + array_walk( $updates->no_update, $sanitize_plugin_update_payload ); - set_site_transient( 'update_plugins', $new_option ); + set_site_transient( 'update_plugins', $updates ); } /** diff --git a/wp-includes/version.php b/wp-includes/version.php index 0c571a07cf..41a1f05f7a 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @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.