PluginProbe
Parse.ly / 3.14.2
Parse.ly v3.14.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.14.2, at src/class-parsely.php

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