PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.8
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.8
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 / third-party / elementor.php

elementor.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.8, at src/integrations/third-party/elementor.php

828 lines 25.9 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\Third_Party;
4
5 use WP_Post;
6 use WP_Screen;
7 use WPSEO_Admin_Asset_Manager;
8 use WPSEO_Admin_Recommended_Replace_Vars;
9 use WPSEO_Meta;
10 use WPSEO_Metabox_Analysis_Inclusive_Language;
11 use WPSEO_Metabox_Analysis_Readability;
12 use WPSEO_Metabox_Analysis_SEO;
13 use WPSEO_Metabox_Formatter;
14 use WPSEO_Post_Metabox_Formatter;
15 use WPSEO_Replace_Vars;
16 use WPSEO_Utils;
17 use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional;
18 use Yoast\WP\SEO\Editors\Application\Site\Website_Information_Repository;
19 use Yoast\WP\SEO\Elementor\Infrastructure\Request_Post;
20 use Yoast\WP\SEO\Helpers\Capability_Helper;
21 use Yoast\WP\SEO\Helpers\Options_Helper;
22 use Yoast\WP\SEO\Integrations\Integration_Interface;
23 use Yoast\WP\SEO\Presenters\Admin\Meta_Fields_Presenter;
24 use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
25
26 /**
27 * Integrates the Yoast SEO metabox in the Elementor editor.
28 */
29 class Elementor implements Integration_Interface {
30
31 /**
32 * Represents the post.
33 *
34 * @var WP_Post|null
35 */
36 protected $post;
37
38 /**
39 * Represents the admin asset manager.
40 *
41 * @var WPSEO_Admin_Asset_Manager
42 */
43 protected $asset_manager;
44
45 /**
46 * Represents the options helper.
47 *
48 * @var Options_Helper
49 */
50 protected $options;
51
52 /**
53 * Represents the capability helper.
54 *
55 * @var Capability_Helper
56 */
57 protected $capability;
58
59 /**
60 * Holds the Request_Post.
61 *
62 * @var Request_Post
63 */
64 private $request_post;
65
66 /**
67 * Holds whether the socials are enabled.
68 *
69 * @var bool
70 */
71 protected $social_is_enabled;
72
73 /**
74 * Holds whether the advanced settings are enabled.
75 *
76 * @var bool
77 */
78 protected $is_advanced_metadata_enabled;
79
80 /**
81 * Helper to determine whether or not the SEO analysis is enabled.
82 *
83 * @var WPSEO_Metabox_Analysis_SEO
84 */
85 protected $seo_analysis;
86
87 /**
88 * Helper to determine whether or not the readability analysis is enabled.
89 *
90 * @var WPSEO_Metabox_Analysis_Readability
91 */
92 protected $readability_analysis;
93
94 /**
95 * Helper to determine whether or not the inclusive language analysis is enabled.
96 *
97 * @var WPSEO_Metabox_Analysis_Inclusive_Language
98 */
99 protected $inclusive_language_analysis;
100
101 /**
102 * Holds the promotion manager.
103 *
104 * @var Promotion_Manager
105 */
106 protected $promotion_manager;
107
108 /**
109 * Returns the conditionals based in which this loadable should be active.
110 *
111 * @return array
112 */
113 public static function get_conditionals() {
114 return [ Elementor_Edit_Conditional::class ];
115 }
116
117 /**
118 * Constructor.
119 *
120 * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
121 * @param Options_Helper $options The options helper.
122 * @param Capability_Helper $capability The capability helper.
123 * @param Request_Post $request_post The Request_Post.
124 */
125 public function __construct(
126 WPSEO_Admin_Asset_Manager $asset_manager,
127 Options_Helper $options,
128 Capability_Helper $capability,
129 Request_Post $request_post
130 ) {
131 $this->asset_manager = $asset_manager;
132 $this->options = $options;
133 $this->capability = $capability;
134 $this->request_post = $request_post;
135
136 $this->seo_analysis = new WPSEO_Metabox_Analysis_SEO();
137 $this->readability_analysis = new WPSEO_Metabox_Analysis_Readability();
138 $this->inclusive_language_analysis = new WPSEO_Metabox_Analysis_Inclusive_Language();
139 $this->social_is_enabled = $this->options->get( 'opengraph', false ) || $this->options->get( 'twitter', false );
140 $this->is_advanced_metadata_enabled = $this->capability->current_user_can( 'wpseo_edit_advanced_metadata' ) || $this->options->get( 'disableadvanced_meta' ) === false;
141 }
142
143 /**
144 * Initializes the integration.
145 *
146 * This is the place to register hooks and filters.
147 *
148 * @return void
149 */
150 public function register_hooks() {
151 \add_action( 'wp_ajax_wpseo_elementor_save', [ $this, 'save_postdata' ] );
152
153 // We need to delay the post type lookup to give other plugins a chance to register custom post types.
154 \add_action( 'init', [ $this, 'register_elementor_hooks' ], \PHP_INT_MAX );
155 }
156
157 /**
158 * Registers our Elementor hooks.
159 * This is done for pages with metabox on page load and not on ajax request.
160 *
161 * @return void
162 */
163 public function register_elementor_hooks() {
164 if ( $this->get_metabox_post() === null || ! $this->display_metabox( $this->get_metabox_post()->post_type ) ) {
165 return;
166 }
167
168 \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'init' ] );
169 \add_action( 'elementor/editor/footer', [ $this, 'start_output_buffering' ], 0 );
170 \add_action( 'elementor/editor/footer', [ $this, 'inject_yoast_tab' ], 999 );
171 }
172
173 /**
174 * Initializes the integration.
175 *
176 * @return void
177 */
178 public function init() {
179 $this->asset_manager->register_assets();
180 $this->enqueue();
181 $this->render_hidden_fields();
182 }
183
184 /**
185 * Start capturing buffer.
186 *
187 * @return void
188 */
189 public function start_output_buffering() {
190 \ob_start();
191 }
192
193 /**
194 * Injects the Yoast SEO tab into the Elements panel of the Elementor editor.
195 *
196 * @return void
197 */
198 public function inject_yoast_tab() {
199 $output = \ob_get_clean();
200
201 // If the buffer is empty or the call failed, bail out.
202 if ( empty( $output ) ) {
203 return;
204 }
205
206 $search = '/(<(div|button) class="elementor-component-tab elementor-panel-navigation-tab" data-tab="global">.*<\/(div|button)>)/m';
207 $replace = '${1}<${2} class="elementor-component-tab elementor-panel-navigation-tab" data-tab="yoast-seo-tab">Yoast SEO</${2}>';
208
209 $modified_output = \preg_replace( $search, $replace, $output );
210
211 // Check if preg_replace failed. If so, fallback to original output.
212 if ( $modified_output === null ) {
213 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Already escaped output.
214 echo $output;
215 return;
216 }
217
218 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Already escaped output.
219 echo $modified_output;
220 }
221
222 // Below is mostly copied from `class-metabox.php`. That constructor has side-effects we do not need.
223
224 /**
225 * Determines whether the metabox should be shown for the passed identifier.
226 *
227 * By default, the check is done for post types, but can also be used for taxonomies.
228 *
229 * @param string|null $identifier The identifier to check.
230 * @param string $type The type of object to check. Defaults to post_type.
231 *
232 * @return bool Whether the metabox should be displayed.
233 */
234 public function display_metabox( $identifier = null, $type = 'post_type' ) {
235 return WPSEO_Utils::is_metabox_active( $identifier, $type );
236 }
237
238 /**
239 * Saves the WP SEO metadata for posts.
240 *
241 * Outputs JSON via wp_send_json then stops code execution.
242 *
243 * {@internal $_POST parameters are validated via sanitize_post_meta().}}
244 *
245 * @return void
246 */
247 public function save_postdata() {
248 global $post;
249
250 if ( ! isset( $_POST['post_id'] ) || ! \is_string( $_POST['post_id'] ) ) {
251 \wp_send_json_error( 'Bad Request', 400 );
252 }
253
254 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: No sanitization needed because we cast to an integer.
255 $post_id = (int) \wp_unslash( $_POST['post_id'] );
256
257 if ( $post_id <= 0 ) {
258 \wp_send_json_error( 'Bad Request', 400 );
259 }
260
261 if ( ! \current_user_can( 'edit_post', $post_id ) ) {
262 \wp_send_json_error( 'Forbidden', 403 );
263 }
264
265 \check_ajax_referer( 'wpseo_elementor_save', '_wpseo_elementor_nonce' );
266
267 // Bail if this is a multisite installation and the site has been switched.
268 if ( \is_multisite() && \ms_is_switched() ) {
269 \wp_send_json_error( 'Switched multisite', 409 );
270 }
271
272 \clean_post_cache( $post_id );
273 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly.
274 $post = \get_post( $post_id );
275
276 if ( ! \is_object( $post ) ) {
277 // Non-existent post.
278 \wp_send_json_error( 'Post not found', 400 );
279 }
280
281 \do_action( 'wpseo_save_compare_data', $post );
282
283 // Initialize meta, amongst other things it registers sanitization.
284 WPSEO_Meta::init();
285
286 $social_fields = [];
287 if ( $this->social_is_enabled ) {
288 $social_fields = WPSEO_Meta::get_meta_field_defs( 'social', $post->post_type );
289 }
290
291 // The below methods use the global post so make sure it is setup.
292 \setup_postdata( $post );
293 $meta_boxes = \apply_filters( 'wpseo_save_metaboxes', [] );
294 $meta_boxes = \array_merge(
295 $meta_boxes,
296 WPSEO_Meta::get_meta_field_defs( 'general', $post->post_type ),
297 WPSEO_Meta::get_meta_field_defs( 'advanced', $post->post_type ),
298 $social_fields,
299 WPSEO_Meta::get_meta_field_defs( 'schema', $post->post_type ),
300 );
301
302 foreach ( $meta_boxes as $key => $meta_box ) {
303 // If analysis is disabled remove that analysis score value from the DB.
304 if ( $this->is_meta_value_disabled( $key ) ) {
305 WPSEO_Meta::delete( $key, $post_id );
306 continue;
307 }
308
309 $data = null;
310 $field_name = WPSEO_Meta::$form_prefix . $key;
311
312 if ( $meta_box['type'] === 'checkbox' ) {
313 $data = isset( $_POST[ $field_name ] ) ? 'on' : 'off';
314 }
315 else {
316 if ( isset( $_POST[ $field_name ] ) ) {
317 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Sanitized through sanitize_post_meta.
318 $data = \wp_unslash( $_POST[ $field_name ] );
319
320 // For multi-select.
321 if ( \is_array( $data ) ) {
322 $data = \array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $data );
323 }
324
325 if ( \is_string( $data ) ) {
326 $data = ( $key !== 'canonical' ) ? WPSEO_Utils::sanitize_text_field( $data ) : WPSEO_Utils::sanitize_url( $data );
327 }
328 }
329
330 // Reset options when no entry is present with multiselect - only applies to `meta-robots-adv` currently.
331 if ( ! isset( $_POST[ $field_name ] ) && ( $meta_box['type'] === 'multiselect' ) ) {
332 $data = [];
333 }
334 }
335
336 if ( $data !== null ) {
337 WPSEO_Meta::set_value( $key, $data, $post_id );
338 }
339 }
340
341 if ( isset( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) && \is_string( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) ) {
342 $slug = \sanitize_title( \wp_unslash( $_POST[ WPSEO_Meta::$form_prefix . 'slug' ] ) );
343 if ( $post->post_name !== $slug ) {
344 $post_array = $post->to_array();
345 $post_array['post_name'] = $slug;
346
347 $save_successful = \wp_insert_post( $post_array );
348 if ( \is_wp_error( $save_successful ) ) {
349 \wp_send_json_error( 'Slug not saved', 400 );
350 }
351
352 // Update the post object to ensure we have the actual slug.
353 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Updating the post is needed to get the current slug.
354 $post = \get_post( $post_id );
355 if ( ! \is_object( $post ) ) {
356 \wp_send_json_error( 'Updated slug not found', 400 );
357 }
358 }
359 }
360
361 \do_action( 'wpseo_saved_postdata' );
362
363 // Output the slug, because it is processed by WP and we need the actual slug again.
364 \wp_send_json_success( [ 'slug' => $post->post_name ] );
365 }
366
367 /**
368 * Determines if the given meta value key is disabled.
369 *
370 * @param string $key The key of the meta value.
371 *
372 * @return bool Whether the given meta value key is disabled.
373 */
374 public function is_meta_value_disabled( $key ) {
375 if ( $key === 'linkdex' && ! $this->seo_analysis->is_enabled() ) {
376 return true;
377 }
378
379 if ( $key === 'content_score' && ! $this->readability_analysis->is_enabled() ) {
380 return true;
381 }
382
383 if ( $key === 'inclusive_language_score' && ! $this->inclusive_language_analysis->is_enabled() ) {
384 return true;
385 }
386
387 return false;
388 }
389
390 /**
391 * Enqueues all the needed JS and CSS.
392 *
393 * @return void
394 */
395 public function enqueue() {
396 $post_id = \get_queried_object_id();
397 if ( empty( $post_id ) ) {
398 $post_id = 0;
399 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
400 if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) ) {
401 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Reason: No sanitization needed because we cast to an integer,We are not processing form information.
402 $post_id = (int) \wp_unslash( $_GET['post'] );
403 }
404 }
405
406 if ( $post_id !== 0 ) {
407 // Enqueue files needed for upload functionality.
408 \wp_enqueue_media( [ 'post' => $post_id ] );
409 }
410
411 $this->asset_manager->enqueue_style( 'admin-global' );
412 $this->asset_manager->enqueue_style( 'metabox-css' );
413 if ( $this->readability_analysis->is_enabled() ) {
414 $this->asset_manager->enqueue_style( 'scoring' );
415 }
416 $this->asset_manager->enqueue_style( 'monorepo' );
417 $this->asset_manager->enqueue_style( 'admin-css' );
418 $this->asset_manager->enqueue_style( 'ai-generator' );
419 $this->asset_manager->enqueue_style( 'elementor' );
420
421 $this->asset_manager->enqueue_script( 'admin-global' );
422 $this->asset_manager->enqueue_script( 'elementor' );
423
424 $this->asset_manager->localize_script( 'elementor', 'wpseoAdminGlobalL10n', \YoastSEO()->helpers->wincher->get_admin_global_links() );
425 $this->asset_manager->localize_script( 'elementor', 'wpseoAdminL10n', WPSEO_Utils::get_admin_l10n() );
426 $this->asset_manager->localize_script( 'elementor', 'wpseoFeaturesL10n', WPSEO_Utils::retrieve_enabled_features() );
427
428 $plugins_script_data = [
429 'replaceVars' => [
430 'replace_vars' => $this->get_replace_vars(),
431 'recommended_replace_vars' => $this->get_recommended_replace_vars(),
432 'hidden_replace_vars' => $this->get_hidden_replace_vars(),
433 'scope' => $this->determine_scope(),
434 'has_taxonomies' => $this->current_post_type_has_taxonomies(),
435 ],
436 'shortcodes' => [
437 'wpseo_shortcode_tags' => $this->get_valid_shortcode_tags(),
438 'wpseo_filter_shortcodes_nonce' => \wp_create_nonce( 'wpseo-filter-shortcodes' ),
439 ],
440 ];
441
442 $worker_script_data = [
443 'url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-analysis-worker' ),
444 'dependencies' => \YoastSEO()->helpers->asset->get_dependency_urls_by_handle( 'yoast-seo-analysis-worker' ),
445 'keywords_assessment_url' => \YoastSEO()->helpers->asset->get_asset_url( 'yoast-seo-used-keywords-assessment' ),
446 'log_level' => WPSEO_Utils::get_analysis_worker_log_level(),
447 // We need to make the feature flags separately available inside of the analysis web worker.
448 'enabled_features' => WPSEO_Utils::retrieve_enabled_features(),
449 ];
450
451 $permalink = $this->get_permalink();
452 $page_on_front = (int) \get_option( 'page_on_front' );
453 $homepage_is_page = \get_option( 'show_on_front' ) === 'page';
454 $is_front_page = $homepage_is_page && $page_on_front === $post_id;
455
456 $script_data = [
457 'metabox' => $this->get_metabox_script_data( $permalink ),
458 'isPost' => true,
459 'isBlockEditor' => WP_Screen::get()->is_block_editor(),
460 'isElementorEditor' => true,
461 'isAlwaysIntroductionV2' => $this->is_elementor_version_compatible_with_introduction_v2(),
462 'postStatus' => \get_post_status( $post_id ),
463 'postType' => \get_post_type( $post_id ),
464 'analysis' => [
465 'plugins' => $plugins_script_data,
466 'worker' => $worker_script_data,
467 ],
468 'usedKeywordsNonce' => \wp_create_nonce( 'wpseo-keyword-usage-and-post-types' ),
469 'isFrontPage' => $is_front_page,
470 ];
471
472 /**
473 * The website information repository.
474 *
475 * @var Website_Information_Repository $repo
476 */
477 $repo = \YoastSEO()->classes->get( Website_Information_Repository::class );
478 $site_information = $repo->get_post_site_information();
479 $site_information->set_permalink( $permalink );
480 $script_data = \array_merge_recursive( $site_information->get_legacy_site_information(), $script_data );
481
482 $this->asset_manager->localize_script( 'elementor', 'wpseoScriptData', $script_data );
483 }
484
485 /**
486 * Checks whether the current Elementor version is compatible with our introduction v2.
487 *
488 * In version 3.30.0, Elementor removed the experimental flag for the editor v2.
489 * Resulting in the editor v2 being the default.
490 *
491 * @return bool Whether the Elementor version is compatible with introduction v2.
492 */
493 private function is_elementor_version_compatible_with_introduction_v2(): bool {
494 if ( ! \defined( 'ELEMENTOR_VERSION' ) ) {
495 return false;
496 }
497
498 // Take the semver version from their version string.
499 $matches = [];
500 $version = ( \preg_match( '/^([0-9]+.[0-9]+.[0-9]+)/', \ELEMENTOR_VERSION, $matches ) > 0 ) ? $matches[1] : \ELEMENTOR_VERSION;
501
502 // Check if the version is 3.30.0 or higher. This is where the editor v2 was taken out of the experimental into the default state.
503 return \version_compare( $version, '3.30.0', '>=' );
504 }
505
506 /**
507 * Renders the metabox hidden fields.
508 *
509 * @return void
510 */
511 protected function render_hidden_fields() {
512 // Wrap in a form with an action and post_id for the submit.
513 \printf(
514 '<form id="yoast-form" method="post" action="%1$s"><input type="hidden" name="action" value="wpseo_elementor_save" /><input type="hidden" id="post_ID" name="post_id" value="%2$s" />',
515 \esc_url( \admin_url( 'admin-ajax.php' ) ),
516 \esc_attr( $this->get_metabox_post()->ID ),
517 );
518
519 \wp_nonce_field( 'wpseo_elementor_save', '_wpseo_elementor_nonce' );
520 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
521 echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'general' );
522
523 if ( $this->is_advanced_metadata_enabled ) {
524 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
525 echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'advanced' );
526 }
527
528 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
529 echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'schema', $this->get_metabox_post()->post_type );
530
531 if ( $this->social_is_enabled ) {
532 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Meta_Fields_Presenter->present is considered safe.
533 echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'social' );
534 }
535
536 \printf(
537 '<input type="hidden" id="%1$s" name="%1$s" value="%2$s" />',
538 \esc_attr( WPSEO_Meta::$form_prefix . 'slug' ),
539 /**
540 * It is important that this slug value is the same as in the database.
541 * If the DB value is empty we can auto-generate a slug.
542 * But if not empty, we should not touch it anymore.
543 */
544 \esc_attr( $this->get_metabox_post()->post_name ),
545 );
546
547 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output should be escaped in the filter.
548 echo \apply_filters( 'wpseo_elementor_hidden_fields', '' );
549
550 echo '</form>';
551 }
552
553 /**
554 * Returns post in metabox context.
555 *
556 * @return WP_Post|null
557 */
558 protected function get_metabox_post() {
559 if ( $this->post !== null ) {
560 return $this->post;
561 }
562
563 $this->post = $this->request_post->get_post();
564
565 return $this->post;
566 }
567
568 /**
569 * Passes variables to js for use with the post-scraper.
570 *
571 * @param string $permalink The permalink.
572 *
573 * @return array
574 */
575 protected function get_metabox_script_data( $permalink ) {
576 $post_formatter = new WPSEO_Metabox_Formatter(
577 new WPSEO_Post_Metabox_Formatter( $this->get_metabox_post(), [], $permalink ),
578 );
579
580 $values = $post_formatter->get_values();
581
582 /** This filter is documented in admin/filters/class-cornerstone-filter.php. */
583 $post_types = \apply_filters( 'wpseo_cornerstone_post_types', \YoastSEO()->helpers->post_type->get_accessible_post_types() );
584 if ( $values['cornerstoneActive'] && ! \in_array( $this->get_metabox_post()->post_type, $post_types, true ) ) {
585 $values['cornerstoneActive'] = false;
586 }
587
588 $values['elementorMarkerStatus'] = $this->is_highlighting_available() ? 'enabled' : 'hidden';
589
590 return $values;
591 }
592
593 /**
594 * Gets the permalink.
595 *
596 * @return string
597 */
598 protected function get_permalink(): string {
599 $permalink = '';
600
601 if ( \is_object( $this->get_metabox_post() ) ) {
602 $permalink = \get_sample_permalink( $this->get_metabox_post()->ID );
603 $permalink = $permalink[0];
604 }
605
606 return $permalink;
607 }
608
609 /**
610 * Checks whether the highlighting functionality is available for Elementor:
611 * - in Free it's always available (as an upsell).
612 * - in Premium it's available as long as the version is 21.8-RC0 or above.
613 *
614 * @return bool Whether the highlighting functionality is available.
615 */
616 private function is_highlighting_available() {
617 $is_premium = \YoastSEO()->helpers->product->is_premium();
618 $premium_version = \YoastSEO()->helpers->product->get_premium_version();
619
620 return ! $is_premium || \version_compare( $premium_version, '21.8-RC0', '>=' );
621 }
622
623 /**
624 * Prepares the replace vars for localization.
625 *
626 * @return array Replace vars.
627 */
628 protected function get_replace_vars() {
629 $cached_replacement_vars = [];
630
631 $vars_to_cache = [
632 'date',
633 'id',
634 'sitename',
635 'sitedesc',
636 'sep',
637 'page',
638 'currentyear',
639 'currentdate',
640 'currentmonth',
641 'currentday',
642 'tag',
643 'category',
644 'category_title',
645 'primary_category',
646 'pt_single',
647 'pt_plural',
648 'modified',
649 'name',
650 'user_description',
651 'pagetotal',
652 'pagenumber',
653 'post_year',
654 'post_month',
655 'post_day',
656 'author_first_name',
657 'author_last_name',
658 'permalink',
659 'post_content',
660 ];
661
662 foreach ( $vars_to_cache as $var ) {
663 $cached_replacement_vars[ $var ] = \wpseo_replace_vars( '%%' . $var . '%%', $this->get_metabox_post() );
664 }
665
666 // Merge custom replace variables with the WordPress ones.
667 return \array_merge( $cached_replacement_vars, $this->get_custom_replace_vars( $this->get_metabox_post() ) );
668 }
669
670 /**
671 * Prepares the recommended replace vars for localization.
672 *
673 * @return array Recommended replacement variables.
674 */
675 protected function get_recommended_replace_vars() {
676 $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
677
678 // What is recommended depends on the current context.
679 $post_type = $recommended_replace_vars->determine_for_post( $this->get_metabox_post() );
680
681 return $recommended_replace_vars->get_recommended_replacevars_for( $post_type );
682 }
683
684 /**
685 * Returns the list of replace vars that should be hidden inside the editor.
686 *
687 * @return string[] The hidden replace vars.
688 */
689 protected function get_hidden_replace_vars() {
690 return ( new WPSEO_Replace_Vars() )->get_hidden_replace_vars();
691 }
692
693 /**
694 * Gets the custom replace variables for custom taxonomies and fields.
695 *
696 * @param WP_Post $post The post to check for custom taxonomies and fields.
697 *
698 * @return array Array containing all the replacement variables.
699 */
700 protected function get_custom_replace_vars( $post ) {
701 return [
702 'custom_fields' => $this->get_custom_fields_replace_vars( $post ),
703 'custom_taxonomies' => $this->get_custom_taxonomies_replace_vars( $post ),
704 ];
705 }
706
707 /**
708 * Gets the custom replace variables for custom taxonomies.
709 *
710 * @param WP_Post $post The post to check for custom taxonomies.
711 *
712 * @return array Array containing all the replacement variables.
713 */
714 protected function get_custom_taxonomies_replace_vars( $post ) {
715 $taxonomies = \get_object_taxonomies( $post, 'objects' );
716 $custom_replace_vars = [];
717
718 foreach ( $taxonomies as $taxonomy_name => $taxonomy ) {
719
720 if ( \is_string( $taxonomy ) ) { // If attachment, see https://core.trac.wordpress.org/ticket/37368 .
721 $taxonomy_name = $taxonomy;
722 $taxonomy = \get_taxonomy( $taxonomy_name );
723 }
724
725 if ( $taxonomy->_builtin && $taxonomy->public ) {
726 continue;
727 }
728
729 $custom_replace_vars[ $taxonomy_name ] = [
730 'name' => $taxonomy->name,
731 'description' => $taxonomy->description,
732 ];
733 }
734
735 return $custom_replace_vars;
736 }
737
738 /**
739 * Gets the custom replace variables for custom fields.
740 *
741 * @param WP_Post $post The post to check for custom fields.
742 *
743 * @return array Array containing all the replacement variables.
744 */
745 protected function get_custom_fields_replace_vars( $post ) {
746 $custom_replace_vars = [];
747
748 // If no post object is passed, return the empty custom_replace_vars array.
749 if ( ! \is_object( $post ) ) {
750 return $custom_replace_vars;
751 }
752
753 $custom_fields = \get_post_custom( $post->ID );
754
755 // Simply concatenate all fields containing replace vars so we can handle them all with a single regex find.
756 $replace_vars_fields = \implode(
757 ' ',
758 [
759 \YoastSEO()->meta->for_post( $post->ID )->presentation->title,
760 \YoastSEO()->meta->for_post( $post->ID )->presentation->meta_description,
761 ],
762 );
763
764 \preg_match_all( '/%%cf_([A-Za-z0-9_]+)%%/', $replace_vars_fields, $matches );
765 $fields_to_include = $matches[1];
766 foreach ( $custom_fields as $custom_field_name => $custom_field ) {
767 // Skip private custom fields.
768 if ( \substr( $custom_field_name, 0, 1 ) === '_' ) {
769 continue;
770 }
771
772 // Skip custom fields that are not used, new ones will be fetched dynamically.
773 if ( ! \in_array( $custom_field_name, $fields_to_include, true ) ) {
774 continue;
775 }
776
777 // Skip custom field values that are serialized.
778 if ( \is_serialized( $custom_field[0] ) ) {
779 continue;
780 }
781
782 $custom_replace_vars[ $custom_field_name ] = $custom_field[0];
783 }
784
785 return $custom_replace_vars;
786 }
787
788 /**
789 * Determines the scope based on the post type.
790 * This can be used by the replacevar plugin to determine if a replacement needs to be executed.
791 *
792 * @return string String describing the current scope.
793 */
794 protected function determine_scope() {
795 if ( $this->get_metabox_post()->post_type === 'page' ) {
796 return 'page';
797 }
798
799 return 'post';
800 }
801
802 /**
803 * Determines whether or not the current post type has registered taxonomies.
804 *
805 * @return bool Whether the current post type has taxonomies.
806 */
807 protected function current_post_type_has_taxonomies() {
808 $post_taxonomies = \get_object_taxonomies( $this->get_metabox_post()->post_type );
809
810 return ! empty( $post_taxonomies );
811 }
812
813 /**
814 * Returns an array with shortcode tags for all registered shortcodes.
815 *
816 * @return array
817 */
818 protected function get_valid_shortcode_tags() {
819 $shortcode_tags = [];
820
821 foreach ( $GLOBALS['shortcode_tags'] as $tag => $description ) {
822 $shortcode_tags[] = $tag;
823 }
824
825 return $shortcode_tags;
826 }
827 }
828