<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[10214] sites/trunk/wordpress.org/public_html/wp-content: Plugin Directory: Add an initial run at Release Confirmation for plugins.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/10214">10214</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/10214","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dd32</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2020-08-28 05:36:38 +0000 (Fri, 28 Aug 2020)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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: <a href="http://meta.trac.wordpress.org/ticket/5352">#5352</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminclasscustomizationsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiclassbasephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclassplugindirectoryphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclasstemplatephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectorycliclassimportphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassbasephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-base.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentthemespubwporgpluginsinctemplatetagsphp">sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/inc/template-tags.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentthemespubwporgpluginstemplatepartspluginsinglephp">sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminmetaboxclassreleaseconfirmationphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-release-confirmation.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiroutesclasspluginreleaseconfirmationphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassreleaseconfirmationaccessphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-access.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassreleaseconfirmationenabledphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-enabled.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassreleaseconfirmationphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryshortcodesclassreleaseconfirmationphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminclasscustomizationsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php  2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php    2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -58,6 +58,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'wp_ajax_add-support-rep', array( __NAMESPACE__ . '\Metabox\Support_Reps', 'add_support_rep' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'wp_ajax_delete-support-rep', array( __NAMESPACE__ . '\Metabox\Support_Reps', 'remove_support_rep' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_ajax_plugin-author-lookup', array( __NAMESPACE__ . '\Metabox\Author', 'lookup_author' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               add_action( 'save_post', array( __NAMESPACE__ . '\Metabox\Release_Confirmation', 'save_post' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -409,6 +411,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_meta_box(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'plugin-release-confirmation',
+                       __( 'Plugin Release Confirmation', 'wporg-plugins' ),
+                       array( __NAMESPACE__ . '\Metabox\Release_Confirmation', 'display' ),
+                       'plugin', 'normal', 'high'
+               );
+
+               add_meta_box(
</ins><span class="cx" style="display: block; padding: 0 10px">                         'plugin-author',
</span><span class="cx" style="display: block; padding: 0 10px">                        __( 'Author Card', 'wporg-plugins' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        array( __NAMESPACE__ . '\Metabox\Author_Card', 'display' ),
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminmetaboxclassreleaseconfirmationphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-release-confirmation.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-release-confirmation.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-release-confirmation.php      2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,66 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\Admin\Metabox;
+
+use WP_REST_Request;
+use WordPressdotorg\Plugin_Directory\Tools;
+use WordPressdotorg\Plugin_Directory\Shortcodes\Release_Confirmation as Release_Confirmation_Shortcode;
+
+/**
+ * Plugin Release Confirmation controls.
+ *
+ * @package WordPressdotorg\Plugin_Directory\Admin\Metabox
+ */
+class Release_Confirmation {
+       static function display() {
+               global $post;
+
+               echo '<p><select name="release_confirmation" onchange="jQuery(this).next().removeClass(\'hidden\');">';
+               foreach ( [
+                       0 => 'No approval required',
+                       1 => 'One confirmation required',
+                       2 => 'Two confirmations required'
+               ] as $num => $text ) {
+                       printf(
+                               '<option value="%s" %s>%s</option>',
+                               $num,
+                               selected( $post->release_confirmation, $num, false ),
+                               $text
+                       );
+               }
+               echo "</select><span class='hidden'>&nbsp;Don't forget to save the changes!</span></p>";
+
+               if ( $post->release_confirmation ) {
+                       Release_Confirmation_Shortcode::single_plugin_row( $post, $include_header = false );
+               }
+       }
+
+       // Save the selection.
+       static function save_post( $post_id ) {
+               if (
+                       isset( $_REQUEST['release_confirmation'] ) &&
+                       is_numeric( $_REQUEST['release_confirmation'] ) &&
+                       current_user_can( 'plugin_admin_edit', $post_id )
+               ) {
+                       if ( 0 == $_REQUEST['release_confirmation'] ) {
+                               // Disable
+                               Tools::audit_log( 'Plugin release approval disabled.', $post_id );
+                               update_post_meta( $post_id, 'release_confirmation', 0 );
+
+                       } else {
+                               // Enable, re-use the API for this one.
+                               $request = new WP_REST_Request(
+                                       'POST',
+                                       '/plugins/v1/plugin/' . get_post( $post_id )->post_name . '/release-confirmation'
+                               );
+                               $request->set_param(
+                                       'confirmations_required',
+                                       (int) $_REQUEST['release_confirmation']
+                               );
+
+                               // For some reason, this is causing a 502 bad gateway - upstream sent too big header
+                               // See if it works in production.
+                               rest_do_request( $request );
+                       }
+               }
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-release-confirmation.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiclassbasephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php      2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php        2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -33,6 +33,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                new Routes\Plugin_Support_Reps();
</span><span class="cx" style="display: block; padding: 0 10px">                new Routes\Plugin_Self_Close();
</span><span class="cx" style="display: block; padding: 0 10px">                new Routes\Plugin_Self_Transfer();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                new Routes\Plugin_Release_Confirmation();
</ins><span class="cx" style="display: block; padding: 0 10px">                 new Routes\Plugin_E2E_Callback();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiroutesclasspluginreleaseconfirmationphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php                                (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php  2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,217 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\API\Routes;
+
+use WP_REST_Response;
+use WordPressdotorg\Plugin_Directory\Plugin_Directory;
+use WordPressdotorg\Plugin_Directory\API\Base;
+use WordPressdotorg\Plugin_Directory\Tools;
+use WordPressdotorg\Plugin_Directory\Jobs\Plugin_Import;
+use WordPressdotorg\Plugin_Directory\Shortcodes\Release_Confirmation as Release_Confirmation_Shortcode;
+use WordPressdotorg\Plugin_Directory\Email\Release_Confirmation_Enabled as Release_Confirmation_Enabled_Email;
+use WordPressdotorg\Plugin_Directory\Email\Release_Confirmation_Access as Release_Confirmation_Access_Email;
+
+/**
+ * An API endpoint for closing a particular plugin.
+ *
+ * @package WordPressdotorg_Plugin_Directory
+ */
+class Plugin_Release_Confirmation extends Base {
+
+       public function __construct() {
+               register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/release-confirmation', [
+                       'methods'             => \WP_REST_Server::CREATABLE,
+                       'callback'            => [ $this, 'enable_release_confirmation' ],
+                       'args'                => [
+                               'plugin_slug' => [
+                                       'validate_callback' => [ $this, 'validate_plugin_slug_callback' ],
+                               ],
+                       ],
+                       'permission_callback' => function( $request ) {
+                               $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
+
+                               return current_user_can( 'plugin_admin_edit', $plugin ) && 'publish' === $plugin->post_status;
+                       },
+               ] );
+
+               register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/release-confirmation/(?P<plugin_tag>[^/]+)', [
+                       'methods'             => \WP_REST_Server::READABLE, // TODO: This really should be a POST
+                       'callback'            => [ $this, 'confirm_release' ],
+                       'args'                => [
+                               'plugin_slug' => [
+                                       'validate_callback' => [ $this, 'validate_plugin_slug_callback' ],
+                               ],
+                               'plugin_tag' => [
+                                       'validate_callback' => [ $this, 'validate_plugin_tag_callback' ],
+                               ]
+                       ],
+                       'permission_callback' => function( $request ) {
+                               $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
+
+                               return (
+                                       Release_Confirmation_Shortcode::can_access() &&
+                                       current_user_can( 'plugin_admin_edit', $plugin ) &&
+                                       'publish' === $plugin->post_status
+                               );
+                       },
+               ] );
+
+               register_rest_route( 'plugins/v1', '/release-confirmation-access', [
+                       'methods'             => \WP_REST_Server::READABLE,
+                       'callback'            => [ $this, 'send_access_email' ],
+                       'args'                => [
+                       ],
+                       'permission_callback' => function( $request ) {
+                               return is_user_logged_in();
+                       },
+               ] );
+
+               add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 );
+       }
+
+       /**
+        * Redirect back to the plugins page when this endpoint is accessed with an invalid nonce.
+        */
+       function override_cookie_expired_message( $result, $obj, $request ) {
+               if (
+                       is_array( $result ) && isset( $result['code'] ) &&
+                       (
+                               preg_match( '!^/plugins/v1/plugin/([^/]+)/release-confirmation(/[^/]+)?$!', $request->get_route(), $m )
+                               ||
+                               '/plugins/v1/release-confirmation-access' === $request->get_route()
+                       )
+               ) {
+                       if ( 'rest_cookie_invalid_nonce' == $result['code'] || 'rest_forbidden' == $result['code'] ) {
+                               wp_die( 'The link you have followed has expired.' );
+                       }
+               }
+
+               return $result;
+       }
+
+       /**
+        * Endpoint to self-close a plugin.
+        *
+        * @param \WP_REST_Request $request The Rest API Request.
+        * @return bool True if the favoriting was successful.
+        */
+       public function enable_release_confirmation( $request ) {
+               $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
+               $result = [
+                       'location' => wp_get_referer() ?: get_permalink( $plugin ),
+               ];
+
+               $confirmations_required = $request['confirmations_required'] ?? 1;
+
+               // Only redirect if we've been called via the rest api, and not an internal api request.
+               if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
+                       header( 'Location: ' . $result['location'] );
+
+                       // When requested via the REST API, confirmations can only be increased.
+                       $confirmations_required = max( (int)$confirmations_required, (int)$plugin->release_confirmation );
+               }
+
+               // Abort early if needed.
+               if ( $plugin->release_confirmation == $confirmations_required ) {
+                       return $result;
+               }
+
+               // Fetch the releases first, to prefill them if needed with the old release_confirmation count.
+               Plugin_Directory::get_releases();
+
+               // Update the Metadata.
+               update_post_meta( $plugin->ID, 'release_confirmation', $confirmations_required );
+
+               // Add an audit-log entry.
+               Tools::audit_log(
+                       sprintf(
+                               'Release Confirmations Enabled. %d confirmations required',
+                               $confirmations_required
+                       ),
+                       $plugin
+               );
+
+               $email = new Release_Confirmation_Enabled_Email(
+                       $plugin,
+                       Tools::get_plugin_committers( $plugin->post_name ),
+                       []
+               );
+               $email->send();
+
+               return $result;
+       }
+
+       /**
+        * A simple endpoint to confirm a release.
+        */
+       public function confirm_release( $request ) {
+               $user_login = wp_get_current_user()->user_login;
+               $plugin     = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
+               $tag        = $request['plugin_tag'];
+               $release    = Plugin_Directory::get_release( $plugin, $tag );
+               $result     = [
+                       'location' => wp_get_referer() ?: home_url( '/developers/releases/' ),
+               ];
+               header( 'Location: ' . $result['location'] );
+
+               if ( ! $release || ! empty( $release['confirmed'][ $user_login ] ) ) {
+                       // Already confirmed.
+                       $result['confirmed'] = false;
+                       return $result;
+               }
+
+               // Record this user as confirming the release.
+               $release['confirmations'][ $user_login ] = time();
+               $result['confirmed']                     = true;
+
+               // Mark the release as confirmed if enough confirmations.
+               if ( count( $release['confirmations'] ) >= $release['confirmations_required'] ) {
+                       $release['confirmed'] = true;
+                       $result['fully_confirmed']     = true;
+               }
+
+               Plugin_Directory::add_release( $release );
+
+               // Trigger the import for the plugin.
+               Plugin_Import::queue(
+                       $plugin->post_name,
+                       // TODO this is not 100% right... but will probably work.
+                       [
+                               'tags_touched'   => [
+                                       'trunk',
+                                       $tag
+                               ],
+                               // Assume everything was modified.
+                               'readme_touched' => true,
+                               'code_touched'   => true,
+                               'assets_touched' => true,
+                               'revisions'      => $release['revision'],
+                       ]
+               );
+
+               return $result;
+       }
+
+       /**
+        * Send a Access email
+        */
+       public function send_access_email( $request ) {
+               $result = [
+                       'location' => wp_get_referer() ?: home_url( '/developers/releases/' ),
+               ];
+               $result['location'] = add_query_arg( 'send_access_email', '1', $result['location'] );
+               header( 'Location: ' . $result['location'] );
+
+               $email = new Release_Confirmation_Access_Email(
+                       wp_get_current_user()
+               );
+               $result['sent'] = $email->send();
+
+               return $result;
+       }
+
+       public function validate_plugin_tag_callback( $tag, $request ) {
+               $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
+
+               return $plugin && (bool) Plugin_Directory::get_release( $plugin, $tag );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclassplugindirectoryphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php      2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php        2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -586,6 +586,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_shortcode( 'wporg-plugins-reviews', array( __NAMESPACE__ . '\Shortcodes\Reviews', 'display' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_shortcode( 'readme-validator', array( __NAMESPACE__ . '\Shortcodes\Readme_Validator', 'display' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_shortcode( 'block-validator', array( __NAMESPACE__ . '\Shortcodes\Block_Validator', 'display' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               add_shortcode( Shortcodes\Release_Confirmation::SHORTCODE, array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'display' ) );
+               add_action( 'template_redirect', array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'template_redirect' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -602,6 +605,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'wporg-plugins-reviews',
</span><span class="cx" style="display: block; padding: 0 10px">                        'readme-validator',
</span><span class="cx" style="display: block; padding: 0 10px">                        'block-validator',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'release-confirmation',
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $not_allowed_shortcodes = array_diff( array_keys( $shortcode_tags ), $allowed_shortcodes );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1506,6 +1510,114 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Get a list of all Plugin Releases.
+        */
+       public static function get_releases( $plugin ) {
+               $plugin   = self::get_plugin_post( $plugin );
+               $releases = get_post_meta( $plugin->ID, 'releases', true );
+
+               // Meta doesn't exist yet? Lets fill it out.
+               if ( false === $releases || ! is_array( $releases ) ) {
+                       update_post_meta( $plugin->ID, 'releases', [] );
+
+                       $tags = get_post_meta( $plugin->ID, 'tags', true );
+                       if ( $tags ) {
+                               foreach ( $tags as $tag_version => $tag ) {
+                                       self::add_release( $plugin, [
+                                               'date' => strtotime( $tag['date'] ),
+                                               'tag'  => $tag['tag'],
+                                               'version' => $tag_version,
+                                               'committer' => [ $tag['author'] ],
+                                               'confirmations_required' => 0, // Old release, assume it's released.
+                                       ] );
+                               }
+                       } else {
+                               // Pull from SVN directly.
+                               $svn_tags = Tools\SVN::ls( "https://plugins.svn.wordpress.org/{$plugin->post_name}/tags/", true ) ?: [];
+                               foreach ( $svn_tags as $entry ) {
+                                       // Discard files
+                                       if ( 'dir' !== $entry['kind'] ) {
+                                               continue;
+                                       }
+
+                                       $tag = $entry['filename'];
+
+                                       // Prefix the 0 for plugin versions like 0.1
+                                       if ( '.' == substr( $tag, 0, 1 ) ) {
+                                               $tag = "0{$tag}";
+                                       }
+
+                                       self::add_release( $plugin, [
+                                               'date' => strtotime( $entry['date'] ),
+                                               'tag'  => $entry['filename'],
+                                               'version' => $tag,
+                                               'committer' => [ $entry['author'] ],
+                                               'confirmations_required' => 0, // Old release, assume it's released.
+                                       ] );
+                               }
+                       }
+
+                       $releases = get_post_meta( $plugin->ID, 'releases', true ) ?: [];
+               }
+
+               return $releases;
+       }
+
+       /**
+        * Fetch a specific release of the plugin, by tag.
+        */
+       public static function get_release( $plugin, $tag ) {
+               $releases = self::get_releases( $plugin );
+
+               $filtered = wp_list_filter( $releases, compact( 'tag' ) );
+
+               if ( $filtered ) {
+                       return array_shift( $filtered );
+               }
+
+               return false;
+       }
+
+       /**
+        * Add a Plugin Release to the internal storage.
+        */
+       public static function add_release( $plugin, $data ) {
+               if ( ! isset( $data['tag'] ) ) {
+                       return false;
+               }
+               $plugin = self::get_plugin_post( $plugin );
+
+               $release = self::get_release( $plugin, $data['tag'] ) ?: [
+                       'date'                   => time(),
+                       'tag'                    => '',
+                       'version'                => '',
+                       'zips_built'             => false,
+                       'confirmations'          => [],
+                       // Confirmed by default if no release confiration.
+                       'confirmed'              => ! $plugin->release_confirmation,
+                       'confirmations_required' => (int) $plugin->release_confirmation,
+                       'committer'              => [],
+                       'revision'               => [],
+               ];
+
+               // Fill
+               foreach ( $data as $k => $v ) {
+                       $release[ $k ] = $v;
+               }
+
+               $releases = self::get_releases( $plugin );
+
+               $releases[] = $release;
+
+               // Sort releases most recent first.
+               uasort( $releases, function( $a, $b ) {
+                       return $b['date'] <=> $a['date'];
+               } );
+
+               return update_post_meta( $plugin->ID, 'releases', $releases );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Retrieve the WP_Post object representing a given plugin.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @static
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclasstemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php      2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-template.php        2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -786,7 +786,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Generates a link to self-transfer a plugin..
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Generates a link to self-transfer a plugin.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string URL to toggle status.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -801,6 +801,49 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Generates a link to enable Release Confirmations.
+        *
+        * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
+        * @return string URL to enable confirmations.
+        */
+       public static function get_enable_release_confirmation_link( $post = null ) {
+               $post = get_post( $post );
+
+               return add_query_arg(
+                       array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
+                       home_url( 'wp-json/plugins/v1/plugin/' . $post->post_name . '/release-confirmation' )
+               );
+       }
+
+       /**
+        * Generates a link to confirm a release.
+        *
+        * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
+        * @return string URL to enable confirmations.
+        */
+       public static function get_release_confirmation_link( $tag, $post = null) {
+               $post = get_post( $post );
+
+               return add_query_arg(
+                       array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
+                       home_url( 'wp-json/plugins/v1/plugin/' . $post->post_name . '/release-confirmation/' . $tag )
+               );
+       }
+
+       /**
+        * Generates a link to email the release confirmation link.
+        *
+        * @param int|\WP_Post|null $post Optional. Post ID or post object. Defaults to global $post.
+        * @return string URL to enable confirmations.
+        */
+       public static function get_release_confirmation_access_link() {
+               return add_query_arg(
+                       array( '_wpnonce' => wp_create_nonce( 'wp_rest' ) ),
+                       home_url( 'wp-json/plugins/v1/release-confirmation-access' )
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Returns the reasons for closing or disabling a plugin.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array Close/disable reason labels.
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectorycliclassimportphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php    2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php      2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6,6 +6,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Jobs\Tide_Sync;
</span><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Block_JSON;
</span><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Plugin_Directory;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+use WordPressdotorg\Plugin_Directory\Email\Release_Confirmation as Release_Confirmation_Email;
</ins><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Readme\Parser;
</span><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Template;
</span><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Tools;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -74,11 +75,66 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $assets          = $data['assets'];
</span><span class="cx" style="display: block; padding: 0 10px">                $headers         = $data['plugin_headers'];
</span><span class="cx" style="display: block; padding: 0 10px">                $stable_tag      = $data['stable_tag'];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $last_committer  = $data['last_committer'];
+               $last_revision   = $data['last_revision'];
</ins><span class="cx" style="display: block; padding: 0 10px">                 $tagged_versions = $data['tagged_versions'];
</span><span class="cx" style="display: block; padding: 0 10px">                $last_modified   = $data['last_modified'];
</span><span class="cx" style="display: block; padding: 0 10px">                $blocks          = $data['blocks'];
</span><span class="cx" style="display: block; padding: 0 10px">                $block_files     = $data['block_files'];
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Release confirmation
+               if ( $plugin->release_confirmation ) {
+                       if ( 'trunk' === $stable_tag ) {
+                               throw new Exception( 'Plugin cannot be released from trunk due to release confirmation being enabled.' );
+                       }
+
+                       $release = Plugin_Directory::get_release( $plugin, $stable_tag );
+
+                       // This tag is unknown? Trigger email.
+                       if ( ! $release ) {
+                               Plugin_Directory::add_release(
+                                       [
+                                               'tag'       => $stable_tag,
+                                               'version'   => $headers->Version,
+                                               'committer' => [ $last_committer ],
+                                               'revision'  => [ $last_revision ]
+                                       ]
+                               );
+
+                               $email = new Release_Confirmation_Email(
+                                       $plugin,
+                                       Tools::get_plugin_committers( $plugin_slug ),
+                                       [
+                                               'release' => $releases[ $stable_tag ],
+                                               'who'     => $last_committer,
+                                               'readme'  => $readme,
+                                               'headers' => $headers,
+                                       ]
+                               );
+                               $email->send();
+
+                               throw new Exception( 'Plugin release not confirmed; email triggered.' );
+                       }
+
+                       // Check that the tag is approved.
+                       if ( ! $release['confirmed'] ) {
+
+                               if ( ! in_array( $last_committer, $release['committer'], true ) ) {
+                                       $release['committer'][] = $last_committer;
+                               }
+                               if ( ! in_array( $last_revision, $release['revision'], true ) ) {
+                                       $release['revision'][] = $last_revision;
+                               }
+
+                               // Update with ^
+                               Plugin_Directory::add_release( $plugin, $release );
+
+                               throw new Exception( 'Plugin release not confirmed.' );
+                       }
+
+                       // At this point we can assume that the release was confirmed, and should be imported.
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $content = '';
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $readme->sections ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        foreach ( $readme->sections as $section => $section_content ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -260,6 +316,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $versions_to_build[] = $stable_tag;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $plugin = Plugin_Directory::get_plugin_post( $plugin_slug );
+
+               // Don't rebuild release-confirmation-required tags.
+               if ( $plugin->release_confirmation ) {
+                       foreach ( $versions_to_build as $i => $tag ) {
+                               $release = Plugin_Directory::get_release( $plugin, $tag );
+
+                               if ( ! $release || ( $release['zips_built'] && $release['confirmations_required'] ) ) {
+                                       unset( $versions_to_build[ $i ] );
+                               } else {
+                                       $release['zips_built'] = true;
+                                       Plugin_Directory::add_release( $release );
+                               }
+
+                       }
+               }
+
+               if ( ! $versions_to_build ) {
+                       return false;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Rebuild/Build $build_zips
</span><span class="cx" style="display: block; padding: 0 10px">                try {
</span><span class="cx" style="display: block; padding: 0 10px">                        // This will rebuild the ZIP.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -389,6 +466,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $last_modified = $m[0];
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $last_committer = $svn_info['result']['Last Changed Author'] ?? '';
+               $last_revision  = $svn_info['result']['Last Changed Rev'] ?? 0;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $svn_export = SVN::export(
</span><span class="cx" style="display: block; padding: 0 10px">                        $stable_url,
</span><span class="cx" style="display: block; padding: 0 10px">                        $tmp_dir . '/export',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -548,7 +628,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return preg_match( '!\.(?:js|jsx|css)$!i', $filename );
</span><span class="cx" style="display: block; padding: 0 10px">                } ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return compact( 'readme', 'stable_tag', 'last_modified', 'tmp_dir', 'plugin_headers', 'assets', 'tagged_versions', 'blocks', 'block_files' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return compact( 'readme', 'stable_tag', 'last_modified', 'last_committer', 'last_revision', 'tmp_dir', 'plugin_headers', 'assets', 'tagged_versions', 'blocks', 'block_files' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassbasephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-base.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-base.php    2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-base.php      2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -24,13 +24,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $users = false;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param $plugin The plugin this email relates to.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param $plugin  The plugin this email relates to.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @param $users[] A list of users to email.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param $args[] A list of args that the email requires.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param $args[]  A list of args that the email requires.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function __construct( $plugin, $users, $args = array() ) {
-               $this->plugin = Plugin_Directory::get_plugin_post( $plugin );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function __construct( $plugin, $users = [], $args = [] ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Sometimes we don't have a plugin context, just a user..
+               if ( $plugin instanceOf WP_User ) {
+                       $users = [ $plugin ];
+                       $args  = $users; // Just assume that args will have been passed in there.
+
+                       $this->plugin = true; // To pass checks..
+               } else {
+                       $this->plugin = Plugin_Directory::get_plugin_post( $plugin );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Don't cast an object to an array, but rather an array of object.
</span><span class="cx" style="display: block; padding: 0 10px">                if ( is_object( $users ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $users = [ $users ];
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassreleaseconfirmationaccessphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-access.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-access.php                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-access.php       2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,29 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\Email;
+
+use WordPressdotorg\Plugin_Directory\Tools;
+use WordPressdotorg\Plugin_Directory\Shortcodes\Release_Confirmation as Release_Confirmation_Shortcode;
+
+class Release_Confirmation_Access extends Base {
+       protected $required_args = [];
+
+       function subject() {
+               return sprintf(
+                       /* translators: 1: User Login */
+                       __( 'Release Management for %s', 'wporg-plugins' ),
+                       $this->user->user_login
+               );
+       }
+
+       function body() {
+               return sprintf(
+                       /* translators: 1: Username; 1: Access URL */
+                       __( 'Howdy %1$s,
+
+To manage your plugin releases, follow the link below:
+%2$s', 'wporg-plugins' ),
+                       $this->user_text( $this->user ),
+                       esc_url( Release_Confirmation_Shortcode::generate_access_url( $this->user ) )
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-access.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassreleaseconfirmationenabledphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-enabled.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-enabled.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-enabled.php      2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,33 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\Email;
+
+use WordPressdotorg\Plugin_Directory\Tools;
+
+class Release_Confirmation_Enabled extends Base {
+       protected $required_args = [];
+
+       function subject() {
+               return sprintf(
+                       /* translators: 1: Plugin Name */
+                       __( 'Release confirmation now required for %s', 'wporg-plugins' ),
+                       $this->plugin->post_title
+               );
+       }
+
+       function body() {
+               /* translators: 1: Plugin Author, 2: Plugin Name, 3: URL to the handbook */
+               return sprintf(
+                       __( 'Howdy %1$s,
+
+Release confirmations are now enabled for %2$s.
+
+This means that each time you release a new version of %2$s you\'ll be required to confirm the release by following a link in an automated email.
+
+For more information, please read the following handbook article:
+%3$s', 'wporg-plugins' ),
+                       $this->user_text( $this->user ),
+                       $this->plugin->post_title,
+                       'https://developer.wordpress.org/plugins/wordpress-org/' // TODO: Handbook page.
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation-enabled.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryemailclassreleaseconfirmationphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation.php                            (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation.php      2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,41 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\Email;
+
+use WordPressdotorg\Plugin_Directory\Tools;
+use WordPressdotorg\Plugin_Directory\Shortcodes\Release_Confirmation as Release_Confirmation_Shortcode;
+
+class Release_Confirmation extends Base {
+       protected $required_args = [
+               'who',
+               'readme',
+               'headers'
+       ];
+
+       function subject() {
+               return sprintf(
+                       /* translators: 1: Plugin Name */
+                       __( 'Pending release for %s', 'wporg-plugins' ),
+                       $this->plugin->post_title
+               );
+       }
+
+       function body() {
+               return sprintf(
+                       /* translators: 1: Name, 2: Committer, 3: Plugin Name, 4: Version identifier, 5: Access URL */
+                       __( 'Howdy %1$s,
+
+%2$s has committed a new version of %3$s - %4$s.
+
+An email confirmation is required before the new version will be released.
+
+Follow the link below to login and confirm the release.
+
+<%5$s>', 'wporg-plugins' ),
+                       $this->user_text( $this->user ),
+                       $this->user_text( $this->args['who'] ),
+                       $this->args['readme']->name,
+                       $this->args['headers']->Version,
+                       esc_url( Release_Confirmation_Shortcode::generate_access_url( $this->user ) )
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/email/class-release-confirmation.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryshortcodesclassreleaseconfirmationphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php 2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,304 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\Shortcodes;
+
+use WordPressdotorg\Plugin_Directory\Plugin_Directory;
+use WordPressdotorg\Plugin_Directory\Template;
+use WordPressdotorg\Plugin_Directory\Tools;
+
+/**
+ * The [release-confirmation] shortcode handler.
+ *
+ * @package WordPressdotorg\Plugin_Directory\Shortcodes
+ */
+class Release_Confirmation {
+
+       const SHORTCODE = 'release-confirmation';
+       const COOKIE    = 'release_confirmation_access_token';
+       const NONCE     = 'plugins-developers-releases-page';
+       const URL_PARAM = 'access_token';
+
+       /**
+        * @return string
+        */
+       static function display() {
+               $plugins = Tools::get_users_write_access_plugins( wp_get_current_user() );
+
+               if ( ! $plugins ) {
+                       wp_safe_redirect( home_url( '/developers/' ) );
+                       // Redirect via JS too, as technically the page output should've already started (but probably hasn't on WordPress.org)
+                       echo '<script>document.location=' . json_encode( home_url( '/developers/' ) ) . '</script>';
+                       exit;
+               }
+
+               $plugins = array_map( function( $slug ) {
+                       return Plugin_Directory::get_plugin_post( $slug );
+               }, $plugins );
+
+               // Remove closed plugins.
+               $plugins = array_filter( $plugins, function( $plugin ) {
+                       return $plugin && 'publish' === $plugin->post_status;
+               } );
+
+               uasort( $plugins, function( $a, $b ) {
+                       // Get the most recent commit confirmation.
+                       $a_releases = Plugin_Directory::get_releases( $a );
+                       $b_releases = Plugin_Directory::get_releases( $b );
+
+                       $a_latest_release = $a_releases ? max( wp_list_pluck( $a_releases, 'date' ) ) : 0;
+                       $b_latest_release = $b_releases ? max( wp_list_pluck( $b_releases, 'date' ) ) : 0;
+
+                       $a_latest_release = max( $a_latest_release, strtotime( $a->last_updated ) );
+                       $b_latest_release = max( $b_latest_release, strtotime( $b->last_updated ) );
+
+                       return $b_latest_release <=> $a_latest_release;
+               } );
+
+               ob_start();
+
+               $should_show_access_notice = false;
+               foreach ( $plugins as $plugin ) {
+                       if ( $plugin->release_confirmation ) {
+                               $should_show_access_notice = true;
+                       }
+               }
+
+               if ( ! self::can_access() && $should_show_access_notice ) {
+                       if ( isset( $_REQUEST['send_access_email'] ) ) {
+                               printf(
+                                       '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
+                                       __( 'Check your email for an access link to perform actions.', 'wporg-plugins')
+                               );
+                       } else {
+                               printf(
+                                       '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
+                                       sprintf(
+                                               /* translators: %s: URL */
+                                               __( 'Check your email for an access link, or <a href="%s">request a new email</a> to perform actions.', 'wporg-plugins'),
+                                               Template::get_release_confirmation_access_link()
+                                       )
+                               );
+                       }
+               }
+
+               echo '<p>' . 'Intro to this page goes here. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.' . '</p>';
+
+               $not_enabled = [];
+               foreach ( $plugins as $plugin ) {
+                       self::single_plugin_row( $plugin );
+
+                       if ( ! $plugin->release_confirmation ) {
+                               $not_enabled[] = $plugin;
+                       }
+               }
+
+               if ( $not_enabled ) {
+                       printf(
+                               '<em>' . __( 'The following plugins do not have release confirmations enabled: %s', 'wporg-plugins') . '</em>',
+                               wp_sprintf_l( '%l', array_filter( array_map( function( $plugin ) {
+                                       if ( 'publish' == get_post_status( $plugin ) ) {
+                                               return sprintf(
+                                                       '<a href="%s">%s</a>',
+                                                       get_permalink( $plugin ),
+                                                       get_the_title( $plugin )
+                                               );
+                                       }
+                               }, $not_enabled ) ) )
+                       );
+               }
+
+               return ob_get_clean();
+       }
+
+       static function single_plugin_row( $plugin, $include_header = true ) {
+               $releases = Plugin_Directory::get_releases( $plugin );
+
+               if ( $include_header ) {
+                       printf(
+                               '<h2><a href="%s">%s</a></h2>',
+                               get_permalink( $plugin ),
+                               get_the_title( $plugin )
+                       );
+               }
+
+               echo '<table class="widefat plugin-releases-listing">
+               <thead>
+                       <tr>
+                               <th>Version</th>
+                               <th>Date</th>
+                               <th>Committer</th>
+                               <th>Approval</th>
+                               <th>Actions</th>
+               </thead>';
+
+               if ( ! $releases ) {
+                       echo '<tr class="no-items"><td colspan="5"><em>' . __( 'No releases.', 'wporg-plugins' ) . '</em></td></tr>';
+               }
+
+               foreach ( $releases as $data ) {
+                       printf(
+                               '<tr>
+                                       <td>%s</td>
+                                       <td title="%s">%s</td>
+                                       <td>%s</td>
+                                       <td>%s</td>
+                                       <td>%s</td>
+                               </tr>',
+                               sprintf(
+                                       '<a href="%s">%s</a>',
+                                       esc_url( sprintf(
+                                               'https://plugins.trac.wordpress.org/browser/%s/tags/%s/',
+                                               $plugin->post_name,
+                                               $data['tag']
+                                       ) ),
+                                       esc_html( $data['version'] )
+                               ),
+                               esc_attr( gmdate( 'Y-m-d H:i:s', $data['date'] ) ),
+                               esc_html( sprintf( __( '%s ago', 'wporg-plugins' ), human_time_diff( $data['date'] ) ) ),
+                               esc_html( implode( ', ', (array) $data['committer'] ) ),
+                               self::get_approval_text( $plugin, $data ),
+                               self::get_actions( $plugin, $data )
+                       );
+               }
+
+               echo '</table>';
+       }
+
+       static function get_approval_text( $plugin, $data ) {
+               ob_start();
+
+               if ( ! $data['confirmations_required'] ) {
+                       _e( 'Release did not require confirmation.', 'wporg-plugins' );
+               } else if ( ! $data['confirmed'] || count( $data['confirmations'] ) >= $data['confirmations_required'] ) {
+                       _e( 'Release confirmed.', 'wporg-plugins' );
+               } else if ( 1 == $data['confirmations_required'] ) {
+                       _e( 'Waiting for confirmation.', 'wporg-plugins' );
+               } else {
+                       printf(
+                               __( '%s of %s required confirmations.', 'wporg-plugins' ),
+                               number_format_i18n( count( $data['confirmations'] ) ),
+                               number_format_i18n( $plugin->release_confirmation )
+                       );
+               }
+
+               echo '<div>';
+               foreach ( $data['confirmations'] as $who => $time ) {
+                       if ( $who === wp_get_current_user()->user_login ) {
+                               $approved_text = sprintf(
+                                       /* translators: 1: '5 hours' */
+                                       __( 'You approved this, %1$s ago.', 'wporg-plugins' ),
+                                       human_time_diff( $time )
+                               );
+                       } else {
+                               $user = get_user_by( 'slug', $who );
+
+                               $approved_text = sprintf(
+                                       /* translators: 1: Username, 2: '5 hours' */
+                                       __( 'Approved by %1$s, %2$s ago.', 'wporg-plugins' ),
+                                       $user->display_name ?: $user->user_nicename,
+                                       human_time_diff( $time )
+                               );
+                       }
+
+                       printf(
+                               '<span title="%s">%s</span><br>',
+                               esc_attr( gmdate( 'Y-m-d H:i:s', $time ) ),
+                               $approved_text
+                       );
+               }
+               echo '</div>';
+
+               return ob_get_clean();
+       }
+
+       static function get_actions( $plugin, $data ) {
+               $buttons = [];
+
+               if ( $data['confirmations_required'] ) {
+                       $current_user_confirmed = isset( $data['confirmations'][ wp_get_current_user()->user_login ] );
+
+                       if ( ! $current_user_confirmed && ! $data['confirmed'] ) {
+                               if ( self::can_access() ) {
+                                       $buttons[] = sprintf(
+                                               '<a href="%s" class="button approve-release button-primary">%s</a>',
+                                               Template::get_release_confirmation_link( $data['tag'], $plugin ),
+                                               __( 'Confirm', 'wporg-plugins' )
+                                       );
+                               } else {
+                                       $buttons[] = sprintf(
+                                               '<a class="button approve-release button-secondary disabled">%s</a>',
+                                               __( 'Confirm', 'wporg-plugins' )
+                                       );
+                               }
+
+                       } elseif ( $current_user_confirmed ) {
+                               $buttons[] = sprintf(
+                                       '<a class="button approve-release button-secondary disabled">%s</a>',
+                                       __( 'Confirmed', 'wporg-plugins' )
+                               );
+                       }
+               }
+
+               return implode( ' ', $buttons );
+       }
+
+       static function can_access() {
+               // Must have an access token..
+               if ( ! is_user_logged_in() || empty( $_COOKIE[ self::COOKIE ] ) ) {
+                       return false;
+               }
+
+               if ( false !== wp_verify_nonce( $_COOKIE[ self::COOKIE ], self::NONCE ) ) {
+                       return true;
+               }
+
+               setcookie( self::COOKIE, false, time() - DAY_IN_SECONDS );
+
+               return false;
+       }
+
+       static function generate_access_url( $user = null ) {
+               if ( ! $user ) {
+                       $user = wp_get_current_user();
+               }
+               if ( ! $user || ! $user->exists() ) {
+                       return false;
+               }
+
+               $current_user = wp_get_current_user()->ID;
+               wp_set_current_user( $user->ID );
+
+               $url = wp_nonce_url(
+                       home_url( '/developers/releases/' ), // TODO: Hardcoded url.
+                       self::NONCE,
+                       self::URL_PARAM
+               );
+
+               wp_set_current_user( $current_user );
+
+               return $url;
+       }
+
+       static function template_redirect() {
+               $post = get_post();
+               if ( ! $post || ! is_page() || ! has_shortcode( $post->post_content, self::SHORTCODE ) ) {
+                       return;
+               }
+
+               // Migrate URL param to cookie.
+               if ( isset( $_REQUEST[ self::URL_PARAM ] ) ) {
+                       setcookie( self::COOKIE, $_REQUEST[ self::URL_PARAM ], time() + DAY_IN_SECONDS, '/plugins/', 'wordpress.org', true, true );
+               }
+
+               // This page requires login.
+               if ( ! is_user_logged_in() ) {
+                       wp_safe_redirect( wp_login_url( get_permalink() ) );
+                       exit;
+               } else if ( isset( $_REQUEST[ self::URL_PARAM ] ) ) {
+                       wp_safe_redirect( remove_query_arg( self::URL_PARAM ) );
+                       exit;
+               }
+
+               // A page with this shortcode has no need to be indexed.
+               add_filter( 'wporg_noindex_request', '__return_true' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-release-confirmation.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentthemespubwporgpluginsinctemplatetagsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/inc/template-tags.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/inc/template-tags.php   2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/inc/template-tags.php     2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -9,6 +9,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> namespace WordPressdotorg\Plugin_Directory\Theme;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+use WordPressdotorg\Plugin_Directory\Plugin_Directory;
</ins><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Template;
</span><span class="cx" style="display: block; padding: 0 10px"> use WordPressdotorg\Plugin_Directory\Tools;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -259,6 +260,30 @@
</span><span class="cx" style="display: block; padding: 0 10px">        return $message;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function the_unconfirmed_releases_notice() {
+       $plugin = get_post();
+
+       if ( ! $plugin->release_confirmation || ! current_user_can( 'plugin_admin_edit', $plugin ) ) {
+               return;
+       }
+
+       $confirmations_required = $plugin->release_confirmation;
+       $releases               = Plugin_Directory::get_releases( $plugin ) ?: [];
+       $unconfirmed_releases   = wp_list_filter( $confirmed_releases, [ 'confirmed' => false ] );
+
+       if ( ! $unconfirmed_releases ) {
+               return;
+       }
+
+       printf(
+               '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
+               sprintf(
+                       __( 'This plugin has <a href="%s">a pending release that requires confirmation</a>.', 'wporg-plugins' ),
+                       home_url( '/developers/releases/' ) // TODO: Hardcoded URL.
+               )
+       );
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Display the ADVANCED Zone.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -346,6 +371,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        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>';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Output the Release Confirmation form.
+       the_plugin_release_confirmation_form();
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Output the transfer form.
</span><span class="cx" style="display: block; padding: 0 10px">        the_plugin_self_transfer_form();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -452,3 +480,48 @@
</span><span class="cx" style="display: block; padding: 0 10px">        echo '</form>';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+function the_plugin_release_confirmation_form() {
+       $post = get_post();
+
+       // Temporary: Plugin Reviewers only.
+       if ( ! current_user_can( 'edit_post', $post ) ) {
+               return;
+       }
+
+       if (
+               ! current_user_can( 'plugin_admin_edit', $post ) ||
+               'publish' != $post->post_status
+       ) {
+               return;
+       }
+
+       $confirmations_required = $post->release_confirmation;
+
+       echo '<h4>' . esc_html__( 'Release Confirmation', 'wporg-plugins' ) . '</h4>';
+       if ( $confirmations_required ) {
+               echo '<p>' . __( 'Release confirmations for this plugin are <strong>enabled</strong>.', 'wporg-plugins' ) . '</p>';
+       } else {
+               echo '<p>' . __( 'Release confirmations for this plugin are <strong>disabled</strong>', 'wporg-plugins' ) . '</p>';
+       }
+       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>';
+
+       if ( ! $confirmations_required && 'trunk' === $post->stable_tag ) {
+               echo '<div class="plugin-notice notice notice-warning notice-alt"><p>';
+                       _e( "Release confirmations currently require tagged releases, as you're releasing from trunk they cannot be enabled.", 'wporg-plugins' );
+               echo '</p></div>';
+
+       } else if ( ! $confirmations_required ) {
+               echo '<div class="plugin-notice notice notice-warning notice-alt"><p>';
+                       _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' );
+               echo '</p></div>';
+
+               echo '<form method="POST" action="' . esc_url( Template::get_enable_release_confirmation_link() ) . '" onsubmit="return confirm( jQuery(this).prev(\'.notice\').text() );">';
+               echo '<p><input class="button" type="submit" value="' . esc_attr__( 'I understand, please enable release confirmations.', 'wporg-plugins' ) . '" /></p>';
+               echo '</form>';
+
+       } else {
+               /* translators: 1: plugins@wordpress.org */
+               echo '<p>' . sprintf( __( 'To disable release confirmations, please contact the plugins team by emailing %s.', 'wporg-plugins' ), 'plugins@wordpress.org' ) . '</p>';
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentthemespubwporgpluginstemplatepartspluginsinglephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php        2020-08-27 07:46:52 UTC (rev 10213)
+++ sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php  2020-08-28 05:36:38 UTC (rev 10214)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -25,6 +25,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        <header class="plugin-header">
</span><span class="cx" style="display: block; padding: 0 10px">                <?php the_active_plugin_notice(); ?>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <?php the_unconfirmed_releases_notice(); ?>
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <div class="entry-thumbnail">
</span><span class="cx" style="display: block; padding: 0 10px">                        <?php
</span></span></pre>
</div>
</div>

</body>
</html>