PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / myyoast-client / user-interface / integrations-page-script-data.php

integrations-page-script-data.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at src/myyoast-client/user-interface/integrations-page-script-data.php

191 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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\Conditionals\MyYoast_Connection_Conditional;
8 use Yoast\WP\SEO\Helpers\Short_Link_Helper;
9 use Yoast\WP\SEO\MyYoast_Client\Application\Callback_Outcome;
10 use Yoast\WP\SEO\MyYoast_Client\Application\Management_Endpoints_Repository;
11 use Yoast\WP\SEO\MyYoast_Client\Application\OAuth_Callback_Handler;
12
13 /**
14 * Builds the MyYoast connection payload exposed to the Integrations page's
15 * `wpseoIntegrationsData` global so the React app has the initial status and
16 * the user-profile pointer without an extra fetch.
17 *
18 * Consumed by `Integrations_Page` through constructor injection.
19 */
20 class Integrations_Page_Script_Data {
21
22 /**
23 * The status presenter.
24 *
25 * @var Status_Presenter
26 */
27 private $status_presenter;
28
29 /**
30 * The MyYoast connection feature-flag conditional.
31 *
32 * @var MyYoast_Connection_Conditional
33 */
34 private $myyoast_connection_conditional;
35
36 /**
37 * The callback handler — reads the pending OAuth callback outcome.
38 *
39 * @var OAuth_Callback_Handler
40 */
41 private $callback_handler;
42
43 /**
44 * The short-link helper — builds the UTM/tracking query params for outbound links.
45 *
46 * @var Short_Link_Helper
47 */
48 private $short_link_helper;
49
50 /**
51 * The management endpoints repository — exposes the REST endpoint paths so the
52 * React app consumes them from one PHP-defined source instead of hardcoding them.
53 *
54 * @var Management_Endpoints_Repository
55 */
56 private $endpoints_repository;
57
58 /**
59 * The MyYoast connection-management permission check.
60 *
61 * @var Connection_Permission
62 */
63 private $connection_permission;
64
65 /**
66 * Integrations_Page_Script_Data constructor.
67 *
68 * @param Status_Presenter $status_presenter The status presenter.
69 * @param MyYoast_Connection_Conditional $myyoast_connection_conditional The MyYoast connection feature-flag conditional.
70 * @param OAuth_Callback_Handler $callback_handler The callback handler.
71 * @param Short_Link_Helper $short_link_helper The short-link helper.
72 * @param Management_Endpoints_Repository $endpoints_repository The management endpoints repository.
73 * @param Connection_Permission $connection_permission The MyYoast connection-management permission check.
74 */
75 public function __construct(
76 Status_Presenter $status_presenter,
77 MyYoast_Connection_Conditional $myyoast_connection_conditional,
78 OAuth_Callback_Handler $callback_handler,
79 Short_Link_Helper $short_link_helper,
80 Management_Endpoints_Repository $endpoints_repository,
81 Connection_Permission $connection_permission
82 ) {
83 $this->status_presenter = $status_presenter;
84 $this->myyoast_connection_conditional = $myyoast_connection_conditional;
85 $this->callback_handler = $callback_handler;
86 $this->short_link_helper = $short_link_helper;
87 $this->endpoints_repository = $endpoints_repository;
88 $this->connection_permission = $connection_permission;
89 }
90
91 /**
92 * Returns the MyYoast connection payload, or `null` when the feature flag
93 * is disabled so the Integrations page can omit the key entirely.
94 *
95 * The `callbackOutcome` slot is populated (and consumed) when an OAuth
96 * callback finished for this user since the last time the Integrations page
97 * was rendered, so the React app can surface a one-shot notification.
98 *
99 * @return array{initialStatus: 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}, callbackOutcome: array{kind: string, key: string}|null, linkParams: array<string, string>, startConnection: bool, endpoints: array<string, string>}|null
100 */
101 public function present(): ?array {
102 if ( ! $this->myyoast_connection_conditional->is_met() ) {
103 return null;
104 }
105
106 return [
107 'initialStatus' => $this->status_presenter->present(),
108 'callbackOutcome' => $this->consume_callback_outcome(),
109 'linkParams' => $this->short_link_helper->get_query_params(),
110 'startConnection' => $this->should_auto_start_connection(),
111 'endpoints' => $this->endpoints_repository->get_all_endpoints()->to_paths_array(),
112 ];
113 }
114
115 /**
116 * Whether the page was opened by the editor's "Connect to MyYoast" link and
117 * should auto-start the connection flow.
118 *
119 * Verifies the one-time nonce so the auto-start trigger can't be forged from
120 * another site, and re-checks the connect capability so only users who may
121 * register a client trigger it. The actual register/authorize REST calls are
122 * independently nonce-protected; this gate is defense-in-depth on the trigger.
123 *
124 * @return bool Whether to auto-start the connection flow.
125 */
126 private function should_auto_start_connection(): bool {
127 if ( ! isset( $_GET['start-myyoast-connection'] ) || ! $this->connection_permission->can_manage() ) {
128 return false;
129 }
130
131 $nonce = isset( $_GET['_wpnonce'] ) ? \sanitize_text_field( \wp_unslash( $_GET['_wpnonce'] ) ) : '';
132
133 return \wp_verify_nonce( $nonce, 'wpseo-start-myyoast-connection' ) !== false;
134 }
135
136 /**
137 * Reads and consumes the pending OAuth callback outcome for the current user
138 * and shapes it for the React app.
139 *
140 * @return array{kind: string, key: string}|null The outcome, or null when none is pending.
141 */
142 private function consume_callback_outcome(): ?array {
143 $outcome = $this->callback_handler->consume_outcome( \get_current_user_id() );
144 if ( $outcome === null ) {
145 return null;
146 }
147
148 if ( $outcome->is_success() ) {
149 return [
150 'kind' => 'success',
151 'key' => 'verify_success',
152 ];
153 }
154
155 return [
156 'kind' => 'error',
157 'key' => $this->error_message_key( $outcome ),
158 ];
159 }
160
161 /**
162 * Maps a failed callback outcome to the front-end message key.
163 *
164 * Translates the neutral, native-OAuth outcome into the message keys the
165 * integrations-page JS understands (see `messageFor()` in
166 * `myyoast-integration.js`). The same missing code means different things per
167 * OAuth phase: a provider error other than `access_denied` is unexpected,
168 * while a token-endpoint error other than `invalid_grant` is a generic token
169 * failure.
170 *
171 * @param Callback_Outcome $outcome The failed callback outcome.
172 *
173 * @return string The message key the front-end maps to copy.
174 */
175 private function error_message_key( Callback_Outcome $outcome ): string {
176 if ( $outcome->get_error_phase() === Callback_Outcome::PHASE_PROVIDER ) {
177 return ( $outcome->get_error_code() === 'access_denied' ) ? 'connection_cancelled' : 'unexpected_error';
178 }
179
180 if ( $outcome->get_error_code() === 'invalid_grant' ) {
181 return 'token_request_failed_invalid_grant';
182 }
183
184 if ( $outcome->get_error_code() === null ) {
185 return 'unexpected_error';
186 }
187
188 return 'token_request_failed';
189 }
190 }
191