diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-admin/includes/user.php --- a/wp/wp-admin/includes/user.php Thu Sep 29 08:06:27 2022 +0200 +++ b/wp/wp-admin/includes/user.php Fri Sep 05 18:40:08 2025 +0200 @@ -29,7 +29,7 @@ */ function edit_user( $user_id = 0 ) { $wp_roles = wp_roles(); - $user = new stdClass; + $user = new stdClass(); $user_id = (int) $user_id; if ( $user_id ) { $update = true; @@ -84,7 +84,7 @@ if ( empty( $_POST['url'] ) || 'http://' === $_POST['url'] ) { $user->user_url = ''; } else { - $user->user_url = esc_url_raw( $_POST['url'] ); + $user->user_url = sanitize_url( $_POST['url'] ); $protocols = implode( '|', array_map( 'preg_quote', wp_allowed_protocols() ) ); $user->user_url = preg_match( '/^(' . $protocols . '):/is', $user->user_url ) ? $user->user_url : 'http://' . $user->user_url; } @@ -119,7 +119,13 @@ } elseif ( '' === $locale ) { $locale = 'en_US'; } elseif ( ! in_array( $locale, get_available_languages(), true ) ) { - $locale = ''; + if ( current_user_can( 'install_languages' ) && wp_can_install_language_pack() ) { + if ( ! wp_download_language_pack( $locale ) ) { + $locale = ''; + } + } else { + $locale = ''; + } } $user->locale = $locale; @@ -143,12 +149,12 @@ /* checking that username has been typed */ if ( '' === $user->user_login ) { - $errors->add( 'user_login', __( 'Error: Please enter a username.' ) ); + $errors->add( 'user_login', __( 'Error: Please enter a username.' ) ); } /* checking that nickname has been typed */ if ( $update && empty( $user->nickname ) ) { - $errors->add( 'nickname', __( 'Error: Please enter a nickname.' ) ); + $errors->add( 'nickname', __( 'Error: Please enter a nickname.' ) ); } /** @@ -164,17 +170,17 @@ // Check for blank password when adding a user. if ( ! $update && empty( $pass1 ) ) { - $errors->add( 'pass', __( 'Error: Please enter a password.' ), array( 'form-field' => 'pass1' ) ); + $errors->add( 'pass', __( 'Error: Please enter a password.' ), array( 'form-field' => 'pass1' ) ); } // Check for "\" in password. - if ( false !== strpos( wp_unslash( $pass1 ), '\\' ) ) { - $errors->add( 'pass', __( 'Error: Passwords may not contain the character "\\".' ), array( 'form-field' => 'pass1' ) ); + if ( str_contains( wp_unslash( $pass1 ), '\\' ) ) { + $errors->add( 'pass', __( 'Error: Passwords may not contain the character "\\".' ), array( 'form-field' => 'pass1' ) ); } // Checking the password has been typed twice the same. - if ( ( $update || ! empty( $pass1 ) ) && $pass1 != $pass2 ) { - $errors->add( 'pass', __( 'Error: Passwords do not match. Please enter the same password in both password fields.' ), array( 'form-field' => 'pass1' ) ); + if ( ( $update || ! empty( $pass1 ) ) && $pass1 !== $pass2 ) { + $errors->add( 'pass', __( 'Error: Passwords do not match. Please enter the same password in both password fields.' ), array( 'form-field' => 'pass1' ) ); } if ( ! empty( $pass1 ) ) { @@ -182,29 +188,29 @@ } if ( ! $update && isset( $_POST['user_login'] ) && ! validate_username( $_POST['user_login'] ) ) { - $errors->add( 'user_login', __( 'Error: This username is invalid because it uses illegal characters. Please enter a valid username.' ) ); + $errors->add( 'user_login', __( 'Error: This username is invalid because it uses illegal characters. Please enter a valid username.' ) ); } if ( ! $update && username_exists( $user->user_login ) ) { - $errors->add( 'user_login', __( 'Error: This username is already registered. Please choose another one.' ) ); + $errors->add( 'user_login', __( 'Error: This username is already registered. Please choose another one.' ) ); } /** This filter is documented in wp-includes/user.php */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); if ( in_array( strtolower( $user->user_login ), array_map( 'strtolower', $illegal_logins ), true ) ) { - $errors->add( 'invalid_username', __( 'Error: Sorry, that username is not allowed.' ) ); + $errors->add( 'invalid_username', __( 'Error: Sorry, that username is not allowed.' ) ); } - /* checking email address */ + // Checking email address. if ( empty( $user->user_email ) ) { - $errors->add( 'empty_email', __( 'Error: Please enter an email address.' ), array( 'form-field' => 'email' ) ); + $errors->add( 'empty_email', __( 'Error: Please enter an email address.' ), array( 'form-field' => 'email' ) ); } elseif ( ! is_email( $user->user_email ) ) { - $errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ), array( 'form-field' => 'email' ) ); + $errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ), array( 'form-field' => 'email' ) ); } else { $owner_id = email_exists( $user->user_email ); - if ( $owner_id && ( ! $update || ( $owner_id != $user->ID ) ) ) { - $errors->add( 'email_exists', __( 'Error: This email is already registered. Please choose another one.' ), array( 'form-field' => 'email' ) ); + if ( $owner_id && ( ! $update || ( $owner_id !== $user->ID ) ) ) { + $errors->add( 'email_exists', __( 'Error: This email is already registered. Please choose another one.' ), array( 'form-field' => 'email' ) ); } } @@ -318,18 +324,21 @@ } /** - * Remove user and optionally reassign posts and links to another user. + * Delete user and optionally reassign posts and links to another user. + * + * Note that on a Multisite installation the user only gets removed from the site + * and does not get deleted from the database. * - * If the $reassign parameter is not assigned to a User ID, then all posts will - * be deleted of that user. The action {@see 'delete_user'} that is passed the User ID + * If the `$reassign` parameter is not assigned to a user ID, then all posts will + * be deleted of that user. The action {@see 'delete_user'} that is passed the user ID * being deleted will be run after the posts are either reassigned or deleted. - * The user meta will also be deleted that are for that User ID. + * The user meta will also be deleted that are for that user ID. * * @since 2.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * - * @param int $id User ID. + * @param int $id User ID. * @param int $reassign Optional. Reassign posts and links to new User ID. * @return bool True when finished. */ @@ -355,7 +364,10 @@ } /** - * Fires immediately before a user is deleted from the database. + * Fires immediately before a user is deleted from the site. + * + * Note that on a Multisite installation the user only gets removed from the site + * and does not get deleted from the database. * * @since 2.0.0 * @since 5.5.0 Added the `$user` parameter. @@ -434,7 +446,11 @@ clean_user_cache( $user ); /** - * Fires immediately after a user is deleted from the database. + * Fires immediately after a user is deleted from the site. + * + * Note that on a Multisite installation the user may not have been deleted from + * the database depending on whether `wp_delete_user()` or `wpmu_delete_user()` + * was called. * * @since 2.9.0 * @since 5.5.0 Added the `$user` parameter. @@ -479,7 +495,7 @@ // get_user_setting() = JS-saved UI setting. Else no-js-fallback code. if ( 'hide' === get_user_setting( 'default_password_nag' ) - || isset( $_GET['default_password_nag'] ) && '0' == $_GET['default_password_nag'] + || isset( $_GET['default_password_nag'] ) && '0' === $_GET['default_password_nag'] ) { delete_user_setting( 'default_password_nag' ); update_user_meta( $user_ID, 'default_password_nag', false ); @@ -501,7 +517,7 @@ $new_data = get_userdata( $user_ID ); // Remove the nag if the password has been changed. - if ( $new_data->user_pass != $old_data->user_pass ) { + if ( $new_data->user_pass !== $old_data->user_pass ) { delete_user_setting( 'default_password_nag' ); update_user_meta( $user_ID, 'default_password_nag', false ); } @@ -520,14 +536,29 @@ return; } - echo '
'; - echo '

