PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.0
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.0
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 / integrations / admin / workouts-integration.php

workouts-integration.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.0, at src/integrations/admin/workouts-integration.php

510 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Integrations\Admin;
4
5 use WPSEO_Addon_Manager;
6 use WPSEO_Admin_Asset_Manager;
7 use WPSEO_Admin_Asset_Yoast_Components_L10n;
8 use Yoast\WP\SEO\Conditionals\Admin_Conditional;
9 use Yoast\WP\SEO\Helpers\Indexing_Helper;
10 use Yoast\WP\SEO\Helpers\Options_Helper;
11 use Yoast\WP\SEO\Helpers\Product_Helper;
12 use Yoast\WP\SEO\Integrations\Integration_Interface;
13 use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;
14
15 /**
16 * WorkoutsIntegration class
17 */
18 class Workouts_Integration implements Integration_Interface {
19
20 /**
21 * The admin asset manager.
22 *
23 * @var WPSEO_Admin_Asset_Manager
24 */
25 private $admin_asset_manager;
26
27 /**
28 * The addon manager.
29 *
30 * @var WPSEO_Addon_Manager
31 */
32 private $addon_manager;
33
34 /**
35 * The options helper.
36 *
37 * @var Options_Helper
38 */
39 private $options_helper;
40
41 /**
42 * The product helper.
43 *
44 * @var Product_Helper
45 */
46 private $product_helper;
47
48 /**
49 * The indexing helper.
50 *
51 * @var Indexing_Helper
52 */
53 private $indexing_helper;
54
55 /**
56 * {@inheritDoc}
57 */
58 public static function get_conditionals() {
59 return [ Admin_Conditional::class ];
60 }
61
62 /**
63 * Workouts_Integration constructor.
64 *
65 * @param WPSEO_Addon_Manager $addon_manager The addon manager.
66 * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
67 * @param Options_Helper $options_helper The options helper.
68 * @param Product_Helper $product_helper The product helper.
69 * @param Indexing_Helper $indexing_helper The indexing helper.
70 */
71 public function __construct(
72 WPSEO_Addon_Manager $addon_manager,
73 WPSEO_Admin_Asset_Manager $admin_asset_manager,
74 Options_Helper $options_helper,
75 Product_Helper $product_helper,
76 Indexing_Helper $indexing_helper
77 ) {
78 $this->addon_manager = $addon_manager;
79 $this->admin_asset_manager = $admin_asset_manager;
80 $this->options_helper = $options_helper;
81 $this->product_helper = $product_helper;
82 $this->indexing_helper = $indexing_helper;
83 }
84
85 /**
86 * {@inheritDoc}
87 */
88 public function register_hooks() {
89 \add_filter( 'wpseo_submenu_pages', [ $this, 'add_submenu_page' ], 8 );
90 \add_filter( 'wpseo_submenu_pages', [ $this, 'remove_old_submenu_page' ], 10 );
91 \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
92 \add_action( 'admin_notices', [ $this, 'configuration_workout_notice' ] );
93 }
94
95 /**
96 * Adds the workouts submenu page.
97 *
98 * @param array $submenu_pages The Yoast SEO submenu pages.
99 *
100 * @return array The filtered submenu pages.
101 */
102 public function add_submenu_page( $submenu_pages ) {
103 $submenu_pages[] = [
104 'wpseo_dashboard',
105 '',
106 \__( 'Workouts', 'wordpress-seo' ),
107 'edit_others_posts',
108 'wpseo_workouts',
109 [ $this, 'render_target' ],
110 ];
111
112 return $submenu_pages;
113 }
114
115 /**
116 * Removes the workouts submenu page from older Premium versions
117 *
118 * @param array $submenu_pages The Yoast SEO submenu pages.
119 *
120 * @return array The filtered submenu pages.
121 */
122 public function remove_old_submenu_page( $submenu_pages ) {
123 if ( ! $this->should_update_premium() ) {
124 return $submenu_pages;
125 }
126
127 // Copy only the Workouts page item that comes first in the array.
128 $result_submenu_pages = [];
129 $workouts_page_encountered = false;
130 foreach ( $submenu_pages as $item ) {
131 if ( $item[4] !== 'wpseo_workouts' || ! $workouts_page_encountered ) {
132 $result_submenu_pages[] = $item;
133 }
134 if ( $item[4] === 'wpseo_workouts' ) {
135 $workouts_page_encountered = true;
136 }
137 }
138
139 return $result_submenu_pages;
140 }
141
142 /**
143 * Enqueue the workouts app.
144 */
145 public function enqueue_assets() {
146 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
147 if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_workouts' ) {
148 return;
149 }
150
151 if ( $this->should_update_premium() ) {
152 \wp_dequeue_script( 'yoast-seo-premium-workouts' );
153 }
154
155 $this->admin_asset_manager->enqueue_style( 'workouts' );
156
157 $workouts_option = $this->get_workouts_option();
158
159 $yoast_components_l10n = new WPSEO_Admin_Asset_Yoast_Components_L10n();
160 $yoast_components_l10n->localize_script( 'workouts' );
161
162 $this->admin_asset_manager->enqueue_script( 'workouts' );
163 $this->admin_asset_manager->localize_script(
164 'workouts',
165 'wpseoWorkoutsData',
166 [
167 'workouts' => $workouts_option,
168 'homeUrl' => \home_url(),
169 'pluginUrl' => \esc_url( \plugins_url( '', WPSEO_FILE ) ),
170 'toolsPageUrl' => \esc_url( \admin_url( 'admin.php?page=wpseo_tools' ) ),
171 'usersPageUrl' => \esc_url( \admin_url( 'users.php' ) ),
172 'isPremium' => $this->product_helper->is_premium(),
173 'shouldUpdatePremium' => $this->should_update_premium(),
174 'upsellText' => $this->get_upsell_text(),
175 'upsellLink' => $this->get_upsell_link(),
176 'canDoConfigurationWorkout' => $this->user_can_do_configuration_workout(),
177 'canEditWordPressOptions' => $this->user_can_edit_wordpress_options(),
178 ]
179 );
180 }
181
182 /**
183 * Renders the target for the React to mount to.
184 */
185 public function render_target() {
186 if ( $this->should_update_premium() ) {
187 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in get_update_premium_notice.
188 echo $this->get_update_premium_notice();
189 }
190
191 echo '<div id="wpseo-workouts-container-free" class="yoast"></div>';
192 }
193
194 /**
195 * Determines whether and where the "First-time SEO Configuration workout" admin notice should be displayed.
196 *
197 * @return bool Whether the "First-time SEO Configuration workout" admin notice should be displayed.
198 */
199 public function should_display_configuration_workout_notice() {
200 if ( ! $this->options_helper->get( 'dismiss_configuration_workout_notice', false ) === false ) {
201 return false;
202 }
203
204 if ( ! $this->user_can_do_configuration_workout() ) {
205 return false;
206 }
207
208 if ( ! $this->on_wpseo_admin_page_or_dashboard() ) {
209 return false;
210 }
211
212 if ( $this->is_configuration_workout_finished() ) {
213 return false;
214 }
215
216 if ( $this->options_helper->get( 'first_time_install', false ) === false ) {
217 return false;
218 }
219
220 return ! $this->are_site_representation_name_and_logo_set() || $this->indexing_helper->get_unindexed_count() > 0;
221 }
222
223 /**
224 * Displays an admin notice when the configuration workout has not been finished yet.
225 *
226 * @return void
227 */
228 public function configuration_workout_notice() {
229 if ( ! $this->should_display_configuration_workout_notice() ) {
230 return;
231 }
232
233 $this->admin_asset_manager->enqueue_style( 'monorepo' );
234
235 $notice = new Notice_Presenter(
236 \__( 'First-time SEO configuration', 'wordpress-seo' ),
237 \sprintf(
238 /* translators: 1: Link start tag to the configuration workout, 2: Yoast SEO, 3: Link closing tag. */
239 __( 'Get started quickly with the %1$s%2$s Configuration workout%3$s and configure Yoast SEO with the optimal SEO settings for your site!', 'wordpress-seo' ),
240 '<a href="' . \esc_url( \self_admin_url( 'admin.php?page=wpseo_workouts' ) ) . '">',
241 'Yoast SEO',
242 '</a>'
243 ),
244 'mirrored_fit_bubble_woman_1_optim.svg',
245 null,
246 true,
247 'yoast-configuration-workout-notice'
248 );
249
250 //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output from present() is considered safe.
251 echo $notice->present();
252
253 // Enable permanently dismissing the notice.
254 echo "<script>
255 function dismiss_configuration_workout_notice(){
256 var data = {
257 'action': 'dismiss_configuration_workout_notice',
258 };
259
260 jQuery.post( ajaxurl, data, function( response ) {
261 jQuery( '#yoast-configuration-workout-notice' ).hide();
262 });
263 }
264
265 jQuery( document ).ready( function() {
266 jQuery( 'body' ).on( 'click', '#yoast-configuration-workout-notice .notice-dismiss', function() {
267 dismiss_configuration_workout_notice();
268 } );
269 } );
270 </script>";
271 }
272
273 /**
274 * Gets the workouts option.
275 *
276 * @return mixed|null Returns workouts option if found, null if not.
277 */
278 private function get_workouts_option() {
279 $workouts_option = $this->options_helper->get( 'workouts_data' );
280
281 // This filter is documented in src/routes/workouts-route.php.
282 return \apply_filters( 'Yoast\WP\SEO\workouts_options', $workouts_option );
283 }
284
285 /**
286 * Returns the notification to show when Premium needs to be updated.
287 *
288 * @return string The notification to update Premium.
289 */
290 private function get_update_premium_notice() {
291 $url = $this->get_upsell_link();
292
293 if ( $this->has_premium_subscription_expired() ) {
294 /* translators: %s: expands to 'Yoast SEO Premium'. */
295 $title = \sprintf( \__( 'Renew your subscription of %s', 'wordpress-seo' ), 'Yoast SEO Premium' );
296 $copy = \sprintf(
297 /* translators: %s: expands to 'Yoast SEO Premium'. */
298 \esc_html__(
299 'Accessing the latest workouts requires an updated version of %s (at least 17.7), but it looks like your subscription has expired. Please renew your subscription to update and gain access to all the latest features.',
300 'wordpress-seo'
301 ),
302 'Yoast SEO Premium'
303 );
304 $button = '<a class="yoast-button yoast-button-upsell yoast-button--small" href="' . \esc_url( $url ) . '" target="_blank">'
305 . \esc_html__( 'Renew your subscription', 'wordpress-seo' )
306 . '<span class="screen-reader-text">' . \__( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
307 . '<span aria-hidden="true" class="yoast-button-upsell__caret"></span>'
308 . '</a>';
309 }
310 elseif ( $this->has_premium_subscription_activated() ) {
311 /* translators: %s: expands to 'Yoast SEO Premium'. */
312 $title = \sprintf( \__( 'Update to the latest version of %s', 'wordpress-seo' ), 'Yoast SEO Premium' );
313 $copy = \sprintf(
314 /* translators: 1: expands to 'Yoast SEO Premium', 2: Link start tag to the page to update Premium, 3: Link closing tag. */
315 \esc_html__( 'It looks like you\'re running an outdated version of %1$s, please %2$supdate to the latest version (at least 17.7)%3$s to gain access to our updated workouts section.', 'wordpress-seo' ),
316 'Yoast SEO Premium',
317 '<a href="' . \esc_url( $url ) . '">',
318 '</a>'
319 );
320 $button = null;
321 }
322 else {
323 /* translators: %s: expands to 'Yoast SEO Premium'. */
324 $title = \sprintf( \__( 'Activate your subscription of %s', 'wordpress-seo' ), 'Yoast SEO Premium' );
325 $url_button = 'https://yoa.st/workouts-activate-notice-help';
326 $copy = \sprintf(
327 /* translators: 1: expands to 'Yoast SEO Premium', 2: Link start tag to the page to update Premium, 3: Link closing tag. */
328 \esc_html__( 'It looks like you’re running an outdated and unactivated version of %1$s, please activate your subscription in %2$sMyYoast%3$s and update to the latest version (at least 17.7) to gain access to our updated workouts section.', 'wordpress-seo' ),
329 'Yoast SEO Premium',
330 '<a href="' . \esc_url( $url ) . '">',
331 '</a>'
332 );
333 $button = '<a class="yoast-button yoast-button--primary yoast-button--small" href="' . \esc_url( $url_button ) . '" target="_blank">'
334 . esc_html__( 'Get help activating your subscription', 'wordpress-seo' )
335 . '<span class="screen-reader-text">' . __( '(Opens in a new browser tab)', 'wordpress-seo' ) . '</span>'
336 . '</a>';
337 }
338
339 $notice = new Notice_Presenter(
340 $title,
341 $copy,
342 'Assistent_Time_bubble_500x570.png',
343 $button
344 );
345
346 return $notice->present();
347 }
348
349 /**
350 * Check whether Premium should be updated.
351 *
352 * @return bool Returns true when Premium is enabled and the version is below 17.7.
353 */
354 private function should_update_premium() {
355 $premium_version = $this->product_helper->get_premium_version();
356 return $premium_version !== null && version_compare( $premium_version, '17.7-RC1', '<' );
357 }
358
359 /**
360 * Check whether the Premium subscription has expired.
361 *
362 * @return bool Returns true when Premium subscription has expired.
363 */
364 private function has_premium_subscription_expired() {
365 $subscription = $this->addon_manager->get_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG );
366
367 return ( isset( $subscription->expiry_date ) && ( strtotime( $subscription->expiry_date ) - time() ) < 0 );
368 }
369
370 /**
371 * Check whether the Premium subscription is activated.
372 *
373 * @return bool Returns true when Premium subscription is activated.
374 */
375 private function has_premium_subscription_activated() {
376 return $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG );
377 }
378
379 /**
380 * Returns the upsell/update copy to show in the card buttons.
381 *
382 * @return string Returns a string with the upsell/update copy for the card buttons.
383 */
384 private function get_upsell_text() {
385 if ( ! $this->product_helper->is_premium() || ! $this->should_update_premium() ) {
386 // Use the default defined in the component.
387 return '';
388 }
389 if ( $this->has_premium_subscription_expired() ) {
390 return sprintf(
391 /* translators: %s: expands to 'Yoast SEO Premium'. */
392 __( 'Renew %s', 'wordpress-seo' ),
393 'Yoast SEO Premium'
394 );
395 }
396 if ( $this->has_premium_subscription_activated() ) {
397 return sprintf(
398 /* translators: %s: expands to 'Yoast SEO Premium'. */
399 __( 'Update %s', 'wordpress-seo' ),
400 'Yoast SEO Premium'
401 );
402 }
403 return sprintf(
404 /* translators: %s: expands to 'Yoast SEO Premium'. */
405 __( 'Activate %s', 'wordpress-seo' ),
406 'Yoast SEO Premium'
407 );
408 }
409
410 /**
411 * Returns the upsell/update link to show in the card buttons.
412 *
413 * @return string Returns a string with the upsell/update link for the card buttons.
414 */
415 private function get_upsell_link() {
416 if ( ! $this->product_helper->is_premium() || ! $this->should_update_premium() ) {
417 // Use the default defined in the component.
418 return '';
419 }
420 if ( $this->has_premium_subscription_expired() ) {
421 return 'https://yoa.st/workout-renew-notice';
422 }
423 if ( $this->has_premium_subscription_activated() ) {
424 return \wp_nonce_url( \self_admin_url( 'update.php?action=upgrade-plugin&plugin=wordpress-seo-premium/wp-seo-premium.php' ), 'upgrade-plugin_wordpress-seo-premium/wp-seo-premium.php' );
425 }
426 return 'https://yoa.st/workouts-activate-notice-myyoast';
427 }
428
429 /**
430 * Whether the user can do the configuration workout.
431 *
432 * @return bool Whether the current user can do the configuration workout.
433 */
434 private function user_can_do_configuration_workout() {
435 return \current_user_can( 'wpseo_manage_options' );
436 }
437
438 /**
439 * Whether the user can edit WordPress options.
440 *
441 * @return bool Whether the current user can edit WordPress options.
442 */
443 private function user_can_edit_wordpress_options() {
444 return \current_user_can( 'manage_options' );
445 }
446
447 /**
448 * Whether the user is currently visiting one of our admin pages or the WordPress dashboard.
449 *
450 * @return bool Whether the current page is a Yoast SEO admin page
451 */
452 private function on_wpseo_admin_page_or_dashboard() {
453 $pagenow = $GLOBALS['pagenow'];
454
455 // Show on the WP Dashboard.
456 if ( $pagenow === 'index.php' ) {
457 return true;
458 }
459
460 $page_from_get = filter_input( INPUT_GET, 'page' );
461
462 // Show on Yoast SEO pages, with some exceptions.
463 if ( $pagenow === 'admin.php' && strpos( $page_from_get, 'wpseo' ) === 0 ) {
464 $exceptions = [
465 'wpseo_workouts',
466 'wpseo_installation_successful',
467 'wpseo_installation_successful_free',
468 ];
469
470 if ( ! \in_array( $page_from_get, $exceptions, true ) ) {
471 return true;
472 }
473 }
474
475 return false;
476 }
477
478 /**
479 * Whether all steps of the configuration workout have been finished.
480 *
481 * @return bool Whether the configuration workout has been finished.
482 */
483 private function is_configuration_workout_finished() {
484 $workouts_option = $this->options_helper->get( 'workouts_data', [] );
485
486 return count( $workouts_option['configuration']['finishedSteps'] ) === 5;
487 }
488
489 /**
490 * Whether the site representation name and logo have been set.
491 *
492 * @return bool Whether the site representation name and logo have been set.
493 */
494 private function are_site_representation_name_and_logo_set() {
495 $company_or_person = $this->options_helper->get( 'company_or_person', '' );
496
497 if ( $company_or_person === '' ) {
498 return false;
499 }
500
501 if ( $company_or_person === 'company' ) {
502 return ! empty( $this->options_helper->get( 'company_name' ) )
503 && ! empty( $this->options_helper->get( 'company_logo', '' ) );
504 }
505
506 return ! empty( $this->options_helper->get( 'company_or_person_user_id' ) )
507 && ! empty( $this->options_helper->get( 'person_logo', '' ) );
508 }
509 }
510