PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.3
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.3
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 19.1 All 128 releases
wordpress-seo / src / integrations / settings-integration.php

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

1,093 lines 36.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;
4
5 use WP_Post;
6 use WP_Post_Type;
7 use WP_Taxonomy;
8 use WP_User;
9 use WPSEO_Addon_Manager;
10 use WPSEO_Admin_Asset_Manager;
11 use WPSEO_Admin_Editor_Specific_Replace_Vars;
12 use WPSEO_Admin_Recommended_Replace_Vars;
13 use WPSEO_Option_Titles;
14 use WPSEO_Options;
15 use WPSEO_Replace_Vars;
16 use WPSEO_Shortlinker;
17 use WPSEO_Sitemaps_Router;
18 use Yoast\WP\SEO\Conditionals\Settings_Conditional;
19 use Yoast\WP\SEO\Config\Schema_Types;
20 use Yoast\WP\SEO\Content_Type_Visibility\Application\Content_Type_Visibility_Dismiss_Notifications;
21 use Yoast\WP\SEO\Helpers\Current_Page_Helper;
22 use Yoast\WP\SEO\Helpers\Language_Helper;
23 use Yoast\WP\SEO\Helpers\Options_Helper;
24 use Yoast\WP\SEO\Helpers\Post_Type_Helper;
25 use Yoast\WP\SEO\Helpers\Product_Helper;
26 use Yoast\WP\SEO\Helpers\Route_Helper;
27 use Yoast\WP\SEO\Helpers\Schema\Article_Helper;
28 use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
29 use Yoast\WP\SEO\Helpers\User_Helper;
30 use Yoast\WP\SEO\Helpers\Woocommerce_Helper;
31 use Yoast\WP\SEO\Llms_Txt\Application\Configuration\Llms_Txt_Configuration;
32 use Yoast\WP\SEO\Llms_Txt\Application\Health_Check\File_Runner;
33 use Yoast\WP\SEO\Llms_Txt\Infrastructure\Content\Manual_Post_Collection;
34 use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
35 use Yoast\WP\SEO\Schema\Application\Configuration\Schema_Configuration;
36
37 /**
38 * Class Settings_Integration.
39 */
40 class Settings_Integration implements Integration_Interface {
41
42 public const PAGE = 'wpseo_page_settings';
43
44 /**
45 * Holds the included WordPress options.
46 *
47 * @var string[]
48 */
49 public const WP_OPTIONS = [ 'blogdescription' ];
50
51 /**
52 * Holds the allowed option groups.
53 *
54 * @var array
55 */
56 public const ALLOWED_OPTION_GROUPS = [ 'wpseo', 'wpseo_titles', 'wpseo_social', 'wpseo_llmstxt' ];
57
58 /**
59 * Holds the disallowed settings, per option group.
60 *
61 * @var array
62 */
63 public const DISALLOWED_SETTINGS = [
64 'wpseo' => [
65 'myyoast-oauth',
66 'semrush_tokens',
67 'custom_taxonomy_slugs',
68 'import_cursors',
69 'workouts_data',
70 'configuration_finished_steps',
71 'importing_completed',
72 'wincher_tokens',
73 'least_readability_ignore_list',
74 'least_seo_score_ignore_list',
75 'most_linked_ignore_list',
76 'least_linked_ignore_list',
77 'indexables_page_reading_list',
78 'show_new_content_type_notification',
79 'new_post_types',
80 'new_taxonomies',
81 ],
82 'wpseo_titles' => [
83 'company_logo_meta',
84 'person_logo_meta',
85 ],
86 ];
87
88 /**
89 * Holds the disabled on multisite settings, per option group.
90 *
91 * @var array
92 */
93 public const DISABLED_ON_MULTISITE_SETTINGS = [
94 'wpseo' => [
95 'deny_search_crawling',
96 'deny_wp_json_crawling',
97 'deny_adsbot_crawling',
98 'deny_ccbot_crawling',
99 'deny_google_extended_crawling',
100 'deny_gptbot_crawling',
101 'enable_llms_txt',
102 ],
103 ];
104
105 /**
106 * Holds the WPSEO_Admin_Asset_Manager.
107 *
108 * @var WPSEO_Admin_Asset_Manager
109 */
110 protected $asset_manager;
111
112 /**
113 * Holds the WPSEO_Replace_Vars.
114 *
115 * @var WPSEO_Replace_Vars
116 */
117 protected $replace_vars;
118
119 /**
120 * Holds the Schema_Types.
121 *
122 * @var Schema_Types
123 */
124 protected $schema_types;
125
126 /**
127 * Holds the Current_Page_Helper.
128 *
129 * @var Current_Page_Helper
130 */
131 protected $current_page_helper;
132
133 /**
134 * Holds the Post_Type_Helper.
135 *
136 * @var Post_Type_Helper
137 */
138 protected $post_type_helper;
139
140 /**
141 * Holds the Language_Helper.
142 *
143 * @var Language_Helper
144 */
145 protected $language_helper;
146
147 /**
148 * Holds the Taxonomy_Helper.
149 *
150 * @var Taxonomy_Helper
151 */
152 protected $taxonomy_helper;
153
154 /**
155 * Holds the Product_Helper.
156 *
157 * @var Product_Helper
158 */
159 protected $product_helper;
160
161 /**
162 * Holds the Woocommerce_Helper.
163 *
164 * @var Woocommerce_Helper
165 */
166 protected $woocommerce_helper;
167
168 /**
169 * Holds the Article_Helper.
170 *
171 * @var Article_Helper
172 */
173 protected $article_helper;
174
175 /**
176 * Holds the User_Helper.
177 *
178 * @var User_Helper
179 */
180 protected $user_helper;
181
182 /**
183 * Holds the Options_Helper instance.
184 *
185 * @var Options_Helper
186 */
187 protected $options;
188
189 /**
190 * Holds the Content_Type_Visibility_Dismiss_Notifications instance.
191 *
192 * @var Content_Type_Visibility_Dismiss_Notifications
193 */
194 protected $content_type_visibility;
195
196 /**
197 * Holds the Llms_Txt_Configuration instance.
198 *
199 * @var Llms_Txt_Configuration
200 */
201 protected $llms_txt_configuration;
202
203 /**
204 * Holds the Schema_Configuration instance.
205 *
206 * @var Schema_Configuration
207 */
208 protected $schema_configuration;
209
210 /**
211 * The manual post collection.
212 *
213 * @var Manual_Post_Collection
214 */
215 private $manual_post_collection;
216
217 /**
218 * Runs the health check.
219 *
220 * @var File_Runner
221 */
222 private $runner;
223
224 /**
225 * Holds the Route_Helper.
226 *
227 * @var Route_Helper
228 */
229 private $route_helper;
230
231 /**
232 * Constructs Settings_Integration.
233 *
234 * @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager.
235 * @param WPSEO_Replace_Vars $replace_vars The WPSEO_Replace_Vars.
236 * @param Schema_Types $schema_types The Schema_Types.
237 * @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
238 * @param Post_Type_Helper $post_type_helper The Post_Type_Helper.
239 * @param Language_Helper $language_helper The Language_Helper.
240 * @param Taxonomy_Helper $taxonomy_helper The Taxonomy_Helper.
241 * @param Product_Helper $product_helper The Product_Helper.
242 * @param Woocommerce_Helper $woocommerce_helper The Woocommerce_Helper.
243 * @param Article_Helper $article_helper The Article_Helper.
244 * @param User_Helper $user_helper The User_Helper.
245 * @param Options_Helper $options The options helper.
246 * @param Content_Type_Visibility_Dismiss_Notifications $content_type_visibility The Content_Type_Visibility_Dismiss_Notifications instance.
247 * @param Llms_Txt_Configuration $llms_txt_configuration The Llms_Txt_Configuration instance.
248 * @param Manual_Post_Collection $manual_post_collection The manual post collection.
249 * @param File_Runner $runner The file runner.
250 * @param Route_Helper $route_helper The Route_Helper.
251 * @param Schema_Configuration $schema_configuration The Schema_Configuration.
252 */
253 public function __construct(
254 WPSEO_Admin_Asset_Manager $asset_manager,
255 WPSEO_Replace_Vars $replace_vars,
256 Schema_Types $schema_types,
257 Current_Page_Helper $current_page_helper,
258 Post_Type_Helper $post_type_helper,
259 Language_Helper $language_helper,
260 Taxonomy_Helper $taxonomy_helper,
261 Product_Helper $product_helper,
262 Woocommerce_Helper $woocommerce_helper,
263 Article_Helper $article_helper,
264 User_Helper $user_helper,
265 Options_Helper $options,
266 Content_Type_Visibility_Dismiss_Notifications $content_type_visibility,
267 Llms_Txt_Configuration $llms_txt_configuration,
268 Manual_Post_Collection $manual_post_collection,
269 File_Runner $runner,
270 Route_Helper $route_helper,
271 Schema_Configuration $schema_configuration
272 ) {
273 $this->asset_manager = $asset_manager;
274 $this->replace_vars = $replace_vars;
275 $this->schema_types = $schema_types;
276 $this->current_page_helper = $current_page_helper;
277 $this->taxonomy_helper = $taxonomy_helper;
278 $this->post_type_helper = $post_type_helper;
279 $this->language_helper = $language_helper;
280 $this->product_helper = $product_helper;
281 $this->woocommerce_helper = $woocommerce_helper;
282 $this->article_helper = $article_helper;
283 $this->user_helper = $user_helper;
284 $this->options = $options;
285 $this->content_type_visibility = $content_type_visibility;
286 $this->llms_txt_configuration = $llms_txt_configuration;
287 $this->manual_post_collection = $manual_post_collection;
288 $this->runner = $runner;
289 $this->route_helper = $route_helper;
290 $this->schema_configuration = $schema_configuration;
291 }
292
293 /**
294 * Returns the conditionals based on which this loadable should be active.
295 *
296 * @return array
297 */
298 public static function get_conditionals() {
299 return [ Settings_Conditional::class ];
300 }
301
302 /**
303 * Initializes the integration.
304 *
305 * This is the place to register hooks and filters.
306 *
307 * @return void
308 */
309 public function register_hooks() {
310 // Add page.
311 \add_filter( 'wpseo_submenu_pages', [ $this, 'add_page' ] );
312 \add_filter( 'admin_menu', [ $this, 'add_settings_saved_page' ] );
313
314 // Are we saving the settings?
315 if ( $this->current_page_helper->get_current_admin_page() === 'options.php' ) {
316 $post_action = '';
317 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information.
318 if ( isset( $_POST['action'] ) && \is_string( $_POST['action'] ) ) {
319 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information.
320 $post_action = \wp_unslash( $_POST['action'] );
321 }
322 $option_page = '';
323 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information.
324 if ( isset( $_POST['option_page'] ) && \is_string( $_POST['option_page'] ) ) {
325 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information.
326 $option_page = \wp_unslash( $_POST['option_page'] );
327 }
328
329 if ( $post_action === 'update' && $option_page === self::PAGE ) {
330 \add_action( 'admin_init', [ $this, 'register_setting' ] );
331 \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX );
332 }
333
334 return;
335 }
336
337 // Are we on the settings page?
338 if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) {
339 \add_action( 'admin_init', [ $this, 'register_setting' ] );
340 \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
341 \add_action( 'in_admin_header', [ $this, 'remove_notices' ], \PHP_INT_MAX );
342 }
343 }
344
345 /**
346 * Registers the different options under the setting.
347 *
348 * @return void
349 */
350 public function register_setting() {
351 foreach ( WPSEO_Options::$options as $name => $instance ) {
352 if ( \in_array( $name, self::ALLOWED_OPTION_GROUPS, true ) ) {
353 \register_setting( self::PAGE, $name );
354 }
355 }
356 // Only register WP options when the user is allowed to manage them.
357 if ( \current_user_can( 'manage_options' ) ) {
358 foreach ( self::WP_OPTIONS as $name ) {
359 \register_setting( self::PAGE, $name );
360 }
361 }
362 }
363
364 /**
365 * Adds the page.
366 *
367 * @param array $pages The pages.
368 *
369 * @return array The pages.
370 */
371 public function add_page( $pages ) {
372 \array_splice(
373 $pages,
374 1,
375 0,
376 [
377 [
378 'wpseo_dashboard',
379 '',
380 \__( 'Settings', 'wordpress-seo' ),
381 'wpseo_manage_options',
382 self::PAGE,
383 [ $this, 'display_page' ],
384 ],
385 ],
386 );
387
388 return $pages;
389 }
390
391 /**
392 * Adds a dummy page.
393 *
394 * Because the options route NEEDS to redirect to something.
395 *
396 * @param array $pages The pages.
397 *
398 * @return array The pages.
399 */
400 public function add_settings_saved_page( $pages ) {
401 $runner = $this->runner;
402 \add_submenu_page(
403 'options.php',
404 '',
405 '',
406 'wpseo_manage_options',
407 self::PAGE . '_saved',
408 static function () use ( $runner ) {
409 // Add success indication to HTML response.
410 $success = empty( \get_settings_errors() ) ? 'true' : 'false';
411 echo \esc_html( "{{ yoast-success: $success }}" );
412
413 $runner->run();
414 if ( ! $runner->is_successful() ) {
415 $failure_reason = $runner->get_generation_failure_reason();
416 echo \esc_html( "{{ yoast-llms-txt-generation-failure: $failure_reason }}" );
417 }
418 },
419 );
420
421 return $pages;
422 }
423
424 /**
425 * Displays the page.
426 *
427 * @return void
428 */
429 public function display_page() {
430 echo '<div id="yoast-seo-settings"></div>';
431 }
432
433 /**
434 * Enqueues the assets.
435 *
436 * @return void
437 */
438 public function enqueue_assets() {
439 // Remove the emoji script as it is incompatible with both React and any contenteditable fields.
440 \remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
441 \wp_enqueue_media();
442 $this->asset_manager->enqueue_script( 'new-settings' );
443 $this->asset_manager->enqueue_style( 'new-settings' );
444 if ( \YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) {
445 $this->asset_manager->enqueue_style( 'black-friday-banner' );
446 }
447 $this->asset_manager->localize_script( 'new-settings', 'wpseoScriptData', $this->get_script_data() );
448 }
449
450 /**
451 * Removes all current WP notices.
452 *
453 * @return void
454 */
455 public function remove_notices() {
456 \remove_all_actions( 'admin_notices' );
457 \remove_all_actions( 'user_admin_notices' );
458 \remove_all_actions( 'network_admin_notices' );
459 \remove_all_actions( 'all_admin_notices' );
460 }
461
462 /**
463 * Creates the script data.
464 *
465 * @return array The script data.
466 */
467 protected function get_script_data() {
468 $default_setting_values = $this->get_default_setting_values();
469 $settings = $this->get_settings( $default_setting_values );
470 $post_types = $this->post_type_helper->get_indexable_post_type_objects();
471 $taxonomies = $this->taxonomy_helper->get_indexable_taxonomy_objects();
472
473 // Check if attachments are included in indexation.
474 if ( ! \array_key_exists( 'attachment', $post_types ) ) {
475 // Always include attachments in the settings, to let the user enable them again.
476 $attachment_object = \get_post_type_object( 'attachment' );
477 if ( ! empty( $attachment_object ) ) {
478 $post_types['attachment'] = $attachment_object;
479 }
480 }
481 // Check if post formats are included in indexation.
482 if ( ! \array_key_exists( 'post_format', $taxonomies ) ) {
483 // Always include post_format in the settings, to let the user enable them again.
484 $post_format_object = \get_taxonomy( 'post_format' );
485 if ( ! empty( $post_format_object ) ) {
486 $taxonomies['post_format'] = $post_format_object;
487 }
488 }
489
490 $transformed_post_types = $this->transform_post_types( $post_types );
491 $transformed_taxonomies = $this->transform_taxonomies( $taxonomies, \array_keys( $transformed_post_types ) );
492
493 // Check if there is a new content type to show notification only once in the settings.
494 $show_new_content_type_notification = $this->content_type_visibility->maybe_add_settings_notification();
495
496 return [
497 'settings' => $this->transform_settings( $settings ),
498 'defaultSettingValues' => $default_setting_values,
499 'disabledSettings' => $this->get_disabled_settings( $settings ),
500 'endpoint' => \admin_url( 'options.php' ),
501 'nonce' => \wp_create_nonce( self::PAGE . '-options' ),
502 'separators' => WPSEO_Option_Titles::get_instance()->get_separator_options_for_display(),
503 'replacementVariables' => $this->get_replacement_variables(),
504 'schema' => $this->get_schema( $transformed_post_types ),
505 'preferences' => $this->get_preferences( $settings ),
506 'linkParams' => WPSEO_Shortlinker::get_query_params(),
507 'postTypes' => $transformed_post_types,
508 'taxonomies' => $transformed_taxonomies,
509 'fallbacks' => $this->get_fallbacks(),
510 'showNewContentTypeNotification' => $show_new_content_type_notification,
511 'currentPromotions' => \YoastSEO()->classes->get( Promotion_Manager::class )->get_current_promotions(),
512 'llmsTxt' => $this->llms_txt_configuration->get_configuration(),
513 'initialLlmTxtPages' => $this->get_site_llms_txt_pages( $settings ),
514 'schemaFrameworkConfiguration' => $this->schema_configuration->get_configuration(),
515 ];
516 }
517
518 /**
519 * Retrieves the preferences.
520 *
521 * @param array $settings The settings.
522 *
523 * @return array The preferences.
524 */
525 protected function get_preferences( $settings ) {
526 $shop_page_id = $this->woocommerce_helper->get_shop_page_id();
527 $homepage_is_latest_posts = \get_option( 'show_on_front' ) === 'posts';
528 $page_on_front = \get_option( 'page_on_front' );
529 $page_for_posts = \get_option( 'page_for_posts' );
530
531 $addon_manager = new WPSEO_Addon_Manager();
532 $woocommerce_seo_active = \is_plugin_active( $addon_manager->get_plugin_file( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) );
533
534 if ( empty( $page_on_front ) ) {
535 $page_on_front = $page_for_posts;
536 }
537
538 $business_settings_url = \get_admin_url( null, 'admin.php?page=wpseo_local' );
539 if ( \defined( 'WPSEO_LOCAL_FILE' ) ) {
540 $local_options = \get_option( 'wpseo_local' );
541 $multiple_locations = $local_options['use_multiple_locations'];
542 $same_organization = $local_options['multiple_locations_same_organization'];
543 if ( $multiple_locations === 'on' && $same_organization !== 'on' ) {
544 $business_settings_url = \get_admin_url( null, 'edit.php?post_type=wpseo_locations' );
545 }
546 }
547
548 return [
549 'isPremium' => $this->product_helper->is_premium(),
550 'isRtl' => \is_rtl(),
551 'isNetworkAdmin' => \is_network_admin(),
552 'isMainSite' => \is_main_site(),
553 'isMultisite' => \is_multisite(),
554 'isWooCommerceActive' => $this->woocommerce_helper->is_active(),
555 'isLocalSeoActive' => \defined( 'WPSEO_LOCAL_FILE' ),
556 'isNewsSeoActive' => \defined( 'WPSEO_NEWS_FILE' ),
557 'isWooCommerceSEOActive' => $woocommerce_seo_active,
558 'siteUrl' => \get_bloginfo( 'url' ),
559 'siteTitle' => \get_bloginfo( 'name' ),
560 'sitemapUrl' => WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ),
561 'hasWooCommerceShopPage' => $shop_page_id !== -1,
562 'editWooCommerceShopPageUrl' => \get_edit_post_link( $shop_page_id, 'js' ),
563 'wooCommerceShopPageSettingUrl' => \get_admin_url( null, 'admin.php?page=wc-settings&tab=products' ),
564 'localSeoPageSettingUrl' => $business_settings_url,
565 'homepageIsLatestPosts' => $homepage_is_latest_posts,
566 'homepagePageEditUrl' => \get_edit_post_link( $page_on_front, 'js' ),
567 'homepagePostsEditUrl' => \get_edit_post_link( $page_for_posts, 'js' ),
568 'createUserUrl' => \admin_url( 'user-new.php' ),
569 'createPageUrl' => \admin_url( 'post-new.php?post_type=page' ),
570 'editUserUrl' => \admin_url( 'user-edit.php' ),
571 'editTaxonomyUrl' => \admin_url( 'edit-tags.php' ),
572 'generalSettingsUrl' => \admin_url( 'options-general.php' ),
573 'companyOrPersonMessage' => \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' ),
574 'currentUserId' => \get_current_user_id(),
575 'canCreateUsers' => \current_user_can( 'create_users' ),
576 'canCreatePages' => \current_user_can( 'edit_pages' ),
577 'canEditUsers' => \current_user_can( 'edit_users' ),
578 'canManageOptions' => \current_user_can( 'manage_options' ),
579 'userLocale' => \str_replace( '_', '-', \get_user_locale() ),
580 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ),
581 'showForceRewriteTitlesSetting' => ! \current_theme_supports( 'title-tag' ) && ! ( \function_exists( 'wp_is_block_theme' ) && \wp_is_block_theme() ),
582 'upsellSettings' => $this->get_upsell_settings(),
583 'siteRepresentsPerson' => $this->get_site_represents_person( $settings ),
584 'siteBasicsPolicies' => $this->get_site_basics_policies( $settings ),
585 ];
586 }
587
588 /**
589 * Retrieves the currently represented person.
590 *
591 * @param array $settings The settings.
592 *
593 * @return array The currently represented person.
594 */
595 protected function get_site_represents_person( $settings ) {
596 $person = [
597 'id' => false,
598 'name' => '',
599 ];
600
601 if ( isset( $settings['wpseo_titles']['company_or_person_user_id'] ) ) {
602 $person['id'] = $settings['wpseo_titles']['company_or_person_user_id'];
603 $user = \get_userdata( $person['id'] );
604 if ( $user instanceof WP_User ) {
605 $person['name'] = $user->get( 'display_name' );
606 }
607 }
608
609 return $person;
610 }
611
612 /**
613 * Get site policy data.
614 *
615 * @param array $settings The settings.
616 *
617 * @return array The policy data.
618 */
619 private function get_site_basics_policies( $settings ) {
620 $policies = [];
621
622 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['publishing_principles_id'], 'publishing_principles_id' );
623 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ownership_funding_info_id'], 'ownership_funding_info_id' );
624 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['actionable_feedback_policy_id'], 'actionable_feedback_policy_id' );
625 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['corrections_policy_id'], 'corrections_policy_id' );
626 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ethics_policy_id'], 'ethics_policy_id' );
627 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_policy_id'], 'diversity_policy_id' );
628 $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_staffing_report_id'], 'diversity_staffing_report_id' );
629
630 return $policies;
631 }
632
633 /**
634 * Adds policy data if it is present.
635 *
636 * @param array $policies The existing policy data.
637 * @param int $policy The policy id to check.
638 * @param string $key The option key name.
639 *
640 * @return array<int, string> The policy data.
641 */
642 private function maybe_add_policy( $policies, $policy, $key ) {
643 $policy_array = [
644 'id' => 0,
645 'name' => \__( 'None', 'wordpress-seo' ),
646 ];
647
648 if ( isset( $policy ) && \is_int( $policy ) ) {
649 $policy_array['id'] = $policy;
650 $post = \get_post( $policy );
651 if ( $post instanceof WP_Post ) {
652 if ( $post->post_status !== 'publish' || $post->post_password !== '' ) {
653 return $policies;
654 }
655 $policy_array['name'] = $post->post_title;
656 }
657 }
658
659 $policies[ $key ] = $policy_array;
660
661 return $policies;
662 }
663
664 /**
665 * Adds page if it is present.
666 *
667 * @param array<int, string> $pages The existing pages.
668 * @param int $page_id The page id to check.
669 * @param string $key The option key name.
670 *
671 * @return array<int, string> The policy data.
672 */
673 private function maybe_add_page( $pages, $page_id, $key ) {
674 if ( isset( $page_id ) && \is_int( $page_id ) && $page_id !== 0 ) {
675 $post = $this->manual_post_collection->get_content_type_entry( $page_id );
676 if ( $post === null ) {
677 return $pages;
678 }
679
680 $pages[ $key ] = [
681 'id' => $page_id,
682 'title' => ( $post->get_title() ) ? $post->get_title() : $post->get_slug(),
683 'slug' => $post->get_slug(),
684 ];
685 }
686
687 return $pages;
688 }
689
690 /**
691 * Get site llms.txt pages.
692 *
693 * @param array $settings The settings.
694 *
695 * @return array<string, array<string, int|string>> The llms.txt pages.
696 */
697 private function get_site_llms_txt_pages( $settings ) {
698 $llms_txt_pages = [];
699
700 $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['about_us_page'], 'about_us_page' );
701 $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['contact_page'], 'contact_page' );
702 $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['terms_page'], 'terms_page' );
703 $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['privacy_policy_page'], 'privacy_policy_page' );
704 $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $settings['wpseo_llmstxt']['shop_page'], 'shop_page' );
705
706 if ( isset( $settings['wpseo_llmstxt']['other_included_pages'] ) && \is_array( $settings['wpseo_llmstxt']['other_included_pages'] ) ) {
707 foreach ( $settings['wpseo_llmstxt']['other_included_pages'] as $key => $page_id ) {
708 $llms_txt_pages = $this->maybe_add_page( $llms_txt_pages, $page_id, 'other_included_pages-' . $key );
709 }
710 }
711
712 return $llms_txt_pages;
713 }
714
715 /**
716 * Returns settings for the Call to Buy (CTB) buttons.
717 *
718 * @return array<string> The array of CTB settings.
719 */
720 public function get_upsell_settings() {
721 return [
722 'actionId' => 'load-nfd-ctb',
723 'premiumCtbId' => 'f6a84663-465f-4cb5-8ba5-f7a6d72224b2',
724 ];
725 }
726
727 /**
728 * Retrieves the default setting values.
729 *
730 * These default values are currently being used in the UI for dummy fields.
731 * Dummy fields should not expose or reflect the actual data.
732 *
733 * @return array The default setting values.
734 */
735 protected function get_default_setting_values() {
736 $defaults = [];
737
738 // Add Yoast settings.
739 foreach ( WPSEO_Options::$options as $option_name => $instance ) {
740 if ( \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) {
741 $option_instance = WPSEO_Options::get_option_instance( $option_name );
742 $defaults[ $option_name ] = ( $option_instance ) ? $option_instance->get_defaults() : [];
743 }
744 }
745 // Add WP settings.
746 foreach ( self::WP_OPTIONS as $option_name ) {
747 $defaults[ $option_name ] = '';
748 }
749
750 // Remove disallowed settings.
751 foreach ( self::DISALLOWED_SETTINGS as $option_name => $disallowed_settings ) {
752 foreach ( $disallowed_settings as $disallowed_setting ) {
753 unset( $defaults[ $option_name ][ $disallowed_setting ] );
754 }
755 }
756
757 if ( \defined( 'WPSEO_LOCAL_FILE' ) ) {
758 $defaults = $this->get_defaults_from_local_seo( $defaults );
759 }
760
761 return $defaults;
762 }
763
764 /**
765 * Retrieves the organization schema values from Local SEO for defaults in Site representation fields.
766 * Specifically for the org-vat-id, org-tax-id, org-email and org-phone options.
767 *
768 * @param array<string|int|bool> $defaults The settings defaults.
769 *
770 * @return array<string|int|bool> The settings defaults with local seo overides.
771 */
772 protected function get_defaults_from_local_seo( $defaults ) {
773 $local_options = \get_option( 'wpseo_local' );
774 $multiple_locations = $local_options['use_multiple_locations'];
775 $same_organization = $local_options['multiple_locations_same_organization'];
776 $shared_info = $local_options['multiple_locations_shared_business_info'];
777 if ( $multiple_locations !== 'on' || ( $multiple_locations === 'on' && $same_organization === 'on' && $shared_info === 'on' ) ) {
778 $defaults['wpseo_titles']['org-vat-id'] = $local_options['location_vat_id'];
779 $defaults['wpseo_titles']['org-tax-id'] = $local_options['location_tax_id'];
780 $defaults['wpseo_titles']['org-email'] = $local_options['location_email'];
781 $defaults['wpseo_titles']['org-phone'] = $local_options['location_phone'];
782 }
783
784 if ( \wpseo_has_primary_location() ) {
785 $primary_location = $local_options['multiple_locations_primary_location'];
786
787 $location_keys = [
788 'org-phone' => [
789 'is_overridden' => '_wpseo_is_overridden_business_phone',
790 'value' => '_wpseo_business_phone',
791 ],
792 'org-email' => [
793 'is_overridden' => '_wpseo_is_overridden_business_email',
794 'value' => '_wpseo_business_email',
795 ],
796 'org-tax-id' => [
797 'is_overridden' => '_wpseo_is_overridden_business_tax_id',
798 'value' => '_wpseo_business_tax_id',
799 ],
800 'org-vat-id' => [
801 'is_overridden' => '_wpseo_is_overridden_business_vat_id',
802 'value' => '_wpseo_business_vat_id',
803 ],
804 ];
805
806 foreach ( $location_keys as $key => $meta_keys ) {
807 $is_overridden = ( $shared_info === 'on' ) ? \get_post_meta( $primary_location, $meta_keys['is_overridden'], true ) : false;
808 if ( $is_overridden === 'on' || $shared_info !== 'on' ) {
809 $post_meta_value = \get_post_meta( $primary_location, $meta_keys['value'], true );
810 $defaults['wpseo_titles'][ $key ] = ( $post_meta_value ) ? $post_meta_value : '';
811 }
812 }
813 }
814
815 return $defaults;
816 }
817
818 /**
819 * Retrieves the settings and their values.
820 *
821 * @param array $default_setting_values The default setting values.
822 *
823 * @return array The settings.
824 */
825 protected function get_settings( $default_setting_values ) {
826 $settings = [];
827
828 // Add Yoast settings.
829 foreach ( WPSEO_Options::$options as $option_name => $instance ) {
830 if ( \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) {
831 $settings[ $option_name ] = \array_merge( $default_setting_values[ $option_name ], WPSEO_Options::get_option( $option_name ) );
832 }
833 }
834 // Add WP settings.
835 foreach ( self::WP_OPTIONS as $option_name ) {
836 $settings[ $option_name ] = \get_option( $option_name );
837 }
838
839 // Remove disallowed settings.
840 foreach ( self::DISALLOWED_SETTINGS as $option_name => $disallowed_settings ) {
841 foreach ( $disallowed_settings as $disallowed_setting ) {
842 unset( $settings[ $option_name ][ $disallowed_setting ] );
843 }
844 }
845
846 return $settings;
847 }
848
849 /**
850 * Transforms setting values.
851 *
852 * @param array $settings The settings.
853 *
854 * @return array The settings.
855 */
856 protected function transform_settings( $settings ) {
857 if ( isset( $settings['wpseo_titles']['breadcrumbs-sep'] ) ) {
858 /**
859 * The breadcrumbs separator default value is the HTML entity `&raquo;`.
860 * Which does not get decoded in our JS, while it did in our Yoast form. Decode it here as an exception.
861 */
862 $settings['wpseo_titles']['breadcrumbs-sep'] = \html_entity_decode(
863 $settings['wpseo_titles']['breadcrumbs-sep'],
864 ( \ENT_NOQUOTES | \ENT_HTML5 ),
865 'UTF-8',
866 );
867 }
868
869 /**
870 * Decode some WP options.
871 */
872 $settings['blogdescription'] = \html_entity_decode(
873 $settings['blogdescription'],
874 ( \ENT_NOQUOTES | \ENT_HTML5 ),
875 'UTF-8',
876 );
877
878 if ( isset( $settings['wpseo_llmstxt']['other_included_pages'] ) ) {
879 // Append an empty page to the other included pages, so that we manage to show an empty field in the UI.
880 $settings['wpseo_llmstxt']['other_included_pages'][] = 0;
881 }
882
883 return $settings;
884 }
885
886 /**
887 * Retrieves the disabled settings.
888 *
889 * @param array $settings The settings.
890 *
891 * @return array The settings.
892 */
893 protected function get_disabled_settings( $settings ) {
894 $disabled_settings = [];
895 $site_language = $this->language_helper->get_language();
896
897 foreach ( WPSEO_Options::$options as $option_name => $instance ) {
898 if ( ! \in_array( $option_name, self::ALLOWED_OPTION_GROUPS, true ) ) {
899 continue;
900 }
901
902 $disabled_settings[ $option_name ] = [];
903 $option_instance = WPSEO_Options::get_option_instance( $option_name );
904 if ( $option_instance === false ) {
905 continue;
906 }
907 foreach ( $settings[ $option_name ] as $setting_name => $setting_value ) {
908 if ( $option_instance->is_disabled( $setting_name ) ) {
909 $disabled_settings[ $option_name ][ $setting_name ] = 'network';
910 }
911 }
912 }
913
914 // Remove disabled on multisite settings.
915 if ( \is_multisite() ) {
916 foreach ( self::DISABLED_ON_MULTISITE_SETTINGS as $option_name => $disabled_ms_settings ) {
917 if ( \array_key_exists( $option_name, $disabled_settings ) ) {
918 foreach ( $disabled_ms_settings as $disabled_ms_setting ) {
919 $disabled_settings[ $option_name ][ $disabled_ms_setting ] = 'multisite';
920 }
921 }
922 }
923 }
924
925 if ( \array_key_exists( 'wpseo', $disabled_settings ) && ! $this->language_helper->has_inclusive_language_support( $site_language ) ) {
926 $disabled_settings['wpseo']['inclusive_language_analysis_active'] = 'language';
927 }
928
929 return $disabled_settings;
930 }
931
932 /**
933 * Retrieves the replacement variables.
934 *
935 * @return array The replacement variables.
936 */
937 protected function get_replacement_variables() {
938 $recommended_replace_vars = new WPSEO_Admin_Recommended_Replace_Vars();
939 $specific_replace_vars = new WPSEO_Admin_Editor_Specific_Replace_Vars();
940 $replacement_variables = $this->replace_vars->get_replacement_variables_with_labels();
941
942 return [
943 'variables' => $replacement_variables,
944 'recommended' => $recommended_replace_vars->get_recommended_replacevars(),
945 'specific' => $specific_replace_vars->get(),
946 'shared' => $specific_replace_vars->get_generic( $replacement_variables ),
947 ];
948 }
949
950 /**
951 * Retrieves the schema.
952 *
953 * @param array $post_types The post types.
954 *
955 * @return array The schema.
956 */
957 protected function get_schema( array $post_types ) {
958 $schema = [];
959
960 foreach ( $this->schema_types->get_article_type_options() as $article_type ) {
961 $schema['articleTypes'][ $article_type['value'] ] = [
962 'label' => $article_type['name'],
963 'value' => $article_type['value'],
964 ];
965 }
966
967 foreach ( $this->schema_types->get_page_type_options() as $page_type ) {
968 $schema['pageTypes'][ $page_type['value'] ] = [
969 'label' => $page_type['name'],
970 'value' => $page_type['value'],
971 ];
972 }
973
974 $schema['articleTypeDefaults'] = [];
975 $schema['pageTypeDefaults'] = [];
976 foreach ( $post_types as $name => $post_type ) {
977 $schema['articleTypeDefaults'][ $name ] = WPSEO_Options::get_default( 'wpseo_titles', "schema-article-type-$name" );
978 $schema['pageTypeDefaults'][ $name ] = WPSEO_Options::get_default( 'wpseo_titles', "schema-page-type-$name" );
979 }
980
981 return $schema;
982 }
983
984 /**
985 * Transforms the post types, to represent them.
986 *
987 * @param WP_Post_Type[] $post_types The WP_Post_Type array to transform.
988 *
989 * @return array The post types.
990 */
991 protected function transform_post_types( $post_types ) {
992 $transformed = [];
993 $new_post_types = $this->options->get( 'new_post_types', [] );
994 foreach ( $post_types as $post_type ) {
995 $transformed[ $post_type->name ] = [
996 'name' => $post_type->name,
997 'route' => $this->route_helper->get_route( $post_type->name, $post_type->rewrite, $post_type->rest_base ),
998 'label' => $post_type->label,
999 'singularLabel' => $post_type->labels->singular_name,
1000 'hasArchive' => $this->post_type_helper->has_archive( $post_type ),
1001 'hasSchemaArticleType' => $this->article_helper->is_article_post_type( $post_type->name ),
1002 'menuPosition' => $post_type->menu_position,
1003 'isNew' => \in_array( $post_type->name, $new_post_types, true ),
1004 ];
1005 }
1006
1007 \uasort( $transformed, [ $this, 'compare_post_types' ] );
1008
1009 return $transformed;
1010 }
1011
1012 /**
1013 * Compares two post types.
1014 *
1015 * @param array $a The first post type.
1016 * @param array $b The second post type.
1017 *
1018 * @return int The order.
1019 */
1020 protected function compare_post_types( $a, $b ) {
1021 if ( $a['menuPosition'] === null && $b['menuPosition'] !== null ) {
1022 return 1;
1023 }
1024 if ( $a['menuPosition'] !== null && $b['menuPosition'] === null ) {
1025 return -1;
1026 }
1027
1028 if ( $a['menuPosition'] === null && $b['menuPosition'] === null ) {
1029 // No position specified, order alphabetically by label.
1030 return \strnatcmp( $a['label'], $b['label'] );
1031 }
1032
1033 return ( ( $a['menuPosition'] < $b['menuPosition'] ) ? -1 : 1 );
1034 }
1035
1036 /**
1037 * Transforms the taxonomies, to represent them.
1038 *
1039 * @param WP_Taxonomy[] $taxonomies The WP_Taxonomy array to transform.
1040 * @param string[] $post_type_names The post type names.
1041 *
1042 * @return array The taxonomies.
1043 */
1044 protected function transform_taxonomies( $taxonomies, $post_type_names ) {
1045 $transformed = [];
1046 $new_taxonomies = $this->options->get( 'new_taxonomies', [] );
1047 foreach ( $taxonomies as $taxonomy ) {
1048 $transformed[ $taxonomy->name ] = [
1049 'name' => $taxonomy->name,
1050 'route' => $this->route_helper->get_route( $taxonomy->name, $taxonomy->rewrite, $taxonomy->rest_base ),
1051 'label' => $taxonomy->label,
1052 'showUi' => $taxonomy->show_ui,
1053 'singularLabel' => $taxonomy->labels->singular_name,
1054 'postTypes' => \array_filter(
1055 $taxonomy->object_type,
1056 static function ( $object_type ) use ( $post_type_names ) {
1057 return \in_array( $object_type, $post_type_names, true );
1058 },
1059 ),
1060 'isNew' => \in_array( $taxonomy->name, $new_taxonomies, true ),
1061 ];
1062 }
1063
1064 \uasort(
1065 $transformed,
1066 static function ( $a, $b ) {
1067 return \strnatcmp( $a['label'], $b['label'] );
1068 },
1069 );
1070
1071 return $transformed;
1072 }
1073
1074 /**
1075 * Retrieves the fallbacks.
1076 *
1077 * @return array The fallbacks.
1078 */
1079 protected function get_fallbacks() {
1080 $site_logo_id = \get_option( 'site_logo' );
1081 if ( ! $site_logo_id ) {
1082 $site_logo_id = \get_theme_mod( 'custom_logo' );
1083 }
1084 if ( ! $site_logo_id ) {
1085 $site_logo_id = '0';
1086 }
1087
1088 return [
1089 'siteLogoId' => $site_logo_id,
1090 ];
1091 }
1092 }
1093