PluginProbe
Parse.ly / 3.23.3
Parse.ly v3.23.3
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.23.3, at src/UI/class-settings-page.php

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