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

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