'; - echo '' . __( 'Notice:' ) . ' '; - _e( 'You’re using the auto-generated password for your account. Would you like to change it?' ); - echo '

'; - printf( '' . __( 'Yes, take me to my profile page' ) . ' | ', get_edit_profile_url() . '#password' ); - printf( '' . __( 'No thanks, do not remind me again' ) . '', '?default_password_nag=0' ); - echo '

'; + $default_password_nag_message = sprintf( + '

%1$s %2$s

', + __( 'Notice:' ), + __( 'You are using the auto-generated password for your account. Would you like to change it?' ) + ); + $default_password_nag_message .= sprintf( + '

%2$s | ', + esc_url( get_edit_profile_url() . '#password' ), + __( 'Yes, take me to my profile page' ) + ); + $default_password_nag_message .= sprintf( + '%2$s

', + '?default_password_nag=0', + __( 'No thanks, do not remind me again' ) + ); + + wp_admin_notice( + $default_password_nag_message, + array( + 'additional_classes' => array( 'error', 'default-password-nag' ), + 'paragraph_wrap' => false, + ) + ); } /** @@ -606,6 +637,8 @@ * Checks if the Authorize Application Password request is valid. * * @since 5.6.0 + * @since 6.2.0 Allow insecure HTTP connections for the local environment. + * @since 6.3.2 Validates the success and reject URLs to prevent `javascript` pseudo protocol from being executed. * * @param array $request { * The array of request data. All arguments are optional and may be empty. @@ -621,24 +654,22 @@ function wp_is_authorize_application_password_request_valid( $request, $user ) { $error = new WP_Error(); - if ( ! empty( $request['success_url'] ) ) { - $scheme = wp_parse_url( $request['success_url'], PHP_URL_SCHEME ); - - if ( 'http' === $scheme ) { + if ( isset( $request['success_url'] ) ) { + $validated_success_url = wp_is_authorize_application_redirect_url_valid( $request['success_url'] ); + if ( is_wp_error( $validated_success_url ) ) { $error->add( - 'invalid_redirect_scheme', - __( 'The success URL must be served over a secure connection.' ) + $validated_success_url->get_error_code(), + $validated_success_url->get_error_message() ); } } - if ( ! empty( $request['reject_url'] ) ) { - $scheme = wp_parse_url( $request['reject_url'], PHP_URL_SCHEME ); - - if ( 'http' === $scheme ) { + if ( isset( $request['reject_url'] ) ) { + $validated_reject_url = wp_is_authorize_application_redirect_url_valid( $request['reject_url'] ); + if ( is_wp_error( $validated_reject_url ) ) { $error->add( - 'invalid_redirect_scheme', - __( 'The rejection URL must be served over a secure connection.' ) + $validated_reject_url->get_error_code(), + $validated_reject_url->get_error_message() ); } } @@ -667,3 +698,59 @@ return true; } + +/** + * Validates the redirect URL protocol scheme. The protocol can be anything except `http` and `javascript`. + * + * @since 6.3.2 + * + * @param string $url The redirect URL to be validated. + * @return true|WP_Error True if the redirect URL is valid, a WP_Error object otherwise. + */ +function wp_is_authorize_application_redirect_url_valid( $url ) { + $bad_protocols = array( 'javascript', 'data' ); + if ( empty( $url ) ) { + return true; + } + + // Based on https://www.rfc-editor.org/rfc/rfc2396#section-3.1 + $valid_scheme_regex = '/^[a-zA-Z][a-zA-Z0-9+.-]*:/'; + if ( ! preg_match( $valid_scheme_regex, $url ) ) { + return new WP_Error( + 'invalid_redirect_url_format', + __( 'Invalid URL format.' ) + ); + } + + /** + * Filters the list of invalid protocols used in applications redirect URLs. + * + * @since 6.3.2 + * + * @param string[] $bad_protocols Array of invalid protocols. + * @param string $url The redirect URL to be validated. + */ + $invalid_protocols = apply_filters( 'wp_authorize_application_redirect_url_invalid_protocols', $bad_protocols, $url ); + $invalid_protocols = array_map( 'strtolower', $invalid_protocols ); + + $scheme = wp_parse_url( $url, PHP_URL_SCHEME ); + $host = wp_parse_url( $url, PHP_URL_HOST ); + $is_local = 'local' === wp_get_environment_type(); + + // Validates if the proper URI format is applied to the URL. + if ( empty( $host ) || empty( $scheme ) || in_array( strtolower( $scheme ), $invalid_protocols, true ) ) { + return new WP_Error( + 'invalid_redirect_url_format', + __( 'Invalid URL format.' ) + ); + } + + if ( 'http' === $scheme && ! $is_local ) { + return new WP_Error( + 'invalid_redirect_scheme', + __( 'The URL must be served over a secure connection.' ) + ); + } + + return true; +}