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