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

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