###SITEURL### The URL to the site. * * @since 5.8.0 * * @param string $content The email content. * @param array $email_data { * Data relating to the account action email. * * @type WP_User_Request $request User request object. * @type string $user_email The email address confirming a request * @type string $description Description of the action being performed so the user knows what the email is for. * @type string $manage_url The link to click manage privacy requests of this type. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * @type string $admin_email The administrator email receiving the mail. * } */ $content = apply_filters( 'user_request_confirmed_email_content', $content, $email_data ); $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###USER_EMAIL###', $email_data['user_email'], $content ); $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content ); $content = str_replace( '###MANAGE_URL###', sanitize_url( $email_data['manage_url'] ), $content ); $content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content ); $headers = ''; /** * Filters the headers of the user request confirmation email. * * @since 5.4.0 * * @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 $user_email The email address confirming a request * @type string $description Description of the action being performed so the user knows what the email is for. * @type string $manage_url The link to click manage privacy requests of this type. * @type string $sitename The site name sending the mail. * @type string $siteurl The site URL sending the mail. * @type string $admin_email The administrator email receiving the mail. * } */ $headers = apply_filters( 'user_request_confirmed_email_headers', $headers, $subject, $content, $request_id, $email_data ); $email_sent = wp_mail( $email_data['admin_email'], $subject, $content, $headers ); if ( $email_sent ) { update_post_meta( $request_id, '_wp_admin_notified', true ); } } /** * Notifies the user when their erasure request is fulfilled. * * Without this, the user would never know if their data was actually erased. * * @since 4.9.6 * * @param int $request_id The privacy request post ID associated with this request. */ function _wp_privacy_send_erasure_fulfillment_notification( $request_id ) { $request = wp_get_user_request( $request_id ); if ( ! ( $request instanceof WP_User_Request ) || 'request-completed' !== $request->status ) { return; } $already_notified = (bool) get_post_meta( $request_id, '_wp_user_notified', true ); if ( $already_notified ) { return; } // 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 ); } else { $switched_locale = switch_to_locale( get_locale() ); } /** * Filters the recipient of the data erasure fulfillment notification. * * @since 4.9.6 * * @param string $user_email The email address of the notification recipient. * @param WP_User_Request $request The request that is initiating the notification. */ $user_email = apply_filters( 'user_erasure_fulfillment_email_to', $request->email, $request ); $email_data = array( 'request' => $request, 'message_recipient' => $user_email, 'privacy_policy_url' => get_privacy_policy_url(), 'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), 'siteurl' => home_url(), ); $subject = sprintf( /* translators: Erasure request fulfilled notification email subject. %s: Site title. */ __( '[%s] Erasure Request Fulfilled' ), $email_data['sitename'] ); /** * Filters the subject of the email sent when an erasure request is completed. * * @since 4.9.8 * @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_subject'} instead. * * @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 $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. * } */ $subject = apply_filters_deprecated( 'user_erasure_complete_email_subject', array( $subject, $email_data['sitename'], $email_data ), '5.8.0', 'user_erasure_fulfillment_email_subject' ); /** * Filters the subject of the email sent when an erasure request is completed. * * @since 5.8.0 * * @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 $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. * } */ $subject = apply_filters( 'user_erasure_fulfillment_email_subject', $subject, $email_data['sitename'], $email_data ); /* translators: Do not translate SITENAME, SITEURL; those are placeholders. */ $content = __( 'Howdy, Your request to erase your personal data on ###SITENAME### has been completed. If you have any follow-up questions or concerns, please contact the site administrator. Regards, All at ###SITENAME### ###SITEURL###' ); if ( ! empty( $email_data['privacy_policy_url'] ) ) { /* translators: Do not translate SITENAME, SITEURL, PRIVACY_POLICY_URL; those are placeholders. */ $content = __( 'Howdy, Your request to erase your personal data on ###SITENAME### has been completed. If you have any follow-up questions or concerns, please contact the site administrator. For more information, you can also read our privacy policy: ###PRIVACY_POLICY_URL### Regards, All at ###SITENAME### ###SITEURL###' ); } /** * Filters the body of the data erasure fulfillment notification. * * The email is sent to a user when their data erasure request is fulfilled * by an administrator. * * The following strings have a special meaning and will get replaced dynamically: * * ###SITENAME### The name of the site. * ###PRIVACY_POLICY_URL### Privacy policy page URL. * ###SITEURL### The URL to the site. * * @since 4.9.6 * @deprecated 5.8.0 Use {@see 'user_erasure_fulfillment_email_content'} instead. * For user request confirmation email content * use {@see 'user_request_confirmed_email_content'} instead. * * @param string $content The email content. * @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. * } */ $content = apply_filters_deprecated( 'user_confirmed_action_email_content', array( $content, $email_data ), '5.8.0', sprintf( /* translators: 1 & 2: Deprecation replacement options. */ __( '%1$s or %2$s' ), 'user_erasure_fulfillment_email_content', 'user_request_confirmed_email_content' ) ); /** * Filters the body of the data erasure fulfillment notification. * * The email is sent to a user when their data erasure request is fulfilled * by an administrator. * * The following strings have a special meaning and will get replaced dynamically: * * ###SITENAME### The name of the site. * ###PRIVACY_POLICY_URL### Privacy policy page URL. * ###SITEURL### The URL to the site. * * @since 5.8.0 * * @param string $content The email content. * @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. * } */ $content = apply_filters( 'user_erasure_fulfillment_email_content', $content, $email_data ); $content = str_replace( '###SITENAME###', $email_data['sitename'], $content ); $content = str_replace( '###PRIVACY_POLICY_URL###', $email_data['privacy_policy_url'], $content ); $content = str_replace( '###SITEURL###', sanitize_url( $email_data['siteurl'] ), $content ); $headers = ''; /** * Filters the headers of the data erasure fulfillment notification. * * @since 5.4.0 * @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 ), '5.8.0', 'user_erasure_fulfillment_email_headers' ); /** * Filters the headers of the data erasure fulfillment notification. * * @since 5.8.0 * * @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(); } if ( $email_sent ) { update_post_meta( $request_id, '_wp_user_notified', true ); } } /** * Returns request confirmation message HTML. * * @since 4.9.6 * @access private * * @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 = '
' . __( 'Action has been confirmed.' ) . '
'; $message .= '' . __( 'The site administrator has been notified and will fulfill your request as soon as possible.' ) . '
'; if ( $request && in_array( $request->action_name, _wp_privacy_action_request_types(), true ) ) { if ( 'export_personal_data' === $request->action_name ) { $message = '' . __( 'Thanks for confirming your export request.' ) . '
'; $message .= '' . __( 'The site administrator has been notified. You will receive a link to download your export via email when they fulfill your request.' ) . '
'; } elseif ( 'remove_personal_data' === $request->action_name ) { $message = '' . __( 'Thanks for confirming your erasure request.' ) . '
'; $message .= '' . __( 'The site administrator has been notified. You will receive an email confirmation when they erase your data.' ) . '
'; } } /** * Filters the message displayed to a user when they confirm a data request. * * @since 4.9.6 * * @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 ); return $message; } /** * 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 4.9.6 * @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; // Check for duplicates. $requests_query = new WP_Query( array( '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. 'post_status' => array( 'request-pending', 'request-confirmed', ), 'fields' => 'ids', ) ); 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( array( '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 ), ), true ); return $request_id; } /** * Gets action description from the name and return a string. * * @since 4.9.6 * * @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' ); break; case 'remove_personal_data': $description = __( 'Erase Personal Data' ); break; default: /* translators: %s: Action name. */ $description = sprintf( __( 'Confirm the "%s" action' ), $action_name ); break; } /** * Filters the user action description. * * @since 4.9.6 * * @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. * * @since 4.9.6 * * @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 ); if ( ! $request ) { 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 ); } else { $switched_locale = switch_to_locale( get_locale() ); } $email_data = array( 'request' => $request, 'email' => $request->email, 'description' => wp_user_request_action_description( $request->action_name ), 'confirm_url' => add_query_arg( array( 'action' => 'confirmaction', 'request_id' => $request_id, 'confirm_key' => wp_generate_user_request_key( $request_id ), ), wp_login_url() ), 'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), 'siteurl' => home_url(), ); /* 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. * * @since 4.9.6 * * @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. */ $content = __( 'Howdy, A request has been made to perform the following action on your account: ###DESCRIPTION### To confirm this, please click on the following link: ###CONFIRM_URL### You can safely ignore and delete this email if you do not want to take this action. Regards, All at ###SITENAME### ###SITEURL###' ); /** * 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. * * @since 4.9.6 * * @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 ); $headers = ''; /** * Filters the headers of the email sent when an account action is attempted. * * @since 5.4.0 * * @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(); } if ( ! $email_sent ) { return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export confirmation email.' ) ); } return true; } /** * Returns a confirmation key for a user action and stores the hashed version for future comparison. * * @since 4.9.6 * * @param int $request_id Request ID. * @return string Confirmation key. */ function wp_generate_user_request_key( $request_id ) { global $wp_hasher; // 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 ); } wp_update_post( array( 'ID' => $request_id, 'post_status' => 'request-pending', 'post_password' => $wp_hasher->HashPassword( $key ), ) ); return $key; } /** * Validates a user request by comparing the key with the request's key. * * @since 4.9.6 * * @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 ) { global $wp_hasher; $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.' ) ); } if ( empty( $key ) ) { 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. * * @since 4.9.6 * * @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 true; } /** * Returns the user request object for the specified request ID. * * @since 4.9.6 * * @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 false; } 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. * * @since 5.9.0 * * @return bool */ 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. * * @since 5.6.0 * * @return bool */ function wp_is_application_passwords_available() { /** * Filters whether Application Passwords is available. * * @since 5.6.0 * * @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. * * By default all users can use Application Passwords. Use {@see 'wp_is_application_passwords_available_for_user'} * to restrict availability to certain users. * * @since 5.6.0 * * @param int|WP_User $user The user to check. * @return bool */ function wp_is_application_passwords_available_for_user( $user ) { if ( ! wp_is_application_passwords_available() ) { return false; } if ( ! is_object( $user ) ) { $user = get_userdata( $user ); } if ( ! $user || ! $user->exists() ) { return false; } /** * Filters whether Application Passwords is available for a specific user. * * @since 5.6.0 * * @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 ); } /** * Registers the user meta property for persisted preferences. * * This property is used to store user preferences across page reloads and is * currently used by the block editor for preferences like 'fullscreenMode' and * 'fixedToolbar'. * * @since 6.1.0 * @access private * * @global wpdb $wpdb WordPress database abstraction object. */ function wp_register_persisted_preferences_meta() { /* * Create a meta key that incorporates the blog prefix so that each site * on a multisite can have distinct user preferences. */ global $wpdb; $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; register_meta( 'user', $meta_key, array( 'type' => 'object', 'single' => true, 'show_in_rest' => array( 'name' => 'persisted_preferences', 'type' => 'object', 'schema' => array( 'type' => 'object', 'context' => array( 'edit' ), 'properties' => array( '_modified' => array( 'description' => __( 'The date and time the preferences were updated.' ), 'type' => 'string', 'format' => 'date-time', 'readonly' => false, ), ), 'additionalProperties' => true, ), ), ) ); } /** * Sets the last changed time for the 'users' cache group. * * @since 6.3.0 */ function wp_cache_set_users_last_changed() { wp_cache_set_last_changed( 'users' ); } /** * Checks if password reset is allowed for a specific user. * * @since 6.3.0 * * @param int|WP_User $user The user to check. * @return bool|WP_Error True if allowed, false or WP_Error otherwise. */ function wp_is_password_reset_allowed_for_user( $user ) { if ( ! is_object( $user ) ) { $user = get_userdata( $user ); } if ( ! $user || ! $user->exists() ) { return false; } $allow = true; if ( is_multisite() && is_user_spammy( $user ) ) { $allow = false; } /** * Filters whether to allow a password to be reset. * * @since 2.7.0 * * @param bool $allow Whether to allow the password to be reset. Default true. * @param int $user_id The ID of the user attempting to reset a password. */ return apply_filters( 'allow_password_reset', $allow, $user->ID ); }