diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php new file mode 100644 index 0000000000..f16eee550a --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php @@ -0,0 +1,141 @@ + 'WordPress.org MCP', + 'hosts' => array(), + ); + + return $apps; + } + + /** + * Renders the MCP client configuration after application password creation. + * + * @param string $new_password The newly created application password. + * @param array $request The request data. + * @param WP_User $user The user who authorized the application. + */ + public static function render_config( string $new_password, array $request, WP_User $user ): void { + if ( ( $request['app_id'] ?? '' ) !== self::APP_ID ) { + return; + } + + $mcp_config = array( + 'mcpServers' => array( + 'wporg-mcp-server' => array( + 'command' => 'npx', + 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ), + 'env' => array( + 'WP_API_URL' => rest_url( 'mcp/wporg' ), + 'WP_API_USERNAME' => $user->user_login, + 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ), + ), + ), + ), + ); + + $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + $json = str_replace( ' ', ' ', $json ); + + ?> +
+

+ + + + +
+ Map of app_id UUID => app config. + */ +function wporg_get_allowed_apps(): array { + /** + * Filters the registered applications allowed to use the application password authorization flow. + * + * Each entry must be keyed by a valid UUID (the app_id) and contain an array + * with 'name' (non-empty string) and optionally 'hosts' (array of allowed + * callback domains). If 'hosts' is omitted, it defaults to an empty array, + * meaning the app does not support callback URLs. + * + * Example: + * + * add_filter( 'wporg_login_application_passwords_allowed_apps', function ( $apps ) { + * $apps['c4c73a54-96d7-47b9-9bdc-1a66b9b04505'] = array( + * 'name' => 'My Application', + * 'hosts' => array( 'example.com' ), + * ); + * return $apps; + * } ); + * + * @param array $apps Map of app_id UUID => app config. + */ + $apps = apply_filters( 'wporg_login_application_passwords_allowed_apps', array() ); + + $validated = array(); + + foreach ( $apps as $app_id => $config ) { + $config = wp_parse_args( $config, array( 'hosts' => array() ) ); + + if ( ! wp_is_uuid( $app_id ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( 'App ID must be a valid UUID. Got: %s', esc_html( $app_id ) ), + '1.0.0' + ); + continue; + } + + if ( ! is_array( $config ) || empty( $config['name'] ) || ! is_string( $config['name'] ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( 'App "%s" must have a non-empty "name" string.', esc_html( $app_id ) ), + '1.0.0' + ); + continue; + } + + if ( ! is_array( $config['hosts'] ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( 'App "%s" "hosts" must be an array.', esc_html( $app_id ) ), + '1.0.0' + ); + continue; + } + + $validated[ $app_id ] = $config; + } + + return $validated; +} + +/** + * Handles the authorize_application action on wp-login.php. + */ +function wporg_handle_authorize_application_login(): void { + if ( ! is_user_logged_in() ) { + $authorize_url = add_query_arg( + array( + 'action' => 'authorize_application', + 'app_id' => sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) ), + 'success_url' => sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ), + 'reject_url' => sanitize_url( wp_unslash( $_REQUEST['reject_url'] ?? '' ) ), + ), + site_url( 'wp-login.php', 'login' ) + ); + + wp_safe_redirect( wp_login_url( $authorize_url ) ); + exit; + } + + $error = null; + $new_password = ''; + + // Handle form submission. + if ( isset( $_SERVER['REQUEST_METHOD'] ) + && 'POST' === $_SERVER['REQUEST_METHOD'] + && isset( $_POST['wp_action'] ) && 'authorize_application_password' === $_POST['wp_action'] + ) { + check_admin_referer( 'authorize_application_password' ); + + $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ?? '' ) ); + $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ?? '' ) ); + $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ?? '' ) ); + $redirect = ''; + + // Re-validate POST values (don't trust hidden form fields). + $user = wp_get_current_user(); + $allowed_apps = wporg_get_allowed_apps(); + $app_name = $allowed_apps[ $app_id ]['name'] ?? ''; + $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); + + $is_valid = wporg_validate_authorize_app_request( $request, $user ); + if ( is_wp_error( $is_valid ) ) { + wp_die( + wp_kses_post( implode( ' ', $is_valid->get_error_messages() ) ), + esc_html__( 'Cannot Authorize Application' ) + ); + } + + if ( isset( $_POST['reject'] ) ) { + $redirect = $reject_url ?: admin_url(); // phpcs:ignore Universal.Operators.DisallowShortTernary + } elseif ( isset( $_POST['approve'] ) ) { + // Revoke any existing password for this app_id. + if ( $app_id ) { + $existing = WP_Application_Passwords::get_user_application_passwords( get_current_user_id() ); + foreach ( $existing as $item ) { + if ( isset( $item['app_id'] ) && $item['app_id'] === $app_id ) { + $deleted = WP_Application_Passwords::delete_application_password( get_current_user_id(), $item['uuid'] ); + if ( is_wp_error( $deleted ) ) { + $error = $deleted; + break; + } + } + } + } + + if ( ! is_wp_error( $error ) ) { + $created = WP_Application_Passwords::create_new_application_password( + get_current_user_id(), + array( + 'name' => $app_name, + 'app_id' => $app_id, + ) + ); + + if ( is_wp_error( $created ) ) { + $error = $created; + } else { + list( $new_password ) = $created; + + if ( $success_url ) { + $redirect = add_query_arg( + array( + 'site_url' => rawurlencode( 'https://wordpress.org' ), + 'user_login' => rawurlencode( wp_get_current_user()->user_login ), + 'password' => rawurlencode( $new_password ), + ), + $success_url + ); + } + } + } + } + + if ( $redirect ) { + $allowed_hosts = $allowed_apps[ $app_id ]['hosts'] ?? array(); + add_filter( + 'allowed_redirect_hosts', + function ( $hosts ) use ( $allowed_hosts ) { + return array_merge( $hosts, $allowed_hosts ); + } + ); + + wp_safe_redirect( $redirect ); + exit; + } + } + + // Extract and validate request parameters. + $app_id = sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) ); + $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null; // phpcs:ignore Universal.Operators.DisallowShortTernary + + if ( ! empty( $_REQUEST['reject_url'] ) ) { + $reject_url = sanitize_url( wp_unslash( $_REQUEST['reject_url'] ) ); + } elseif ( $success_url ) { + $reject_url = add_query_arg( 'success', 'false', $success_url ); + } else { + $reject_url = null; + } + + $user = wp_get_current_user(); + $allowed_apps = wporg_get_allowed_apps(); + $app_name = $allowed_apps[ $app_id ]['name'] ?? ''; + $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); + + $is_valid = wporg_validate_authorize_app_request( $request, $user ); + if ( is_wp_error( $is_valid ) ) { + wp_die( + wp_kses_post( implode( ' ', $is_valid->get_error_messages() ) ), + esc_html__( 'Cannot Authorize Application' ) + ); + } + + if ( ! wp_is_application_passwords_available_for_user( $user ) ) { + if ( wp_is_application_passwords_available() ) { + $message = __( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' ); + } else { + $message = __( 'Application passwords are not available.' ); + } + + wp_die( + esc_html( $message ), + esc_html__( 'Cannot Authorize Application' ), + array( + 'response' => 501, + 'link_text' => esc_html__( 'Go Back' ), + 'link_url' => esc_url( $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url() ), + ) + ); + } + + // Render the authorization form. + $title = __( 'Authorize Application' ); + $wp_error = new WP_Error(); + + if ( is_wp_error( $error ) ) { + $wp_error->add( 'authorize_error', $error->get_error_message() ); + } + + add_filter( 'login_site_html_link', '__return_empty_string' ); + + login_header( $title, '', $wp_error ); + + ?> +
+ + + + + + + +
+

