| 1 |
<?php |
| 2 |
|
| 3 |
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. |
| 4 |
|
| 5 |
namespace Yoast\WP\SEO\AI\Authentication\Application; |
| 6 |
|
| 7 |
use WP_User; |
| 8 |
use Yoast\WP\SEO\AI\Authentication\Domain\Auth_Method; |
| 9 |
use Yoast\WP\SEO\Conditionals\MyYoast_Connection_Conditional; |
| 10 |
use YoastSEO_Vendor\Psr\Log\LoggerAwareInterface; |
| 11 |
use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait; |
| 12 |
use YoastSEO_Vendor\Psr\Log\NullLogger; |
| 13 |
|
| 14 |
/** |
| 15 |
* Builds an AI_Request_Sender configured with the right auth strategy (primary + optional fallback) |
| 16 |
* for each outbound AI request. |
| 17 |
* |
| 18 |
* Selection order (first match wins): wpseo_ai_auth_method filter override → MyYoast connection |
| 19 |
* feature flag. The auth model is site-wide: once any admin has completed the auth-code flow, |
| 20 |
* every WP user on the site uses the OAuth path. |
| 21 |
* |
| 22 |
* @makePublic |
| 23 |
*/ |
| 24 |
class AI_Request_Sender_Factory implements LoggerAwareInterface { |
| 25 |
|
| 26 |
use LoggerAwareTrait; |
| 27 |
|
| 28 |
/** |
| 29 |
* The MyYoast connection feature flag conditional. |
| 30 |
* |
| 31 |
* @var MyYoast_Connection_Conditional |
| 32 |
*/ |
| 33 |
private $myyoast_connection_conditional; |
| 34 |
|
| 35 |
/** |
| 36 |
* The OAuth strategy. |
| 37 |
* |
| 38 |
* @var OAuth_Auth_Strategy |
| 39 |
*/ |
| 40 |
private $oauth_strategy; |
| 41 |
|
| 42 |
/** |
| 43 |
* The Token strategy. |
| 44 |
* |
| 45 |
* @var Token_Auth_Strategy |
| 46 |
*/ |
| 47 |
private $token_strategy; |
| 48 |
|
| 49 |
/** |
| 50 |
* Constructor. |
| 51 |
* |
| 52 |
* @param MyYoast_Connection_Conditional $myyoast_connection_conditional The MyYoast connection feature flag. |
| 53 |
* @param OAuth_Auth_Strategy $oauth_strategy The OAuth strategy. |
| 54 |
* @param Token_Auth_Strategy $token_strategy The Token strategy. |
| 55 |
*/ |
| 56 |
public function __construct( |
| 57 |
MyYoast_Connection_Conditional $myyoast_connection_conditional, |
| 58 |
OAuth_Auth_Strategy $oauth_strategy, |
| 59 |
Token_Auth_Strategy $token_strategy |
| 60 |
) { |
| 61 |
$this->myyoast_connection_conditional = $myyoast_connection_conditional; |
| 62 |
$this->oauth_strategy = $oauth_strategy; |
| 63 |
$this->token_strategy = $token_strategy; |
| 64 |
$this->logger = new NullLogger(); |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* Returns the sender configured to authenticate AI requests for the given user. |
| 69 |
* |
| 70 |
* @param WP_User $user The WP user. |
| 71 |
* |
| 72 |
* @return AI_Request_Sender The configured sender. |
| 73 |
*/ |
| 74 |
public function create( WP_User $user ): AI_Request_Sender { |
| 75 |
$forced = $this->get_filter_override( $user ); |
| 76 |
switch ( $forced ) { |
| 77 |
case Auth_Method::OAUTH: |
| 78 |
$this->logger->debug( 'AI auth: wpseo_ai_auth_method filter pinned oauth.' ); |
| 79 |
$sender = new AI_Request_Sender( $this->oauth_strategy ); |
| 80 |
break; |
| 81 |
case Auth_Method::TOKEN: |
| 82 |
$this->logger->debug( 'AI auth: wpseo_ai_auth_method filter pinned token.' ); |
| 83 |
$sender = new AI_Request_Sender( $this->token_strategy ); |
| 84 |
break; |
| 85 |
default: |
| 86 |
if ( $this->myyoast_connection_conditional->is_met() ) { |
| 87 |
$this->logger->debug( 'AI auth: routing to oauth strategy (feature flag on) with a fallback to legacy token auth.' ); |
| 88 |
$sender = new AI_Request_Sender( $this->oauth_strategy, $this->token_strategy ); |
| 89 |
} |
| 90 |
else { |
| 91 |
$this->logger->debug( 'AI auth: routing to token strategy (MYYOAST_CONNECTION feature flag is off).' ); |
| 92 |
$sender = new AI_Request_Sender( $this->token_strategy ); |
| 93 |
} |
| 94 |
break; |
| 95 |
} |
| 96 |
|
| 97 |
// Logger_Aware_Pass only auto-wires container-registered services; the sender is hand-built |
| 98 |
// here so its logger must be propagated explicitly, otherwise its fallback warning is dropped. |
| 99 |
$sender->setLogger( $this->logger ); |
| 100 |
|
| 101 |
return $sender; |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Returns the strategy forced by the wpseo_ai_auth_method filter, or null when not pinned. |
| 106 |
* |
| 107 |
* @param WP_User $user The WP user. |
| 108 |
* |
| 109 |
* @return string|null One of the Auth_Method constants, or null. |
| 110 |
*/ |
| 111 |
private function get_filter_override( WP_User $user ): ?string { |
| 112 |
/** |
| 113 |
* Filter: 'wpseo_ai_auth_method' - Pin a specific AI auth strategy for QA / staged rollout. |
| 114 |
* |
| 115 |
* Return 'oauth' to force MyYoast OAuth. |
| 116 |
* Return 'token' to force the legacy access_jwt flow. |
| 117 |
* Return any other value (including the default null) to let the factory's normal selection logic run: |
| 118 |
* If OAuth is available, it will be the primary strategy with a fallback to the legacy token flow. |
| 119 |
* If OAuth is unavailable, the legacy token flow will be used exclusively. |
| 120 |
* |
| 121 |
* @internal |
| 122 |
* |
| 123 |
* @param string|null $method The forced strategy, or null for default selection. |
| 124 |
* @param WP_User $user The WP user the request is on behalf of. |
| 125 |
*/ |
| 126 |
$forced = \apply_filters( 'wpseo_ai_auth_method', null, $user ); |
| 127 |
|
| 128 |
if ( $forced === Auth_Method::OAUTH || $forced === Auth_Method::TOKEN ) { |
| 129 |
return $forced; |
| 130 |
} |
| 131 |
|
| 132 |
return null; |
| 133 |
} |
| 134 |
} |
| 135 |
|