1<?php 2/** 3 * WP_Application_Passwords class 4 * 5 * @package WordPress 6 * @since 5.6.0 7 */ 8 9/** 10 * Class for displaying, modifying, and sanitizing application passwords. 11 * 12 * @package WordPress 13 */ 14class WP_Application_Passwords { 15 16 /** 17 * The application passwords user meta key. 18 * 19 * @since 5.6.0 20 * 21 * @var string 22 */ 23 const USERMETA_KEY_APPLICATION_PASSWORDS = '_application_passwords'; 24 25 /** 26 * The option name used to store whether application passwords is in use. 27 * 28 * @since 5.6.0 29 * 30 * @var string 31 */ 32 const OPTION_KEY_IN_USE = 'using_application_passwords'; 33 34 /** 35 * The generated application password length. 36 * 37 * @since 5.6.0 38 * 39 * @var int 40 */ 41 const PW_LENGTH = 24; 42 43 /** 44 * Checks if Application Passwords are being used by the site. 45 * 46 * This returns true if at least one App Password has ever been created. 47 * 48 * @since 5.6.0 49 * 50 * @return bool 51 */ 52 public static function is_in_use() { 53 $network_id = get_main_network_id(); 54 return (bool) get_network_option( $network_id, self::OPTION_KEY_IN_USE ); 55 } 56 57 /** 58 * Creates a new application password. 59 * 60 * @since 5.6.0 61 * @since 5.7.0 Returns WP_Error if application name already exists. 62 * 63 * @param int $user_id User ID. 64 * @param array $args Information about the application password. 65 * @return array|WP_Error The first key in the array is the new password, the second is its detailed information. 66 * A WP_Error instance is returned on error. 67 */ 68 public static function create_new_application_password( $user_id, $args = array() ) { 69 if ( ! empty( $args['name'] ) ) { 70 $args['name'] = sanitize_text_field( $args['name'] ); 71 } 72 73 if ( empty( $args['name'] ) ) { 74 return new WP_Error( 'application_password_empty_name', __( 'An application name is required to create an application password.' ), array( 'status' => 400 ) ); 75 } 76 77 if ( self::application_name_exists_for_user( $user_id, $args['name'] ) ) { 78 return new WP_Error( 'application_password_duplicate_name', __( 'Each application name should be unique.' ), array( 'status' => 409 ) ); 79 } 80 81 $new_password = wp_generate_password( static::PW_LENGTH, false ); 82 $hashed_password = wp_hash_password( $new_password ); 83 84 $new_item = array( 85 'uuid' => wp_generate_uuid4(), 86 'app_id' => empty( $args['app_id'] ) ? '' : $args['app_id'], 87 'name' => $args['name'], 88 'password' => $hashed_password, 89 'created' => time(), 90 'last_used' => null, 91 'last_ip' => null, 92 ); 93 94 $passwords = static::get_user_application_passwords( $user_id ); 95 $passwords[] = $new_item; 96 $saved = static::set_user_application_passwords( $user_id, $passwords ); 97 98 if ( ! $saved ) { 99 return new WP_Error( 'db_error', __( 'Could not save application password.' ) ); 100 } 101 102 $network_id = get_main_network_id(); 103 if ( ! get_network_option( $network_id, self::OPTION_KEY_IN_USE ) ) { 104 update_network_option( $network_id, self::OPTION_KEY_IN_USE, true ); 105 } 106 107 /** 108 * Fires when an application password is created. 109 * 110 * @since 5.6.0 111 * 112 * @param int $user_id The user ID. 113 * @param array $new_item The details about the created password. 114 * @param string $new_password The unhashed generated app password. 115 * @param array $args Information used to create the application password. 116 */ 117 do_action( 'wp_create_application_password', $user_id, $new_item, $new_password, $args ); 118 119 return array( $new_password, $new_item ); 120 } 121 122 /** 123 * Gets a user's application passwords. 124 * 125 * @since 5.6.0 126 * 127 * @param int $user_id User ID. 128 * @return array The list of app passwords. 129 */ 130 public static function get_user_application_passwords( $user_id ) { 131 $passwords = get_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, true ); 132 133 if ( ! is_array( $passwords ) ) { 134 return array(); 135 } 136 137 $save = false; 138 139 foreach ( $passwords as $i => $password ) { 140 if ( ! isset( $password['uuid'] ) ) { 141 $passwords[ $i ]['uuid'] = wp_generate_uuid4(); 142 $save = true; 143 } 144 } 145 146 if ( $save ) { 147 static::set_user_application_passwords( $user_id, $passwords ); 148 } 149 150 return $passwords; 151 } 152 153 /** 154 * Gets a user's application password with the given uuid. 155 * 156 * @since 5.6.0 157 * 158 * @param int $user_id User ID. 159 * @param string $uuid The password's uuid. 160 * @return array|null The application password if found, null otherwise. 161 */ 162 public static function get_user_application_password( $user_id, $uuid ) { 163 $passwords = static::get_user_application_passwords( $user_id ); 164 165 foreach ( $passwords as $password ) { 166 if ( $password['uuid'] === $uuid ) { 167 return $password; 168 } 169 } 170 171 return null; 172 } 173 174 /** 175 * Checks if application name exists for this user. 176 * 177 * @since 5.7.0 178 * 179 * @param int $user_id User ID. 180 * @param string $name Application name. 181 * @return bool Whether provided application name exists or not. 182 */ 183 public static function application_name_exists_for_user( $user_id, $name ) { 184 $passwords = static::get_user_application_passwords( $user_id ); 185 186 foreach ( $passwords as $password ) { 187 if ( strtolower( $password['name'] ) === strtolower( $name ) ) { 188 return true; 189 } 190 } 191 192 return false; 193 } 194 195 /** 196 * Updates an application password. 197 * 198 * @since 5.6.0 199 * 200 * @param int $user_id User ID. 201 * @param string $uuid The password's uuid. 202 * @param array $update Information about the application password to update. 203 * @return true|WP_Error True if successful, otherwise a WP_Error instance is returned on error. 204 */ 205 public static function update_application_password( $user_id, $uuid, $update = array() ) { 206 $passwords = static::get_user_application_passwords( $user_id ); 207 208 foreach ( $passwords as &$item ) { 209 if ( $item['uuid'] !== $uuid ) { 210 continue; 211 } 212 213 if ( ! empty( $update['name'] ) ) { 214 $update['name'] = sanitize_text_field( $update['name'] ); 215 } 216 217 $save = false; 218 219 if ( ! empty( $update['name'] ) && $item['name'] !== $update['name'] ) { 220 $item['name'] = $update['name']; 221 $save = true; 222 } 223 224 if ( $save ) { 225 $saved = static::set_user_application_passwords( $user_id, $passwords ); 226 227 if ( ! $saved ) { 228 return new WP_Error( 'db_error', __( 'Could not save application password.' ) ); 229 } 230 } 231 232 /** 233 * Fires when an application password is updated. 234 * 235 * @since 5.6.0 236 * 237 * @param int $user_id The user ID. 238 * @param array $item The updated app password details. 239 * @param array $update The information to update. 240 */ 241 do_action( 'wp_update_application_password', $user_id, $item, $update ); 242 243 return true; 244 } 245 246 return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) ); 247 } 248 249 /** 250 * Records that an application password has been used. 251 * 252 * @since 5.6.0 253 * 254 * @param int $user_id User ID. 255 * @param string $uuid The password's uuid. 256 * @return true|WP_Error True if the usage was recorded, a WP_Error if an error occurs. 257 */ 258 public static function record_application_password_usage( $user_id, $uuid ) { 259 $passwords = static::get_user_application_passwords( $user_id ); 260 261 foreach ( $passwords as &$password ) { 262 if ( $password['uuid'] !== $uuid ) { 263 continue; 264 } 265 266 // Only record activity once a day. 267 if ( $password['last_used'] + DAY_IN_SECONDS > time() ) { 268 return true; 269 } 270 271 $password['last_used'] = time(); 272 $password['last_ip'] = $_SERVER['REMOTE_ADDR']; 273 274 $saved = static::set_user_application_passwords( $user_id, $passwords ); 275 276 if ( ! $saved ) { 277 return new WP_Error( 'db_error', __( 'Could not save application password.' ) ); 278 } 279 280 return true; 281 } 282 283 // Specified Application Password not found! 284 return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) ); 285 } 286 287 /** 288 * Deletes an application password. 289 * 290 * @since 5.6.0 291 * 292 * @param int $user_id User ID. 293 * @param string $uuid The password's uuid. 294 * @return true|WP_Error Whether the password was successfully found and deleted, a WP_Error otherwise. 295 */ 296 public static function delete_application_password( $user_id, $uuid ) { 297 $passwords = static::get_user_application_passwords( $user_id ); 298 299 foreach ( $passwords as $key => $item ) { 300 if ( $item['uuid'] === $uuid ) { 301 unset( $passwords[ $key ] ); 302 $saved = static::set_user_application_passwords( $user_id, $passwords ); 303 304 if ( ! $saved ) { 305 return new WP_Error( 'db_error', __( 'Could not delete application password.' ) ); 306 } 307 308 /** 309 * Fires when an application password is deleted. 310 * 311 * @since 5.6.0 312 * 313 * @param int $user_id The user ID. 314 * @param array $item The data about the application password. 315 */ 316 do_action( 'wp_delete_application_password', $user_id, $item ); 317 318 return true; 319 } 320 } 321 322 return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) ); 323 } 324 325 /** 326 * Deletes all application passwords for the given user. 327 * 328 * @since 5.6.0 329 * 330 * @param int $user_id User ID. 331 * @return int|WP_Error The number of passwords that were deleted or a WP_Error on failure. 332 */ 333 public static function delete_all_application_passwords( $user_id ) { 334 $passwords = static::get_user_application_passwords( $user_id ); 335 336 if ( $passwords ) { 337 $saved = static::set_user_application_passwords( $user_id, array() ); 338 339 if ( ! $saved ) { 340 return new WP_Error( 'db_error', __( 'Could not delete application passwords.' ) ); 341 } 342 343 foreach ( $passwords as $item ) { 344 /** This action is documented in wp-includes/class-wp-application-passwords.php */ 345 do_action( 'wp_delete_application_password', $user_id, $item ); 346 } 347 348 return count( $passwords ); 349 } 350 351 return 0; 352 } 353 354 /** 355 * Sets a users application passwords. 356 * 357 * @since 5.6.0 358 * 359 * @param int $user_id User ID. 360 * @param array $passwords Application passwords. 361 * 362 * @return bool 363 */ 364 protected static function set_user_application_passwords( $user_id, $passwords ) { 365 return update_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, $passwords ); 366 } 367 368 /** 369 * Sanitizes and then splits a password into smaller chunks. 370 * 371 * @since 5.6.0 372 * 373 * @param string $raw_password The raw application password. 374 * @return string The chunked password. 375 */ 376 public static function chunk_password( $raw_password ) { 377 $raw_password = preg_replace( '/[^a-z\d]/i', '', $raw_password ); 378 379 return trim( chunk_split( $raw_password, 4, ' ' ) ); 380 } 381} 382