PluginProbe
Parse.ly / 3.13.2
Parse.ly v3.13.2
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / src / class-parsely.php

class-parsely.php in Parse.ly 3.13.2, at src/class-parsely.php

849 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Parsely class
4 *
5 * @package Parsely
6 * @since 2.5.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely;
12
13 use Parsely\UI\Metadata_Renderer;
14 use Parsely\UI\Settings_Page;
15 use WP_Post;
16
17 /**
18 * Holds most of the logic for the plugin.
19 *
20 * @since 1.0.0
21 * @since 2.5.0 Moved from plugin root file to this file.
22 *
23 * @phpstan-type Parsely_Options array{
24 * apikey: string,
25 * content_id_prefix: string,
26 * api_secret: string,
27 * use_top_level_cats: bool,
28 * custom_taxonomy_section: string,
29 * cats_as_tags: bool,
30 * track_authenticated_users: bool,
31 * lowercase_tags: bool,
32 * force_https_canonicals: bool,
33 * track_post_types: string[],
34 * track_page_types: string[],
35 * track_post_types_as?: array<string, string>,
36 * disable_javascript: bool,
37 * disable_amp: bool,
38 * meta_type: string,
39 * logo: string,
40 * metadata_secret: string,
41 * disable_autotrack: bool,
42 * plugin_version: string,
43 * }
44 *
45 * @phpstan-type WP_HTTP_Request_Args array{
46 * method: string,
47 * timeout: float,
48 * blocking: bool,
49 * headers: array<string, string>,
50 * body: string,
51 * data_format: string,
52 * }
53 *
54 * @phpstan-import-type Metadata_Attributes from Metadata
55 */
56 class Parsely {
57 /**
58 * Declare our constants
59 */
60 public const VERSION = PARSELY_VERSION;
61 public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php.
62 public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
63 public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
64 public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
65 public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2';
66 public const PUBLIC_SUGGESTIONS_API_BASE_URL = 'https://content-suggestions-beta.parsely-recspod.net';
67
68 /**
69 * Declare some class properties
70 *
71 * @var Parsely_Options $option_defaults The defaults we need for the class.
72 */
73 private $option_defaults = array(
74 'apikey' => '',
75 'content_id_prefix' => '',
76 'api_secret' => '',
77 'use_top_level_cats' => false,
78 'custom_taxonomy_section' => 'category',
79 'cats_as_tags' => false,
80 'track_authenticated_users' => false,
81 'lowercase_tags' => true,
82 'force_https_canonicals' => false,
83 'track_post_types' => array(),
84 'track_page_types' => array(),
85 'disable_javascript' => false,
86 'disable_amp' => false,
87 'meta_type' => 'json_ld',
88 'logo' => '',
89 'metadata_secret' => '',
90 'disable_autotrack' => false,
91 'plugin_version' => self::VERSION,
92 );
93
94 /**
95 * Declare post types that Parse.ly will process as "posts".
96 *
97 * @since 2.5.0
98 * @var string[]
99 *
100 * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
101 */
102 public const SUPPORTED_JSONLD_POST_TYPES = array(
103 'NewsArticle',
104 'Article',
105 'TechArticle',
106 'BlogPosting',
107 'LiveBlogPosting',
108 'Report',
109 'Review',
110 'CreativeWork',
111 'OpinionNewsArticle',
112 'AnalysisNewsArticle',
113 'BackgroundNewsArticle',
114 'ReviewNewsArticle',
115 'ReportageNewsArticle',
116 'Recipe',
117 'AdvertiserContentArticle',
118 'MedicalWebPage',
119 'PodcastEpisode',
120 );
121
122 /**
123 * Declare post types that Parse.ly will process as "non-posts".
124 *
125 * @since 2.5.0
126 * @var string[]
127 *
128 * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
129 */
130 public const SUPPORTED_JSONLD_NON_POST_TYPES = array(
131 'WebPage',
132 'Event',
133 'Hotel',
134 'Restaurant',
135 'Movie',
136 );
137
138 /**
139 * Declare all supported types (both post and non-post types).
140 *
141 * @since 3.7.0
142 * @var string[]
143 */
144 private static $all_supported_types;
145
146 /**
147 * Returns whether credentials are being managed at the platform level.
148 *
149 * This allows hosting providers to provide a more customized experience for
150 * the plugin by handling credentials automatically.
151 *
152 * @since 3.9.0
153 * @access private
154 * @var bool
155 */
156 public $are_credentials_managed;
157
158 /**
159 * Holds the managed options and their values.
160 *
161 * This allows hosting providers to provide a more customized experience for
162 * the plugin by handling options automatically.
163 *
164 * @since 3.9.0
165 * @access private
166 * @var array<empty>|array<string, bool|string|null>
167 */
168 public $managed_options = array();
169
170 /**
171 * Constructor.
172 */
173 public function __construct() {
174 self::$all_supported_types = array_merge( self::SUPPORTED_JSONLD_POST_TYPES, self::SUPPORTED_JSONLD_NON_POST_TYPES );
175
176 $this->are_credentials_managed = $this->are_credentials_managed();
177 $this->set_managed_options();
178
179 $this->allow_parsely_remote_requests();
180 }
181
182 /**
183 * Registers action and filter hook callbacks, and immediately upgrades
184 * options if needed.
185 */
186 public function run(): void {
187 // Run upgrade options if they exist for the version currently defined.
188 $options = $this->get_options();
189 if ( self::VERSION !== $options['plugin_version'] ) {
190 $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
191 if ( method_exists( $this, $method ) ) {
192 /**
193 * Variable.
194 *
195 * @var callable
196 */
197 $callable = array( $this, $method );
198 call_user_func_array( $callable, array( $options ) );
199 }
200
201 // Update our version info.
202 $options['plugin_version'] = self::VERSION;
203 update_option( self::OPTIONS_KEY, $options );
204 }
205
206 add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
207 }
208
209 /**
210 * Gets the full URL of the JavaScript tracker file for the site. If an API
211 * key is not set, return an empty string.
212 *
213 * @since 3.2.0
214 *
215 * @return string
216 */
217 public function get_tracker_url(): string {
218 if ( $this->site_id_is_set() ) {
219 $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
220 return esc_url( $tracker_url );
221 }
222 return '';
223 }
224
225 /**
226 * Deprecated.
227 * Inserts the code for the <meta name='parsely-page'> parameter within the
228 * head tag.
229 *
230 * @since 3.2.0
231 * @deprecated 3.3.0
232 * @see Metadata_Renderer::render_metadata
233 *
234 * @param string $meta_type `json_ld` or `repeated_metas`.
235 */
236 public function render_metadata( string $meta_type ): void {
237 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
238 $metadata_renderer = new Metadata_Renderer( $this );
239 $metadata_renderer->render_metadata( $meta_type );
240 }
241
242 /**
243 * Deprecated.
244 * Insert the code for the <meta name='parsely-page'> parameter within the
245 * head tag.
246 *
247 * @since 3.0.0
248 * @deprecated 3.3.0
249 * @see Metadata_Renderer::render_metadata
250 */
251 public function insert_page_header_metadata(): void {
252 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
253 $parsely_options = $this->get_options();
254 $metadata_renderer = new Metadata_Renderer( $this );
255 $metadata_renderer->render_metadata( $parsely_options['meta_type'] );
256 }
257
258 /**
259 * Compares the post_status key against an allowed list.
260 *
261 * By default, only 'publish'ed content includes tracking data.
262 *
263 * @since 2.5.0
264 *
265 * @param int|WP_Post $post Which post object or ID to check.
266 * @return bool Should the post status be tracked for the provided post's post_type.
267 * By default,only 'publish' is allowed.
268 */
269 public static function post_has_trackable_status( $post ): bool {
270 static $cache = array();
271 $post_id = is_int( $post ) ? $post : $post->ID;
272 if ( isset( $cache[ $post_id ] ) ) {
273 return $cache[ $post_id ];
274 }
275
276 /**
277 * Filters whether the post password check should be skipped when getting
278 * the post trackable status.
279 *
280 * @since 3.0.1
281 *
282 * @param bool $skip True if the password check should be skipped.
283 * @param int|WP_Post $post Which post object or ID is being checked.
284 *
285 * @return bool
286 */
287 $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
288 if ( ! $skip_password_check && post_password_required( $post ) ) {
289 $cache[ $post_id ] = false;
290 return false;
291 }
292
293 /**
294 * Filters the statuses that are permitted to be tracked.
295 *
296 * By default, the only status tracked is 'publish'. Use this filter if
297 * you have other published content that has a different (custom) status.
298 *
299 * @since 2.5.0
300 *
301 * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
302 * @param int|WP_Post $post Which post object or ID is being checked.
303 */
304 $statuses = apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
305 $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true );
306 return $cache[ $post_id ];
307 }
308
309 /**
310 * Deprecated. Please use the `Metadata` class instead.
311 *
312 * Creates parsely metadata object from post metadata.
313 *
314 * @deprecated 3.3.0
315 * @see \Parsely\Metadata::construct_metadata
316 *
317 * @param array<string, mixed> $parsely_options parsely_options array.
318 * @param WP_Post $post object.
319 * @return Metadata_Attributes
320 */
321 public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
322 _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
323 $metadata = new Metadata( $this );
324 return $metadata->construct_metadata( $post );
325 }
326
327 /**
328 * Updates the Parsely metadata endpoint with the new metadata of the post.
329 *
330 * @param int $post_id id of the post to update.
331 */
332 public function update_metadata_endpoint( int $post_id ): void {
333 $parsely_options = $this->get_options();
334 if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) {
335 return;
336 }
337
338 $post = get_post( $post_id );
339 if ( null === $post ) {
340 return;
341 }
342
343 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
344
345 $endpoint_metadata = array(
346 'canonical_url' => $metadata['url'] ?? '',
347 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
348 'title' => $metadata['headline'] ?? '',
349 'image_url' => $metadata['image']['url'] ?? '',
350 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
351 'section' => $metadata['articleSection'] ?? '',
352 'authors' => $metadata['creator'] ?? '',
353 'tags' => $metadata['keywords'] ?? '',
354 );
355
356 $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts';
357 $parsely_metadata_secret = $parsely_options['metadata_secret'];
358
359 $headers = array( 'Content-Type' => 'application/json' );
360 $body = wp_json_encode(
361 array(
362 'secret' => $parsely_metadata_secret,
363 'apikey' => $this->get_site_id(),
364 'metadata' => $endpoint_metadata,
365 )
366 );
367
368 /**
369 * POST request options.
370 *
371 * @var WP_HTTP_Request_Args $options
372 */
373 $options = array(
374 'method' => 'POST',
375 'headers' => $headers,
376 'blocking' => false,
377 'body' => $body,
378 'data_format' => 'body',
379 );
380
381 $response = wp_remote_post( $parsely_api_endpoint, $options );
382
383 if ( ! is_wp_error( $response ) ) {
384 $current_timestamp = time();
385 update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
386 }
387 }
388
389 /**
390 * Safely returns options for the plugin by assigning defaults contained in
391 * optionDefaults.
392 *
393 * As soon as actual options are saved, they override the defaults. This
394 * prevents us from having to do a lot of isset() checking on variables.
395 *
396 * @return Parsely_Options
397 */
398 public function get_options() {
399 /**
400 * Variable.
401 *
402 * @var Parsely_Options|null
403 */
404 $options = get_option( self::OPTIONS_KEY, null );
405
406 if ( ! is_array( $options ) ) {
407 $this->set_default_track_as_values();
408 $options = $this->option_defaults;
409 }
410
411 /**
412 * Final options including managed credentials and options.
413 *
414 * @var Parsely_Options
415 */
416 return array_merge(
417 $this->option_defaults,
418 $options,
419 $this->get_managed_credentials(),
420 $this->managed_options
421 );
422 }
423
424 /**
425 * Sets the default values for the track_post_types and track_page_types
426 * options.
427 *
428 * @since 3.9.0
429 */
430 public function set_default_track_as_values(): void {
431 $this->option_defaults['track_page_types'] = array();
432 $this->option_defaults['track_post_types'] = array();
433
434 $post_types = get_post_types( array( 'public' => true ) );
435
436 foreach ( $post_types as $post_type ) {
437 if ( ! post_type_supports( $post_type, 'editor' ) ) {
438 continue;
439 }
440
441 if ( is_post_type_hierarchical( $post_type ) ) {
442 $this->option_defaults['track_page_types'][] = $post_type;
443 } else {
444 $this->option_defaults['track_post_types'][] = $post_type;
445 }
446 }
447 }
448
449 /**
450 * Gets the URL of the plugin's settings page.
451 *
452 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
453 * for context (Default null for current).
454 * @return string
455 */
456 public static function get_settings_url( int $_blog_id = null ): string {
457 return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
458 }
459
460 /**
461 * Returns the URL of the Parse.ly dashboard for a specific page. If a page
462 * is not specified, the home dashboard URL for the specified Site ID is
463 * returned.
464 *
465 * @since 3.7.0
466 *
467 * @param string $site_id The Site ID for which to get the URL.
468 * @param string $page_url Optional. The page for which to get the URL.
469 * @return string The complete dashboard URL.
470 */
471 public static function get_dash_url( string $site_id, string $page_url = '' ): string {
472 $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
473
474 if ( '' !== $page_url ) {
475 $page_url = self::get_url_with_itm_source( $page_url, null );
476 $result .= '?url=' . rawurlencode( $page_url );
477 }
478
479 return $result;
480 }
481
482 /**
483 * Adds or replaces the itm_source parameter in the URL. Removes the
484 * parameter if the passed value is null or an empty string.
485 *
486 * @since 3.9.0
487 *
488 * @param string $url The URL to modify.
489 * @param string|null $itm_source The value of the itm_source parameter.
490 * @return string The resulting URL.
491 */
492 public static function get_url_with_itm_source( string $url, $itm_source ): string {
493 if ( null === $itm_source || '' === $itm_source ) {
494 return remove_query_arg( 'itm_source', $url );
495 }
496
497 $itm_source = rawurlencode( $itm_source );
498
499 return add_query_arg( 'itm_source', $itm_source, $url );
500 }
501
502 /**
503 * Checks to see if the current user is a member of the current blog.
504 *
505 * @return bool
506 */
507 public function is_blog_member_logged_in(): bool {
508 // Can't use $blog_id here because it futzes with the global $blog_id.
509 $current_blog_id = get_current_blog_id();
510 $current_user_id = get_current_user_id();
511
512 return is_user_member_of_blog( $current_user_id, $current_blog_id );
513 }
514
515 /**
516 * Converts JSON-LD type to respective Parse.ly page type.
517 *
518 * If the JSON-LD type is one of the types Parse.ly supports as a "post",
519 * then "post" will be returned. Otherwise, for "non-posts" and unknown
520 * types, "index" is returned.
521 *
522 * @since 2.5.0
523 *
524 * @see https://docs.parse.ly/metatags/#h-field-description
525 *
526 * @param string $type JSON-LD type.
527 * @return string "post" or "index".
528 */
529 public function convert_jsonld_to_parsely_type( string $type ): string {
530 return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
531 }
532
533 /**
534 * Determines if a Site ID is saved in the options.
535 *
536 * @since 2.6.0
537 * @since 3.7.0 renamed from api_key_is_set.
538 *
539 * @return bool True is Site ID is set, false if it is missing.
540 */
541 public function site_id_is_set(): bool {
542 $options = $this->get_options();
543
544 return '' !== $options['apikey'];
545 }
546
547 /**
548 * Determines if a Site ID is not saved in the options.
549 *
550 * @since 2.6.0
551 * @since 3.7.0 renamed from api_key_is_missing.
552 *
553 * @return bool True if Site ID is missing, false if it is set.
554 */
555 public function site_id_is_missing(): bool {
556 return ! $this->site_id_is_set();
557 }
558
559 /**
560 * Gets the Site ID if set.
561 *
562 * @since 2.6.0
563 * @since 3.7.0 renamed from get_site_id.
564 *
565 * @return string Site ID if set, or empty string if not.
566 */
567 public function get_site_id(): string {
568 $options = $this->get_options();
569
570 return $this->site_id_is_set() ? $options['apikey'] : '';
571 }
572
573 /**
574 * Returns whether the API Secret is set in the plugin's options.
575 *
576 * @since 3.4.0
577 *
578 * @return bool True if the API Secret is set, false if not set.
579 */
580 public function api_secret_is_set(): bool {
581 $options = $this->get_options();
582
583 return '' !== $options['api_secret'];
584 }
585
586 /**
587 * Returns the API Secret stored in the plugin's options.
588 *
589 * @since 3.4.0
590 *
591 * @return string The API Secret, empty string if the API secret is not set.
592 */
593 public function get_api_secret(): string {
594 $options = $this->get_options();
595
596 return $this->api_secret_is_set() ? $options['api_secret'] : '';
597 }
598
599 /**
600 * Returns all supported post and non-post types.
601 *
602 * @since 3.7.0
603 *
604 * @return string[] all supported types
605 */
606 public function get_all_supported_types(): array {
607 return self::$all_supported_types;
608 }
609
610 /**
611 * Gets all tracked post types.
612 *
613 * @since 3.7.0
614 *
615 * @return array<string>
616 */
617 public function get_all_track_types(): array {
618 $options = $this->get_options();
619
620 return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
621 }
622
623 /**
624 * Gets default options.
625 *
626 * @since 3.8.0
627 *
628 * @return Parsely_Options
629 */
630 public function get_default_options() {
631 return $this->option_defaults;
632 }
633
634 /**
635 * Returns the credentials that are being managed at the platform level.
636 *
637 * @since 3.9.0
638 * @access private
639 *
640 * @return Parsely_Options|array<empty> The managed credentials.
641 */
642 private function get_managed_credentials() {
643 if ( true !== $this->are_credentials_managed ) {
644 return array();
645 }
646
647 $credentials = apply_filters( 'wp_parsely_credentials', array() );
648
649 if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
650 return array();
651 }
652
653 $result = array();
654
655 if ( isset( $credentials['site_id'] ) ) {
656 $result['apikey'] = $credentials['site_id'];
657 }
658
659 if ( isset( $credentials['api_secret'] ) ) {
660 $result['api_secret'] = $credentials['api_secret'];
661 }
662
663 if ( isset( $credentials['metadata_secret'] ) ) {
664 $result['metadata_secret'] = $credentials['metadata_secret'];
665 }
666
667 return $result;
668 }
669
670 /**
671 * Returns whether credentials are being managed at the platform level.
672 *
673 * @since 3.9.0
674 * @access private
675 *
676 * @return bool Whether credentials are being managed at the platform level.
677 */
678 private function are_credentials_managed(): bool {
679 $credentials = apply_filters( 'wp_parsely_credentials', array() );
680
681 if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
682 return false;
683 }
684
685 return $credentials['is_managed'] ?? false;
686 }
687
688 /**
689 * Sets the values of managed options.
690 *
691 * This function won't accept managing credentials or certain plugin options
692 * that are being managed through other means. For managing credentials,
693 * please use the `wp_parsely_credentials` filter.
694 *
695 * @since 3.9.0
696 * @access private
697 */
698 private function set_managed_options(): void {
699 $managed_options = apply_filters( 'wp_parsely_managed_options', false );
700
701 if ( ! is_array( $managed_options ) ) {
702 return;
703 }
704
705 // Don't allow certain options to be set as managed.
706 unset(
707 $managed_options['apikey'],
708 $managed_options['api_secret'],
709 $managed_options['metadata_secret'],
710 $managed_options['track_post_types'],
711 $managed_options['track_page_types'],
712 $managed_options['plugin_version']
713 );
714
715 if ( 0 === count( $managed_options ) ) {
716 return;
717 }
718
719 /**
720 * Current options.
721 *
722 * @var Parsely_Options $current_options
723 */
724 $current_options = get_option( self::OPTIONS_KEY, array() );
725
726 // Set managed options values.
727 foreach ( $managed_options as $key => $value ) {
728 $is_option_valid = isset( $this->option_defaults[ $key ] );
729
730 if ( $is_option_valid ) {
731 if ( null === $value ) {
732 // When null, the option gets its value from the database.
733 $this->managed_options[ $key ] =
734 $current_options[ $key ] ?? $this->option_defaults[ $key ];
735 } else {
736 $this->managed_options[ $key ] =
737 $this->sanitize_managed_option( $key, $value );
738 }
739 }
740 }
741 }
742
743 /**
744 * Sanitizes the value of the passed managed option.
745 *
746 * @since 3.9.0
747 * @access private
748 *
749 * @param string $option_id The option's ID.
750 * @param bool|string $value The option's value.
751 * @return bool|string The sanitized option value.
752 */
753 private function sanitize_managed_option( string $option_id, $value ) {
754 $option_value_type = gettype( $this->option_defaults[ $option_id ] );
755
756 if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
757 _doing_it_wrong(
758 __FUNCTION__,
759 esc_html(
760 sprintf( /* translators: 1: Option ID */
761 __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
762 $option_id
763 )
764 ),
765 ''
766 );
767
768 return false;
769 }
770
771 if ( 'string' === $option_value_type ) {
772 if ( ! is_string( $value ) ) {
773 _doing_it_wrong(
774 __FUNCTION__,
775 esc_html(
776 sprintf( /* translators: 1: Option ID */
777 __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
778 $option_id
779 )
780 ),
781 ''
782 );
783
784 $value = strval( $value );
785 }
786
787 // String options that are restricted to specific values.
788 $restricted_value_options = array(
789 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
790 'meta_type' => array( 'json_ld', 'repeated_metas' ),
791 );
792
793 // Verify that the above values are respected.
794 foreach ( $restricted_value_options as $option_key => $valid_values ) {
795 if ( $option_id === $option_key ) {
796 if ( ! in_array( $value, $valid_values, true ) ) {
797 _doing_it_wrong(
798 __FUNCTION__,
799 esc_html(
800 sprintf( /* translators: 1: Option value 2: Option ID */
801 __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
802 $value,
803 $option_id
804 )
805 ),
806 ''
807 );
808
809 $value = $this->option_defaults[ $option_id ];
810 }
811 }
812 }
813 }
814
815 return $value;
816 }
817
818 /**
819 * Allows remote requests to Parse.ly.
820 *
821 * This is needed for environments, such as wp-now, that block remote requests.
822 *
823 * @since 3.13.0
824 * @access private
825 */
826 private function allow_parsely_remote_requests(): void {
827 $allowed_urls = array(
828 self::DASHBOARD_BASE_URL,
829 self::PUBLIC_API_BASE_URL,
830 self::PUBLIC_SUGGESTIONS_API_BASE_URL,
831 );
832
833 add_filter(
834 'http_request_host_is_external',
835 function ( $external, $host, $url ) use ( $allowed_urls ) {
836 // Check if the URL matches any URLs on the allowed list.
837 foreach ( $allowed_urls as $allowed_url ) {
838 if ( \Parsely\Utils\str_starts_with( $url, $allowed_url ) ) {
839 return true;
840 }
841 }
842 return $external;
843 },
844 10,
845 3
846 );
847 }
848 }
849