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

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