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 / ai / generator / user-interface / ai-generator-integration.php

ai-generator-integration.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at src/ai/generator/user-interface/ai-generator-integration.php

305 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4
5 namespace Yoast\WP\SEO\AI\Generator\User_Interface;
6
7 use WPSEO_Addon_Manager;
8 use WPSEO_Admin_Asset_Manager;
9 use Yoast\WP\SEO\AI\Consent\Application\Consent_Endpoints_Repository;
10 use Yoast\WP\SEO\AI\Free_Sparks\Application\Free_Sparks_Endpoints_Repository;
11 use Yoast\WP\SEO\AI\Generator\Application\Generator_Endpoints_Repository;
12 use Yoast\WP\SEO\AI\HTTP_Request\Infrastructure\API_Client;
13 use Yoast\WP\SEO\Conditionals\AI_Conditional;
14 use Yoast\WP\SEO\Conditionals\AI_Editor_Conditional;
15 use Yoast\WP\SEO\Conditionals\MyYoast_Connection_Conditional;
16 use Yoast\WP\SEO\Conditionals\New_Premium_Or_Free_AI_Conditional;
17 use Yoast\WP\SEO\Helpers\Current_Page_Helper;
18 use Yoast\WP\SEO\Helpers\Options_Helper;
19 use Yoast\WP\SEO\Helpers\Short_Link_Helper;
20 use Yoast\WP\SEO\Helpers\User_Helper;
21 use Yoast\WP\SEO\Integrations\Admin\Integrations_Page;
22 use Yoast\WP\SEO\Integrations\Integration_Interface;
23 use Yoast\WP\SEO\Introductions\Application\Ai_Fix_Assessments_Upsell;
24 use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
25 use Yoast\WP\SEO\MyYoast_Client\User_Interface\Connection_Permission;
26 use Yoast\WP\SEO\MyYoast_Client\User_Interface\Status_Presenter;
27
28 /**
29 * Ai_Generator_Integration class.
30 */
31 class Ai_Generator_Integration implements Integration_Interface {
32
33 /**
34 * Represents the admin asset manager.
35 *
36 * @var WPSEO_Admin_Asset_Manager
37 */
38 private $asset_manager;
39
40 /**
41 * Represents the add-on manager.
42 *
43 * @var WPSEO_Addon_Manager
44 */
45 private $addon_manager;
46
47 /**
48 * Holds the API client instance.
49 *
50 * @var API_Client
51 */
52 private $api_client;
53
54 /**
55 * Represents the current page helper.
56 *
57 * @var Current_Page_Helper
58 */
59 private $current_page_helper;
60
61 /**
62 * Represents the options manager.
63 *
64 * @var Options_Helper
65 */
66 private $options_helper;
67
68 /**
69 * Represents the user helper.
70 *
71 * @var User_Helper
72 */
73 private $user_helper;
74
75 /**
76 * Represents the introductions seen repository.
77 *
78 * @var Introductions_Seen_Repository
79 */
80 private $introductions_seen_repository;
81
82 /**
83 * Represents the endpoints repository.
84 *
85 * @var Generator_Endpoints_Repository
86 */
87 private $generator_endpoints_repository;
88
89 /**
90 * Represents the consent endpoints repository.
91 *
92 * @var Consent_Endpoints_Repository
93 */
94 private $consent_endpoints_repository;
95
96 /**
97 * Represents the free sparks endpoints repository.
98 *
99 * @var Free_Sparks_Endpoints_Repository
100 */
101 private $free_sparks_endpoints_repository;
102
103 /**
104 * The MyYoast connection feature-flag conditional.
105 *
106 * @var MyYoast_Connection_Conditional
107 */
108 private $myyoast_connection_conditional;
109
110 /**
111 * The MyYoast connection status presenter.
112 *
113 * @var Status_Presenter
114 */
115 private $status_presenter;
116
117 /**
118 * The short-link helper.
119 *
120 * @var Short_Link_Helper
121 */
122 private $short_link_helper;
123
124 /**
125 * The MyYoast connection-management permission check.
126 *
127 * @var Connection_Permission
128 */
129 private $connection_permission;
130
131 /**
132 * Returns the conditionals based in which this loadable should be active.
133 *
134 * @return array<string>
135 */
136 public static function get_conditionals() {
137 return [ AI_Conditional::class, AI_Editor_Conditional::class, New_Premium_Or_Free_AI_Conditional::class ];
138 }
139
140 /**
141 * Constructs the class.
142 *
143 * @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
144 * @param WPSEO_Addon_Manager $addon_manager The addon manager.
145 * @param API_Client $api_client The API client.
146 * @param Current_Page_Helper $current_page_helper The current page helper.
147 * @param Options_Helper $options_helper The options helper.
148 * @param User_Helper $user_helper The user helper.
149 * @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository.
150 * @param Generator_Endpoints_Repository $generator_endpoints_repository The Generator endpoints repository.
151 * @param Consent_Endpoints_Repository $consent_endpoints_repository The Consent endpoints repository.
152 * @param Free_Sparks_Endpoints_Repository $free_sparks_endpoints_repository The Free Sparks endpoints repository.
153 * @param MyYoast_Connection_Conditional $myyoast_connection_conditional The MyYoast connection feature-flag conditional.
154 * @param Status_Presenter $status_presenter The MyYoast connection status presenter.
155 * @param Short_Link_Helper $short_link_helper The short-link helper.
156 * @param Connection_Permission $connection_permission The MyYoast connection-management permission check.
157 */
158 public function __construct(
159 WPSEO_Admin_Asset_Manager $asset_manager,
160 WPSEO_Addon_Manager $addon_manager,
161 API_Client $api_client,
162 Current_Page_Helper $current_page_helper,
163 Options_Helper $options_helper,
164 User_Helper $user_helper,
165 Introductions_Seen_Repository $introductions_seen_repository,
166 Generator_Endpoints_Repository $generator_endpoints_repository,
167 Consent_Endpoints_Repository $consent_endpoints_repository,
168 Free_Sparks_Endpoints_Repository $free_sparks_endpoints_repository,
169 MyYoast_Connection_Conditional $myyoast_connection_conditional,
170 Status_Presenter $status_presenter,
171 Short_Link_Helper $short_link_helper,
172 Connection_Permission $connection_permission
173 ) {
174 $this->asset_manager = $asset_manager;
175 $this->addon_manager = $addon_manager;
176 $this->api_client = $api_client;
177 $this->current_page_helper = $current_page_helper;
178 $this->options_helper = $options_helper;
179 $this->user_helper = $user_helper;
180 $this->introductions_seen_repository = $introductions_seen_repository;
181 $this->generator_endpoints_repository = $generator_endpoints_repository;
182 $this->consent_endpoints_repository = $consent_endpoints_repository;
183 $this->free_sparks_endpoints_repository = $free_sparks_endpoints_repository;
184 $this->myyoast_connection_conditional = $myyoast_connection_conditional;
185 $this->status_presenter = $status_presenter;
186 $this->short_link_helper = $short_link_helper;
187 $this->connection_permission = $connection_permission;
188 }
189
190 /**
191 * Initializes the integration.
192 *
193 * This is the place to register hooks and filters.
194 *
195 * @return void
196 */
197 public function register_hooks() {
198 \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
199 // Enqueue after Elementor_Premium integration, which re-registers the assets.
200 \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
201 }
202
203 /**
204 * Gets the subscription status for Yoast SEO Premium and Yoast WooCommerce SEO.
205 *
206 * @return array<string, bool>
207 */
208 public function get_product_subscriptions() {
209 return [
210 'premiumSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ),
211 'wooCommerceSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ),
212 ];
213 }
214
215 /**
216 * Returns the data that should be passed to the script.
217 *
218 * @return array<string|array<string>>
219 */
220 public function get_script_data() {
221 $user_id = $this->user_helper->get_current_user_id();
222
223 $endpoints = $this->generator_endpoints_repository->get_all_endpoints()
224 ->merge_with(
225 $this->consent_endpoints_repository->get_all_endpoints(),
226 )->merge_with(
227 $this->free_sparks_endpoints_repository->get_all_endpoints(),
228 )->to_paths_array();
229 return [
230 'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ),
231 'productSubscriptions' => $this->get_product_subscriptions(),
232 'hasSeenIntroduction' => $this->introductions_seen_repository->is_introduction_seen( $user_id, AI_Fix_Assessments_Upsell::ID ),
233 'requestTimeout' => $this->api_client->get_request_timeout(),
234 'isFreeSparks' => $this->options_helper->get( 'ai_free_sparks_started_on', null ) !== null,
235 'endpoints' => $endpoints,
236 'myyoastConnection' => $this->get_myyoast_connection_data(),
237 ];
238 }
239
240 /**
241 * Builds the read-only MyYoast connection payload used to pick the
242 * "Yoast AI cannot reach your site" notification variant in the editor.
243 *
244 * Returns `null` when the feature flag is disabled, so the editor treats the
245 * connection as unavailable and shows the informational-only variant. The
246 * payload is deliberately minimal — no store, actions, or tokens reach the
247 * editor; the connect call-to-action is just a nonce-protected link that
248 * auto-starts the flow on the Integrations page in a new tab.
249 *
250 * @return array{isProvisioned: bool, canConnect: bool, connectUrl: string|null, learnMoreUrl: string}|null
251 */
252 public function get_myyoast_connection_data() {
253 if ( ! $this->myyoast_connection_conditional->is_met() ) {
254 return null;
255 }
256
257 $status = $this->status_presenter->present();
258 $can_connect = $this->connection_permission->can_manage();
259
260 return [
261 'isProvisioned' => \is_bool( $status['is_provisioned'] ) && $status['is_provisioned'],
262 'canConnect' => $can_connect,
263 // Only users who can manage options can start the flow; for everyone
264 // else the link is omitted and the editor shows the "ask your admin" variant.
265 'connectUrl' => ( $can_connect ) ? $this->get_connect_url() : null,
266 'learnMoreUrl' => $this->short_link_helper->get( 'https://yoa.st/ai-myyoast-connection' ),
267 ];
268 }
269
270 /**
271 * Builds the nonce-protected Integrations-page URL that auto-starts the
272 * MyYoast connection flow when opened.
273 *
274 * The nonce keeps the auto-start trigger from being cross-site forgeable; the
275 * Integrations page verifies it before kicking off the flow.
276 *
277 * Built with `add_query_arg()` + `wp_create_nonce()` rather than `wp_nonce_url()`:
278 * the latter HTML-encodes the `&` separators for markup output, but this URL is
279 * localized and assigned to a React `href`, where it isn't decoded — the browser
280 * would then send `amp;start-myyoast-connection` as the query-arg name and the
281 * flow would never start. This form keeps the separators as plain `&`.
282 *
283 * @return string The connect URL.
284 */
285 private function get_connect_url() {
286 return \add_query_arg(
287 '_wpnonce',
288 \wp_create_nonce( 'wpseo-start-myyoast-connection' ),
289 \self_admin_url( 'admin.php?page=' . Integrations_Page::PAGE . '&start-myyoast-connection=1' ),
290 );
291 }
292
293 /**
294 * Enqueues the required assets.
295 *
296 * @return void
297 */
298 public function enqueue_assets() {
299
300 $this->asset_manager->enqueue_script( 'ai-generator' );
301 $this->asset_manager->localize_script( 'ai-generator', 'wpseoAiGenerator', $this->get_script_data() );
302 $this->asset_manager->enqueue_style( 'ai-generator' );
303 }
304 }
305