<!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>[45211] trunk: Bootstrap/Load: Allow more than one recovery link to be valid at a time.</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="https://core.trac.wordpress.org/changeset/45211">45211</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/45211","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>flixos90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2019-04-16 05:08:16 +0000 (Tue, 16 Apr 2019)</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'>Bootstrap/Load: Allow more than one recovery link to be valid at a time.

While currently a recovery link is only made available via the admin email address, this will be expanded in the future. In order to accomplish that, the mechanisms to store and validate recovery keys must support multiple keys to be valid at the same time.

This changeset adds that support, adding an additional token parameter which is part of a recovery link in addition to the key. A key itself is always associated with a token, so the two are only valid in combination. These associations are stored in a new `recovery_keys` option, which is regularly cleared in a new Cron hook, to prevent potential cluttering from unused recovery keys.

This changeset does not have any user-facing implications otherwise.

Props pbearne, timothyblynjacobs.
Fixes <a href="https://core.trac.wordpress.org/ticket/46595">#46595</a>. See <a href="https://core.trac.wordpress.org/ticket/46130">#46130</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswprecoverymodekeyservicephp">trunk/src/wp-includes/class-wp-recovery-mode-key-service.php</a></li>
<li><a href="#trunksrcwpincludesclasswprecoverymodelinkservicephp">trunk/src/wp-includes/class-wp-recovery-mode-link-service.php</a></li>
<li><a href="#trunksrcwpincludesclasswprecoverymodephp">trunk/src/wp-includes/class-wp-recovery-mode.php</a></li>
<li><a href="#trunktestsphpunittestserrorprotectionrecoverymodekeyservicephp">trunk/tests/phpunit/tests/error-protection/recovery-mode-key-service.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswprecoverymodekeyservicephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-recovery-mode-key-service.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-recovery-mode-key-service.php      2019-04-16 04:37:46 UTC (rev 45210)
+++ trunk/src/wp-includes/class-wp-recovery-mode-key-service.php        2019-04-16 05:08:16 UTC (rev 45211)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,6 +1,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Error Protection API: WP_Recovery_Mode_Key_service class
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Error Protection API: WP_Recovery_Mode_Key_Service class
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @package WordPress
</span><span class="cx" style="display: block; padding: 0 10px">  * @since   5.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -14,6 +14,25 @@
</span><span class="cx" style="display: block; padding: 0 10px"> final class WP_Recovery_Mode_Key_Service {
</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">+         * The option name used to store the keys.
+        *
+        * @since 5.2.0
+        * @var string
+        */
+       private $option_name = 'recovery_keys';
+
+       /**
+        * Creates a recovery mode token.
+        *
+        * @since 5.2.0
+        *
+        * @return string $token A random string to identify its associated key in storage.
+        */
+       public function generate_recovery_mode_token() {
+               return wp_generate_password( 22, false );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Creates a recovery mode key.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -20,23 +39,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @global PasswordHash $wp_hasher
</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 string Recovery mode key.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string $token A token generated by {@see generate_recovery_mode_token()}.
+        * @return string $key Recovery mode key.
</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 generate_and_store_recovery_mode_key() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function generate_and_store_recovery_mode_key( $token ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                global $wp_hasher;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $key = wp_generate_password( 22, false );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /**
-                * Fires when a recovery mode key is generated for a user.
-                *
-                * @since 5.2.0
-                *
-                * @param string $key The recovery mode key.
-                */
-               do_action( 'generate_recovery_mode_key', $key );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 if ( empty( $wp_hasher ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        require_once ABSPATH . WPINC . '/class-phpass.php';
</span><span class="cx" style="display: block; padding: 0 10px">                        $wp_hasher = new PasswordHash( 8, true );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -44,14 +55,25 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $hashed = $wp_hasher->HashPassword( $key );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                update_option(
-                       'recovery_key',
-                       array(
-                               'hashed_key' => $hashed,
-                               'created_at' => time(),
-                       )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $records = $this->get_keys();
+
+               $records[ $token ] = array(
+                       'hashed_key' => $hashed,
+                       'created_at' => time(),
</ins><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">+                $this->update_keys( $records );
+
+               /**
+                * Fires when a recovery mode key is generated.
+                *
+                * @since 5.2.0
+                *
+                * @param string $token The recovery data token.
+                * @param string $key   The recovery mode key.
+                */
+               do_action( 'generate_recovery_mode_key', $token, $key );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return $key;
</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">@@ -58,20 +80,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Verifies if the recovery mode key is correct.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Recovery mode keys can only be used once; the key will be consumed in the process.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 5.2.0
</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 string $key The unhashed key.
-        * @param int    $ttl Time in seconds for the key to be valid for.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string $token The token used when generating the given key.
+        * @param string $key   The unhashed key.
+        * @param int    $ttl   Time in seconds for the key to be valid for.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return true|WP_Error True on success, error object on failure.
</span><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 validate_recovery_mode_key( $key, $ttl ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function validate_recovery_mode_key( $token, $key, $ttl ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $record = get_option( 'recovery_key' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $records = $this->get_keys();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! $record ) {
-                       return new WP_Error( 'no_recovery_key_set', __( 'Recovery Mode not initialized.' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! isset( $records[ $token ] ) ) {
+                       return new WP_Error( 'token_not_found', __( 'Recovery Mode not initialized.' ) );
</ins><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">+                $record = $records[ $token ];
+
+               $this->remove_key( $token );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -86,4 +115,69 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return true;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Removes expired recovery mode keys.
+        *
+        * @since 5.2.0
+        *
+        * @param int $ttl Time in seconds for the keys to be valid for.
+        */
+       public function clean_expired_keys( $ttl ) {
+
+               $records = $this->get_keys();
+
+               foreach ( $records as $key => $record ) {
+                       if ( ! isset( $record['created_at'] ) || time() > $record['created_at'] + $ttl ) {
+                               unset( $records[ $key ] );
+                       }
+               }
+
+               $this->update_keys( $records );
+       }
+
+       /**
+        * Removes a used recovery key.
+        *
+        * @since 5.2.0
+        *
+        * @param string $token The token used when generating a recovery mode key.
+        */
+       private function remove_key( $token ) {
+
+               $records = $this->get_keys();
+
+               if ( ! isset( $records[ $token ] ) ) {
+                       return;
+               }
+
+               unset( $records[ $token ] );
+
+               $this->update_keys( $records );
+       }
+
+       /**
+        * Gets the recovery key records.
+        *
+        * @since 5.2.0
+        *
+        * @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key'
+        *               and 'created_at'.
+        */
+       private function get_keys() {
+               return (array) get_option( $this->option_name, array() );
+       }
+
+       /**
+        * Updates the recovery key records.
+        *
+        * @since 5.2.0
+        *
+        * @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key'
+        *                    and 'created_at'.
+        * @return bool True on success, false on failure.
+        */
+       private function update_keys( array $keys ) {
+               return update_option( $this->option_name, $keys );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunksrcwpincludesclasswprecoverymodelinkservicephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-recovery-mode-link-service.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-recovery-mode-link-service.php     2019-04-16 04:37:46 UTC (rev 45210)
+++ trunk/src/wp-includes/class-wp-recovery-mode-link-service.php       2019-04-16 05:08:16 UTC (rev 45211)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -37,10 +37,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param WP_Recovery_Mode_Cookie_Service $cookie_service Service to handle setting the recovery mode cookie.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param WP_Recovery_Mode_Key_Service    $key_service    Service to handle generating recovery mode keys.
</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( WP_Recovery_Mode_Cookie_Service $cookie_service ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function __construct( WP_Recovery_Mode_Cookie_Service $cookie_service, WP_Recovery_Mode_Key_Service $key_service ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->cookie_service = $cookie_service;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->key_service    = new WP_Recovery_Mode_Key_Service();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->key_service    = $key_service;
</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">@@ -53,9 +54,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return string Generated URL.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function generate_url() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $key = $this->key_service->generate_and_store_recovery_mode_key();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $token = $this->key_service->generate_recovery_mode_token();
+               $key   = $this->key_service->generate_and_store_recovery_mode_key( $token );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->get_recovery_mode_begin_url( $key );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $this->get_recovery_mode_begin_url( $token, $key );
</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">@@ -70,7 +72,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</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">-                if ( ! isset( $_GET['action'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! isset( $_GET['action'], $_GET['rm_token'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return;
</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">@@ -78,7 +80,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        require_once ABSPATH . WPINC . '/pluggable.php';
</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">-                $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_key'], $ttl );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_token'], $_GET['rm_key'], $ttl );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( is_wp_error( $validated ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_die( $validated, '' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -96,15 +98,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</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 string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param string $token Recovery Mode token created by {@see generate_recovery_mode_token()}.
+        * @param string $key   Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return string Recovery mode begin URL.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private function get_recovery_mode_begin_url( $key ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private function get_recovery_mode_begin_url( $token, $key ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $url = add_query_arg(
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'action' => self::LOGIN_ACTION_ENTER,
-                               'rm_key' => $key,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'action'   => self::LOGIN_ACTION_ENTER,
+                               'rm_token' => $token,
+                               'rm_key'   => $key,
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_login_url()
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -114,9 +118,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="cx" style="display: block; padding: 0 10px">                 * @since 5.2.0
</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 string $url
-                * @param string $key
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * @param string $url   The generated recovery mode begin URL.
+                * @param string $token The token used to identify the key.
+                * @param string $key   The recovery mode key.
</ins><span class="cx" style="display: block; padding: 0 10px">                  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return apply_filters( 'recovery_mode_begin_url', $url, $key );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return apply_filters( 'recovery_mode_begin_url', $url, $token, $key );
</ins><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="trunksrcwpincludesclasswprecoverymodephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-recovery-mode.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-recovery-mode.php  2019-04-16 04:37:46 UTC (rev 45210)
+++ trunk/src/wp-includes/class-wp-recovery-mode.php    2019-04-16 05:08:16 UTC (rev 45211)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -16,14 +16,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">        const EXIT_ACTION = 'exit_recovery_mode';
</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">-         * Service to handle sending an email with a recovery mode link.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Service to handle cookies.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @var WP_Recovery_Mode_Email_Service
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @var WP_Recovery_Mode_Cookie_Service
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private $email_service;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private $cookie_service;
</ins><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">+         * Service to generate a recovery mode key.
+        *
+        * @since 5.2.0
+        * @var WP_Recovery_Mode_Key_Service
+        */
+       private $key_service;
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Service to generate and validate recovery mode links.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -32,12 +40,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">        private $link_service;
</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">-         * Service to handle cookies.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Service to handle sending an email with a recovery mode link.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @var WP_Recovery_Mode_Cookie_Service
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @var WP_Recovery_Mode_Email_Service
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        private $cookie_service;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private $email_service;
</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">         * Is recovery mode initialized.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -70,7 +78,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct() {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->cookie_service = new WP_Recovery_Mode_Cookie_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->link_service   = new WP_Recovery_Mode_Link_Service( $this->cookie_service );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->key_service    = new WP_Recovery_Mode_Key_Service();
+               $this->link_service   = new WP_Recovery_Mode_Link_Service( $this->cookie_service, $this->key_service );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->email_service  = new WP_Recovery_Mode_Email_Service( $this->link_service );
</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">@@ -84,7 +93,12 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'wp_logout', array( $this, 'exit_recovery_mode' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'login_form_' . self::EXIT_ACTION, array( $this, 'handle_exit_recovery_mode' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                add_action( 'recovery_mode_clean_expired_keys', array( $this, 'clean_expired_keys' ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( ! wp_next_scheduled( 'recovery_mode_clean_expired_keys' ) && ! wp_installing() ) {
+                       wp_schedule_event( time(), 'daily', 'recovery_mode_clean_expired_keys' );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( defined( 'WP_RECOVERY_MODE_SESSION_ID' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->is_active  = true;
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->session_id = WP_RECOVERY_MODE_SESSION_ID;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -233,6 +247,17 @@
</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">+         * Cleans any recovery mode keys that have expired according to the link TTL.
+        *
+        * Executes on a daily cron schedule.
+        *
+        * @since 5.2.0
+        */
+       public function clean_expired_keys() {
+               $this->key_service->clean_expired_keys( $this->get_link_ttl() );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Handles checking for the recovery mode cookie and validating it.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.2.0
</span></span></pre></div>
<a id="trunktestsphpunittestserrorprotectionrecoverymodekeyservicephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/error-protection/recovery-mode-key-service.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/error-protection/recovery-mode-key-service.php  2019-04-16 04:37:46 UTC (rev 45210)
+++ trunk/tests/phpunit/tests/error-protection/recovery-mode-key-service.php    2019-04-16 05:08:16 UTC (rev 45211)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -10,7 +10,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_generate_and_store_recovery_mode_key_returns_recovery_key() {
</span><span class="cx" style="display: block; padding: 0 10px">                $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $key     = $service->generate_and_store_recovery_mode_key();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $token   = $service->generate_recovery_mode_token();
+               $key     = $service->generate_and_store_recovery_mode_key( $token );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotWPError( $key );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -20,20 +21,49 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_validate_recovery_mode_key_returns_wp_error_if_no_key_set() {
</span><span class="cx" style="display: block; padding: 0 10px">                $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $error   = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertWPError( $error );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( 'no_recovery_key_set', $error->get_error_code() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEquals( 'token_not_found', $error->get_error_code() );
</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="cx" style="display: block; padding: 0 10px">         * @ticket 46130
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function test_validate_recovery_mode_key_returns_wp_error_if_data_missing() {
+               update_option( 'recovery_keys', 'gibberish' );
+
+               $service = new WP_Recovery_Mode_Key_Service();
+               $error   = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS );
+
+               $this->assertWPError( $error );
+               $this->assertEquals( 'token_not_found', $error->get_error_code() );
+       }
+
+       /**
+        * @ticket 46130
+        */
+       public function test_validate_recovery_mode_key_returns_wp_error_if_bad() {
+               update_option( 'recovery_keys', array( 'token' => 'gibberish' ) );
+
+               $service = new WP_Recovery_Mode_Key_Service();
+               $error   = $service->validate_recovery_mode_key( 'token', 'abcd', HOUR_IN_SECONDS );
+
+               $this->assertWPError( $error );
+               $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
+       }
+
+
+       /**
+        * @ticket 46130
+        */
</ins><span class="cx" style="display: block; padding: 0 10px">         public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                update_option( 'recovery_key', 'gibberish' );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $token = wp_generate_password( 22, false );
+               update_option( 'recovery_keys', array( $token => 'gibberish' ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $error   = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertWPError( $error );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -44,8 +74,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() {
</span><span class="cx" style="display: block; padding: 0 10px">                $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $service->generate_and_store_recovery_mode_key();
-               $error = $service->validate_recovery_mode_key( '', HOUR_IN_SECONDS );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $token   = $service->generate_recovery_mode_token();
+               $service->generate_and_store_recovery_mode_key( $token );
+               $error = $service->validate_recovery_mode_key( $token, '', HOUR_IN_SECONDS );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertWPError( $error );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'hash_mismatch', $error->get_error_code() );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -56,8 +87,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() {
</span><span class="cx" style="display: block; padding: 0 10px">                $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $service->generate_and_store_recovery_mode_key();
-               $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $token   = $service->generate_recovery_mode_token();
+               $service->generate_and_store_recovery_mode_key( $token );
+               $error = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertWPError( $error );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'hash_mismatch', $error->get_error_code() );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -68,13 +100,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_validate_recovery_mode_key_returns_wp_error_if_expired() {
</span><span class="cx" style="display: block; padding: 0 10px">                $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $key     = $service->generate_and_store_recovery_mode_key();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $token   = $service->generate_recovery_mode_token();
+               $key     = $service->generate_and_store_recovery_mode_key( $token );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $record               = get_option( 'recovery_key' );
-               $record['created_at'] = time() - HOUR_IN_SECONDS - 30;
-               update_option( 'recovery_key', $record );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $records                         = get_option( 'recovery_keys' );
+               $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
+               update_option( 'recovery_keys', $records );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertWPError( $error );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 'key_expired', $error->get_error_code() );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -85,7 +118,66 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_validate_recovery_mode_key_returns_true_for_valid_key() {
</span><span class="cx" style="display: block; padding: 0 10px">                $service = new WP_Recovery_Mode_Key_Service();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $key     = $service->generate_and_store_recovery_mode_key();
-               $this->assertTrue( $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $token   = $service->generate_recovery_mode_token();
+               $key     = $service->generate_and_store_recovery_mode_key( $token );
+               $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @ticket 46595
+        */
+       public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once() {
+               $service = new WP_Recovery_Mode_Key_Service();
+               $token   = $service->generate_recovery_mode_token();
+               $key     = $service->generate_and_store_recovery_mode_key( $token );
+
+               $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
+
+               // data should be remove by first call
+               $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
+
+               $this->assertWPError( $error );
+               $this->assertEquals( 'token_not_found', $error->get_error_code() );
+       }
+
+       /**
+        * @ticket 46595
+        */
+       public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once_more_than_key_stored() {
+               $service = new WP_Recovery_Mode_Key_Service();
+
+               // create an extra key
+               $token = $service->generate_recovery_mode_token();
+               $service->generate_and_store_recovery_mode_key( $token );
+
+               $token = $service->generate_recovery_mode_token();
+               $key   = $service->generate_and_store_recovery_mode_key( $token );
+
+               $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
+
+               // data should be remove by first call
+               $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
+
+               $this->assertWPError( $error );
+               $this->assertEquals( 'token_not_found', $error->get_error_code() );
+       }
+
+       /**
+        * @ticket 46595
+        */
+       public function test_clean_expired_keys() {
+               $service = new WP_Recovery_Mode_Key_Service();
+               $token   = $service->generate_recovery_mode_token();
+               $service->generate_and_store_recovery_mode_key( $token );
+
+               $records = get_option( 'recovery_keys' );
+
+               $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
+
+               update_option( 'recovery_keys', $records );
+
+               $service->clean_expired_keys( HOUR_IN_SECONDS );
+
+               $this->assertEmpty( get_option( 'recovery_keys' ) );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>