WordPress.org

Making WordPress.org

Changeset 10214


Ignore:
Timestamp:
08/28/2020 05:36:38 AM (15 months ago)
Author:
dd32
Message:

Plugin Directory: Add an initial run at Release Confirmation for plugins.

This is currently only enabled for plugin review members, as it needs some testing in production prior to being available to others.

Notably, this requires that a plugin be using tagged releases, it doesn't handle trunk releases (yet), that will be added next.

See: #5352

Location:
sites/trunk/wordpress.org/public_html/wp-content
Files:
6 added
8 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php

    r7798 r10214  
    5959        add_filter( 'wp_ajax_delete-support-rep', array( __NAMESPACE__ . '\Metabox\Support_Reps', 'remove_support_rep' ) );
    6060        add_action( 'wp_ajax_plugin-author-lookup', array( __NAMESPACE__ . '\Metabox\Author', 'lookup_author' ) );
     61
     62        add_action( 'save_post', array( __NAMESPACE__ . '\Metabox\Release_Confirmation', 'save_post' ) );
    6163    }
    6264
     
    410412
    411413        add_meta_box(
     414            'plugin-release-confirmation',
     415            __( 'Plugin Release Confirmation', 'wporg-plugins' ),
     416            array( __NAMESPACE__ . '\Metabox\Release_Confirmation', 'display' ),
     417            'plugin', 'normal', 'high'
     418        );
     419
     420        add_meta_box(
    412421            'plugin-author',
    413422            __( 'Author Card', 'wporg-plugins' ),
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php

    r10073 r10214  
    3434        new Routes\Plugin_Self_Close();
    3535        new Routes\Plugin_Self_Transfer();
     36        new Routes\Plugin_Release_Confirmation();
    3637        new Routes\Plugin_E2E_Callback();
    3738    }
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php

    r10119 r10214  
    587587        add_shortcode( 'readme-validator', array( __NAMESPACE__ . '\Shortcodes\Readme_Validator', 'display' ) );
    588588        add_shortcode( 'block-validator', array( __NAMESPACE__ . '\Shortcodes\Block_Validator', 'display' ) );
     589
     590        add_shortcode( Shortcodes\Release_Confirmation::SHORTCODE, array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'display' ) );
     591        add_action( 'template_redirect', array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'template_redirect' ) );
    589592    }
    590593
     
    603606            'readme-validator',
    604607            'block-validator',
     608            'release-confirmation',
    605609        );
    606610
     
    15041508
    15051509        return $content_pages;
     1510    }
     1511
     1512    /**
     1513     * Get a list of all Plugin Releases.
     1514     */
     1515    public static function get_releases( $plugin ) {
     1516        $plugin   = self::get_plugin_post( $plugin );
     1517        $releases = get_post_meta( $plugin->ID, 'releases', true );
     1518
     1519        // Meta doesn't exist yet? Lets fill it out.
     1520        if ( false === $releases || ! is_array( $releases ) ) {
     1521            update_post_meta( $plugin->ID, 'releases', [] );
     1522
     1523            $tags = get_post_meta( $plugin->ID, 'tags', true );
     1524            if ( $tags ) {
     1525                foreach ( $tags as $tag_version => $tag ) {
     1526                    self::add_release( $plugin, [
     1527                        'date' => strtotime( $tag['date'] ),
     1528                        'tag'  => $tag['tag'],
     1529                        'version' => $tag_version,
     1530                        'committer' => [ $tag['author'] ],
     1531                        'confirmations_required' => 0, // Old release, assume it's released.
     1532                    ] );
     1533                }
     1534            } else {
     1535                // Pull from SVN directly.
     1536                $svn_tags = Tools\SVN::ls( "https://plugins.svn.wordpress.org/{$plugin->post_name}/tags/", true ) ?: [];
     1537                foreach ( $svn_tags as $entry ) {
     1538                    // Discard files
     1539                    if ( 'dir' !== $entry['kind'] ) {
     1540                        continue;
     1541                    }
     1542
     1543                    $tag = $entry['filename'];
     1544
     1545                    // Prefix the 0 for plugin versions like 0.1
     1546                    if ( '.' == substr( $tag, 0, 1 ) ) {
     1547                        $tag = "0{$tag}";
     1548                    }
     1549
     1550                    self::add_release( $plugin, [
     1551                        'date' => strtotime( $entry['date'] ),
     1552                        'tag'  => $entry['filename'],
     1553                        'version' => $tag,
     1554                        'committer' => [ $entry['author'] ],
     1555                        'confirmations_required' => 0, // Old release, assume it's released.
     1556                    ] );
     1557                }
     1558            }
     1559
     1560            $releases = get_post_meta( $plugin->ID, 'releases', true ) ?: [];
     1561        }
     1562
     1563        return $releases;
     1564    }
     1565
     1566    /**
     1567     * Fetch a specific release of the plugin, by tag.
     1568     */
     1569    public static function get_release( $plugin, $tag ) {
     1570        $releases = self::get_releases( $plugin );
     1571
     1572        $filtered = wp_list_filter( $releases, compact( 'tag' ) );
     1573
     1574        if ( $filtered ) {
     1575            return array_shift( $filtered );
     1576        }
     1577
     1578        return false;
     1579    }
     1580
     1581    /**
     1582     * Add a Plugin Release to the internal storage.
     1583     */
     1584    public static function add_release( $plugin, $data ) {
     1585        if ( ! isset( $data['tag'] ) ) {
     1586            return false;
     1587        }
     1588        $plugin = self::get_plugin_post( $plugin );
     1589
     1590        $release = self::get_release( $plugin, $data['tag'] ) ?: [
     1591            'date'                   => time(),
     1592            'tag'                    => '',
     1593            'version'                => '',
     1594            'zips_built'             => false,
     1595            'confirmations'          => [],
     1596            // Confirmed by default if no release confiration.
     1597            'confirmed'              => ! $plugin->release_confirmation,
     1598            'confirmations_required' => (int) $plugin->release_confirmation,
     1599            'committer'              => [],
     1600            'revision'               => [],
     1601        ];
     1602
     1603        // Fill
     1604        foreach ( $data as $k => $v ) {
     1605            $release[ $k ] = $v;
     1606        }
     1607
     1608        $releases = self::get_releases( $plugin );
     1609
     1610        $releases[] = $release;
     1611
     1612        // Sort releases most recent first.
     1613        uasort( $releases, function( $a, $b ) {
     1614            return $b['date'] <=> $a['date'];
     1615        } );
     1616
     1617        return update_post_meta( $plugin->ID, 'releases', $releases );
    15061618    }
    15071619
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php

    r10211 r10214  
    787787
    788788    /**
    789      * Generates a link to self-transfer a plugin..
     789     * Generates a link to self-transfer a plugin.
    790790     *
    791791     * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
     
    798798            array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
    799799            home_url( 'wp-json/plugins/v1/plugin/' . $post->post_name . '/self-transfer' )
     800        );
     801    }
     802
     803    /**
     804     * Generates a link to enable Release Confirmations.
     805     *
     806     * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
     807     * @return string URL to enable confirmations.
     808     */
     809    public static function get_enable_release_confirmation_link( $post = null ) {
     810        $post = get_post( $post );
     811
     812        return add_query_arg(
     813            array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
     814            home_url( 'wp-json/plugins/v1/plugin/' . $post->post_name . '/release-confirmation' )
     815        );
     816    }
     817
     818    /**
     819     * Generates a link to confirm a release.
     820     *
     821     * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
     822     * @return string URL to enable confirmations.
     823     */
     824    public static function get_release_confirmation_link( $tag, $post = null) {
     825        $post = get_post( $post );
     826
     827        return add_query_arg(
     828            array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
     829            home_url( 'wp-json/plugins/v1/plugin/' . $post->post_name . '/release-confirmation/' . $tag )
     830        );
     831    }
     832
     833    /**
     834     * Generates a link to email the release confirmation link.
     835     *
     836     * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
     837     * @return string URL to enable confirmations.
     838     */
     839    public static function get_release_confirmation_access_link() {
     840        return add_query_arg(
     841            array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
     842            home_url( 'wp-json/plugins/v1/release-confirmation-access' )
    800843        );
    801844    }
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php

    r10199 r10214  
    77use WordPressdotorg\Plugin_Directory\Block_JSON;
    88use WordPressdotorg\Plugin_Directory\Plugin_Directory;
     9use WordPressdotorg\Plugin_Directory\Email\Release_Confirmation as Release_Confirmation_Email;
    910use WordPressdotorg\Plugin_Directory\Readme\Parser;
    1011use WordPressdotorg\Plugin_Directory\Template;
     
    7576        $headers         = $data['plugin_headers'];
    7677        $stable_tag      = $data['stable_tag'];
     78        $last_committer  = $data['last_committer'];
     79        $last_revision   = $data['last_revision'];
    7780        $tagged_versions = $data['tagged_versions'];
    7881        $last_modified   = $data['last_modified'];
    7982        $blocks          = $data['blocks'];
    8083        $block_files     = $data['block_files'];
     84
     85        // Release confirmation
     86        if ( $plugin->release_confirmation ) {
     87            if ( 'trunk' === $stable_tag ) {
     88                throw new Exception( 'Plugin cannot be released from trunk due to release confirmation being enabled.' );
     89            }
     90
     91            $release = Plugin_Directory::get_release( $plugin, $stable_tag );
     92
     93            // This tag is unknown? Trigger email.
     94            if ( ! $release ) {
     95                Plugin_Directory::add_release(
     96                    [
     97                        'tag'       => $stable_tag,
     98                        'version'   => $headers->Version,
     99                        'committer' => [ $last_committer ],
     100                        'revision'  => [ $last_revision ]
     101                    ]
     102                );
     103
     104                $email = new Release_Confirmation_Email(
     105                    $plugin,
     106                    Tools::get_plugin_committers( $plugin_slug ),
     107                    [
     108                        'release' => $releases[ $stable_tag ],
     109                        'who'     => $last_committer,
     110                        'readme'  => $readme,
     111                        'headers' => $headers,
     112                    ]
     113                );
     114                $email->send();
     115
     116                throw new Exception( 'Plugin release not confirmed; email triggered.' );
     117            }
     118
     119            // Check that the tag is approved.
     120            if ( ! $release['confirmed'] ) {
     121
     122                if ( ! in_array( $last_committer, $release['committer'], true ) ) {
     123                    $release['committer'][] = $last_committer;
     124                }
     125                if ( ! in_array( $last_revision, $release['revision'], true ) ) {
     126                    $release['revision'][] = $last_revision;
     127                }
     128
     129                // Update with ^
     130                Plugin_Directory::add_release( $plugin, $release );
     131
     132                throw new Exception( 'Plugin release not confirmed.' );
     133            }
     134
     135            // At this point we can assume that the release was confirmed, and should be imported.
     136        }
    81137
    82138        $content = '';
     
    261317        }
    262318
     319        $plugin = Plugin_Directory::get_plugin_post( $plugin_slug );
     320
     321        // Don't rebuild release-confirmation-required tags.
     322        if ( $plugin->release_confirmation ) {
     323            foreach ( $versions_to_build as $i => $tag ) {
     324                $release = Plugin_Directory::get_release( $plugin, $tag );
     325
     326                if ( ! $release || ( $release['zips_built'] && $release['confirmations_required'] ) ) {
     327                    unset( $versions_to_build[ $i ] );
     328                } else {
     329                    $release['zips_built'] = true;
     330                    Plugin_Directory::add_release( $release );
     331                }
     332
     333            }
     334        }
     335
     336        if ( ! $versions_to_build ) {
     337            return false;
     338        }
     339
    263340        // Rebuild/Build $build_zips
    264341        try {
     
    390467        }
    391468
     469        $last_committer = $svn_info['result']['Last Changed Author'] ?? '';
     470        $last_revision  = $svn_info['result']['Last Changed Rev'] ?? 0;
     471
    392472        $svn_export = SVN::export(
    393473            $stable_url,
     
    549629        } ) );
    550630
    551         return compact( 'readme', 'stable_tag', 'last_modified', 'tmp_dir', 'plugin_headers', 'assets', 'tagged_versions', 'blocks', 'block_files' );
     631        return compact( 'readme', 'stable_tag', 'last_modified', 'last_committer', 'last_revision', 'tmp_dir', 'plugin_headers', 'assets', 'tagged_versions', 'blocks', 'block_files' );
    552632    }
    553633
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-base.php

    r10124 r10214  
    2525
    2626    /**
    27      * @param $plugin The plugin this email relates to.
     27     * @param $plugin  The plugin this email relates to.
    2828     * @param $users[] A list of users to email.
    29      * @param $args[] A list of args that the email requires.
     29     * @param $args[]  A list of args that the email requires.
    3030     */
    31     public function __construct( $plugin, $users, $args = array() ) {
    32         $this->plugin = Plugin_Directory::get_plugin_post( $plugin );
     31    public function __construct( $plugin, $users = [], $args = [] ) {
     32
     33        // Sometimes we don't have a plugin context, just a user..
     34        if ( $plugin instanceOf WP_User ) {
     35            $users = [ $plugin ];
     36            $args  = $users; // Just assume that args will have been passed in there.
     37
     38            $this->plugin = true; // To pass checks..
     39        } else {
     40            $this->plugin = Plugin_Directory::get_plugin_post( $plugin );
     41        }
    3342
    3443        // Don't cast an object to an array, but rather an array of object.
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/inc/template-tags.php

    r9989 r10214  
    1010namespace WordPressdotorg\Plugin_Directory\Theme;
    1111
     12use WordPressdotorg\Plugin_Directory\Plugin_Directory;
    1213use WordPressdotorg\Plugin_Directory\Template;
    1314use WordPressdotorg\Plugin_Directory\Tools;
     
    260261}
    261262
     263function the_unconfirmed_releases_notice() {
     264    $plugin = get_post();
     265
     266    if ( ! $plugin->release_confirmation || ! current_user_can( 'plugin_admin_edit', $plugin ) ) {
     267        return;
     268    }
     269
     270    $confirmations_required = $plugin->release_confirmation;
     271    $releases               = Plugin_Directory::get_releases( $plugin ) ?: [];
     272    $unconfirmed_releases   = wp_list_filter( $confirmed_releases, [ 'confirmed' => false ] );
     273
     274    if ( ! $unconfirmed_releases ) {
     275        return;
     276    }
     277
     278    printf(
     279        '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
     280        sprintf(
     281            __( 'This plugin has <a href="%s">a pending release that requires confirmation</a>.', 'wporg-plugins' ),
     282            home_url( '/developers/releases/' ) // TODO: Hardcoded URL.
     283        )
     284    );
     285}
     286
    262287/**
    263288 * Display the ADVANCED Zone.
     
    346371
    347372    echo '<div class="plugin-notice notice notice-error notice-alt"><p>' . esc_html__( 'These features often cannot be undone without intervention. Please do not attempt to use them unless you are absolutely certain. When in doubt, contact the plugins team for assistance.', 'wporg-plugins' ) . '</p></div>';
     373
     374    // Output the Release Confirmation form.
     375    the_plugin_release_confirmation_form();
    348376
    349377    // Output the transfer form.
     
    453481
    454482}
     483
     484function the_plugin_release_confirmation_form() {
     485    $post = get_post();
     486
     487    // Temporary: Plugin Reviewers only.
     488    if ( ! current_user_can( 'edit_post', $post ) ) {
     489        return;
     490    }
     491
     492    if (
     493        ! current_user_can( 'plugin_admin_edit', $post ) ||
     494        'publish' != $post->post_status
     495    ) {
     496        return;
     497    }
     498
     499    $confirmations_required = $post->release_confirmation;
     500
     501    echo '<h4>' . esc_html__( 'Release Confirmation', 'wporg-plugins' ) . '</h4>';
     502    if ( $confirmations_required ) {
     503        echo '<p>' . __( 'Release confirmations for this plugin are <strong>enabled</strong>.', 'wporg-plugins' ) . '</p>';
     504    } else {
     505        echo '<p>' . __( 'Release confirmations for this plugin are <strong>disabled</strong>', 'wporg-plugins' ) . '</p>';
     506    }
     507    echo '<p>' . esc_html__( 'All future releases will require email confirmation before being made available. This increases security and ensures that plugin releases are only made when intended.', 'wporg-plugins' ) . '</p>';
     508
     509    if ( ! $confirmations_required && 'trunk' === $post->stable_tag ) {
     510        echo '<div class="plugin-notice notice notice-warning notice-alt"><p>';
     511            _e( "Release confirmations currently require tagged releases, as you're releasing from trunk they cannot be enabled.", 'wporg-plugins' );
     512        echo '</p></div>';
     513
     514    } else if ( ! $confirmations_required ) {
     515        echo '<div class="plugin-notice notice notice-warning notice-alt"><p>';
     516            _e( '<strong>Warning:</strong> Enabling release confirmations is intended to be a <em>permanent</em> action. There is no way to disable this without contacting the plugins team.', 'wporg-plugins' );
     517        echo '</p></div>';
     518
     519        echo '<form method="POST" action="' . esc_url( Template::get_enable_release_confirmation_link() ) . '" onsubmit="return confirm( jQuery(this).prev(\'.notice\').text() );">';
     520        echo '<p><input class="button" type="submit" value="' . esc_attr__( 'I understand, please enable release confirmations.', 'wporg-plugins' ) . '" /></p>';
     521        echo '</form>';
     522
     523    } else {
     524        /* translators: 1: plugins@wordpress.org */
     525        echo '<p>' . sprintf( __( 'To disable release confirmations, please contact the plugins team by emailing %s.', 'wporg-plugins' ), 'plugins@wordpress.org' ) . '</p>';
     526    }
     527}
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php

    r9680 r10214  
    2626    <header class="plugin-header">
    2727        <?php the_active_plugin_notice(); ?>
     28        <?php the_unconfirmed_releases_notice(); ?>
    2829
    2930        <div class="entry-thumbnail">
Note: See TracChangeset for help on using the changeset viewer.