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

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