callback_handler = $callback_handler; $this->auth_code_handler = $auth_code_handler; $this->redirect_helper = $redirect_helper; } /** * Returns the conditionals on which this integration should be loaded. * * @return array */ public static function get_conditionals() { return [ MyYoast_Connection_Conditional::class ]; } /** * Registers the callback endpoint and points the site's OAuth redirect URI at it. * * @return void */ public function register_hooks() { \add_action( 'admin_post_' . self::CALLBACK_ACTION, [ $this, 'handle' ] ); } /** * Returns this site's dedicated OAuth callback endpoint URL. * * @return string The callback URL. */ public static function get_callback_url(): string { return \get_admin_url( null, 'admin-post.php?action=' . self::CALLBACK_ACTION ); } /** * Handles the OAuth callback request. * * @return void */ public function handle(): void { $user_id = \get_current_user_id(); $return_url = $this->resolve_return_url( $user_id ); // admin_post_* (no _nopriv variant) only fires for logged-in users, so $user_id should // always be > 0 here. Defensive check in case the hook is dispatched manually. if ( $user_id <= 0 ) { $this->redirect_helper->do_safe_redirect( $return_url ); return; } // The handler records the outcome for the next page load to surface; this // endpoint only needs to send the browser back where the flow started. $this->callback_handler->handle( $user_id, $this->read_query_arg( 'code' ), $this->read_query_arg( 'state' ), $this->read_query_arg( 'error' ), ); $this->redirect_helper->do_safe_redirect( $return_url ); } /** * Resolves the URL to send the browser back to after the callback runs. * * Falls back to the integrations page when no return URL is stored * (stale bookmark, no pending flow). * * @param int $user_id The WordPress user ID. * * @return string The return URL. */ private function resolve_return_url( int $user_id ): string { $fallback = \admin_url( 'admin.php?page=' . General_Page_Integration::PAGE ); if ( $user_id > 0 ) { $stored = $this->auth_code_handler->get_return_url( $user_id ); if ( \is_string( $stored ) && $stored !== '' ) { // Defense in depth: the stored URL is only ever written as an // admin_url by the management route, but validate it against the // site's own host before redirecting so a tampered store entry // can't become an open redirect. return \wp_validate_redirect( $stored, $fallback ); } } return $fallback; } /** * Reads a query argument, returning an empty string when missing. * * @param string $name The query argument name. * * @return string The sanitized value. */ private function read_query_arg( string $name ): string { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- CSRF defense is OAuth `state` validated inside exchange_code. if ( ! isset( $_GET[ $name ] ) || ! \is_string( $_GET[ $name ] ) ) { return ''; } return \sanitize_text_field( \wp_unslash( $_GET[ $name ] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } }