a { * An array of name-value pairs of additional user data items. Default empty array. * * @type string $name The user-facing name of an item name-value pair,e.g. 'IP Address'. * @type string $value The user-facing value of an item data pair, e.g. '50.60.70.0'. * } * @param WP_User $user The user whose data is being exported. * @param string[] $reserved_names An array of reserved names. Any item in `$additional_user_data` * that uses one of these for its `name` will not be included in the export. */ $_extra_data = apply_filters( 'wp_privacy_additional_user_profile_data', array(), $user, $reserved_names ); if ( is_array( $_extra_data ) && ! empty( $_extra_data ) ) { // Remove items that use reserved names. $extra_data = array_filter( $_extra_data, static function ( $item ) use ( $reserved_names ) { return ! in_array( $item['name'], $reserved_names, true ); } ); if ( count( $extra_data ) !== count( $_extra_data ) ) { _doing_it_wrong( __FUNCTION__, sprintf( /* translators: %s: wp_privacy_additional_user_profile_data */ __( 'Filter %s returned items with reserved names.' ), 'wp_privacy_additional_user_profile_data' ), '5.4.0' ); } if ( ! empty( $extra_data ) ) { $user_data_to_export = array_merge( $user_data_to_export, $extra_data ); } } $data_to_export[] = array( 'group_id' => 'user', 'group_label' => __( 'User' ), 'group_description' => __( 'User’s profile data.' ), 'item_id' => "user-{$user->ID}", 'data' => $user_data_to_export, ); if ( isset( $user_meta['community-events-location'] ) ) { $location = maybe_unserialize( $user_meta['community-events-location'][0] ); $location_props_to_export = array( 'description' => __( 'City' ), 'country' => __( 'Country' ), 'latitude' => __( 'Latitude' ), 'longitude' => __( 'Longitude' ), 'ip' => __( 'IP' ), ); $location_data_to_export = array(); foreach ( $location_props_to_export as $key => $name ) { if ( ! empty( $location[ $key ] ) ) { $location_data_to_export[] = array( 'name' => $name, 'value' => $location[ $key ], ); } } $data_to_export[] = array( 'group_id' => 'community-events-location', 'group_label' => __( 'Community Events Location' ), 'group_description' => __( 'User’s location data used for the Community Events in the WordPress Events and News dashboard widget.' ), 'item_id' => "community-events-location-{$user->ID}", 'data' => $location_data_to_export, ); } if ( isset( $user_meta['session_tokens'] ) ) { $session_tokens = maybe_unserialize( $user_meta['session_tokens'][0] ); $session_tokens_props_to_export = array( 'expiration' => __( 'Expiration' ), 'ip' => __( 'IP' ), 'ua' => __( 'User Agent' ), 'login' => __( 'Last Login' ), ); foreach ( $session_tokens as $token_key => $session_token ) { $session_tokens_data_to_export = array(); foreach ( $session_tokens_props_to_export as $key => $name ) { if ( ! empty( $session_token[ $key ] ) ) { $value = $session_token[ $key ]; if ( in_array( $key, array( 'expiration', 'login' ), true ) ) { $value = date_i18n( 'F d, Y H:i A', $value ); } $session_tokens_data_to_export[] = array( 'name' => $name, 'value' => $value, ); } } $data_to_export[] = array( 'group_id' => 'session-tokens', 'group_label' => __( 'Session Tokens' ), 'group_description' => __( 'User’s Session Tokens data.' ), 'item_id' => "session-tokens-{$user->ID}-{$token_key}", 'data' => $session_tokens_data_to_export, ); } } return array( 'data' => $data_to_export, 'done' => true, ); } /** * Updates log when privacy request is confirmed. * * @since 4.9.6 * @access private * * @param int $request_id ID of the request. */ function _wp_privacy_account_request_confirmed( $request_id ) { $request = wp_get_user_request( $request_id ); if ( ! $request ) { return; } if ( ! in_array( $request->status, array( 'request-pending', 'request-failed' ), true ) ) { return; } update_post_meta( $request_id, '_wp_user_request_confirmed_timestamp', time() ); wp_update_post( array( 'ID' => $request_id, 'post_status' => 'request-confirmed', ) ); } /** * Notifies the site administrator via email when a request is confirmed. * * Without this, the admin would have to manually check the site to see if any * action was needed on their part yet. * * @since 4.9.6 * * @param int $request_id The ID of the request. */ function _wp_privacy_send_request_confirmation_notification( $request_id ) { $request = wp_get_user_request( $request_id ); if ( ! ( $request instanceof WP_User_Request ) || 'request-confirmed' !== $request->status ) { return; } $already_notified = (bool) get_post_meta( $request_id, '_wp_admin_notified', true ); if ( $already_notified ) { return; } if ( 'export_personal_data' === $request->action_name ) { $manage_url = admin_url( 'export-personal-data.php' ); } elseif ( 'remove_personal_data' === $request->action_name ) { $manage_url = admin_url( 'erase-personal-data.php' ); } $action_description = wp_user_request_action_description( $request->action_name ); /** * Filters the recipient of the data request confirmation notification. * * In a Multisite environment, this will default to the email address of the * network admin because, by default, single site admins do not have the * capabilities required to process requests. Some networks may wish to * delegate those capabilities to a single-site admin, or a dedicated person * responsible for managing privacy requests. * * @since 4.9.6 * * @param string $admin_email The email address of the notification recipient. * @param WP_User_Request $request The request that is initiating the notification. */ $admin_email = apply_filters( 'user_request_confirmed_email_to', get_site_option( 'admin_email' ), $request ); $email_data = array( 'request' => $request, 'user_email' => $request->email, 'description' => $action_description, 'manage_url' => $manage_url, 'sitename' => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), 'siteurl' => home_url(), 'admin_email' => $admin_email, ); $subject = sprintf( /* translators: Privacy data request confirmed notification email subject. 1: Site title, 2: Name of the confirmed action. */ __( '[%1$s] Action Confirmed: %2$s' ), $email_data['sitename'], $action_description ); /** * Filters the subject of the user request confirmation email. * * @since 4.9.8 * * @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 $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. * } */ $subject = apply_filters( 'user_request_confirmed_email_subject', $subject, $email_data['sitename'], $email_data ); /* translators: Do not translate SITENAME, USER_EMAIL, DESCRIPTION, MANAGE_URL, SITEURL; those are placeholders. */ $content = __( 'Howdy, A user data privacy request has been confirmed on ###SITENAME###: User: ###USER_EMAIL### Request: ###DESCRIPTION### You can view and manage these data privacy requests here: ###MANAGE_URL### Regards, All at ###SITENAME### ###SITEURL###' ); /** * Filters the body of the user request confirmation email. * * The email is sent to an administrator when a user request is confirmed. * * The following strings have a special meaning and will get replaced dynamically: * * ###SITENAME### The name of the site. * ###USER_EMAIL### The user email for the request. * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. * ###MANAGE_URL### The URL to manage requests. * ###SITEURL### The URL to the site. * * @since 4.9.6 * @deprecated 5.8.0 Use {@see 'user_request_confirmed_email_content'} instead. * For user erasure fulfillment email content * use {@see 'user_erasure_fulfillment_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 $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_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_request_confirmed_email_content', 'user_erasure_fulfillment_email_content' ) ); /** * Filters the body of the user request confirmation email. * * The email is sent to an administrator when a user request is confirmed. * The following strings have a special meaning and will get replaced dynamically: * * ###SITENAME### The name of the site. * ###USER_EMAIL### The user email for the request. * ###DESCRIPTION### Description of the action being performed so the user knows what the email is for. * ###MANAGE_URL### The URL to manage requests. * ###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 * * @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 ) { 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 * * @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 ) { 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 ); } o the file * @since 1.0.3 * @since-jetpack 5.6.0 */ public static function get_file_url_for_environment( $min_path, $non_min_path, $package_path = '' ) { $path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) ) ? $non_min_path : $min_path; /* * If the path is actually a full URL, keep that. * We look for a host value, since enqueues are sometimes without a scheme. */ $file_parts = wp_parse_url( $path ); if ( ! empty( $file_parts['host'] ) ) { $url = $path; } else { $plugin_path = empty( $package_path ) ? Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) : $package_path; $url = plugins_url( $path, $plugin_path ); } /** * Filters the URL for a file passed through the get_file_url_for_environment function. * * @since 1.0.3 * * @package assets * * @param string $url The URL to the file. * @param string $min_path The minified path. * @param string $non_min_path The non-minified path. */ return apply_filters( 'jetpack_get_file_for_environment', $url, $min_path, $non_min_path ); } /** * Passes an array of URLs to wp_resource_hints. * * @since 1.5.0 * * @param string|array $urls URLs to hint. * @param string $type One of the supported resource types: dns-prefetch (default), preconnect, prefetch, or prerender. */ public static function add_resource_hint( $urls, $type = 'dns-prefetch' ) { add_filter( 'wp_resource_hints', function ( $hints, $resource_type ) use ( $urls, $type ) { if ( $resource_type === $type ) { // Type casting to array required since the function accepts a single string. foreach ( (array) $urls as $url ) { $hints[] = $url; } } return $hints; }, 10, 2 ); } /** * Serve a WordPress.com static resource via a randomized wp.com subdomain. * * @since 1.9.0 * * @param string $url WordPress.com static resource URL. * * @return string $url */ public static function staticize_subdomain( $url ) { // Extract hostname from URL. $host = wp_parse_url( $url, PHP_URL_HOST ); // Explode hostname on '.'. $exploded_host = explode( '.', $host ); // Retrieve the name and TLD. if ( count( $exploded_host ) > 1 ) { $name = $exploded_host[ count( $exploded_host ) - 2 ]; $tld = $exploded_host[ count( $exploded_host ) - 1 ]; // Rebuild domain excluding subdomains. $domain = $name . '.' . $tld; } else { $domain = $host; } // Array of Automattic domains. $domains_allowed = array( 'wordpress.com', 'wp.com' ); // Return $url if not an Automattic domain. if ( ! in_array( $domain, $domains_allowed, true ) ) { return $url; } if ( \is_ssl() ) { return preg_replace( '|https?://[^/]++/|', 'https://s-ssl.wordpress.com/', $url ); } /* * Generate a random subdomain id by taking the modulus of the crc32 value of the URL. * Valid values are 0, 1, and 2. */ $static_counter = abs( crc32( basename( $url ) ) % 3 ); return preg_replace( '|://[^/]+?/|', "://s$static_counter.wp.com/", $url ); } /** * Resolve '.' and '..' components in a path or URL. * * @since 1.12.0 * @param string $path Path or URL. * @return string Normalized path or URL. */ public static function normalize_path( $path ) { $parts = wp_parse_url( $path ); if ( ! isset( $parts['path'] ) ) { return $path; } $ret = ''; $ret .= isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : ''; if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) { $ret .= $parts['user'] ?? ''; $ret .= isset( $parts['pass'] ) ? ':' . $parts['pass'] : ''; $ret .= '@'; } $ret .= $parts['host'] ?? ''; $ret .= isset( $parts['port'] ) ? ':' . $parts['port'] : ''; $pp = explode( '/', $parts['path'] ); if ( '' === $pp[0] ) { $ret .= '/'; array_shift( $pp ); } $i = 0; while ( $i < count( $pp ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found if ( '' === $pp[ $i ] || '.' === $pp[ $i ] || 0 === $i && '..' === $pp[ $i ] ) { array_splice( $pp, $i, 1 ); } elseif ( '..' === $pp[ $i ] ) { array_splice( $pp, --$i, 2 ); } else { ++$i; } } $ret .= implode( '/', $pp ); $ret .= isset( $parts['query'] ) ? '?' . $parts['query'] : ''; $ret .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : ''; return $ret; } // endregion . // //////////////////// // region Webpack-built script registration /** * Register a Webpack-built script. * * Our Webpack-built scripts tend to need a bunch of boilerplate: * - A call to `Assets::get_file_url_for_environment()` for possible debugging. * - A call to `wp_register_style()` for extracted CSS, possibly with detection of RTL. * - Loading of dependencies and version provided by `@wordpress/dependency-extraction-webpack-plugin`. * - Avoiding WPCom's broken minifier. * * This wrapper handles all of that. * * @since 1.12.0 * @since 2.1.0 Add a new `strategy` option to leverage WP >= 6.3 script strategy feature. The `async` option is deprecated. * @param string $handle Name of the script. Should be unique across both scripts and styles. * @param string $path Minimized script path. * @param string $relative_to File that `$path` is relative to. Pass `__FILE__`. * @param array $options Additional options: * - `asset_path`: (string|null) `.asset.php` to load. Default is to base it on `$path`. * - `async`: (bool) Set true to register the script as deferred, like `Assets::enqueue_async_script()`. Deprecated in favor of `strategy`. * - `css_dependencies`: (string[]) Additional style dependencies to queue. * - `css_path`: (string|null) `.css` to load. Default is to base it on `$path`. * - `dependencies`: (string[]) Additional script dependencies to queue. * - `enqueue`: (bool) Set true to enqueue the script immediately. * - `in_footer`: (bool) Set true to register script for the footer. * - `media`: (string) Media for the css file. Default 'all'. * - `minify`: (bool|null) Set true to pass `minify=true` in the query string, or `null` to suppress the normal `minify=false`. * - `nonmin_path`: (string) Non-minified script path. * - `strategy`: (string) Specify a script strategy to use, eg. `defer` or `async`. Default is `""`. * - `textdomain`: (string) Text domain for the script. Required if the script depends on wp-i18n. * - `version`: (string) Override the version from the `asset_path` file. * @phan-param array{asset_path?:?string,async?:bool,css_dependencies?:string[],css_path?:?string,dependencies?:string[],enqueue?:bool,in_footer?:bool,media?:string,minify?:?bool,nonmin_path?:string,strategy?:string,textdomain?:string,version?:string} $options * @throws \InvalidArgumentException If arguments are invalid. */ public static function register_script( $handle, $path, $relative_to, array $options = array() ) { if ( substr( $path, -3 ) !== '.js' ) { throw new \InvalidArgumentException( '$path must end in ".js"' ); } if ( isset( $options['async'] ) ) { _deprecated_argument( __METHOD__, '2.1.0', 'The `async` option is deprecated in favor of `strategy`' ); } $dir = dirname( $relative_to ); $base = substr( $path, 0, -3 ); $options += array( 'asset_path' => "$base.asset.php", 'async' => false, 'css_dependencies' => array(), 'css_path' => "$base.css", 'dependencies' => array(), 'enqueue' => false, 'in_footer' => false, 'media' => 'all', 'minify' => false, 'strategy' => '', 'textdomain' => null, ); '@phan-var array{asset_path:?string,async:bool,css_dependencies:string[],css_path:?string,dependencies:string[],enqueue:bool,in_footer:bool,media:string,minify:?bool,nonmin_path?:string,strategy:string,textdomain:string,version?:string} $options'; // Phan gets confused by the array addition. if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && substr( $options['css_path'], -4 ) !== '.css' ) { throw new \InvalidArgumentException( '$options[\'css_path\'] must end in ".css"' ); } if ( isset( $options['nonmin_path'] ) ) { $url = self::get_file_url_for_environment( $path, $options['nonmin_path'], $relative_to ); } else { $url = plugins_url( $path, $relative_to ); } $url = self::normalize_path( $url ); if ( null !== $options['minify'] ) { $url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url ); } if ( $options['asset_path'] && file_exists( "$dir/{$options['asset_path']}" ) ) { $asset = require "$dir/{$options['asset_path']}"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath $options['dependencies'] = array_merge( $asset['dependencies'], $options['dependencies'] ); $options['css_dependencies'] = array_merge( array_filter( $asset['dependencies'], function ( $d ) { return wp_style_is( $d, 'registered' ); } ), $options['css_dependencies'] ); $ver = $options['version'] ?? $asset['version']; } else { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $ver = $options['version'] ?? @filemtime( "$dir/$path" ); } if ( $options['async'] && '' === $options['strategy'] ) { // Handle the deprecated `async` option $options['strategy'] = 'defer'; } wp_register_script( $handle, $url, $options['dependencies'], $ver, array( 'in_footer' => $options['in_footer'], 'strategy' => $options['strategy'], ) ); if ( $options['textdomain'] ) { // phpcs:ignore Jetpack.Functions.I18n.DomainNotLiteral wp_set_script_translations( $handle, $options['textdomain'] ); } elseif ( in_array( 'wp-i18n', $options['dependencies'], true ) ) { _doing_it_wrong( __METHOD__, /* translators: %s is the script handle. */ esc_html( sprintf( __( 'Script "%s" depends on wp-i18n but does not specify "textdomain"', 'jetpack-assets' ), $handle ) ), '' ); } if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && file_exists( "$dir/{$options['css_path']}" ) ) { $csspath = $options['css_path']; if ( is_rtl() ) { $rtlcsspath = substr( $csspath, 0, -4 ) . '.rtl.css'; if ( file_exists( "$dir/$rtlcsspath" ) ) { $csspath = $rtlcsspath; } } $url = self::normalize_path( plugins_url( $csspath, $relative_to ) ); if ( null !== $options['minify'] ) { $url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url ); } wp_register_style( $handle, $url, $options['css_dependencies'], $ver, $options['media'] ); wp_script_add_data( $handle, 'Jetpack::Assets::hascss', true ); } else { wp_script_add_data( $handle, 'Jetpack::Assets::hascss', false ); } if ( $options['enqueue'] ) { self::enqueue_script( $handle ); } } /** * Enqueue a script registered with `Assets::register_script`. * * @since 1.12.0 * @param string $handle Name of the script. Should be unique across both scripts and styles. */ public static function enqueue_script( $handle ) { wp_enqueue_script( $handle ); if ( wp_scripts()->get_data( $handle, 'Jetpack::Assets::hascss' ) ) { wp_enqueue_style( $handle ); } } /** * 'wp_default_scripts' action handler. * * This registers the `wp-jp-i18n-loader` script for use by Webpack bundles built with * `@automattic/i18n-loader-webpack-plugin`. * * @since 1.14.0 * @param \WP_Scripts $wp_scripts WP_Scripts instance. */ public static function wp_default_scripts_hook( $wp_scripts ) { $data = array( 'baseUrl' => false, 'locale' => determine_locale(), 'domainMap' => array(), 'domainPaths' => array(), ); $lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' ); $content_dir = Jetpack_Constants::get_constant( 'WP_CONTENT_DIR' ); $abspath = Jetpack_Constants::get_constant( 'ABSPATH' ); // Note: str_starts_with() is not used here, as wp-includes/compat.php may not be loaded at this point. if ( strpos( $lang_dir, $content_dir ) === 0 ) { $data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) ); } elseif ( strpos( $lang_dir, $abspath ) === 0 ) { $data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) ); } foreach ( self::$domain_map as $from => list( $to, $type, , $path ) ) { $data['domainMap'][ $from ] = ( 'core' === $type ? '' : "{$type}/" ) . $to; if ( '' !== $path ) { $data['domainPaths'][ $from ] = trailingslashit( $path ); } } /** * Filters the i18n state data for use by Webpack bundles built with * `@automattic/i18n-loader-webpack-plugin`. * * @since 1.14.0 * @package assets * @param array $data The state data to generate. Expected fields are: * - `baseUrl`: (string|false) The URL to the languages directory. False if no URL could be determined. * - `locale`: (string) The locale for the page. * - `domainMap`: (string[]) A mapping from Composer package textdomains to the corresponding * `plugins/textdomain` or `themes/textdomain` (or core `textdomain`, but that's unlikely). * - `domainPaths`: (string[]) A mapping from Composer package textdomains to the corresponding package * paths. */ $data = apply_filters( 'jetpack_i18n_state', $data ); // Can't use self::register_script(), this action is called too early. if ( file_exists( __DIR__ . '/../build/i18n-loader.asset.php' ) ) { $path = '../build/i18n-loader.js'; $asset = require __DIR__ . '/../build/i18n-loader.asset.php'; } else { $path = 'js/i18n-loader.js'; $asset = array( 'dependencies' => array( 'wp-i18n' ), 'version' => filemtime( __DIR__ . "/$path" ), ); } $url = self::normalize_path( plugins_url( $path, __FILE__ ) ); $url = add_query_arg( 'minify', 'true', $url ); $handle = 'wp-jp-i18n-loader'; $wp_scripts->add( $handle, $url, $asset['dependencies'], $asset['version'] ); // Ensure the script is loaded in the footer and deferred. $wp_scripts->add_data( $handle, 'group', 1 ); if ( ! is_array( $data ) || ! isset( $data['baseUrl'] ) || ! ( is_string( $data['baseUrl'] ) || false === $data['baseUrl'] ) || ! isset( $data['locale'] ) || ! is_string( $data['locale'] ) || ! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) || ! isset( $data['domainPaths'] ) || ! is_array( $data['domainPaths'] ) ) { $wp_scripts->add_inline_script( $handle, 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' ); } elseif ( ! $data['baseUrl'] ) { $wp_scripts->add_inline_script( $handle, 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' ); } else { $data['domainMap'] = (object) $data['domainMap']; // Ensure it becomes a json object. $data['domainPaths'] = (object) $data['domainPaths']; // Ensure it becomes a json object. $wp_scripts->add_inline_script( $handle, 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';' ); } // Deprecated state module: Depend on wp-i18n to ensure global `wp` exists and because anything needing this will need that too. $wp_scripts->add( 'wp-jp-i18n-state', false, array( 'wp-deprecated', $handle ) ); $wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.deprecated( "wp-jp-i18n-state", { alternative: "wp-jp-i18n-loader" } );' ); $wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.jpI18nState = wp.jpI18nLoader.state;' ); // Register the React JSX runtime script - used as a polyfill until we can update JSX transforms. See https://github.com/Automattic/jetpack/issues/38424. // @todo Remove this when we drop support for WordPress 6.5, as well as the script inclusion in test_wp_default_scripts_hook. $jsx_url = self::normalize_path( plugins_url( '../build/react-jsx-runtime.js', __FILE__ ) ); $wp_scripts->add( 'react-jsx-runtime', $jsx_url, array( 'react' ), '18.3.1', true ); $wp_scripts->add_data( 'react-jsx-runtime', 'group', 1 ); } // endregion . // //////////////////// // region Textdomain aliasing /** * Register a textdomain alias. * * Composer packages included in plugins will likely not use the textdomain of the plugin, while * WordPress's i18n infrastructure will include the translations in the plugin's domain. This * allows for mapping the package's domain to the plugin's. * * Since multiple plugins may use the same package, we include the package's version here so * as to choose the most recent translations (which are most likely to match the package * selected by jetpack-autoloader). * * @since 1.15.0 * @param string $from Domain to alias. * @param string $to Domain to alias it to. * @param string $totype What is the target of the alias: 'plugins', 'themes', or 'core'. * @param string $ver Version of the `$from` domain. * @param string $path Path to prepend when lazy-loading from JavaScript. * @throws InvalidArgumentException If arguments are invalid. */ public static function alias_textdomain( $from, $to, $totype, $ver, $path = '' ) { if ( ! in_array( $totype, array( 'plugins', 'themes', 'core' ), true ) ) { throw new InvalidArgumentException( 'Type must be "plugins", "themes", or "core"' ); } if ( did_action( 'wp_default_scripts' ) && // Don't complain during plugin activation. ! defined( 'WP_SANDBOX_SCRAPING' ) ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: 1: wp_default_scripts. 2: Name of the domain being aliased. */ esc_html__( 'Textdomain aliases should be registered before the %1$s hook. This notice was triggered by the %2$s domain.', 'jetpack-assets' ), 'wp_default_scripts', '' . esc_html( $from ) . '' ), '' ); } if ( empty( self::$domain_map[ $from ] ) ) { self::init_domain_map_hooks( $from, array() === self::$domain_map ); self::$domain_map[ $from ] = array( $to, $totype, $ver, $path ); } elseif ( Semver::compare( $ver, self::$domain_map[ $from ][2] ) > 0 ) { self::$domain_map[ $from ] = array( $to, $totype, $ver, $path ); } } /** * Register textdomain aliases from a mapping file. * * The mapping file is simply a PHP file that returns an array * with the following properties: * - 'domain': String, `$to` * - 'type': String, `$totype` * - 'packages': Array, mapping `$from` to `array( 'path' => $path, 'ver' => $ver )` (or to the string `$ver` for back compat). * * @since 1.15.0 * @param string $file Mapping file. */ public static function alias_textdomains_from_file( $file ) { $data = require $file; foreach ( $data['packages'] as $from => $fromdata ) { if ( ! is_array( $fromdata ) ) { $fromdata = array( 'path' => '', 'ver' => $fromdata, ); } self::alias_textdomain( $from, $data['domain'], $data['type'], $fromdata['ver'], $fromdata['path'] ); } } /** * Register the hooks for textdomain aliasing. * * @param string $domain Domain to alias. * @param bool $firstcall If this is the first call. */ private static function init_domain_map_hooks( $domain, $firstcall ) { // If WordPress's plugin API is available already, use it. If not, // drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`. if ( function_exists( 'add_filter' ) ) { $add_filter = 'add_filter'; } else { $add_filter = function ( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { global $wp_filter; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_filter[ $hook_name ][ $priority ][] = array( 'accepted_args' => $accepted_args, 'function' => $callback, ); }; } $add_filter( "gettext_{$domain}", array( self::class, 'filter_gettext' ), 10, 3 ); $add_filter( "ngettext_{$domain}", array( self::class, 'filter_ngettext' ), 10, 5 ); $add_filter( "gettext_with_context_{$domain}", array( self::class, 'filter_gettext_with_context' ), 10, 4 ); $add_filter( "ngettext_with_context_{$domain}", array( self::class, 'filter_ngettext_with_context' ), 10, 6 ); if ( $firstcall ) { $add_filter( 'load_script_translation_file', array( self::class, 'filter_load_script_translation_file' ), 10, 3 ); } } /** * Filter for `gettext`. * * @since 1.15.0 * @param string $translation Translated text. * @param string $text Text to translate. * @param string $domain Text domain. * @return string Translated text. */ public static function filter_gettext( $translation, $text, $domain ) { if ( $translation === $text ) { // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages $newtext = __( $text, self::$domain_map[ $domain ][0] ); if ( $newtext !== $text ) { return $newtext; } } return $translation; } /** * Filter for `ngettext`. * * @since 1.15.0 * @param string $translation Translated text. * @param string $single The text to be used if the number is singular. * @param string $plural The text to be used if the number is plural. * @param int $number The number to compare against to use either the singular or plural form. * @param string $domain Text domain. * @return string Translated text. */ public static function filter_ngettext( $translation, $single, $plural, $number, $domain ) { if ( $translation === $single || $translation === $plural ) { // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages $translation = _n( $single, $plural, $number, self::$domain_map[ $domain ][0] ); } return $translation; } /** * Filter for `gettext_with_context`. * * @since 1.15.0 * @param string $translation Translated text. * @param string $text Text to translate. * @param string $context Context information for the translators. * @param string $domain Text domain. * @return string Translated text. */ public static function filter_gettext_with_context( $translation, $text, $context, $domain ) { if ( $translation === $text ) { // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages $translation = _x( $text, $context, self::$domain_map[ $domain ][0] ); } return $translation; } /** * Filter for `ngettext_with_context`. * * @since 1.15.0 * @param string $translation Translated text. * @param string $single The text to be used if the number is singular. * @param string $plural The text to be used if the number is plural. * @param int $number The number to compare against to use either the singular or plural form. * @param string $context Context information for the translators. * @param string $domain Text domain. * @return string Translated text. */ public static function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ) { if ( $translation === $single || $translation === $plural ) { // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages $translation = _nx( $single, $plural, $number, $context, self::$domain_map[ $domain ][0] ); } return $translation; } /** * Filter for `load_script_translation_file`. * * @since 1.15.0 * @param string|false $file Path to the translation file to load. False if there isn't one. * @param string $handle Name of the script to register a translation domain to. * @param string $domain The text domain. */ public static function filter_load_script_translation_file( $file, $handle, $domain ) { if ( false !== $file && isset( self::$domain_map[ $domain ] ) && ! is_readable( $file ) ) { // Determine the part of the filename after the domain. $suffix = basename( $file ); $l = strlen( $domain ); if ( substr( $suffix, 0, $l ) !== $domain || '-' !== $suffix[ $l ] ) { return $file; } $suffix = substr( $suffix, $l ); $lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' ); // Look for replacement files. list( $newdomain, $type ) = self::$domain_map[ $domain ]; $newfile = $lang_dir . ( 'core' === $type ? '/' : "/{$type}/" ) . $newdomain . $suffix; if ( is_readable( $newfile ) ) { return $newfile; } } return $file; } // endregion . } // Enable section folding in vim: // vim: foldmarker=//\ region,//\ endregion foldmethod=marker // .