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

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