PluginProbe
Parse.ly / 3.14.1
Parse.ly v3.14.1
3.24.2 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 All 106 releases
← All changes | src/UI/class-settings-page.php +61 -784 3.24.1 → 3.14.1 View file →
@@ -9,15 +9,13 @@
9 9 declare(strict_types=1);
10 10
11 11 namespace Parsely\UI;
12 12
13 -use Parsely\Content_Helper\Excerpt_Suggestions;
14 -use Parsely\Content_Helper\Suggestion_Defaults;
15 13 use Parsely\Parsely;
16 -use Parsely\Permissions;
17 -use Parsely\Utils\Utils;
18 14 use Parsely\Validator;
19 15
16 +use function Parsely\Utils\get_asset_info;
17 +
20 18 use const Parsely\PARSELY_FILE;
21 19
22 20 /**
23 21 * Renders the wp-admin Parse.ly plugin settings page.
@@ -23,11 +21,11 @@
23 21 * Renders the wp-admin Parse.ly plugin settings page.
24 22 *
25 23 * @since 3.0.0
26 24 *
25 + * @phpstan-import-type Parsely_Options from Parsely
26 + *
27 27 * @phpstan-type Setting_Arguments array{
28 - * add_fieldset?: bool,
29 - * legend?: string,
30 28 * option_key: string,
31 29 * label_for: string,
32 30 * title?: string,
33 31 * help_text?: string,
@@ -61,33 +59,12 @@
61 59 * content_id_prefix?: string,
62 60 * use_top_level_cats?:bool|string,
63 61 * custom_taxonomy_section?: string,
64 62 * cats_as_tags?: bool|string,
65 - * content_helper: Parsely_Settings_Options_Content_Helper,
66 - * headline_testing?: Parsely_Options_Headline_Testing,
67 63 * lowercase_tags?: bool,
68 64 * force_https_canonicals?: bool,
69 65 * disable_autotrack?: bool|string,
70 66 * }
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 67 */
91 68 final class Settings_Page {
92 69 /**
93 70 * Instance of Parsely class.
@@ -114,36 +91,8 @@
114 91 */
115 92 private $managed_options_badge = array();
116 93
117 94 /**
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 95 * Constructor.
147 96 *
148 97 * @param Parsely $parsely Instance of Parsely class.
149 98 */
@@ -176,35 +125,33 @@
176 125
177 126 /**
178 127 * Enqueues all needed scripts and styles for Parse.ly plugin settings page.
179 128 *
180 - * @param string|null $hook_suffix The current page being loaded.
129 + * @param string $hook_suffix The current page being loaded.
181 130 */
182 - public function enqueue_settings_assets( ?string $hook_suffix ): void {
183 - if ( ! is_string( $hook_suffix ) || $this->hook_suffix !== $hook_suffix ) {
184 - return;
185 - }
131 + public function enqueue_settings_assets( string $hook_suffix ): void {
132 + if ( 'settings_page_parsely' === $hook_suffix ) {
133 + add_filter( 'media_library_months_with_files', '__return_empty_array' );
134 + wp_enqueue_media();
186 135
187 - add_filter( 'media_library_months_with_files', '__return_empty_array' );
188 - wp_enqueue_media();
136 + $admin_settings_asset = get_asset_info( 'build/admin-settings.asset.php' );
137 + $built_assets_url = plugin_dir_url( PARSELY_FILE ) . '/build/';
189 138
190 - $admin_settings_asset = Utils::get_asset_info( 'build/admin-settings.asset.php' );
191 - $built_assets_url = plugin_dir_url( PARSELY_FILE ) . 'build/';
139 + wp_enqueue_script(
140 + 'parsely-admin-settings',
141 + $built_assets_url . 'admin-settings.js',
142 + $admin_settings_asset['dependencies'],
143 + $admin_settings_asset['version'],
144 + true
145 + );
192 146
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 - );
147 + wp_enqueue_style(
148 + 'parsely-admin-settings',
149 + $built_assets_url . 'admin-settings.css',
150 + $admin_settings_asset['dependencies'],
151 + $admin_settings_asset['version']
152 + );
153 + }
207 154 }
208 155
209 156 /**
210 157 * Adds the Parse.ly settings page in WordPress settings menu.
@@ -209,12 +156,11 @@
209 156 /**
210 157 * Adds the Parse.ly settings page in WordPress settings menu.
211 158 */
212 159 public function add_settings_sub_menu(): void {
213 - $suffix = add_submenu_page(
214 - 'parsely-dashboard-page',
160 + $suffix = add_options_page(
215 161 __( 'Parse.ly Settings', 'wp-parsely' ),
216 - __( 'Settings', 'wp-parsely' ),
162 + __( 'Parse.ly', 'wp-parsely' ),
217 163 Parsely::CAPABILITY, // phpcs:ignore WordPress.WP.Capabilities.Undetermined
218 164 Parsely::MENU_SLUG,
219 165 array( $this, 'display_settings' )
220 166 );
@@ -248,9 +194,9 @@
248 194 );
249 195 }
250 196
251 197 /**
252 - * Displays the Parse.ly settings screen (admin.php?page=[SLUG]).
198 + * Displays the Parse.ly settings screen (options-general.php?page=[SLUG]).
253 199 */
254 200 public function display_settings(): void {
255 201 // phpcs:ignore WordPress.WP.Capabilities.Undetermined
256 202 if ( ! current_user_can( Parsely::CAPABILITY ) ) {
@@ -256,9 +202,9 @@
256 202 if ( ! current_user_can( Parsely::CAPABILITY ) ) {
257 203 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-parsely' ) );
258 204 }
259 205
260 - include_once plugin_dir_path( PARSELY_FILE ) . 'src/UI/settings-page.php';
206 + include_once plugin_dir_path( PARSELY_FILE ) . 'views/parsely-settings.php';
261 207 }
262 208
263 209 /**
264 210 * Initializes the settings for Parse.ly.
@@ -279,10 +225,8 @@
279 225 )
280 226 );
281 227
282 228 $this->initialize_basic_section();
283 - $this->initialize_content_helper_section();
284 - $this->initialize_headline_testing_section();
285 229 $this->initialize_recrawl_section();
286 230 $this->initialize_advanced_section();
287 231 }
288 232
@@ -449,239 +393,8 @@
449 393 }
450 394 }
451 395
452 396 /**
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 397 * Registers section and settings for Recrawl section.
685 398 *
686 399 * @since 3.2.0
687 400 */
@@ -714,8 +427,9 @@
714 427 array(
715 428 'title' => __( 'Track Post Types as', 'wp-parsely' ),
716 429 'option_key' => $field_id,
717 430 'help_text' => $field_help,
431 + 'filter' => 'wp_parsely_trackable_statuses',
718 432 )
719 433 );
720 434
721 435 // Use full metadata in non-posts.
@@ -987,27 +701,27 @@
987 701 *
988 702 * @param Setting_Arguments $args The arguments for text tag.
989 703 */
990 704 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 : '';
705 + $options = $this->parsely->get_options();
706 + $name = $args['option_key'];
707 + /**
708 + * Variable.
709 + *
710 + * @var string
711 + */
712 + $value = $options[ $name ] ?? '';
995 713 $optional_args = $args['optional_args'] ?? array();
996 714 $id = esc_attr( $name );
997 - $html_name = $this->get_html_name_attribute( $name );
715 + $name = Parsely::OPTIONS_KEY . "[$id]";
998 716 $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 );
717 + $value = $is_obfuscated_value ? $this->get_obfuscated_value( $value ) : esc_attr( $value );
718 + $accepted_args = array( 'placeholder', 'required', 'disabled' );
1000 719 $type = $optional_args['type'] ?? 'text';
1001 - $accepted_args = array( 'placeholder', 'required', 'disabled' );
1002 720
1003 - if ( 'number' === $type ) {
1004 - $accepted_args = array_merge( $accepted_args, array( 'min', 'max', 'step' ) );
1005 - }
1006 -
1007 721 $is_managed = key_exists( $id, $this->parsely->managed_options );
1008 722 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 ) );
723 + printf( "<input type='%s' name='%s' id='%s' value='%s'", esc_attr( $type ), esc_attr( $name ), esc_attr( $id ), esc_attr( $value ) );
1010 724
1011 725 if ( isset( $args['help_text'] ) ) {
1012 726 echo ' aria-describedby="' . esc_attr( $id ) . '-description"';
1013 727 }
@@ -1031,217 +745,33 @@
1031 745 $this->print_description_text( $args );
1032 746 }
1033 747
1034 748 /**
1035 - * Prints a checkbox tag.
749 + * Prints a checkbox tag in the settings page.
1036 750 *
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.
751 + * @param Setting_Arguments $args Arguments to print to checkbox tag.
1041 752 */
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 );
753 + public function print_checkbox_tag( $args ): void {
754 + $options = $this->parsely->get_options();
755 + $name = $args['option_key'];
756 + $value = $options[ $name ];
757 + $id = esc_attr( $name );
758 + $name = Parsely::OPTIONS_KEY . "[$id]";
759 + $yes_text = $args['yes_text'] ?? '';
1050 760
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 - );
761 + $is_managed = key_exists( $id, $this->parsely->managed_options );
762 + echo '<fieldset', $is_managed ? ' disabled>' : '>';
763 + printf( "<input type='checkbox' name='%s' id='%s_true' value='true' ", esc_attr( $name ), esc_attr( $id ) );
1067 764 if ( isset( $args['help_text'] ) ) {
1068 - echo ' aria-describedby="' . esc_attr( $html_id ) . '-description"';
765 + echo ' aria-describedby="' . esc_attr( $id ) . '-description"';
1069 766 }
1070 - checked( true === $value, true );
1071 - printf( ' /> %s</label>', esc_html( $yes_text ) );
767 + echo checked( true === $value, true, false );
768 + printf( " /> <label for='%s_true'>%s</label><fieldset>", esc_attr( $id ), esc_html( $yes_text ) );
1072 769
1073 - // Fieldset end.
1074 - if ( $has_fieldset ) {
1075 - echo '</fieldset>';
1076 - }
1077 -
1078 770 $this->print_description_text( $args );
1079 771 }
1080 772
1081 773 /**
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 774 * Prints out the select tags
1245 775 *
1246 776 * @param Setting_Arguments $args The arguments for the select dropdowns.
1247 777 */
@@ -1278,13 +808,11 @@
1278 808 *
1279 809 * @param Setting_Arguments $args The arguments for the radio buttons.
1280 810 */
1281 811 public function print_radio_tags( $args ): void {
1282 - $options = $this->parsely->get_options();
1283 812 $name = $args['option_key'];
1284 813 $id = esc_attr( $name );
1285 - $selected = $this->get_option_value( $name, $options );
1286 - $html_name = $this->get_html_name_attribute( $name );
814 + $selected = $this->parsely->get_options()[ $name ];
1287 815 $title = $args['title'] ?? '';
1288 816 $radio_options = $args['radio_options'] ?? array();
1289 817
1290 818 if ( is_bool( $selected ) ) {
@@ -1300,9 +828,9 @@
1300 828 <?php foreach ( $radio_options as $value => $text ) { ?>
1301 829 <label for="<?php echo esc_attr( "{$id}_{$value}" ); ?>">
1302 830 <input
1303 831 type="radio"
1304 - name="<?php echo esc_attr( $html_name ); ?>"
832 + name="<?php echo esc_attr( Parsely::OPTIONS_KEY . "[$id]" ); ?>"
1305 833 id="<?php echo esc_attr( "{$id}_{$value}" ); ?>"
1306 834 value="<?php echo esc_attr( $value ); ?>"
1307 835 <?php checked( $selected, $value ); ?>
1308 836 />
@@ -1348,55 +876,8 @@
1348 876 $this->print_description_text( $args );
1349 877 }
1350 878
1351 879 /**
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 880 * Prints out the post tracking options table.
1400 881 *
1401 882 * @since 3.2.0
1402 883 *
@@ -1492,10 +973,8 @@
1492 973 * @return ParselySettingOptions
1493 974 */
1494 975 public function validate_options( $input ) {
1495 976 $input = $this->validate_basic_section( $input );
1496 - $input = $this->validate_content_helper_section( $input );
1497 - $input = $this->validate_headline_testing_section( $input );
1498 977 $input = $this->validate_recrawl_section( $input );
1499 978 $input = $this->validate_advanced_section( $input );
1500 979
1501 980 return $input;
@@ -1506,9 +985,9 @@
1506 985 *
1507 986 * @param ParselySettingOptions $input Options from the settings page.
1508 987 * @return ParselySettingOptions Validated inputs.
1509 988 */
1510 - private function validate_basic_section( $input ): array {
989 + private function validate_basic_section( $input ) {
1511 990 $are_credentials_managed = $this->parsely->are_credentials_managed;
1512 991 $options = $this->parsely->get_options();
1513 992
1514 993 if ( $are_credentials_managed ) {
@@ -1525,12 +1004,9 @@
1525 1004 if ( isset( $_POST['e2e_parsely_skip_api_validate'] ) && 'y' === $_POST['e2e_parsely_skip_api_validate'] ) {
1526 1005 $valid_credentials = true;
1527 1006 }
1528 1007
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 - ) {
1008 + if ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) {
1533 1009 add_settings_error(
1534 1010 Parsely::OPTIONS_KEY,
1535 1011 'api_secret',
1536 1012 __( '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' )
@@ -1596,207 +1072,8 @@
1596 1072 $input['disable_amp'] = 'true' === $input['disable_amp'];
1597 1073 }
1598 1074
1599 1075 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 1076 }
1800 1077
1801 1078 /**
1802 1079 * Validates fields of Recrawl Section.