'post_title' => $email_address,
'post_content' => wp_json_encode( $request_data ),
'post_status' => 'request-' . $status,
'post_type' => 'user_request',
'post_date' => current_time( 'mysql', false ),
'post_date_gmt' => current_time( 'mysql', true ),
* Get action description from the name and return a string.
* @param string $action_name Action name of the request.
* @return string Human readable action name.
function wp_user_request_action_description( $action_name ) {
switch ( $action_name ) {
case 'export_personal_data':
$description = __( 'Export Personal Data' );
case 'remove_personal_data':
$description = __( 'Erase Personal Data' );
/* translators: %s: Action name. */
$description = sprintf( __( 'Confirm the "%s" action' ), $action_name );
* Filters the user action description.
* @param string $description The default description.
* @param string $action_name The name of the request.
return apply_filters( 'user_request_action_description', $description, $action_name );
* Send a confirmation request email to confirm an action.
* If the request is not already pending, it will be updated.
* @param string $request_id ID of the request created via wp_create_user_request().
* @return true|WP_Error True on success, `WP_Error` on failure.
function wp_send_user_request( $request_id ) {
$request_id = absint( $request_id );
$request = wp_get_user_request( $request_id );
return new WP_Error( 'invalid_request', __( 'Invalid personal data request.' ) );
// Localize message content for user; fallback to site default for visitors.
if ( ! empty( $request->user_id ) ) {
$locale = get_user_locale( $request->user_id );
$switched_locale = switch_to_locale( $locale );
'email' => $request->email,
'description' => wp_user_request_action_description( $request->action_name ),
'confirm_url' => add_query_arg(
'action' => 'confirmaction',
'request_id' => $request_id,
'confirm_key' => wp_generate_user_request_key( $request_id ),
'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ),
/* translators: Do not translate DESCRIPTION, CONFIRM_URL, SITENAME, SITEURL: those are placeholders. */
A request has been made to perform the following action on your account:
To confirm this, please click on the following link:
You can safely ignore and delete this email if you do not want to
* Filters the text of the email sent when an account action is attempted.
* The following strings have a special meaning and will get replaced dynamically:
* ###DESCRIPTION### Description of the action being performed so the user knows what the email is for.
* ###CONFIRM_URL### The link to click on to confirm the account action.
* ###SITENAME### The name of the site.
* ###SITEURL### The URL to the site.
* @param string $email_text Text in the email.
* @param array $email_data {
* Data relating to the account action email.
* @type WP_User_Request $request User request object.
* @type string $email The email address this is being sent to.
* @type string $description Description of the action being performed so the user knows what the email is for.
* @type string $confirm_url The link to click on to confirm the account action.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
$content = apply_filters( 'user_request_action_email_content', $email_text, $email_data );
$content = str_replace( '###DESCRIPTION###', $email_data['description'], $content );
$content = str_replace( '###CONFIRM_URL###', esc_url_raw( $email_data['confirm_url'] ), $content );
$content = str_replace( '###EMAIL###', $email_data['email'], $content );
$content = str_replace( '###SITENAME###', $email_data['sitename'], $content );
$content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content );
/* translators: Confirm privacy data request notification email subject. 1: Site title, 2: Name of the action. */
$subject = sprintf( __( '[%1$s] Confirm Action: %2$s' ), $email_data['sitename'], $email_data['description'] );
* Filters the subject of the email sent when an account action is attempted.
* @param string $subject The email subject.
* @param string $sitename The name of the site.
* @param array $email_data {
* Data relating to the account action email.
* @type WP_User_Request $request User request object.
* @type string $email The email address this is being sent to.
* @type string $description Description of the action being performed so the user knows what the email is for.
* @type string $confirm_url The link to click on to confirm the account action.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
$subject = apply_filters( 'user_request_action_email_subject', $subject, $email_data['sitename'], $email_data );
* Filters the headers of the email sent when an account action is attempted.
* @param string|array $headers The email headers.
* @param string $subject The email subject.
* @param string $content The email content.
* @param int $request_id The request ID.
* @param array $email_data {
* Data relating to the account action email.
* @type WP_User_Request $request User request object.
* @type string $email The email address this is being sent to.
* @type string $description Description of the action being performed so the user knows what the email is for.
* @type string $confirm_url The link to click on to confirm the account action.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
$headers = apply_filters( 'user_request_action_email_headers', $headers, $subject, $content, $request_id, $email_data );
$email_sent = wp_mail( $email_data['email'], $subject, $content, $headers );
if ( $switched_locale ) {
restore_previous_locale();
return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export confirmation email.' ) );
* Returns a confirmation key for a user action and stores the hashed version for future comparison.
* @param int $request_id Request ID.
* @return string Confirmation key.
function wp_generate_user_request_key( $request_id ) {
// Generate something random for a confirmation key.
$key = wp_generate_password( 20, false );
// Return the key, hashed.
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
'post_status' => 'request-pending',
'post_password' => $wp_hasher->HashPassword( $key ),
* Validate a user request by comparing the key with the request's key.
* @param string $request_id ID of the request being confirmed.
* @param string $key Provided key to validate.
* @return true|WP_Error True on success, WP_Error on failure.
function wp_validate_user_request_key( $request_id, $key ) {
$request_id = absint( $request_id );
$request = wp_get_user_request( $request_id );
$saved_key = $request->confirm_key;
$key_request_time = $request->modified_timestamp;
if ( ! $request || ! $saved_key || ! $key_request_time ) {
return new WP_Error( 'invalid_request', __( 'Invalid personal data request.' ) );
if ( ! in_array( $request->status, array( 'request-pending', 'request-failed' ), true ) ) {
return new WP_Error( 'expired_request', __( 'This personal data request has expired.' ) );
return new WP_Error( 'missing_key', __( 'The confirmation key is missing from this personal data request.' ) );
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
* Filters the expiration time of confirm keys.
* @param int $expiration The expiration time in seconds.
$expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
$expiration_time = $key_request_time + $expiration_duration;
if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) {
return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) );
if ( ! $expiration_time || time() > $expiration_time ) {
return new WP_Error( 'expired_key', __( 'The confirmation key has expired for this personal data request.' ) );
* Return the user request object for the specified request ID.
* @param int $request_id The ID of the user request.
* @return WP_User_Request|false
function wp_get_user_request( $request_id ) {
$request_id = absint( $request_id );
$post = get_post( $request_id );
if ( ! $post || 'user_request' !== $post->post_type ) {
return new WP_User_Request( $post );
* Checks if Application Passwords is globally available.
* By default, Application Passwords is available to all sites using SSL or to local environments.
* Use {@see 'wp_is_application_passwords_available'} to adjust its availability.
function wp_is_application_passwords_available() {
$available = is_ssl() || 'local' === wp_get_environment_type();
* Filters whether Application Passwords is available.
* @param bool $available True if available, false otherwise.
return apply_filters( 'wp_is_application_passwords_available', $available );
* Checks if Application Passwords is available for a specific user.
* By default all users can use Application Passwords. Use {@see 'wp_is_application_passwords_available_for_user'}
* to restrict availability to certain users.
* @param int|WP_User $user The user to check.
function wp_is_application_passwords_available_for_user( $user ) {
if ( ! wp_is_application_passwords_available() ) {
if ( ! is_object( $user ) ) {
$user = get_userdata( $user );
if ( ! $user || ! $user->exists() ) {
* Filters whether Application Passwords is available for a specific user.
* @param bool $available True if available, false otherwise.
* @param WP_User $user The user to check.
return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user );