Make WordPress Core

Changeset 50921


Ignore:
Timestamp:
05/17/2021 05:02:49 PM (5 years ago)
Author:
SergeyBiryukov
Message:

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.

Location:
trunk/src
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-wp-plugin-install-list-table.php

    r50808 r50921  
    4848                if ( isset( $plugin_info->no_update ) ) {
    4949                        foreach ( $plugin_info->no_update as $plugin ) {
    50                                 $plugin->upgrade          = false;
    51                                 $plugins[ $plugin->slug ] = $plugin;
     50                                if ( isset( $plugin->slug ) ) {
     51                                        $plugin->upgrade          = false;
     52                                        $plugins[ $plugin->slug ] = $plugin;
     53                                }
    5254                        }
    5355                }
     
    5557                if ( isset( $plugin_info->response ) ) {
    5658                        foreach ( $plugin_info->response as $plugin ) {
    57                                 $plugin->upgrade          = true;
    58                                 $plugins[ $plugin->slug ] = $plugin;
     59                                if ( isset( $plugin->slug ) ) {
     60                                        $plugin->upgrade          = true;
     61                                        $plugins[ $plugin->slug ] = $plugin;
     62                                }
    5963                        }
    6064                }
  • trunk/src/wp-admin/includes/plugin.php

    r50788 r50921  
    4545 * @since 1.5.0
    4646 * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers.
     47 * @since 5.8.0 Added support for `Update URI` header.
    4748 *
    4849 * @param string $plugin_file Absolute path to the main plugin file.
     
    6465 *     @type string $RequiresWP  Minimum required version of WordPress.
    6566 *     @type string $RequiresPHP Minimum required version of PHP.
     67 *     @type string $UpdateURI   ID of the plugin for update purposes, should be a URI.
    6668 * }
    6769 */
     
    8082                'RequiresWP'  => 'Requires at least',
    8183                'RequiresPHP' => 'Requires PHP',
     84                'UpdateURI'   => 'Update URI',
    8285                // Site Wide Only is deprecated in favor of Network.
    8386                '_sitewide'   => 'Site Wide Only',
  • trunk/src/wp-admin/includes/update.php

    r50121 r50921  
    436436
    437437        $plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags );
    438         $details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $response->slug . '&section=changelog&TB_iframe=true&width=600&height=800' );
     438        $plugin_slug = isset( $response->slug ) ? $response->slug : $response->id;
     439
     440        if ( isset( $response->slug ) ) {
     441                $details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $plugin_slug . '&section=changelog' );
     442        } elseif ( isset( $response->url ) ) {
     443                $details_url = $response->url;
     444        } else {
     445                $details_url = $plugin_data['PluginURI'];
     446        }
     447
     448        $details_url = add_query_arg(
     449                array(
     450                        'TB_iframe' => 'true',
     451                        'width'     => 600,
     452                        'height'    => 800,
     453                ),
     454                $details_url
     455        );
    439456
    440457        /** @var WP_Plugins_List_Table $wp_list_table */
     
    462479                        '<div class="update-message notice inline %s notice-alt"><p>',
    463480                        $active_class,
    464                         esc_attr( $response->slug . '-update' ),
    465                         esc_attr( $response->slug ),
     481                        esc_attr( $plugin_slug . '-update' ),
     482                        esc_attr( $plugin_slug ),
    466483                        esc_attr( $file ),
    467484                        esc_attr( $wp_list_table->get_column_count() ),
  • trunk/src/wp-includes/update.php

    r50082 r50921  
    297297        }
    298298
    299         $new_option               = new stdClass;
    300         $new_option->last_checked = time();
     299        $updates               = new stdClass;
     300        $updates->last_checked = time();
     301        $updates->response     = array();
     302        $updates->translations = array();
     303        $updates->no_update    = array();
    301304
    302305        $doing_cron = wp_doing_cron();
     
    328331
    329332                foreach ( $plugins as $file => $p ) {
    330                         $new_option->checked[ $file ] = $p['Version'];
     333                        $updates->checked[ $file ] = $p['Version'];
    331334
    332335                        if ( ! isset( $current->checked[ $file ] ) || (string) $current->checked[ $file ] !== (string) $p['Version'] ) {
     
    419422        $response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
    420423
    421         foreach ( $response['plugins'] as &$plugin ) {
    422                 $plugin = (object) $plugin;
    423 
    424                 if ( isset( $plugin->compatibility ) ) {
    425                         $plugin->compatibility = (object) $plugin->compatibility;
    426 
    427                         foreach ( $plugin->compatibility as &$data ) {
    428                                 $data = (object) $data;
     424        if ( $response && is_array( $response ) ) {
     425                $updates->response     = $response['plugins'];
     426                $updates->translations = $response['translations'];
     427                $updates->no_update    = $response['no_update'];
     428        }
     429
     430        // Support updates for any plugins using the `Update URI` header field.
     431        foreach ( $plugins as $plugin_file => $plugin_data ) {
     432                if ( ! $plugin_data['UpdateURI'] || isset( $updates->response[ $plugin_file ] ) ) {
     433                        continue;
     434                }
     435
     436                $hostname = wp_parse_url( esc_url_raw( $plugin_data['UpdateURI'] ), PHP_URL_HOST );
     437
     438                /**
     439                 * Filters the update response for a given plugin hostname.
     440                 *
     441                 * The dynamic portion of the hook name, `$hostname`, refers to the hostname
     442                 * of the URI specified in the `Update URI` header field.
     443                 *
     444                 * @since 5.8.0
     445                 *
     446                 * @param array|false $update {
     447                 *     The plugin update data with the latest details. Default false.
     448                 *
     449                 *     @type string $id           Optional. ID of the plugin for update purposes, should be a URI
     450                 *                                specified in the `Update URI` header field.
     451                 *     @type string $slug         Slug of the plugin.
     452                 *     @type string $version      The version of the plugin.
     453                 *     @type string $url          The URL for details of the plugin.
     454                 *     @type string $package      Optional. The update ZIP for the plugin.
     455                 *     @type string $tested       Optional. The version of WordPress the plugin is tested against.
     456                 *     @type string $requires_php Optional. The version of PHP which the plugin requires.
     457                 *     @type bool   $autoupdate   Optional. Whether the plugin should automatically update.
     458                 *     @type array  $icons        Optional. Array of plugin icons.
     459                 *     @type array  $banners      Optional. Array of plugin banners.
     460                 *     @type array  $banners_rtl  Optional. Array of plugin RTL banners.
     461                 *     @type array  $translations {
     462                 *         Optional. List of translation updates for the plugin.
     463                 *
     464                 *         @type string $language   The language the translation update is for.
     465                 *         @type string $version    The version of the plugin this translation is for.
     466                 *                                  This is not the version of the language file.
     467                 *         @type string $updated    The update timestamp of the translation file.
     468                 *                                  Should be a date in the `YYYY-MM-DD HH:MM:SS` format.
     469                 *         @type string $package    The ZIP location containing the translation update.
     470                 *         @type string $autoupdate Whether the translation should be automatically installed.
     471                 *     }
     472                 * }
     473                 * @param array       $plugin_data      Plugin headers.
     474                 * @param string      $plugin_file      Plugin filename.
     475                 * @param array       $locales          Installed locales to look translations for.
     476                 */
     477                $update = apply_filters( "update_plugins_{$hostname}", false, $plugin_data, $plugin_file, $locales );
     478
     479                if ( ! $update ) {
     480                        continue;
     481                }
     482
     483                $update = (object) $update;
     484
     485                // Is it valid? We require at least a version.
     486                if ( ! isset( $update->version ) ) {
     487                        continue;
     488                }
     489
     490                // These should remain constant.
     491                $update->id     = $plugin_data['UpdateURI'];
     492                $update->plugin = $plugin_file;
     493
     494                // WordPress needs the version field specified as 'new_version'.
     495                if ( ! isset( $update->new_version ) ) {
     496                        $update->new_version = $update->version;
     497                }
     498
     499                // Handle any translation updates.
     500                if ( ! empty( $update->translations ) ) {
     501                        foreach ( $update->translations as $translation ) {
     502                                if ( isset( $translation['language'], $translation['package'] ) ) {
     503                                        $translation['type'] = 'plugin';
     504                                        $translation['slug'] = isset( $update->slug ) ? $update->slug : $update->id;
     505
     506                                        $updates->translations[] = $translation;
     507                                }
    429508                        }
    430509                }
    431         }
    432 
    433         unset( $plugin, $data );
    434 
    435         foreach ( $response['no_update'] as &$plugin ) {
    436                 $plugin = (object) $plugin;
    437         }
    438 
    439         unset( $plugin );
    440 
    441         if ( is_array( $response ) ) {
    442                 $new_option->response     = $response['plugins'];
    443                 $new_option->translations = $response['translations'];
    444                 // TODO: Perhaps better to store no_update in a separate transient with an expiry?
    445                 $new_option->no_update = $response['no_update'];
    446         } else {
    447                 $new_option->response     = array();
    448                 $new_option->translations = array();
    449                 $new_option->no_update    = array();
    450         }
    451 
    452         set_site_transient( 'update_plugins', $new_option );
     510
     511                unset( $updates->no_update[ $plugin_file ], $updates->response[ $plugin_file ] );
     512
     513                if ( version_compare( $update->new_version, $plugin_data['Version'], '>' ) ) {
     514                        $updates->response[ $plugin_file ] = $update;
     515                } else {
     516                        $updates->no_update[ $plugin_file ] = $update;
     517                }
     518        }
     519
     520        $sanitize_plugin_update_payload = function( &$item ) {
     521                $item = (object) $item;
     522
     523                unset( $item->translations, $item->compatibility );
     524
     525                return $item;
     526        };
     527
     528        array_walk( $updates->response, $sanitize_plugin_update_payload );
     529        array_walk( $updates->no_update, $sanitize_plugin_update_payload );
     530
     531        set_site_transient( 'update_plugins', $updates );
    453532}
    454533
Note: See TracChangeset for help on using the changeset viewer.