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