| 1 |
<?php |
| 2 |
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded |
| 3 |
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. |
| 4 |
|
| 5 |
namespace Yoast\WP\SEO\MyYoast_Client\User_Interface; |
| 6 |
|
| 7 |
use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface; |
| 8 |
use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Redirect_URI_Provider_Interface; |
| 9 |
use Yoast\WP\SEO\MyYoast_Client\Domain\Registered_Client; |
| 10 |
use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config; |
| 11 |
|
| 12 |
/** |
| 13 |
* Builds the status payload surfaced on the MyYoast connection card on the integrations page. |
| 14 |
* |
| 15 |
* Intentionally narrow: registration state, registration date, and the |
| 16 |
* stored redirect URIs with their verification state. Software statement, |
| 17 |
* IAT, RAT, client ID, scopes, and token data are never included. |
| 18 |
*/ |
| 19 |
class Status_Presenter { |
| 20 |
|
| 21 |
/** |
| 22 |
* The client registration port. |
| 23 |
* |
| 24 |
* @var Client_Registration_Interface |
| 25 |
*/ |
| 26 |
private $client_registration; |
| 27 |
|
| 28 |
/** |
| 29 |
* The issuer configuration. |
| 30 |
* |
| 31 |
* @var Issuer_Config |
| 32 |
*/ |
| 33 |
private $issuer_config; |
| 34 |
|
| 35 |
/** |
| 36 |
* The redirect URI provider. |
| 37 |
* |
| 38 |
* @var Redirect_URI_Provider_Interface |
| 39 |
*/ |
| 40 |
private $redirect_uri_provider; |
| 41 |
|
| 42 |
/** |
| 43 |
* Status_Presenter constructor. |
| 44 |
* |
| 45 |
* @param Client_Registration_Interface $client_registration The client registration port. |
| 46 |
* @param Issuer_Config $issuer_config The issuer configuration. |
| 47 |
* @param Redirect_URI_Provider_Interface $redirect_uri_provider The redirect URI provider. |
| 48 |
*/ |
| 49 |
public function __construct( |
| 50 |
Client_Registration_Interface $client_registration, |
| 51 |
Issuer_Config $issuer_config, |
| 52 |
Redirect_URI_Provider_Interface $redirect_uri_provider |
| 53 |
) { |
| 54 |
$this->client_registration = $client_registration; |
| 55 |
$this->issuer_config = $issuer_config; |
| 56 |
$this->redirect_uri_provider = $redirect_uri_provider; |
| 57 |
} |
| 58 |
|
| 59 |
/** |
| 60 |
* Returns the current status payload. |
| 61 |
* |
| 62 |
* @return array{is_provisioned: bool, is_registered: bool, registered_at: int|null, registered_at_iso: string|null, redirect_uris: array<int, array{uri: string, origin: string, is_verified: bool}>, redirect_uris_match: bool} |
| 63 |
*/ |
| 64 |
public function present(): array { |
| 65 |
$is_provisioned = ( $this->issuer_config->get_software_statement() !== '' ) |
| 66 |
&& ( $this->issuer_config->get_initial_access_token() !== '' ); |
| 67 |
|
| 68 |
$registered_client = $this->client_registration->get_registered_client(); |
| 69 |
$is_registered = ( $registered_client !== null ); |
| 70 |
|
| 71 |
$registered_at = null; |
| 72 |
$registered_at_iso = null; |
| 73 |
$redirect_uris = []; |
| 74 |
$redirect_uris_match = true; |
| 75 |
|
| 76 |
if ( $registered_client !== null ) { |
| 77 |
$registered_at = $this->extract_registered_at( $registered_client ); |
| 78 |
$registered_at_iso = ( $registered_at !== null ) ? \gmdate( 'c', $registered_at ) : null; |
| 79 |
$redirect_uris = $this->extract_redirect_uris( $registered_client ); |
| 80 |
$redirect_uris_match = $this->redirect_uris_match( $registered_client ); |
| 81 |
} |
| 82 |
|
| 83 |
return [ |
| 84 |
'is_provisioned' => $is_provisioned, |
| 85 |
'is_registered' => $is_registered, |
| 86 |
'registered_at' => $registered_at, |
| 87 |
'registered_at_iso' => $registered_at_iso, |
| 88 |
'redirect_uris' => $redirect_uris, |
| 89 |
'redirect_uris_match' => $redirect_uris_match, |
| 90 |
]; |
| 91 |
} |
| 92 |
|
| 93 |
/** |
| 94 |
* Extracts the registration timestamp (RFC 7591 `client_id_issued_at`). |
| 95 |
* |
| 96 |
* @param Registered_Client $client The registered client. |
| 97 |
* |
| 98 |
* @return int|null Unix timestamp, or null if absent or not coercible. |
| 99 |
*/ |
| 100 |
private function extract_registered_at( Registered_Client $client ): ?int { |
| 101 |
$metadata = $client->get_metadata(); |
| 102 |
if ( ! isset( $metadata['client_id_issued_at'] ) ) { |
| 103 |
return null; |
| 104 |
} |
| 105 |
|
| 106 |
$value = $metadata['client_id_issued_at']; |
| 107 |
if ( ! \is_numeric( $value ) ) { |
| 108 |
return null; |
| 109 |
} |
| 110 |
|
| 111 |
$timestamp = (int) $value; |
| 112 |
return ( $timestamp > 0 ) ? $timestamp : null; |
| 113 |
} |
| 114 |
|
| 115 |
/** |
| 116 |
* Whether the registration's redirect URIs still match what this site would register today. |
| 117 |
* |
| 118 |
* A mismatch means the site's URL has changed since it was connected, and the |
| 119 |
* registration needs re-syncing. |
| 120 |
* |
| 121 |
* @param Registered_Client $client The registered client. |
| 122 |
* |
| 123 |
* @return bool |
| 124 |
*/ |
| 125 |
private function redirect_uris_match( Registered_Client $client ): bool { |
| 126 |
return $client->has_redirect_uris( $this->redirect_uri_provider->get_redirect_uris() ); |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Returns the stored redirect URIs annotated with their origin (scheme + |
| 131 |
* host + optional port) and their verification state. |
| 132 |
* |
| 133 |
* A URI is verified once a user has completed the authorization-code flow for |
| 134 |
* it on this site; that state is tracked on the registration. |
| 135 |
* |
| 136 |
* @param Registered_Client $client The registered client. |
| 137 |
* |
| 138 |
* @return array<int, array{uri: string, origin: string, is_verified: bool}> |
| 139 |
*/ |
| 140 |
private function extract_redirect_uris( Registered_Client $client ): array { |
| 141 |
$result = []; |
| 142 |
foreach ( $client->get_redirect_uris() as $uri ) { |
| 143 |
if ( ! \is_string( $uri ) || $uri === '' ) { |
| 144 |
continue; |
| 145 |
} |
| 146 |
|
| 147 |
$origin = $this->extract_origin( $uri ); |
| 148 |
if ( $origin === null ) { |
| 149 |
continue; |
| 150 |
} |
| 151 |
|
| 152 |
$result[] = [ |
| 153 |
'uri' => $uri, |
| 154 |
'origin' => $origin, |
| 155 |
'is_verified' => $client->is_uri_validated( $uri ), |
| 156 |
]; |
| 157 |
} |
| 158 |
|
| 159 |
return $result; |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* Extracts the origin (scheme + host + optional port) from a URI. |
| 164 |
* |
| 165 |
* @param string $uri The URI to parse. |
| 166 |
* |
| 167 |
* @return string|null The origin, or null if the URI couldn't be parsed. |
| 168 |
*/ |
| 169 |
private function extract_origin( string $uri ): ?string { |
| 170 |
$parts = \wp_parse_url( $uri ); |
| 171 |
if ( ! \is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) { |
| 172 |
return null; |
| 173 |
} |
| 174 |
|
| 175 |
$origin = $parts['scheme'] . '://' . $parts['host']; |
| 176 |
if ( isset( $parts['port'] ) ) { |
| 177 |
$origin .= ':' . $parts['port']; |
| 178 |
} |
| 179 |
|
| 180 |
return $origin; |
| 181 |
} |
| 182 |
} |
| 183 |
|