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 / integrations / third-party / elementor.php

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

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