Make WordPress Core

Changeset 50921


Ignore:
Timestamp:
05/17/2021 05:02:49 PM (3 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.