: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_headers'} instead.
* @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 $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
$headers = apply_filters_deprecated(
'user_erasure_complete_email_headers',
array( $headers, $subject, $content, $request_id, $email_data ),
'user_erasure_fulfillment_email_headers'
* Filters the headers of the data erasure fulfillment notification.
* @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 $message_recipient The address that the email will be sent to. Defaults
* to the value of `$request->email`, but can be changed
* by the `user_erasure_fulfillment_email_to` filter.
* @type string $privacy_policy_url Privacy policy URL.
* @type string $sitename The site name sending the mail.
* @type string $siteurl The site URL sending the mail.
$headers = apply_filters( 'user_erasure_fulfillment_email_headers', $headers, $subject, $content, $request_id, $email_data );
$email_sent = wp_mail( $user_email, $subject, $content, $headers );
if ( $switched_locale ) {
restore_previous_locale();
update_post_meta( $request_id, '_wp_user_notified', true );
* Returns request confirmation message HTML.
* @param int $request_id The request ID being confirmed.
* @return string The confirmation message.
function _wp_privacy_account_request_confirmed_message( $request_id ) {
$request = wp_get_user_request( $request_id );
$message = '<p class="success">' . __( 'Action has been confirmed.' ) . '</p>';
$message .= '<p>' . __( 'The site administrator has been notified and will fulfill your request as soon as possible.' ) . '</p>';
if ( $request && in_array( $request->action_name, _wp_privacy_action_request_types(), true ) ) {
if ( 'export_personal_data' === $request->action_name ) {
$message = '<p class="success">' . __( 'Thanks for confirming your export request.' ) . '</p>';
$message .= '<p>' . __( 'The site administrator has been notified. You will receive a link to download your export via email when they fulfill your request.' ) . '</p>';
} elseif ( 'remove_personal_data' === $request->action_name ) {
$message = '<p class="success">' . __( 'Thanks for confirming your erasure request.' ) . '</p>';
$message .= '<p>' . __( 'The site administrator has been notified. You will receive an email confirmation when they erase your data.' ) . '</p>';
* Filters the message displayed to a user when they confirm a data request.
* @param string $message The message to the user.
* @param int $request_id The ID of the request being confirmed.
$message = apply_filters( 'user_request_action_confirmed_message', $message, $request_id );
* Creates and logs a user request to perform a specific action.
* Requests are stored inside a post type named `user_request` since they can apply to both
* users on the site, or guests without a user account.
* @since 5.7.0 Added the `$status` parameter.
* @param string $email_address User email address. This can be the address of a registered
* or non-registered user.
* @param string $action_name Name of the action that is being confirmed. Required.
* @param array $request_data Misc data you want to send with the verification request and pass
* to the actions once the request is confirmed.
* @param string $status Optional request status (pending or confirmed). Default 'pending'.
* @return int|WP_Error Returns the request ID if successful, or a WP_Error object on failure.
function wp_create_user_request( $email_address = '', $action_name = '', $request_data = array(), $status = 'pending' ) {
$email_address = sanitize_email( $email_address );
$action_name = sanitize_key( $action_name );
if ( ! is_email( $email_address ) ) {
return new WP_Error( 'invalid_email', __( 'Invalid email address.' ) );
if ( ! in_array( $action_name, _wp_privacy_action_request_types(), true ) ) {
return new WP_Error( 'invalid_action', __( 'Invalid action name.' ) );
if ( ! in_array( $status, array( 'pending', 'confirmed' ), true ) ) {
return new WP_Error( 'invalid_status', __( 'Invalid request status.' ) );
$user = get_user_by( 'email', $email_address );
$user_id = $user && ! is_wp_error( $user ) ? $user->ID : 0;
$requests_query = new WP_Query(
'post_type' => 'user_request',
'post_name__in' => array( $action_name ), // Action name stored in post_name column.
'title' => $email_address, // Email address stored in post_title column.
if ( $requests_query->found_posts ) {
return new WP_Error( 'duplicate_request', __( 'An incomplete personal data request for this email address already exists.' ) );
$request_id = wp_insert_post(
'post_author' => $user_id,
'post_name' => $action_name,
'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 ),
* Gets 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 ) ) {
$switched_locale = switch_to_user_locale( $request->user_id );
$switched_locale = switch_to_locale( get_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: 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 );
/* 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 $content 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', $content, $email_data );
$content = str_replace( '###DESCRIPTION###', $email_data['description'], $content );
$content = str_replace( '###CONFIRM_URL###', sanitize_url( $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###', sanitize_url( $email_data['siteurl'] ), $content );
* 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.
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
* @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 ),
* Validates a user request by comparing the key with the request's key.
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
* @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.' ) );
* Returns 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 supported.
* Application Passwords is supported only by sites using SSL or local environments
* but may be made available using the {@see 'wp_is_application_passwords_available'} filter.
function wp_is_application_passwords_supported() {
return is_ssl() || 'local' === wp_get_environment_type();
* Checks if Application Passwords is globally available.
* By default, Application Passwords is available to all sites using SSL or to local environments.
* Use the {@see 'wp_is_application_passwords_available'} filter to adjust its availability.
function wp_is_application_passwords_available() {
* Filters whether Application Passwords is available.
* @param bool $available True if available, false otherwise.
return apply_filters( 'wp_is_application_passwords_available', wp_is_application_passwords_supported() );
* Checks if Application Passwords is available for a specific user.