Ticket #46595: 46595.3.diff
File 46595.3.diff, 18.0 KB (added by , 5 years ago) |
---|
-
src/wp-includes/class-wp-recovery-mode-key-service.php
1 1 <?php 2 2 /** 3 * Error Protection API: WP_Recovery_Mode_Key_ service class3 * Error Protection API: WP_Recovery_Mode_Key_Service class 4 4 * 5 5 * @package WordPress 6 6 * @since 5.2.0 … … 14 14 final class WP_Recovery_Mode_Key_Service { 15 15 16 16 /** 17 * The option name used to store the keys. 18 * 19 * @since 5.2.0 20 * @var string 21 */ 22 private $option_name = 'recovery_keys'; 23 24 /** 25 * Creates a recovery mode token. 26 * 27 * @since 5.2.0 28 * 29 * @return string $token A random string to identify its associated key in storage. 30 */ 31 public function generate_recovery_mode_token() { 32 return wp_generate_password( 22, false ); 33 } 34 35 /** 17 36 * Creates a recovery mode key. 18 37 * 19 38 * @since 5.2.0 … … 20 39 * 21 40 * @global PasswordHash $wp_hasher 22 41 * 23 * @return string Recovery mode key. 42 * @param string $token A token generated by {@see generate_recovery_mode_token()}. 43 * @return string $key Recovery mode key. 24 44 */ 25 public function generate_and_store_recovery_mode_key( ) {45 public function generate_and_store_recovery_mode_key( $token ) { 26 46 27 47 global $wp_hasher; 28 48 29 49 $key = wp_generate_password( 22, false ); 30 50 31 /**32 * Fires when a recovery mode key is generated for a user.33 *34 * @since 5.2.035 *36 * @param string $key The recovery mode key.37 */38 do_action( 'generate_recovery_mode_key', $key );39 40 51 if ( empty( $wp_hasher ) ) { 41 52 require_once ABSPATH . WPINC . '/class-phpass.php'; 42 53 $wp_hasher = new PasswordHash( 8, true ); … … 44 55 45 56 $hashed = $wp_hasher->HashPassword( $key ); 46 57 47 update_option( 48 'recovery_key', 49 array( 50 'hashed_key' => $hashed, 51 'created_at' => time(), 52 ) 58 $records = $this->get_keys(); 59 60 $records[ $token ] = array( 61 'hashed_key' => $hashed, 62 'created_at' => time(), 53 63 ); 54 64 65 $this->update_keys( $records ); 66 67 /** 68 * Fires when a recovery mode key is generated. 69 * 70 * @since 5.2.0 71 * 72 * @param string $token The recovery data token. 73 * @param string $key The recovery mode key. 74 */ 75 do_action( 'generate_recovery_mode_key', $token, $key ); 76 55 77 return $key; 56 78 } 57 79 … … 58 80 /** 59 81 * Verifies if the recovery mode key is correct. 60 82 * 83 * Recovery mode keys can only be used once; the key will be consumed in the process. 84 * 61 85 * @since 5.2.0 62 86 * 63 * @param string $key The unhashed key. 64 * @param int $ttl Time in seconds for the key to be valid for. 87 * @param string $token The token used when generating the given key. 88 * @param string $key The unhashed key. 89 * @param int $ttl Time in seconds for the key to be valid for. 65 90 * @return true|WP_Error True on success, error object on failure. 66 91 */ 67 public function validate_recovery_mode_key( $ key, $ttl ) {92 public function validate_recovery_mode_key( $token, $key, $ttl ) { 68 93 69 $record = get_option( 'recovery_key');94 $records = $this->get_keys(); 70 95 71 if ( ! $record) {72 return new WP_Error( ' no_recovery_key_set', __( 'Recovery Mode not initialized.' ) );96 if ( ! isset( $records[ $token ] ) ) { 97 return new WP_Error( 'token_not_found', __( 'Recovery Mode not initialized.' ) ); 73 98 } 74 99 100 $record = $records[ $token ]; 101 102 $this->remove_key( $token ); 103 75 104 if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) { 76 105 return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) ); 77 106 } … … 86 115 87 116 return true; 88 117 } 118 119 /** 120 * Removes expired recovery mode keys. 121 * 122 * @since 5.2.0 123 * 124 * @param int $ttl Time in seconds for the keys to be valid for. 125 */ 126 public function clean_expired_keys( $ttl ) { 127 128 $records = $this->get_keys(); 129 130 foreach ( $records as $key => $record ) { 131 if ( ! isset( $record['created_at'] ) || time() > $record['created_at'] + $ttl ) { 132 unset( $records[ $key ] ); 133 } 134 } 135 136 $this->update_keys( $records ); 137 } 138 139 /** 140 * Removes a used recovery key. 141 * 142 * @since 5.2.0 143 * 144 * @param string $token The token used when generating a recovery mode key. 145 */ 146 private function remove_key( $token ) { 147 148 $records = $this->get_keys(); 149 150 if ( ! isset( $records[ $token ] ) ) { 151 return; 152 } 153 154 unset( $records[ $token ] ); 155 156 $this->update_keys( $records ); 157 } 158 159 /** 160 * Gets the recovery key records. 161 * 162 * @since 5.2.0 163 * 164 * @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key' 165 * and 'created_at'. 166 */ 167 private function get_keys() { 168 return (array) get_option( $this->option_name, array() ); 169 } 170 171 /** 172 * Updates the recovery key records. 173 * 174 * @since 5.2.0 175 * 176 * @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key' 177 * and 'created_at'. 178 * @return bool True on success, false on failure. 179 */ 180 private function update_keys( array $keys ) { 181 return update_option( $this->option_name, $keys ); 182 } 89 183 } -
src/wp-includes/class-wp-recovery-mode-link-service.php
37 37 * @since 5.2.0 38 38 * 39 39 * @param WP_Recovery_Mode_Cookie_Service $cookie_service Service to handle setting the recovery mode cookie. 40 * @param WP_Recovery_Mode_Key_Service $key_service Service to handle generating recovery mode keys. 40 41 */ 41 public function __construct( WP_Recovery_Mode_Cookie_Service $cookie_service ) {42 public function __construct( WP_Recovery_Mode_Cookie_Service $cookie_service, WP_Recovery_Mode_Key_Service $key_service ) { 42 43 $this->cookie_service = $cookie_service; 43 $this->key_service = new WP_Recovery_Mode_Key_Service();44 $this->key_service = $key_service; 44 45 } 45 46 46 47 /** … … 53 54 * @return string Generated URL. 54 55 */ 55 56 public function generate_url() { 56 $key = $this->key_service->generate_and_store_recovery_mode_key(); 57 $token = $this->key_service->generate_recovery_mode_token(); 58 $key = $this->key_service->generate_and_store_recovery_mode_key( $token ); 57 59 58 return $this->get_recovery_mode_begin_url( $ key );60 return $this->get_recovery_mode_begin_url( $token, $key ); 59 61 } 60 62 61 63 /** … … 70 72 return; 71 73 } 72 74 73 if ( ! isset( $_GET['action'], $_GET['rm_ key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {75 if ( ! isset( $_GET['action'], $_GET['rm_token'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) { 74 76 return; 75 77 } 76 78 … … 78 80 require_once ABSPATH . WPINC . '/pluggable.php'; 79 81 } 80 82 81 $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_ key'], $ttl );83 $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_token'], $_GET['rm_key'], $ttl ); 82 84 83 85 if ( is_wp_error( $validated ) ) { 84 86 wp_die( $validated, '' ); … … 96 98 * 97 99 * @since 5.2.0 98 100 * 99 * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()} 101 * @param string $token Recovery Mode token created by {@see generate_recovery_mode_token()}. 102 * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}. 100 103 * @return string Recovery mode begin URL. 101 104 */ 102 private function get_recovery_mode_begin_url( $ key ) {105 private function get_recovery_mode_begin_url( $token, $key ) { 103 106 104 107 $url = add_query_arg( 105 108 array( 106 'action' => self::LOGIN_ACTION_ENTER, 107 'rm_key' => $key, 109 'action' => self::LOGIN_ACTION_ENTER, 110 'rm_token' => $token, 111 'rm_key' => $key, 108 112 ), 109 113 wp_login_url() 110 114 ); … … 114 118 * 115 119 * @since 5.2.0 116 120 * 117 * @param string $url 118 * @param string $key 121 * @param string $url The generated recovery mode begin URL. 122 * @param string $token The token used to identify the key. 123 * @param string $key The recovery mode key. 119 124 */ 120 return apply_filters( 'recovery_mode_begin_url', $url, $ key );125 return apply_filters( 'recovery_mode_begin_url', $url, $token, $key ); 121 126 } 122 127 } -
src/wp-includes/class-wp-recovery-mode.php
16 16 const EXIT_ACTION = 'exit_recovery_mode'; 17 17 18 18 /** 19 * Service to handle sending an email with a recovery mode link.19 * Service to handle cookies. 20 20 * 21 21 * @since 5.2.0 22 * @var WP_Recovery_Mode_ Email_Service22 * @var WP_Recovery_Mode_Cookie_Service 23 23 */ 24 private $ email_service;24 private $cookie_service; 25 25 26 26 /** 27 * Service to generate a recovery mode key. 28 * 29 * @since 5.2.0 30 * @var WP_Recovery_Mode_Key_Service 31 */ 32 private $key_service; 33 34 /** 27 35 * Service to generate and validate recovery mode links. 28 36 * 29 37 * @since 5.2.0 … … 32 40 private $link_service; 33 41 34 42 /** 35 * Service to handle cookies.43 * Service to handle sending an email with a recovery mode link. 36 44 * 37 45 * @since 5.2.0 38 * @var WP_Recovery_Mode_ Cookie_Service46 * @var WP_Recovery_Mode_Email_Service 39 47 */ 40 private $ cookie_service;48 private $email_service; 41 49 42 50 /** 43 51 * Is recovery mode initialized. … … 70 78 */ 71 79 public function __construct() { 72 80 $this->cookie_service = new WP_Recovery_Mode_Cookie_Service(); 73 $this->link_service = new WP_Recovery_Mode_Link_Service( $this->cookie_service ); 81 $this->key_service = new WP_Recovery_Mode_Key_Service(); 82 $this->link_service = new WP_Recovery_Mode_Link_Service( $this->cookie_service, $this->key_service ); 74 83 $this->email_service = new WP_Recovery_Mode_Email_Service( $this->link_service ); 75 84 } 76 85 … … 84 93 85 94 add_action( 'wp_logout', array( $this, 'exit_recovery_mode' ) ); 86 95 add_action( 'login_form_' . self::EXIT_ACTION, array( $this, 'handle_exit_recovery_mode' ) ); 96 add_action( 'recovery_mode_clean_expired_keys', array( $this, 'clean_expired_keys' ) ); 87 97 98 if ( ! wp_next_scheduled( 'recovery_mode_clean_expired_keys' ) && ! wp_installing() ) { 99 wp_schedule_event( time(), 'daily', 'recovery_mode_clean_expired_keys' ); 100 } 101 88 102 if ( defined( 'WP_RECOVERY_MODE_SESSION_ID' ) ) { 89 103 $this->is_active = true; 90 104 $this->session_id = WP_RECOVERY_MODE_SESSION_ID; … … 233 247 } 234 248 235 249 /** 250 * Cleans any recovery mode keys that have expired according to the link TTL. 251 * 252 * Executes on a daily cron schedule. 253 * 254 * @since 5.2.0 255 */ 256 public function clean_expired_keys() { 257 $this->key_service->clean_expired_keys( $this->get_link_ttl() ); 258 } 259 260 /** 236 261 * Handles checking for the recovery mode cookie and validating it. 237 262 * 238 263 * @since 5.2.0 -
tests/phpunit/tests/error-protection/recovery-mode-key-service.php
10 10 */ 11 11 public function test_generate_and_store_recovery_mode_key_returns_recovery_key() { 12 12 $service = new WP_Recovery_Mode_Key_Service(); 13 $key = $service->generate_and_store_recovery_mode_key(); 13 $token = $service->generate_recovery_mode_token(); 14 $key = $service->generate_and_store_recovery_mode_key( $token ); 14 15 15 16 $this->assertNotWPError( $key ); 16 17 } … … 20 21 */ 21 22 public function test_validate_recovery_mode_key_returns_wp_error_if_no_key_set() { 22 23 $service = new WP_Recovery_Mode_Key_Service(); 23 $error = $service->validate_recovery_mode_key( ' abcd', HOUR_IN_SECONDS );24 $error = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS ); 24 25 25 26 $this->assertWPError( $error ); 26 $this->assertEquals( ' no_recovery_key_set', $error->get_error_code() );27 $this->assertEquals( 'token_not_found', $error->get_error_code() ); 27 28 } 28 29 29 30 /** 30 31 * @ticket 46130 31 32 */ 33 public function test_validate_recovery_mode_key_returns_wp_error_if_data_missing() { 34 update_option( 'recovery_keys', 'gibberish' ); 35 36 $service = new WP_Recovery_Mode_Key_Service(); 37 $error = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS ); 38 39 $this->assertWPError( $error ); 40 $this->assertEquals( 'token_not_found', $error->get_error_code() ); 41 } 42 43 /** 44 * @ticket 46130 45 */ 46 public function test_validate_recovery_mode_key_returns_wp_error_if_bad() { 47 update_option( 'recovery_keys', array( 'token' => 'gibberish' ) ); 48 49 $service = new WP_Recovery_Mode_Key_Service(); 50 $error = $service->validate_recovery_mode_key( 'token', 'abcd', HOUR_IN_SECONDS ); 51 52 $this->assertWPError( $error ); 53 $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() ); 54 } 55 56 57 /** 58 * @ticket 46130 59 */ 32 60 public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() { 33 update_option( 'recovery_key', 'gibberish' );34 61 62 $token = wp_generate_password( 22, false ); 63 update_option( 'recovery_keys', array( $token => 'gibberish' ) ); 64 35 65 $service = new WP_Recovery_Mode_Key_Service(); 36 $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );66 $error = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS ); 37 67 38 68 $this->assertWPError( $error ); 39 69 $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() ); … … 44 74 */ 45 75 public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() { 46 76 $service = new WP_Recovery_Mode_Key_Service(); 47 $service->generate_and_store_recovery_mode_key(); 48 $error = $service->validate_recovery_mode_key( '', HOUR_IN_SECONDS ); 77 $token = $service->generate_recovery_mode_token(); 78 $service->generate_and_store_recovery_mode_key( $token ); 79 $error = $service->validate_recovery_mode_key( $token, '', HOUR_IN_SECONDS ); 49 80 50 81 $this->assertWPError( $error ); 51 82 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); … … 56 87 */ 57 88 public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() { 58 89 $service = new WP_Recovery_Mode_Key_Service(); 59 $service->generate_and_store_recovery_mode_key(); 60 $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS ); 90 $token = $service->generate_recovery_mode_token(); 91 $service->generate_and_store_recovery_mode_key( $token ); 92 $error = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS ); 61 93 62 94 $this->assertWPError( $error ); 63 95 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); … … 68 100 */ 69 101 public function test_validate_recovery_mode_key_returns_wp_error_if_expired() { 70 102 $service = new WP_Recovery_Mode_Key_Service(); 71 $key = $service->generate_and_store_recovery_mode_key(); 103 $token = $service->generate_recovery_mode_token(); 104 $key = $service->generate_and_store_recovery_mode_key( $token ); 72 105 73 $record = get_option( 'recovery_key' );74 $record ['created_at'] = time() - HOUR_IN_SECONDS - 30;75 update_option( 'recovery_key ', $record);106 $records = get_option( 'recovery_keys' ); 107 $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30; 108 update_option( 'recovery_keys', $records ); 76 109 77 $error = $service->validate_recovery_mode_key( $ key, HOUR_IN_SECONDS );110 $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ); 78 111 79 112 $this->assertWPError( $error ); 80 113 $this->assertEquals( 'key_expired', $error->get_error_code() ); … … 85 118 */ 86 119 public function test_validate_recovery_mode_key_returns_true_for_valid_key() { 87 120 $service = new WP_Recovery_Mode_Key_Service(); 88 $key = $service->generate_and_store_recovery_mode_key(); 89 $this->assertTrue( $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ) ); 121 $token = $service->generate_recovery_mode_token(); 122 $key = $service->generate_and_store_recovery_mode_key( $token ); 123 $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) ); 90 124 } 125 126 /** 127 * @ticket 46595 128 */ 129 public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once() { 130 $service = new WP_Recovery_Mode_Key_Service(); 131 $token = $service->generate_recovery_mode_token(); 132 $key = $service->generate_and_store_recovery_mode_key( $token ); 133 134 $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) ); 135 136 // data should be remove by first call 137 $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ); 138 139 $this->assertWPError( $error ); 140 $this->assertEquals( 'token_not_found', $error->get_error_code() ); 141 } 142 143 /** 144 * @ticket 46595 145 */ 146 public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once_more_than_key_stored() { 147 $service = new WP_Recovery_Mode_Key_Service(); 148 149 // create an extra key 150 $token = $service->generate_recovery_mode_token(); 151 $service->generate_and_store_recovery_mode_key( $token ); 152 153 $token = $service->generate_recovery_mode_token(); 154 $key = $service->generate_and_store_recovery_mode_key( $token ); 155 156 $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) ); 157 158 // data should be remove by first call 159 $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ); 160 161 $this->assertWPError( $error ); 162 $this->assertEquals( 'token_not_found', $error->get_error_code() ); 163 } 164 165 /** 166 * @ticket 46595 167 */ 168 public function test_clean_expired_keys() { 169 $service = new WP_Recovery_Mode_Key_Service(); 170 $token = $service->generate_recovery_mode_token(); 171 $service->generate_and_store_recovery_mode_key( $token ); 172 173 $records = get_option( 'recovery_keys' ); 174 175 $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30; 176 177 update_option( 'recovery_keys', $records ); 178 179 $service->clean_expired_keys( HOUR_IN_SECONDS ); 180 181 $this->assertEmpty( get_option( 'recovery_keys' ) ); 182 } 91 183 }