PluginProbe
Parse.ly / 3.20.6
Parse.ly v3.20.6
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / src / UI / class-settings-page.php

class-settings-page.php in Parse.ly 3.20.6, at src/UI/class-settings-page.php

1,730 lines 55.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * UI: Settings page class
4 *
5 * @package Parsely
6 * @since 3.0.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely\UI;
12
13 use Parsely\Content_Helper\Excerpt_Suggestions;
14 use Parsely\Parsely;
15 use Parsely\Permissions;
16 use Parsely\Utils\Utils;
17 use Parsely\Validator;
18
19 use const Parsely\PARSELY_FILE;
20
21 /**
22 * Renders the wp-admin Parse.ly plugin settings page.
23 *
24 * @since 3.0.0
25 *
26 * @phpstan-type Setting_Arguments array{
27 * add_fieldset?: bool,
28 * legend?: string,
29 * option_key: string,
30 * label_for: string,
31 * title?: string,
32 * help_text?: string,
33 * yes_text?: string,
34 * filter?: string,
35 * optional_args?: Setting_Optional_Args,
36 * select_options?: array<string, string>,
37 * radio_options?: array<string, string>,
38 * }
39 *
40 * @phpstan-type Setting_Optional_Args array{
41 * type?: string,
42 * placeholder?: string,
43 * required?: string,
44 * is_obfuscated_value: bool,
45 * }
46 *
47 * @phpstan-type ParselySettingOptions array{
48 * apikey: string,
49 * api_secret: string,
50 * metadata_secret: string,
51 * meta_type?: string,
52 * logo: string,
53 * track_authenticated_users: bool|string,
54 * disable_javascript: bool|string,
55 * disable_amp?: bool,
56 * track_post_types_as?: array<string, string>,
57 * track_post_types: string[],
58 * track_page_types: string[],
59 * full_metadata_in_non_posts: ?bool,
60 * content_id_prefix?: string,
61 * use_top_level_cats?:bool|string,
62 * custom_taxonomy_section?: string,
63 * cats_as_tags?: bool|string,
64 * content_helper: Parsely_Settings_Options_Content_Helper,
65 * lowercase_tags?: bool,
66 * force_https_canonicals?: bool,
67 * disable_autotrack?: bool|string,
68 * }
69 *
70 * @phpstan-type Parsely_Settings_Options_Content_Helper array{
71 * ai_features_enabled?: bool,
72 * smart_linking?: Parsely_Settings_Options_Content_Helper_Feature,
73 * title_suggestions?: Parsely_Settings_Options_Content_Helper_Feature,
74 * excerpt_suggestions?: Parsely_Settings_Options_Content_Helper_Feature,
75 * traffic_boost?: Parsely_Settings_Options_Content_Helper_Feature,
76 * }
77 *
78 * @phpstan-type Parsely_Settings_Options_Content_Helper_Feature array{
79 * enabled?: bool,
80 * allowed_user_roles?: array<string, string>|array<string, bool>
81 * }
82 *
83 * @phpstan-import-type Parsely_Options from Parsely
84 */
85 final class Settings_Page {
86 /**
87 * Instance of Parsely class.
88 *
89 * @var Parsely
90 */
91 private $parsely;
92
93 /**
94 * Admin page name used for hook suffixes.
95 *
96 * @since 3.2.0
97 *
98 * @var string
99 */
100 private $hook_suffix;
101
102 /**
103 * Options for badges that are displayed for managed options.
104 *
105 * @since 3.9.0
106 *
107 * @var array<string, mixed>
108 */
109 private $managed_options_badge = array();
110
111 /**
112 * The Content Intelligence features that can be configured in the settings
113 * page.
114 *
115 * @since 3.16.0
116 *
117 * @var string[]
118 */
119 private $configurable_pch_features = array(
120 'smart_linking',
121 'title_suggestions',
122 'excerpt_suggestions',
123 'traffic_boost',
124 );
125
126 /**
127 * Constructor.
128 *
129 * @param Parsely $parsely Instance of Parsely class.
130 */
131 public function __construct( Parsely $parsely ) {
132 $this->parsely = $parsely;
133
134 $managed_options_badge = apply_filters(
135 'wp_parsely_managed_options_badge',
136 array(
137 'text' => __( 'Upgrade', 'wp-parsely' ),
138 'url' => 'https://www.parse.ly/getdemo/',
139 )
140 );
141
142 if ( is_array( $managed_options_badge ) ) {
143 $this->managed_options_badge = $managed_options_badge;
144 }
145 }
146
147 /**
148 * Registers settings page.
149 *
150 * @since 3.0.0
151 */
152 public function run(): void {
153 add_action( 'admin_menu', array( $this, 'add_settings_sub_menu' ) );
154 add_action( 'admin_init', array( $this, 'initialize_settings' ) );
155 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_settings_assets' ) );
156 }
157
158 /**
159 * Enqueues all needed scripts and styles for Parse.ly plugin settings page.
160 *
161 * @param string|null $hook_suffix The current page being loaded.
162 */
163 public function enqueue_settings_assets( ?string $hook_suffix ): void {
164 if ( ! is_string( $hook_suffix ) || $this->hook_suffix !== $hook_suffix ) {
165 return;
166 }
167
168 add_filter( 'media_library_months_with_files', '__return_empty_array' );
169 wp_enqueue_media();
170
171 $admin_settings_asset = Utils::get_asset_info( 'build/admin-settings.asset.php' );
172 $built_assets_url = plugin_dir_url( PARSELY_FILE ) . 'build/';
173
174 wp_enqueue_script(
175 'parsely-admin-settings',
176 $built_assets_url . 'admin-settings.js',
177 $admin_settings_asset['dependencies'],
178 $admin_settings_asset['version'],
179 true
180 );
181
182 wp_enqueue_style(
183 'parsely-admin-settings',
184 $built_assets_url . 'admin-settings.css',
185 array(),
186 $admin_settings_asset['version']
187 );
188 }
189
190 /**
191 * Adds the Parse.ly settings page in WordPress settings menu.
192 */
193 public function add_settings_sub_menu(): void {
194 $suffix = add_submenu_page(
195 'parsely-dashboard-page',
196 __( 'Parse.ly Settings', 'wp-parsely' ),
197 __( 'Settings', 'wp-parsely' ),
198 Parsely::CAPABILITY, // phpcs:ignore WordPress.WP.Capabilities.Undetermined
199 Parsely::MENU_SLUG,
200 array( $this, 'display_settings' )
201 );
202
203 if ( is_string( $suffix ) ) {
204 $this->hook_suffix = $suffix;
205
206 // Adds help text when admin page loads.
207 add_action( 'load-' . $this->hook_suffix, array( $this, 'add_help_text' ) );
208 }
209 }
210
211 /**
212 * Adds the help tab to the settings page.
213 *
214 * @since 3.1.0
215 */
216 public function add_help_text(): void {
217 $screen = get_current_screen();
218 if ( null === $screen ) {
219 return;
220 }
221
222 $screen->add_help_tab(
223 array(
224 'id' => 'overview',
225 'title' => __( 'Overview', 'wp-parsely' ),
226 'content' => '<p>' . __( 'The only required setting on this page is the Site ID. All of the other settings are optional.', 'wp-parsely' ) . '</p>' .
227 '<p>' . __( 'You must click the Save Changes button at the bottom of the screen for new settings to take effect.', 'wp-parsely' ) . '</p>',
228 )
229 );
230 }
231
232 /**
233 * Displays the Parse.ly settings screen (admin.php?page=[SLUG]).
234 */
235 public function display_settings(): void {
236 // phpcs:ignore WordPress.WP.Capabilities.Undetermined
237 if ( ! current_user_can( Parsely::CAPABILITY ) ) {
238 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-parsely' ) );
239 }
240
241 include_once plugin_dir_path( PARSELY_FILE ) . 'src/UI/settings-page.php';
242 }
243
244 /**
245 * Initializes the settings for Parse.ly.
246 */
247 public function initialize_settings(): void {
248 // Add the option first, to prevent double sanitization of the uninitialized option as reported
249 // in https://core.trac.wordpress.org/ticket/21989.
250 // The option will be initialized with the default values.
251 add_option( Parsely::OPTIONS_KEY, $this->parsely->get_options() );
252
253 // All our options are actually stored in one single array to reduce DB queries.
254 register_setting(
255 Parsely::OPTIONS_KEY,
256 Parsely::OPTIONS_KEY,
257 array(
258 'type' => 'array',
259 'sanitize_callback' => array( $this, 'validate_options' ),
260 )
261 );
262
263 $this->initialize_basic_section();
264 $this->initialize_content_helper_section();
265 $this->initialize_recrawl_section();
266 $this->initialize_advanced_section();
267 }
268
269 /**
270 * Registers section and settings for Basic section.
271 *
272 * @since 3.2.0
273 */
274 private function initialize_basic_section(): void {
275 $are_credentials_managed = $this->parsely->are_credentials_managed;
276 $section_key = 'basic-section';
277
278 add_settings_section(
279 $section_key,
280 __( 'Basic', 'wp-parsely' ),
281 '__return_null',
282 Parsely::MENU_SLUG
283 );
284
285 // Site ID.
286 $field_id = 'apikey';
287 $field_args = array(
288 'option_key' => $field_id,
289 'help_text' => __( 'Your Site ID is typically your own site domain without <code>http(s)://</code> prefixes or trailing <code>/</code> (e.g. <code>mydomain.com</code>).', 'wp-parsely' ),
290 'label_for' => $field_id,
291 'optional_args' => array(
292 'required' => 'required',
293 'placeholder' => 'mydomain.com',
294 'disabled' => $are_credentials_managed,
295 ),
296
297 );
298 add_settings_field(
299 $field_id,
300 __( 'Site ID <em>(required)</em>', 'wp-parsely' ),
301 array( $this, 'print_text_tag' ),
302 Parsely::MENU_SLUG,
303 $section_key,
304 $field_args
305 );
306
307 if ( ! $are_credentials_managed ) {
308 // API Secret.
309 $field_id = 'api_secret';
310 $field_args = array(
311 'option_key' => $field_id,
312 'help_text' => __( 'Your API secret is your secret code to <a href="https://docs.parse.ly/the-parsely-api/">access our API</a>. It can be found at <code>dash.parsely.com/<var>yoursitedomain</var>/settings/api</code> (replace <var>yoursitedomain</var> with your domain name, e.g. <samp>mydomain.com</samp>).<br />If you haven\'t purchased access to the API and would like to do so, email your account manager or <a href="mailto:support@parsely.com">support@parsely.com</a>.', 'wp-parsely' ),
313 'label_for' => $field_id,
314 'optional_args' => array(
315 'type' => 'password',
316 'is_obfuscated_value' => true,
317 ),
318 );
319 add_settings_field(
320 $field_id,
321 __( 'API Secret', 'wp-parsely' ),
322 array( $this, 'print_text_tag' ),
323 Parsely::MENU_SLUG,
324 $section_key,
325 $field_args
326 );
327 }
328
329 // Metadata Format.
330 $field_id = 'meta_type';
331 $field_args = array(
332 'title' => __( 'Metadata Format', 'wp-parsely' ),
333 'option_key' => $field_id,
334 'help_text' => __( 'Choose the metadata format for our crawlers to access. Most publishers are fine with <a href="https://docs.parse.ly/metadata-jsonld/">JSON-LD</a>, but if you prefer to use our proprietary metadata format then you can do so here.', 'wp-parsely' ),
335 'radio_options' => array(
336 'json_ld' => 'json_ld',
337 'repeated_metas' => 'repeated_metas',
338 ),
339 'label_for' => Parsely::OPTIONS_KEY . "[$field_id]",
340 'filter' => 'wp_parsely_metadata',
341 );
342 add_settings_field(
343 $field_id,
344 $this->set_field_label_contents( __( 'Metadata Format', 'wp-parsely' ), $field_id ),
345 array( $this, 'print_radio_tags' ),
346 Parsely::MENU_SLUG,
347 $section_key,
348 $field_args
349 );
350
351 // Logo.
352 $field_help = __( 'Here you can specify your logo\'s URL by using the "Browse" button or typing the URL manually.', 'wp-parsely' );
353 $field_id = 'logo';
354 add_settings_field(
355 $field_id,
356 $this->set_field_label_contents( __( 'Logo', 'wp-parsely' ), $field_id ),
357 array( $this, 'print_media_single_image' ),
358 Parsely::MENU_SLUG,
359 $section_key,
360 array(
361 'title' => __( 'Logo', 'wp-parsely' ), // Passed for legend element.
362 'option_key' => $field_id,
363 'label_for' => $field_id,
364 'help_text' => $field_help,
365 )
366 );
367
368 // Track logged-in users.
369 $field_id = 'track_authenticated_users';
370 add_settings_field(
371 $field_id,
372 $this->set_field_label_contents( __( 'Track Logged-in Users', 'wp-parsely' ), $field_id ),
373 array( $this, 'print_radio_tags' ),
374 Parsely::MENU_SLUG,
375 $section_key,
376 array(
377 'title' => __( 'Track Logged-in Users', 'wp-parsely' ), // Passed for legend element.
378 'option_key' => $field_id,
379 'radio_options' => array(
380 'true' => __( 'Yes, track logged-in users.', 'wp-parsely' ),
381 'false' => __( 'No, do not track logged-in users. I do not want to see the Parse.ly tracking code on my site when browsing while logged in.', 'wp-parsely' ),
382 ),
383 'help_text' => (
384 is_multisite() ?
385 __( ' Note: For WordPress multisite, a user must be logged-in to the current site to be considered logged-in.', 'wp-parsely' ) :
386 null
387 ),
388 )
389 );
390
391 // Disable JavaScript.
392 $field_id = 'disable_javascript';
393 add_settings_field(
394 $field_id,
395 $this->set_field_label_contents( __( 'Disable JavaScript', 'wp-parsely' ), $field_id ),
396 array( $this, 'print_radio_tags' ),
397 Parsely::MENU_SLUG,
398 $section_key,
399 array(
400 'title' => __( 'Disable JavaScript', 'wp-parsely' ), // Passed for legend element.
401 'option_key' => $field_id,
402 'radio_options' => array(
403 'true' => __( 'Yes, disable JavaScript tracking. I want to use a separate system for tracking instead of the Parse.ly plugin.', 'wp-parsely' ),
404 'false' => __( 'No, do not disable JavaScript tracking. I want the Parse.ly plugin to load the tracker.', 'wp-parsely' ),
405 ),
406 'help_text' => __( '<span style="color:#d63638">WARNING:</span> Changing this setting to "Yes" will prevent the plugin from injecting the Parse.ly tracker. Only do so if you are setting the tracker elsewhere (e.g. hardcoded, GTM, another tag manager, etc.).', 'wp-parsely' ),
407 'filter' => 'wp_parsely_load_js_tracker',
408 )
409 );
410
411 if ( defined( 'AMP__VERSION' ) ) {
412 // Disable AMP tracking.
413 $field_id = 'disable_amp';
414 add_settings_field(
415 $field_id,
416 $this->set_field_label_contents( __( 'Disable AMP Tracking', 'wp-parsely' ), $field_id ),
417 array( $this, 'print_radio_tags' ),
418 Parsely::MENU_SLUG,
419 $section_key,
420 array(
421 'title' => __( 'Disable AMP Tracking', 'wp-parsely' ), // Passed for legend element.
422 'option_key' => $field_id,
423 'radio_options' => array(
424 'true' => __( 'Yes, disable Parse.ly tracking on AMP pages. I use a different system for JavaScript tracking on AMP pages.', 'wp-parsely' ),
425 'false' => __( 'No, do not disable Parse.ly tracking on AMP pages.', 'wp-parsely' ),
426 ),
427 )
428 );
429 }
430 }
431
432 /**
433 * Registers the Content Intelligence section and its settings.
434 *
435 * @since 3.16.0
436 */
437 private function initialize_content_helper_section(): void {
438 $section_key = 'content-intelligence-section';
439
440 add_settings_section(
441 $section_key,
442 __( 'Content Intelligence', 'wp-parsely' ),
443 '__return_null',
444 Parsely::MENU_SLUG
445 );
446
447 // AI Features.
448 $field_id = 'content_helper[ai_features_enabled]';
449 $field_args = array(
450 'option_key' => $field_id,
451 'label_for' => $field_id,
452 'yes_text' => __( 'Enabled', 'wp-parsely' ),
453 'add_fieldset' => true,
454 'legend' => __( 'AI Features', 'wp-parsely' ),
455 );
456 add_settings_field(
457 $field_id,
458 __( 'AI Features', 'wp-parsely' ),
459 array( $this, 'print_checkbox_tag' ),
460 Parsely::MENU_SLUG,
461 $section_key,
462 $field_args
463 );
464
465 // Smart Linking.
466 $field_id = 'content_helper[smart_linking]';
467 $field_args = array(
468 'option_key' => $field_id,
469 'label_for' => $field_id,
470 'legend' => __( 'Smart Linking', 'wp-parsely' ),
471 );
472 add_settings_field(
473 $field_id,
474 __( 'Smart Linking', 'wp-parsely' ),
475 array( $this, 'print_content_helper_ai_feature_section' ),
476 Parsely::MENU_SLUG,
477 $section_key,
478 $field_args
479 );
480
481 // Title Suggestions.
482 $field_id = 'content_helper[title_suggestions]';
483 $field_args = array(
484 'option_key' => $field_id,
485 'label_for' => $field_id,
486 'legend' => __( 'Title Suggestions', 'wp-parsely' ),
487 );
488 add_settings_field(
489 $field_id,
490 __( 'Title Suggestions', 'wp-parsely' ),
491 array( $this, 'print_content_helper_ai_feature_section' ),
492 Parsely::MENU_SLUG,
493 $section_key,
494 $field_args
495 );
496
497 // Excerpt Suggestions.
498 $field_id = 'content_helper[excerpt_suggestions]';
499 $field_args = array(
500 'option_key' => $field_id,
501 'label_for' => $field_id,
502 'legend' => __( 'Excerpt Suggestions', 'wp-parsely' ),
503 'filter' => Excerpt_Suggestions::get_feature_filter_name(),
504 );
505 add_settings_field(
506 $field_id,
507 __( 'Excerpt Suggestions', 'wp-parsely' ),
508 array( $this, 'print_content_helper_ai_feature_section' ),
509 Parsely::MENU_SLUG,
510 $section_key,
511 $field_args
512 );
513
514 // Traffic Boost.
515 $field_id = 'content_helper[traffic_boost]';
516 $field_args = array(
517 'option_key' => $field_id,
518 'label_for' => $field_id,
519 'legend' => __( 'Engagement Boost (beta)', 'wp-parsely' ),
520 );
521 add_settings_field(
522 $field_id,
523 __( 'Engagement Boost (beta)', 'wp-parsely' ),
524 array( $this, 'print_content_helper_ai_feature_section' ),
525 Parsely::MENU_SLUG,
526 $section_key,
527 $field_args
528 );
529 }
530
531 /**
532 * Registers section and settings for Recrawl section.
533 *
534 * @since 3.2.0
535 */
536 private function initialize_recrawl_section(): void {
537 $section_key = 'recrawl-section';
538
539 add_settings_section(
540 $section_key,
541 __( 'Recrawl', 'wp-parsely' ),
542 function (): void {
543 echo '<br /><strong>' . wp_kses_post( __( '<span style="color:#d63638">Important:</span> Changing any of these values below on a site currently tracked with Parse.ly will require reprocessing of your Parse.ly data.', 'wp-parsely' ) ) . '</strong><br />';
544 printf(
545 /* translators: Mailto link */
546 esc_html__( 'Once you have changed a value and saved, please contact %s to request a recrawl.', 'wp-parsely' ),
547 wp_kses_post( '<a href="mailto:support@parsely.com?subject=' . rawurlencode( 'Please reprocess ' . $this->parsely->get_site_id() ) . '">support@parsely.com</a>' )
548 );
549 },
550 Parsely::MENU_SLUG
551 );
552
553 // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
554 $field_id = 'track_post_types_as';
555 $field_help = __( 'By default, Parse.ly only tracks posts and pages. If you want to track other post types, select how you want to track them here.', 'wp-parsely' );
556 add_settings_field(
557 $field_id,
558 __( 'Track Post Types as', 'wp-parsely' ),
559 array( $this, 'print_track_post_types_table' ),
560 Parsely::MENU_SLUG,
561 $section_key,
562 array(
563 'title' => __( 'Track Post Types as', 'wp-parsely' ),
564 'option_key' => $field_id,
565 'help_text' => $field_help,
566 )
567 );
568
569 // Use full metadata in non-posts.
570 $field_id = 'full_metadata_in_non_posts';
571 add_settings_field(
572 $field_id,
573 $this->set_field_label_contents( __( 'Use Full Metadata in Non-Posts', 'wp-parsely' ), $field_id ),
574 array( $this, 'print_radio_tags' ),
575 Parsely::MENU_SLUG,
576 $section_key,
577 array(
578 'title' => __( 'Use Full Metadata in Non-Posts', 'wp-parsely' ), // Passed for legend element.
579 'option_key' => $field_id,
580 'radio_options' => array(
581 'true' => __( 'Yes, add full metadata to Post Types being tracked as Non-Posts.', 'wp-parsely' ),
582 'false' => __( 'No, we have code that modifies metadata and that has not been tested for compatibility yet.', 'wp-parsely' ),
583 ),
584 'help_text' => __( '<strong><span style="color:#d63638">Important: This setting will be removed in the future, force-enabling this behavior.</span></strong> If you\'re using any code that modifies metadata in a way that conflicts with this setting when it is enabled, please apply any needed fixes.', 'wp-parsely' ),
585 )
586 );
587
588 // Content ID Prefix.
589 $field_id = 'content_id_prefix';
590 $field_args = array(
591 'option_key' => $field_id,
592 'optional_args' => array(
593 'placeholder' => 'WP-',
594 ),
595 'help_text' => __( 'If you use more than one content management system (e.g. WordPress and Drupal), you may end up with duplicate content IDs. Adding a Content ID Prefix will ensure the content IDs from WordPress will not conflict with other content management systems. We recommend using "WP-" for your prefix.', 'wp-parsely' ),
596 'label_for' => $field_id,
597 );
598 add_settings_field(
599 $field_id,
600 $this->set_field_label_contents( __( 'Content ID Prefix', 'wp-parsely' ), $field_id ),
601 array( $this, 'print_text_tag' ),
602 Parsely::MENU_SLUG,
603 $section_key,
604 $field_args
605 );
606
607 // Use top-level categories.
608 $field_id = 'use_top_level_cats';
609 add_settings_field(
610 $field_id,
611 $this->set_field_label_contents( __( 'Use Top-Level Categories for Section', 'wp-parsely' ), $field_id ),
612 array( $this, 'print_radio_tags' ),
613 Parsely::MENU_SLUG,
614 $section_key,
615 array(
616 'title' => __( 'Use Top-Level Categories for Section', 'wp-parsely' ), // Passed for legend element.
617 'option_key' => $field_id,
618 'radio_options' => array(
619 'true' => __( 'Yes, use the first category assigned to a post as the section name.', 'wp-parsely' ),
620 'false' => __( 'No, do not use the first category assigned to a post as the section name.', 'wp-parsely' ),
621 ),
622 'help_text' => __( 'If you choose Yes, and post a story to News > National > Florida, the plugin will use "News" for the section name in your dashboard instead of "Florida".', 'wp-parsely' ),
623 )
624 );
625
626 // Allow use of custom taxonomy to populate articleSection in parselyPage; defaults to category.
627 $field_id = 'custom_taxonomy_section';
628 $field_args = array(
629 'option_key' => $field_id,
630 'help_text' => __( 'By default, the section value in your Parse.ly dashboard maps to a post\'s category. You can optionally choose a custom taxonomy, if you\'ve created one, to populate the section value instead.', 'wp-parsely' ),
631 'select_options' => self::get_section_taxonomies(),
632 'label_for' => Parsely::OPTIONS_KEY . "[$field_id]",
633 );
634 add_settings_field(
635 $field_id,
636 $this->set_field_label_contents( __( 'Use Custom Taxonomy for Section', 'wp-parsely' ), $field_id ),
637 array( $this, 'print_select_tag' ),
638 Parsely::MENU_SLUG,
639 $section_key,
640 $field_args
641 );
642
643 // Use categories and custom taxonomies as tags.
644 $field_id = 'cats_as_tags';
645 add_settings_field(
646 $field_id,
647 $this->set_field_label_contents( __( 'Add Categories to Tags', 'wp-parsely' ), $field_id ),
648 array( $this, 'print_radio_tags' ),
649 Parsely::MENU_SLUG,
650 $section_key,
651 array(
652 'title' => __( 'Add Categories to Tags', 'wp-parsely' ), // Passed for legend element.
653 'option_key' => $field_id,
654 'radio_options' => array(
655 'true' => __( 'Yes, add all assigned categories and taxonomies to my tags.', 'wp-parsely' ),
656 'false' => __( 'No, do not add all assigned categories and taxonomies to my tags.', 'wp-parsely' ),
657 ),
658 'help_text' => __( 'If you choose Yes, then a post that has been assigned the categories "Business/Tech" and "Business/Social" will automatically include "Business/Tech" and "Business/Social" as tags, too.', 'wp-parsely' ),
659 )
660 );
661
662 // Lowercase all tags.
663 $field_id = 'lowercase_tags';
664 add_settings_field(
665 $field_id,
666 $this->set_field_label_contents( __( 'Lowercase All Tags', 'wp-parsely' ), $field_id ),
667 array( $this, 'print_radio_tags' ),
668 Parsely::MENU_SLUG,
669 $section_key,
670 array(
671 'title' => __( 'Lowercase All Tags', 'wp-parsely' ), // Passed for legend element.
672 'option_key' => $field_id,
673 'radio_options' => array(
674 'true' => __( 'Yes, use lowercase versions of my tags to correct for potential misspellings.', 'wp-parsely' ),
675 'false' => __( 'No, do not use lowercase versions of my tags to correct for potential misspellings.', 'wp-parsely' ),
676 ),
677 )
678 );
679
680 $field_id = 'force_https_canonicals';
681 add_settings_field(
682 $field_id,
683 $this->set_field_label_contents( __( 'Force HTTPS Canonicals', 'wp-parsely' ), $field_id ),
684 array( $this, 'print_radio_tags' ),
685 Parsely::MENU_SLUG,
686 $section_key,
687 array(
688 'title' => __( 'Force HTTPS Canonicals', 'wp-parsely' ), // Passed for legend element.
689 'option_key' => $field_id,
690 'radio_options' => array(
691 'true' => __( 'Yes, force <code>https</code> canonical URLs by default.', 'wp-parsely' ),
692 'false' => __( 'No, I want to use <code>http</code>.', 'wp-parsely' ),
693 ),
694 'help_text' => __( 'Note: the plugin uses <code>http</code> by default, and this is fine for most publishers. It is unlikely you will have to change this unless directed to do so by a Parse.ly support representative.', 'wp-parsely' ),
695 )
696 );
697 }
698
699 /**
700 * Registers section and settings for Advanced section.
701 *
702 * @since 3.2.0
703 */
704 private function initialize_advanced_section(): void {
705 $are_credentials_managed = $this->parsely->are_credentials_managed;
706 $section_key = 'advanced-section';
707
708 add_settings_section(
709 $section_key,
710 __( 'Advanced', 'wp-parsely' ),
711 '__return_null',
712 Parsely::MENU_SLUG
713 );
714
715 if ( ! $are_credentials_managed ) {
716 // Metadata Secret.
717 $field_id = 'metadata_secret';
718 $field_args = array(
719 'option_key' => $field_id,
720 'help_text' => __( 'Your metadata secret is given to you by Parse.ly support. DO NOT enter anything here unless given to you by Parse.ly support!', 'wp-parsely' ),
721 'label_for' => $field_id,
722 'optional_args' => array(
723 'type' => 'password',
724 'is_obfuscated_value' => true,
725 ),
726 );
727 add_settings_field(
728 $field_id,
729 __( 'Metadata Secret', 'wp-parsely' ),
730 array( $this, 'print_text_tag' ),
731 Parsely::MENU_SLUG,
732 $section_key,
733 $field_args
734 );
735 }
736
737 // Disable autotrack.
738 $field_id = 'disable_autotrack';
739 add_settings_field(
740 $field_id,
741 $this->set_field_label_contents( __( 'Disable Autotracking', 'wp-parsely' ), $field_id ),
742 array( $this, 'print_radio_tags' ),
743 Parsely::MENU_SLUG,
744 $section_key,
745 array(
746 'title' => __( 'Disable Autotracking', 'wp-parsely' ), // Passed for legend element.
747 'option_key' => $field_id,
748 'radio_options' => array(
749 'true' => __( 'Yes, disable autotracking. I do not want the tracking code to report an event as soon as the script has finished loading. I plan to implement Dynamic Tracking myself.', 'wp-parsely' ),
750 'false' => __( 'No, do not disable autotracking. I want to make sure the default behavior of the tracking code is in place. The tracking code should report an event as soon as the script has finished loading.', 'wp-parsely' ),
751 ),
752 )
753 );
754 }
755
756 /**
757 * Shows setting tabs.
758 *
759 * @since 3.8.0
760 */
761 public function show_setting_tabs(): void {
762 global $wp_settings_sections;
763 ?>
764
765 <nav class="nav-tab-wrapper">
766 <?php foreach ( $wp_settings_sections[ Parsely::MENU_SLUG ] as $section ) { ?>
767 <a
768 class="nav-tab <?php echo esc_attr( $section['id'] . '-tab' ); ?>"
769 href=<?php echo esc_url_raw( '?page=' . Parsely::MENU_SLUG . '#' . $section['id'] ); ?>
770 >
771 <?php echo esc_html( $section['title'] ); ?>
772 </a>
773 <?php } ?>
774 </nav>
775
776 <?php
777 }
778
779 /**
780 * Shows content of setting tabs.
781 *
782 * @since 3.8.0
783 */
784 public function show_setting_tabs_content(): void {
785 global $wp_settings_sections;
786
787 foreach ( $wp_settings_sections[ Parsely::MENU_SLUG ] as $section ) {
788 ?>
789
790 <div class="tab-content <?php echo esc_attr( $section['id'] ); ?>">
791 <?php
792 if ( $section['callback'] ) {
793 call_user_func( $section['callback'], $section );
794 }
795 ?>
796
797 <table class="form-table" role="presentation">
798 <?php do_settings_fields( Parsely::MENU_SLUG, $section['id'] ); ?>
799 </table>
800 </div>
801
802 <?php
803 }
804 }
805
806 /**
807 * Prints out a warning if the filter for the setting is defined, if any.
808 *
809 * @since 3.4.0
810 *
811 * @param Setting_Arguments $args The arguments for the form field. May contain 'filter'.
812 */
813 private function print_filter_text( $args ): void {
814 if ( isset( $args['filter'] ) && has_filter( $args['filter'] ) ) {
815 echo '<p>';
816 echo '<b><code>' . esc_html( $args['filter'] ) . '</code>' . esc_html__( 'filter hook is in use!', 'wp-parsely' ) . '</b> ';
817 echo esc_html__( 'A callback is attached to the filter hook that might interfere and override this setting.', 'wp-parsely' );
818 echo '</p>';
819 }
820 }
821
822 /**
823 * Prints out the description text, if there is any.
824 *
825 * @since 3.1.0
826 *
827 * @param Setting_Arguments $args The arguments for the form field. May contain 'help_text'.
828 */
829 private function print_description_text( $args ): void {
830 echo isset( $args['help_text'] ) ? '<p class="description" id="' . esc_attr( $args['option_key'] ) . '-description">' . wp_kses_post( $args['help_text'] ) . '</p>' : '';
831 }
832
833 /**
834 * Prints out an input text tag.
835 *
836 * @param Setting_Arguments $args The arguments for text tag.
837 */
838 public function print_text_tag( $args ): void {
839 $options = $this->parsely->get_options();
840 $name = $args['option_key'];
841 /**
842 * Variable.
843 *
844 * @var string
845 */
846 $value = $options[ $name ] ?? '';
847 $optional_args = $args['optional_args'] ?? array();
848 $id = esc_attr( $name );
849 $name = Parsely::OPTIONS_KEY . "[$id]";
850 $is_obfuscated_value = $optional_args['is_obfuscated_value'] ?? false;
851 $value = $is_obfuscated_value ? $this->get_obfuscated_value( $value ) : esc_attr( $value );
852 $accepted_args = array( 'placeholder', 'required', 'disabled' );
853 $type = $optional_args['type'] ?? 'text';
854
855 $is_managed = key_exists( $id, $this->parsely->managed_options );
856 echo '<fieldset', $is_managed ? ' disabled>' : '>';
857 printf( "<input type='%s' name='%s' id='%s' value='%s'", esc_attr( $type ), esc_attr( $name ), esc_attr( $id ), esc_attr( $value ) );
858
859 if ( isset( $args['help_text'] ) ) {
860 echo ' aria-describedby="' . esc_attr( $id ) . '-description"';
861 }
862
863 foreach ( $optional_args as $key => $val ) {
864 if ( \in_array( $key, $accepted_args, true ) && false !== $val ) {
865 // Don't add a placeholder to managed option fields.
866 if ( $is_managed && 'placeholder' === $key ) {
867 continue;
868 }
869
870 // Support attributes without values.
871 echo ' ' . esc_attr( $key );
872 if ( true !== $val ) {
873 echo '="' . esc_attr( $val ) . '"';
874 }
875 }
876 }
877 echo ' /></fieldset>';
878
879 $this->print_description_text( $args );
880 }
881
882 /**
883 * Prints a checkbox tag.
884 *
885 * @since 3.16.0
886 *
887 * @param Setting_Arguments $args Arguments for the checkbox tag.
888 * @param Parsely_Options|null $options The options to use.
889 */
890 public function print_checkbox_tag( $args, $options = null ): void {
891 $options = $options ?? $this->parsely->get_options();
892 $name = $args['option_key'];
893 $has_fieldset = isset( $args['add_fieldset'] ) && true === $args['add_fieldset'];
894 $html_id = rtrim( str_replace( array( '[', ']', '__' ), '_', $name ), '_' );
895 $html_name = str_replace(
896 'content_helper',
897 '[content_helper]',
898 Parsely::OPTIONS_KEY . esc_attr( $name )
899 );
900 $yes_text = $args['yes_text'] ?? '';
901
902 // Get option value.
903 if ( false === strpos( $name, '[' ) ) {
904 $value = $options[ $name ];
905 } else {
906 $value = Parsely::get_nested_option_value( $name, $options );
907 }
908
909 // Fieldset start.
910 if ( $has_fieldset ) {
911 echo '<fieldset>';
912 if ( isset( $args['legend'] ) ) {
913 echo '<legend class="screen-reader-text"><span>';
914 echo esc_html( $args['legend'] ) . '</span></legend>';
915 }
916 }
917
918 // Label and contained checkbox.
919 printf( '<label for="%s">', esc_attr( $html_id ) );
920 printf(
921 '<input type="checkbox" name="%s" id="%s" value="true" ',
922 esc_attr( $html_name ),
923 esc_attr( $html_id )
924 );
925 if ( isset( $args['help_text'] ) ) {
926 echo ' aria-describedby="' . esc_attr( $html_id ) . '-description"';
927 }
928 checked( true === $value, true );
929 printf( ' /> %s</label>', esc_html( $yes_text ) );
930
931 // Fieldset end.
932 if ( $has_fieldset ) {
933 echo '</fieldset>';
934 }
935
936 $this->print_description_text( $args );
937 }
938
939 /**
940 * Prints a Content Intelligence AI feature section.
941 *
942 * @since 3.16.0
943 *
944 * @param Setting_Arguments $args The arguments for the section.
945 */
946 public function print_content_helper_ai_feature_section( $args ): void {
947 $options = $this->parsely->get_options();
948 $feature_id = str_replace(
949 array( 'content_helper[', ']' ),
950 '',
951 $args['option_key']
952 );
953
954 // Convert user role settings to make them work in the settings page.
955 $option = &$options['content_helper'][ $feature_id ];
956 if ( isset( $option['allowed_user_roles'] ) ) {
957 foreach ( $option['allowed_user_roles'] as $role ) {
958 $option['allowed_user_roles'][ $role ] = true;
959 }
960 }
961
962 // Feature "Enabled" checkbox.
963 $enabled_args = array(
964 'option_key' => $args['option_key'] . '[enabled]',
965 'label_for' => $args['option_key'] . '[enabled]',
966 'yes_text' => __( 'Enabled', 'wp-parsely' ),
967 'add_fieldset' => true,
968 );
969 if ( isset( $args['legend'] ) ) {
970 $enabled_args['legend'] = $args['legend'];
971 }
972 $this->print_checkbox_tag( $enabled_args, $options );
973
974 // User role permissions fieldset.
975 echo '<p>' . esc_html__( 'User Permissions', 'wp-parsely' ) . '</p>';
976 echo '<fieldset class="user-role-permissions">';
977 echo '<legend class="screen-reader-text"><span>';
978 printf(
979 /* translators: %s: Feature name */
980 esc_html__( '%s User Permissions', 'wp-parsely' ),
981 esc_html( $args['legend'] ?? __( 'Feature', 'wp-parsely' ) )
982 );
983 echo '</span></legend>';
984
985 // User role checkboxes.
986 $user_roles = Permissions::get_user_roles_with_edit_posts_cap();
987 foreach ( $user_roles as $key => $name ) {
988 $option_key = $args['option_key'] . '[allowed_user_roles][' . $key . ']';
989 $role_args = array(
990 'option_key' => $option_key,
991 'label_for' => $option_key,
992 'yes_text' => translate_user_role( $name ),
993 );
994
995 $this->print_checkbox_tag( $role_args, $options );
996 }
997
998 echo '</fieldset>';
999
1000 $this->print_filter_text( $args );
1001 }
1002
1003 /**
1004 * Prints out the select tags
1005 *
1006 * @param Setting_Arguments $args The arguments for the select dropdowns.
1007 */
1008 public function print_select_tag( $args ): void {
1009 $options = $this->parsely->get_options();
1010 $name = $args['option_key'];
1011 $select_options = $args['select_options'] ?? array();
1012 $selected = $options[ $name ] ?? null;
1013 $id = esc_attr( $name );
1014 $name = Parsely::OPTIONS_KEY . "[$id]";
1015
1016 $is_managed = key_exists( $id, $this->parsely->managed_options );
1017 echo '<fieldset', $is_managed ? ' disabled>' : '>';
1018 printf( "<select name='%s' id='%s'", esc_attr( $name ), esc_attr( $name ) );
1019 if ( isset( $args['help_text'] ) ) {
1020 echo ' aria-describedby="' . esc_attr( $id ) . '-description"';
1021 }
1022 echo '>';
1023
1024 foreach ( $select_options as $key => $val ) {
1025 echo '<option value="' . esc_attr( $key ) . '" ';
1026 echo selected( $selected, $key, false ) . '>';
1027 echo esc_html( $val );
1028 echo '</option>';
1029 }
1030 echo '</select></fieldset>';
1031
1032 $this->print_filter_text( $args );
1033 $this->print_description_text( $args );
1034 }
1035
1036 /**
1037 * Prints the radio buttons.
1038 *
1039 * @param Setting_Arguments $args The arguments for the radio buttons.
1040 */
1041 public function print_radio_tags( $args ): void {
1042 $name = $args['option_key'];
1043 $id = esc_attr( $name );
1044 $selected = $this->parsely->get_options()[ $name ];
1045 $title = $args['title'] ?? '';
1046 $radio_options = $args['radio_options'] ?? array();
1047
1048 if ( is_bool( $selected ) ) {
1049 // Converting boolean to string so that we have string type keys for all cases.
1050 $selected = $selected ? 'true' : 'false';
1051 }
1052
1053 $is_managed = key_exists( $name, $this->parsely->managed_options );
1054 ?>
1055 <fieldset <?php echo $is_managed ? 'disabled' : ''; ?>>
1056 <legend class="screen-reader-text"><span><?php echo esc_html( $title ); ?></span></legend>
1057 <p>
1058 <?php foreach ( $radio_options as $value => $text ) { ?>
1059 <label for="<?php echo esc_attr( "{$id}_{$value}" ); ?>">
1060 <input
1061 type="radio"
1062 name="<?php echo esc_attr( Parsely::OPTIONS_KEY . "[$id]" ); ?>"
1063 id="<?php echo esc_attr( "{$id}_{$value}" ); ?>"
1064 value="<?php echo esc_attr( $value ); ?>"
1065 <?php checked( $selected, $value ); ?>
1066 />
1067 <?php echo wp_kses_post( $text ); ?>
1068 </label>
1069 <br />
1070 <?php } ?>
1071 </p>
1072 </fieldset>
1073 <?php
1074 $this->print_filter_text( $args );
1075 $this->print_description_text( $args );
1076 }
1077
1078 /**
1079 * Prints out a "single-image browse control" which includes a text input to
1080 * store image path and a button to browse for images.
1081 *
1082 * @param Setting_Arguments $args The arguments for the control.
1083 */
1084 public function print_media_single_image( $args ): void {
1085 $key = $args['option_key'];
1086 $title = $args['title'] ?? '';
1087 /**
1088 * Variable.
1089 *
1090 * @var string
1091 */
1092 $input_value = $this->parsely->get_options()[ $key ];
1093 $input_name = Parsely::OPTIONS_KEY . "[$key]";
1094 $button_text = __( 'Browse', 'wp-parsely' );
1095
1096 $is_managed = key_exists( $key, $this->parsely->managed_options );
1097 ?>
1098
1099 <fieldset class="media-single-image" id="media-single-image-<?php echo esc_attr( $key ); ?>"<?php echo $is_managed ? ' disabled' : ''; ?>>
1100 <legend class="screen-reader-text"><span><?php echo esc_html( $title ); ?></span></legend>
1101 <input class="file-path" type="text" name="<?php echo esc_attr( $input_name ); ?>" id="logo" value="<?php echo esc_attr( $input_value ); ?>" />
1102 <button data-option="<?php echo esc_attr( $key ); ?>" class="browse button" type="button"><?php echo esc_html( $button_text ); ?></button>
1103 </fieldset>
1104
1105 <?php
1106 $this->print_description_text( $args );
1107 }
1108
1109 /**
1110 * Prints out the post tracking options table.
1111 *
1112 * @since 3.2.0
1113 *
1114 * @param Setting_Arguments $args The arguments used in the output HTML elements.
1115 */
1116 public function print_track_post_types_table( $args ): void {
1117 $option_key = esc_attr( $args['option_key'] );
1118 $title = $args['title'] ?? '';
1119 /**
1120 * Variable.
1121 *
1122 * @var array<string>
1123 */
1124 $post_types = get_post_types( array( 'public' => true ) );
1125 $values = $this->get_tracking_values_for_display();
1126 ?>
1127 <fieldset>
1128 <legend class="screen-reader-text"><span><?php echo esc_html( $title ); ?></span></legend>
1129 <table class="form-table widefat striped" id="track-post-types">
1130 <caption class="screen-reader-text"><?php echo esc_html( $title ); ?></caption>
1131 <thead>
1132 <tr>
1133 <th scope="col"><?php echo esc_html__( 'Post Type', 'wp-parsely' ); ?></th>
1134 <th id="track-post-types--post" scope="col"><?php echo esc_html__( 'Track as Post', 'wp-parsely' ); ?></th>
1135 <th id="track-post-types--page" scope="col"><?php echo esc_html__( 'Track as Non-Post', 'wp-parsely' ); ?></th>
1136 <th id="track-post-types--none" scope="col"><?php echo esc_html__( 'Do not track', 'wp-parsely' ); ?></th>
1137 </tr>
1138 </thead>
1139 <tbody>
1140 <?php
1141 foreach ( $post_types as $post_type ) {
1142 $group_name = "parsely[{$option_key}][{$post_type}]";
1143 $id_post = "{$option_key}_{$post_type}_post";
1144 $id_page = "{$option_key}_{$post_type}_page";
1145 $id_none = "{$option_key}_{$post_type}_none";
1146 $value = $values[ $post_type ] ?? 'none';
1147 ?>
1148 <tr>
1149 <th scope="row"><?php echo esc_html( $post_type ); ?></th>
1150 <td>
1151 <label aria-labelledby="track-post-types--post" for="<?php echo esc_attr( $id_post ); ?>">
1152 <input id="<?php echo esc_attr( $id_post ); ?>" name="<?php echo esc_attr( $group_name ); ?>" type="radio" value="post" <?php checked( $value, 'post' ); ?> />
1153 </label>
1154 </td>
1155 <td>
1156 <label aria-labelledby="track-post-types--page" for="<?php echo esc_attr( $id_page ); ?>">
1157 <input id="<?php echo esc_attr( $id_page ); ?>" name="<?php echo esc_attr( $group_name ); ?>" type="radio" value="page" <?php checked( $value, 'page' ); ?> />
1158 </label>
1159 </td>
1160 <td>
1161 <label aria-labelledby="track-post-types--none" for="<?php echo esc_attr( $id_none ); ?>">
1162 <input id="<?php echo esc_attr( $id_none ); ?>" name="<?php echo esc_attr( $group_name ); ?>" type="radio" value="none" <?php checked( $value, 'none' ); ?> />
1163 </label>
1164 </td>
1165 </tr>
1166 <?php } ?>
1167 </tbody>
1168 </table>
1169 </fieldset>
1170 <?php
1171 $this->print_filter_text( $args );
1172 $this->print_description_text( $args );
1173 }
1174
1175 /**
1176 * Returns the custom post type tracking values in a format that is easily
1177 * consumable by the print_track_post_types_table() function.
1178 *
1179 * @since 3.2.0
1180 *
1181 * @return array<string> Key-value pairs with post type and their 'track as' value.
1182 */
1183 public function get_tracking_values_for_display(): array {
1184 $options = $this->parsely->get_options();
1185 $types = array( 'post', 'page' );
1186 $result = array();
1187
1188 foreach ( $types as $type ) {
1189 $array_value = $options[ "track_{$type}_types" ];
1190
1191 foreach ( $array_value as $post_type ) {
1192 $result[ $post_type ] = $type;
1193 }
1194 }
1195
1196 return $result;
1197 }
1198
1199 /**
1200 * Validates the options provided by the user.
1201 *
1202 * @param ParselySettingOptions $input Options from the settings page.
1203 * @return ParselySettingOptions
1204 */
1205 public function validate_options( $input ) {
1206 $input = $this->validate_basic_section( $input );
1207 $input = $this->validate_content_helper_section( $input );
1208 $input = $this->validate_recrawl_section( $input );
1209 $input = $this->validate_advanced_section( $input );
1210
1211 return $input;
1212 }
1213
1214 /**
1215 * Validates fields of Basic Section.
1216 *
1217 * @param ParselySettingOptions $input Options from the settings page.
1218 * @return ParselySettingOptions Validated inputs.
1219 */
1220 private function validate_basic_section( $input ): array {
1221 $are_credentials_managed = $this->parsely->are_credentials_managed;
1222 $options = $this->parsely->get_options();
1223
1224 if ( $are_credentials_managed ) {
1225 $input['apikey'] = '';
1226 $input['api_secret'] = '';
1227 } else {
1228 $site_id = $this->sanitize_site_id( $input['apikey'] );
1229 $api_secret = $this->get_unobfuscated_value( $input['api_secret'], $this->parsely->get_api_secret() );
1230
1231 $valid_credentials = Validator::validate_api_credentials( $this->parsely, $site_id, $api_secret );
1232
1233 // When running e2e tests, we need to update the API keys without validating them, as they will be invalid.
1234 // phpcs:ignore WordPress.Security.NonceVerification.Missing
1235 if ( isset( $_POST['e2e_parsely_skip_api_validate'] ) && 'y' === $_POST['e2e_parsely_skip_api_validate'] ) {
1236 $valid_credentials = true;
1237 }
1238
1239 if (
1240 ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) ||
1241 ( is_bool( $valid_credentials ) && ! $valid_credentials )
1242 ) {
1243 add_settings_error(
1244 Parsely::OPTIONS_KEY,
1245 'api_secret',
1246 __( 'The Site ID and API Secret weren\'t saved as they failed to authenticate with the Parse.ly API. Try again with different credentials or contact Parse.ly support', 'wp-parsely' )
1247 );
1248 $input['apikey'] = $options['apikey'];
1249 $input['api_secret'] = $options['api_secret'];
1250 }
1251
1252 // Since the API secret is obfuscated, we need to make sure that the value
1253 // is not changed when the credentials are valid.
1254 if ( true === $valid_credentials && $input['api_secret'] !== $api_secret ) {
1255 $input['api_secret'] = $api_secret;
1256 }
1257 }
1258
1259 if ( ! isset( $input['meta_type'] ) ) {
1260 $input['meta_type'] = $options['meta_type'];
1261 } else {
1262 $input['meta_type'] = sanitize_text_field( $input['meta_type'] );
1263 }
1264
1265 if ( '' === $input['logo'] ) {
1266 $input['logo'] = self::get_logo_default();
1267 }
1268
1269 // Track authenticated users.
1270 if ( 'true' !== $input['track_authenticated_users'] && 'false' !== $input['track_authenticated_users'] ) {
1271 add_settings_error(
1272 Parsely::OPTIONS_KEY,
1273 'track_authenticated_users',
1274 __( 'Value passed for track_authenticated_users must be either "true" or "false".', 'wp-parsely' )
1275 );
1276 } else {
1277 $input['track_authenticated_users'] = 'true' === $input['track_authenticated_users'];
1278 }
1279
1280 if ( 'true' !== $input['disable_javascript'] && 'false' !== $input['disable_javascript'] ) {
1281 add_settings_error(
1282 Parsely::OPTIONS_KEY,
1283 'disable_javascript',
1284 __( 'Value passed for disable_javascript must be either "Yes" or "No".', 'wp-parsely' )
1285 );
1286 } else {
1287 $input['disable_javascript'] = 'true' === $input['disable_javascript'];
1288 }
1289
1290 // Allow for Disable AMP setting to be conditionally included on the page.
1291 // If it's not shown, then set the value as what was previously saved.
1292 if ( ! isset( $input['disable_amp'] ) ) {
1293 $input['disable_amp'] = 'true';
1294 if ( false === $options['disable_amp'] ) {
1295 $input['disable_amp'] = 'false';
1296 }
1297 }
1298
1299 if ( 'true' !== $input['disable_amp'] && 'false' !== $input['disable_amp'] ) {
1300 add_settings_error(
1301 Parsely::OPTIONS_KEY,
1302 'disable_amp',
1303 __( 'Value passed for disable_amp must be either "true" or "false".', 'wp-parsely' )
1304 );
1305 } else {
1306 $input['disable_amp'] = 'true' === $input['disable_amp'];
1307 }
1308
1309 return $input;
1310 }
1311
1312 /**
1313 * Validates the fields of the Content Intelligence section.
1314 *
1315 * @since 3.16.0
1316 *
1317 * @param ParselySettingOptions $input The settings page options.
1318 * @return ParselySettingOptions The validated input.
1319 */
1320 private function validate_content_helper_section( $input ) {
1321 /**
1322 * Sanitizes the Content Intelligence data.
1323 *
1324 * @since 3.16.0
1325 */
1326 $sanitize = function ( $input ) use ( &$sanitize ) {
1327 foreach ( $input as $key => $value ) {
1328 if ( is_array( $value ) ) {
1329 if ( 'allowed_user_roles' === $key && count( $input[ $key ] ) > 0 ) {
1330 $passed_roles = array_keys( $input[ $key ] );
1331 $valid_roles = array_keys(
1332 Permissions::get_user_roles_with_edit_posts_cap()
1333 );
1334 $sanitized_roles = array();
1335
1336 // Sanitize passed user roles and remove invalid ones.
1337 foreach ( $passed_roles as $user_role ) {
1338 if ( ! is_string( $user_role ) ) {
1339 continue;
1340 }
1341
1342 $user_role = sanitize_text_field( $user_role );
1343 if ( in_array( $user_role, $valid_roles, true ) ) {
1344 $sanitized_roles[] = $user_role;
1345 }
1346 }
1347
1348 $input[ $key ] = $sanitized_roles;
1349 } else {
1350 // Recurse when we have an array that's not user roles.
1351 $input[ $key ] = $sanitize( $value );
1352 }
1353 } else {
1354 // Enforce a boolean value.
1355 $input[ $key ] = 'true' === $value || true === $value;
1356 }
1357 }
1358
1359 return $input;
1360 };
1361
1362 // Add any missing data due to unchecked checkboxes.
1363 if ( ! isset( $input['content_helper']['ai_features_enabled'] ) ) {
1364 $input['content_helper']['ai_features_enabled'] = false;
1365 }
1366 foreach ( $this->configurable_pch_features as $feature_id ) {
1367 if ( ! isset( $input['content_helper'][ $feature_id ] ) ) {
1368 $input['content_helper'][ $feature_id ] = array(
1369 'enabled' => false,
1370 'allowed_user_roles' => array(),
1371 );
1372 } else {
1373 if ( ! isset( $input['content_helper'][ $feature_id ]['enabled'] ) ) {
1374 // @phpstan-ignore-next-line
1375 $input['content_helper'][ $feature_id ]['enabled'] = false;
1376 }
1377 if ( ! isset( $input['content_helper'][ $feature_id ]['allowed_user_roles'] ) ) {
1378 // @phpstan-ignore-next-line
1379 $input['content_helper'][ $feature_id ]['allowed_user_roles'] = array();
1380 }
1381 }
1382 }
1383
1384 // Produce the final array.
1385 $options = $this->parsely->get_options()['content_helper'];
1386 $merged = array_merge( $options, $input['content_helper'] );
1387
1388 $input['content_helper'] = $sanitize( $merged );
1389
1390 return $input;
1391 }
1392
1393 /**
1394 * Validates fields of Recrawl Section.
1395 *
1396 * @param ParselySettingOptions $input Options from the settings page.
1397 * @return ParselySettingOptions Validated inputs.
1398 */
1399 private function validate_recrawl_section( $input ) {
1400 $options = $this->parsely->get_options();
1401
1402 $this->validate_options_post_type_tracking( $input );
1403
1404 // Content ID prefix.
1405 if ( ! isset( $input['content_id_prefix'] ) ) {
1406 $input['content_id_prefix'] = $options['content_id_prefix'];
1407 } else {
1408 $input['content_id_prefix'] = sanitize_text_field( $input['content_id_prefix'] );
1409 }
1410
1411 // Full metadata in non-posts.
1412 if ( ! isset( $input['full_metadata_in_non_posts'] ) ) {
1413 $input['full_metadata_in_non_posts'] = true;
1414 } else {
1415 // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1416 $input['full_metadata_in_non_posts'] = 'true' == $input['full_metadata_in_non_posts'];
1417 }
1418
1419 // Allow for Top-level categories setting to be conditionally included on the page.
1420 // If it's not shown, then set the value as what was previously saved.
1421 if ( ! isset( $input['use_top_level_cats'] ) ) {
1422 $input['use_top_level_cats'] = 'true';
1423 if ( false === $options['use_top_level_cats'] ) {
1424 $input['use_top_level_cats'] = 'false';
1425 }
1426 }
1427
1428 // Top-level categories.
1429 if ( 'true' !== $input['use_top_level_cats'] && 'false' !== $input['use_top_level_cats'] ) {
1430 add_settings_error(
1431 Parsely::OPTIONS_KEY,
1432 'use_top_level_cats',
1433 __( 'Value passed for use_top_level_cats must be either "true" or "false".', 'wp-parsely' )
1434 );
1435 } else {
1436 $input['use_top_level_cats'] = 'true' === $input['use_top_level_cats'];
1437 }
1438
1439 // Custom taxonomy as section.
1440 if ( ! isset( $input['custom_taxonomy_section'] ) ) {
1441 $input['custom_taxonomy_section'] = $options['custom_taxonomy_section'];
1442 } else {
1443 $input['custom_taxonomy_section'] = sanitize_text_field( $input['custom_taxonomy_section'] );
1444 }
1445
1446 // Allow for Categories as Tags setting to be conditionally included on the page.
1447 // If it's not shown, then set the value as what was previously saved.
1448 if ( ! isset( $input['cats_as_tags'] ) ) {
1449 $input['cats_as_tags'] = 'true';
1450 if ( false === $options['cats_as_tags'] ) {
1451 $input['cats_as_tags'] = 'false';
1452 }
1453 }
1454
1455 // Child categories as tags.
1456 if ( 'true' !== $input['cats_as_tags'] && 'false' !== $input['cats_as_tags'] ) {
1457 add_settings_error(
1458 Parsely::OPTIONS_KEY,
1459 'cats_as_tags',
1460 __( 'Value passed for cats_as_tags must be either "true" or "false".', 'wp-parsely' )
1461 );
1462 } else {
1463 $input['cats_as_tags'] = 'true' === $input['cats_as_tags'];
1464 }
1465
1466 // Allow for Lowercase Tags setting to be conditionally included on the page.
1467 // If it's not shown, then set the value as what was previously saved.
1468 if ( ! isset( $input['lowercase_tags'] ) ) {
1469 $input['lowercase_tags'] = 'true';
1470 if ( false === $options['lowercase_tags'] ) {
1471 $input['lowercase_tags'] = 'false';
1472 }
1473 }
1474
1475 // Lowercase tags.
1476 if ( 'true' !== $input['lowercase_tags'] && 'false' !== $input['lowercase_tags'] ) {
1477 add_settings_error(
1478 Parsely::OPTIONS_KEY,
1479 'lowercase_tags',
1480 __( 'Value passed for lowercase_tags must be either "true" or "false".', 'wp-parsely' )
1481 );
1482 } else {
1483 $input['lowercase_tags'] = 'true' === $input['lowercase_tags'];
1484 }
1485
1486 // Allow for Force HTTPS Canonical setting to be conditionally included on the page.
1487 // If it's not shown, then set the value as what was previously saved.
1488 if ( ! isset( $input['force_https_canonicals'] ) ) {
1489 $input['force_https_canonicals'] = 'true';
1490 if ( false === $options['force_https_canonicals'] ) {
1491 $input['force_https_canonicals'] = 'false';
1492 }
1493 }
1494
1495 if ( 'true' !== $input['force_https_canonicals'] && 'false' !== $input['force_https_canonicals'] ) {
1496 add_settings_error(
1497 Parsely::OPTIONS_KEY,
1498 'force_https_canonicals',
1499 __( 'Value passed for force_https_canonicals must be either "true" or "false".', 'wp-parsely' )
1500 );
1501 } else {
1502 $input['force_https_canonicals'] = 'true' === $input['force_https_canonicals'];
1503 }
1504
1505 return $input;
1506 }
1507
1508 /**
1509 * Validates fields of Advanced Section.
1510 *
1511 * @param ParselySettingOptions $input Options from the settings page.
1512 * @return ParselySettingOptions Validated inputs.
1513 */
1514 private function validate_advanced_section( $input ) {
1515 $are_credentials_managed = $this->parsely->are_credentials_managed;
1516 $options = $this->parsely->get_options();
1517
1518 if ( $are_credentials_managed ) {
1519 $input['metadata_secret'] = '';
1520 } else {
1521 $input['metadata_secret'] = $this->get_unobfuscated_value( $input['metadata_secret'], $this->parsely->get_options()['metadata_secret'] );
1522 $metadata_secret_length = strlen( $input['metadata_secret'] );
1523 if ( $metadata_secret_length > 0 &&
1524 false === Validator::validate_metadata_secret( $input['metadata_secret'] ) ) {
1525 add_settings_error(
1526 Parsely::OPTIONS_KEY,
1527 'metadata_secret',
1528 __( 'The Metadata Secret was not saved because it is incorrect. Please contact Parse.ly support!', 'wp-parsely' )
1529 );
1530 $input['metadata_secret'] = $options['metadata_secret'];
1531 }
1532 }
1533
1534 if ( ! isset( $input['disable_autotrack'] ) ) {
1535 $input['disable_autotrack'] = $options['disable_autotrack'];
1536 } elseif ( 'true' !== $input['disable_autotrack'] && 'false' !== $input['disable_autotrack'] ) {
1537 add_settings_error(
1538 Parsely::OPTIONS_KEY,
1539 'disable_autotrack',
1540 __( 'Value passed for disable_autotrack must be either "Yes" or "No".', 'wp-parsely' )
1541 );
1542 } else {
1543 $input['disable_autotrack'] = 'true' === $input['disable_autotrack'];
1544 }
1545
1546 return $input;
1547 }
1548
1549 /**
1550 * Sanitizes the passed Site ID.
1551 *
1552 * @since 3.3.0
1553 *
1554 * @param string $site_id The Site ID to be sanitized.
1555 * @return string
1556 */
1557 private function sanitize_site_id( string $site_id ): string {
1558 return strtolower( sanitize_text_field( $site_id ) );
1559 }
1560
1561 /**
1562 * Receives the $input array from the validate_options() function and
1563 * validate post tracking options.
1564 *
1565 * This function will mutate the $input array.
1566 *
1567 * @since 3.2.0
1568 *
1569 * @param ParselySettingOptions $input Array passed to validate_options() function.
1570 */
1571 private function validate_options_post_type_tracking( &$input ): void {
1572 $options = $this->parsely->get_options();
1573 $posts = 'track_post_types';
1574 $pages = 'track_page_types';
1575 $track_as = 'track_post_types_as';
1576 $input[ $posts ] = $options[ $posts ];
1577 $input[ $pages ] = $options[ $pages ];
1578
1579 // @phpstan-ignore-next-line
1580 if ( isset( $input[ $track_as ] ) && is_array( $input[ $track_as ] ) && 0 < count( $input[ $track_as ] ) ) {
1581 $post_types = get_post_types( array( 'public' => true ) );
1582 $temp_posts = array();
1583 $temp_pages = array();
1584
1585 // Create temporary Post and Page arrays, disallowing non-existent post types.
1586 foreach ( $input[ $track_as ] as $key => $value ) {
1587 if ( false === in_array( $key, $post_types, true ) ) {
1588 continue;
1589 }
1590
1591 if ( 'post' === $value ) {
1592 $temp_posts[] = $key;
1593 } elseif ( 'page' === $value ) {
1594 $temp_pages[] = $key;
1595 }
1596 }
1597
1598 // Cleanup and sanitized values assignment.
1599 $input [ $posts ] = self::sanitize_option_array( $temp_posts );
1600 $input [ $pages ] = self::sanitize_option_array( $temp_pages );
1601 }
1602
1603 if ( isset( $input[ $track_as ] ) ) {
1604 unset( $input[ $track_as ] );
1605 }
1606 }
1607
1608 /**
1609 * Returns default logo if one can be found.
1610 *
1611 * @return string
1612 */
1613 private static function get_logo_default(): string {
1614 /**
1615 * Variable.
1616 *
1617 * @var int
1618 */
1619 $custom_logo_id = get_theme_mod( 'custom_logo' );
1620 if ( (bool) $custom_logo_id ) {
1621 $logo_attrs = wp_get_attachment_image_src( $custom_logo_id, 'full' );
1622 if ( isset( $logo_attrs[0] ) ) {
1623 return $logo_attrs[0];
1624 }
1625 }
1626
1627 // get_site_icon_url returns an empty string if one isn't found,
1628 // which is what we want to use as the default anyway.
1629 return get_site_icon_url();
1630 }
1631
1632 /**
1633 * Sanitizes all elements in an option array.
1634 *
1635 * @param array<int, string> $options Array of options to be sanitized.
1636 * @return array<int, string>
1637 */
1638 private static function sanitize_option_array( array $options ): array {
1639 $sanitized_options = $options;
1640 foreach ( $options as $key => $val ) {
1641 $sanitized_options[ $key ] = sanitize_text_field( $val );
1642 }
1643
1644 return $sanitized_options;
1645 }
1646
1647 /**
1648 * Gets obfuscated value.
1649 *
1650 * @param string $current_value Current value of the field.
1651 * @return string
1652 */
1653 private function get_obfuscated_value( $current_value ): string {
1654 return str_repeat( '*', strlen( $current_value ) );
1655 }
1656
1657 /**
1658 * Gets unobfuscated value.
1659 *
1660 * @param string $current_value Current value of the field.
1661 * @param string $previous_value Previous value of the field. If current
1662 * value is obfuscated then we will use this.
1663 * @return string
1664 */
1665 private function get_unobfuscated_value( $current_value, $previous_value ): string {
1666 if ( $current_value === $this->get_obfuscated_value( $current_value ) ) {
1667 return '' === $current_value ? $current_value : $previous_value;
1668 }
1669
1670 return $current_value;
1671 }
1672
1673 /**
1674 * Returns the field label's title. If the option is managed, a badge is
1675 * also included.
1676 *
1677 * @since 3.9.0
1678 *
1679 * @param string $title The field's title.
1680 * @param string $option_id The option's ID.
1681 * @return string The resulting content.
1682 */
1683 public function set_field_label_contents( string $title, string $option_id ): string {
1684 $is_managed = key_exists( $option_id, $this->parsely->managed_options );
1685
1686 if ( $is_managed ) {
1687 $text = $this->managed_options_badge['text'] ?? '';
1688 if ( ! is_string( $text ) || '' === $text ) {
1689 return $title;
1690 }
1691
1692 $url = filter_var( $this->managed_options_badge['url'] ?? '', FILTER_VALIDATE_URL );
1693
1694 if ( false === $url ) {
1695 $badge = '<span class="managed-option-badge">' . esc_html( $text ) . '</span>';
1696 } else {
1697 $badge = '<a class="managed-option-badge" href="' . esc_url( $url ) .
1698 '" target="_blank" rel="noopener">' . esc_html( $text ) . '</a>';
1699 }
1700
1701 return "{$title}&nbsp;&nbsp;{$badge}";
1702 }
1703
1704 return $title;
1705 }
1706
1707 /**
1708 * Returns the taxonomies allowed to be used as custom_taxonomy_section
1709 * option values.
1710 *
1711 * @since 3.9.0
1712 *
1713 * @return array<string> Array of taxonomies.
1714 */
1715 public static function get_section_taxonomies(): array {
1716 return array_diff(
1717 get_taxonomies(),
1718 array(
1719 'post_tag',
1720 'nav_menu',
1721 'author',
1722 'link_category',
1723 'post_format',
1724 'wp_theme',
1725 'wp_template_part_area',
1726 )
1727 );
1728 }
1729 }
1730