+ +

+ +

+
+ + +

+ + +

+ ' . esc_html( $app_name ) . '' + ); + ?> +

+ +

+ + + ID, true ); + $blogs_count = count( $blogs ); + + if ( $blogs_count > 1 ) : + ?> +

+ the %2$s site in this installation that you have permissions on.', + 'This will grant access to all %2$s sites in this installation that you have permissions on.', + $blogs_count + ); + + if ( is_super_admin() ) { + /* translators: 1: URL to My Sites, 2: Number of sites. */ + $message = _n( + 'This will grant access to the %2$s site on the network as you have Super Admin rights.', + 'This will grant access to all %2$s sites on the network as you have Super Admin rights.', + $blogs_count + ); + } + + printf( wp_kses_post( $message ), esc_url( admin_url( 'my-sites.php' ) ), esc_html( number_format_i18n( $blogs_count ) ) ); + ?> +

+ + +

+ + +

+ +

+ ' . esc_html( $host ) . '' + ); + } else { + esc_html_e( 'You will be given a password to manually enter into the application in question.' ); + } + ?> +

+ +
+ ' . esc_html( $app_name ) . '' + ); + } else { + $message = sprintf( + /* translators: %s: Website name. */ + esc_html__( 'Please log in to %s to proceed with authorization.' ), + get_bloginfo( 'name', 'display' ) + ); + } + + $errors->add( 'authorize_application', $message, 'message' ); + + return $errors; +} +add_filter( 'wp_login_errors', 'wporg_authorize_application_login_message', 10, 2 ); + +/** + * Enqueues the authorize_application stylesheet on the login page. + */ +function wporg_authorize_application_login_styles(): void { + global $action; + + if ( 'authorize_application' !== $action ) { + return; + } + + wp_enqueue_style( + 'login-authorize-application', + plugins_url( 'css/authorize-application.css', __FILE__ ), + array(), + filemtime( __DIR__ . '/css/authorize-application.css' ) + ); +} +add_action( 'login_enqueue_scripts', 'wporg_authorize_application_login_styles' ); + +/** + * Validates the authorize application request. + * + * Inlined from wp_is_authorize_application_password_request_valid() in + * wp-admin/includes/user.php, which is not loaded in the login context. + * + * @param array $request The request data. + * @param WP_User $user The user authorizing the application. + * + * @return true|WP_Error True if valid, WP_Error otherwise. + */ +function wporg_validate_authorize_app_request( array $request, WP_User $user ): bool|WP_Error { + $error = new WP_Error(); + + // Validate redirect URLs. + $is_local = 'local' === wp_get_environment_type(); + + foreach ( array( 'success_url', 'reject_url' ) as $key ) { + if ( empty( $request[ $key ] ) ) { + continue; + } + + if ( ! wp_http_validate_url( $request[ $key ] ) ) { + $error->add( 'invalid_redirect_url', __( 'The provided URL is not valid.' ) ); + } elseif ( 'https' !== wp_parse_url( $request[ $key ], PHP_URL_SCHEME ) && ! $is_local ) { + $error->add( 'invalid_redirect_scheme', __( 'The URL must be served over a secure connection.' ) ); + } + } + + // Validate app_id format. + if ( empty( $request['app_id'] ) ) { + $error->add( 'missing_app_id', __( 'An application ID is required.' ) ); + } elseif ( ! wp_is_uuid( $request['app_id'] ) ) { + $error->add( 'invalid_app_id', __( 'The application ID must be a UUID.' ) ); + } + + // Validate app_id against the allowlist and check callback domains. + $allowed_apps = wporg_get_allowed_apps(); + + if ( ! empty( $request['app_id'] ) && wp_is_uuid( $request['app_id'] ) ) { + if ( ! isset( $allowed_apps[ $request['app_id'] ] ) ) { + $error->add( 'unauthorized_app', __( 'This application is not authorized.' ) ); + } else { + $allowed_hosts = $allowed_apps[ $request['app_id'] ]['hosts']; + + if ( empty( $allowed_hosts ) ) { + if ( ! empty( $request['success_url'] ) || ! empty( $request['reject_url'] ) ) { + $error->add( 'redirect_not_allowed', __( 'This application does not support callback URLs.' ) ); + } + } else { + foreach ( array( 'success_url', 'reject_url' ) as $key ) { + if ( empty( $request[ $key ] ) ) { + continue; + } + + $host = strtolower( wp_parse_url( $request[ $key ], PHP_URL_HOST ) ); + if ( ! $host || ! in_array( $host, array_map( 'strtolower', $allowed_hosts ), true ) ) { + $error->add( 'unauthorized_redirect', __( 'The callback URL does not point to an allowed domain.' ) ); + } + } + } + } + } + + /** This action is documented in wp-admin/includes/user.php */ + do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user ); + + if ( $error->has_errors() ) { + return $error; + } + + return true; +} +