'invalid_request' ), array( 'Allow' => 'POST', 'Cache-Control' => 'no-store' ) ); } if ( ! AgentAdapt_Signin::offers( 'device' ) ) { AgentAdapt_Http::oauth_error( 400, 'unsupported_grant_type', 'this store does not offer device sign-in' ); } $form = AgentAdapt_Http::form(); $agent = AgentAdapt_Client_Auth::authenticate( $form ); if ( is_wp_error( $agent ) ) { if ( 'rate_limited' === $agent->get_error_code() ) { AgentAdapt_Http::rate_limited( 3600 ); } AgentAdapt_Http::oauth_error( 401, 'invalid_client', $agent->get_error_message() ); } if ( ! empty( $agent['public'] ) ) { AgentAdapt_Http::oauth_error( 400, 'unauthorized_client', 'device sign-in needs private_key_jwt client authentication' ); } $scopes = AgentAdapt_Signin::parse( (string) ( $form['scope'] ?? '' ), 'device' ); if ( is_wp_error( $scopes ) ) { AgentAdapt_Http::oauth_error( 400, 'invalid_scope', $scopes->get_error_message() ); } $scopes = AgentAdapt_Signin::for_agent( $scopes, $agent['client_hash'] ); if ( ! $scopes ) { AgentAdapt_Http::oauth_error( 400, 'invalid_scope', 'none of the requested scopes is available to this agent' ); } $codes = AgentAdapt_Codes::create_device( $agent['client_hash'], implode( ' ', $scopes ) ); AgentAdapt_Http::json( 200, array( 'device_code' => $codes['device_code'], 'user_code' => $codes['user_code'], 'verification_uri' => self::verification_uri(), 'verification_uri_complete' => add_query_arg( 'user_code', rawurlencode( $codes['user_code'] ), self::verification_uri() ), 'expires_in' => AgentAdapt_Codes::DEVICE_TTL, 'interval' => AgentAdapt_Codes::DEVICE_POLL, ), array( 'Cache-Control' => 'no-store', 'Pragma' => 'no-cache' ) ); } /** * GET|POST /agent-adapt/device — the customer's side. */ public static function page(): void { $method = AgentAdapt_Http::method(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the code is only displayed back; approving needs the nonce below. $typed = isset( $_REQUEST['user_code'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['user_code'] ) ) : ''; $here = add_query_arg( 'user_code', rawurlencode( $typed ), self::verification_uri() ); if ( ! is_user_logged_in() ) { wp_safe_redirect( wp_login_url( '' !== $typed ? $here : self::verification_uri() ) ); exit; } $user_id = get_current_user_id(); if ( ! AgentAdapt_Eligibility::eligible( $user_id ) ) { AgentAdapt_Pages::send( 403, __( 'This account can\'t be connected', 'agent-adapt' ), '
' . esc_html__( 'Store staff and administrator accounts cannot be connected to a personal assistant. Sign in with your customer account instead.', 'agent-adapt' ) . '
' . AgentAdapt_Pages::account_line( $here ) ); } if ( '' === $typed ) { self::entry_form( '' ); } $row = AgentAdapt_Codes::device_by_user_code( $typed ); if ( ! $row ) { $rate = AgentAdapt_Rate_Limit::hit( 'device-wrong:' . $user_id, self::WRONG_CODES, 10 * MINUTE_IN_SECONDS ); if ( ! $rate['allowed'] ) { AgentAdapt_Pages::send( 429, __( 'Too many tries', 'agent-adapt' ), '' . esc_html__( 'Wait a few minutes, then ask your assistant for a new code.', 'agent-adapt' ) . '
' ); } self::entry_form( __( 'That code isn\'t valid or has expired. Check the code your assistant shows.', 'agent-adapt' ) ); } $agent = self::agent( (string) $row['client_hash'] ); $scope = array_values( array_filter( explode( ' ', (string) $row['scope'] ) ) ); if ( 'POST' === $method && isset( $_POST['decision'] ) ) { $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'agentadapt-device|' . $row['code_hash'] ) ) { AgentAdapt_Pages::send( 403, __( 'This page expired', 'agent-adapt' ), '' . esc_html__( 'Enter the code again.', 'agent-adapt' ) . '
' ); } $approve = 'approve' === sanitize_key( wp_unslash( $_POST['decision'] ) ); $picked = isset( $_POST['scopes'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['scopes'] ) ) : array(); $chosen = array_values( array_intersect( $scope, $picked ) ); $ok = AgentAdapt_Codes::decide_device( (string) $row['code_hash'], $approve && (bool) $chosen, $user_id, implode( ' ', $chosen ) ); $done = $ok && $approve && $chosen; AgentAdapt_Pages::send( 200, $done ? __( 'Your assistant is connected', 'agent-adapt' ) : __( 'Not connected', 'agent-adapt' ), '' . esc_html( $done ? __( 'You can go back to your assistant now.', 'agent-adapt' ) : __( 'Nothing was shared. You can close this page.', 'agent-adapt' ) ) . '
' ); } // Show, never approve: the customer still has to press Allow. $body = '' . esc_html__( 'Check this matches the code your assistant shows:', 'agent-adapt' ) . '
' . '' . esc_html( AgentAdapt_Codes::format( (string) $row['user_code'] ) ) . '
' . AgentAdapt_Pages::agent_block( $agent ) . ''; AgentAdapt_Pages::send( 200, __( 'Connect your assistant', 'agent-adapt' ), $body ); } /** * The code entry form. * * @param string $error Message, or ''. */ private static function entry_form( string $error ): void { $body = ( '' !== $error ? '' . esc_html( $error ) . '
' : '' ) . ''; AgentAdapt_Pages::send( '' !== $error ? 400 : 200, __( 'Connect your assistant', 'agent-adapt' ), $body ); } /** * Agent display data by hash. * * @param string $client_hash Agent hash. * @return array