PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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 trunk, at src/integrations/third-party/elementor.php

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