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

1,075 lines 28.8 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 // @phpstan-ignore return.void
286 add_action( 'save_post', array( $this, 'call_update_metadata_endpoint' ) );
287 }
288
289 /**
290 * Returns the Content API service.
291 *
292 * This method returns the Content API service, which is used to interact with the Parse.ly Content API.
293 *
294 * @since 3.17.0
295 *
296 * @return Content_API_Service
297 */
298 public function get_content_api(): Content_API_Service {
299 if ( ! isset( $this->content_api_service ) ) {
300 $this->content_api_service = new Content_API_Service( $this );
301 }
302
303 return $this->content_api_service;
304 }
305
306 /**
307 * Returns the Suggestions API service.
308 *
309 * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
310 *
311 * @since 3.17.0
312 *
313 * @return Suggestions_API_Service
314 */
315 public function get_suggestions_api(): Suggestions_API_Service {
316 if ( ! isset( $this->suggestions_api_service ) ) {
317 $this->suggestions_api_service = new Suggestions_API_Service( $this );
318 }
319
320 return $this->suggestions_api_service;
321 }
322
323 /**
324 * Gets the REST API controller.
325 *
326 * If the controller is not set, a new instance is created.
327 *
328 * @since 3.17.0
329 *
330 * @return REST_API_Controller
331 */
332 public function get_rest_api_controller(): REST_API_Controller {
333 if ( ! isset( $this->rest_api_controller ) ) {
334 $this->rest_api_controller = new REST_API_Controller( $this );
335 }
336
337 return $this->rest_api_controller;
338 }
339
340 /**
341 * Gets the full URL of the JavaScript tracker file for the site. If an API
342 * key is not set, return an empty string.
343 *
344 * @since 3.2.0
345 *
346 * @return string
347 */
348 public function get_tracker_url(): string {
349 if ( $this->site_id_is_set() ) {
350 $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
351 return esc_url( $tracker_url );
352 }
353 return '';
354 }
355
356 /**
357 * Deprecated.
358 * Inserts the code for the <meta name='parsely-page'> parameter within the
359 * head tag.
360 *
361 * @since 3.2.0
362 * @deprecated 3.3.0
363 * @see Metadata_Renderer::render_metadata
364 *
365 * @param string $meta_type `json_ld` or `repeated_metas`.
366 */
367 public function render_metadata( string $meta_type ): void {
368 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
369 $metadata_renderer = new Metadata_Renderer( $this );
370 $metadata_renderer->render_metadata( $meta_type );
371 }
372
373 /**
374 * Deprecated.
375 * Insert the code for the <meta name='parsely-page'> parameter within the
376 * head tag.
377 *
378 * @since 3.0.0
379 * @deprecated 3.3.0
380 * @see Metadata_Renderer::render_metadata
381 */
382 public function insert_page_header_metadata(): void {
383 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
384 $parsely_options = $this->get_options();
385 $metadata_renderer = new Metadata_Renderer( $this );
386 $metadata_renderer->render_metadata( $parsely_options['meta_type'] );
387 }
388
389 /**
390 * Compares the post_status key against an allowed list.
391 *
392 * By default, only 'publish'ed content includes tracking data.
393 *
394 * @since 2.5.0
395 *
396 * @param int|WP_Post $post Which post object or ID to check.
397 * @return bool Should the post status be tracked for the provided post's post_type.
398 * By default,only 'publish' is allowed.
399 */
400 public static function post_has_trackable_status( $post ): bool {
401 /**
402 * Filters whether the post password check should be skipped when getting
403 * the post trackable status.
404 *
405 * @since 3.0.1
406 *
407 * @param bool $skip True if the password check should be skipped.
408 * @param int|WP_Post $post Which post object or ID is being checked.
409 *
410 * @return bool
411 */
412 $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
413 if ( ! $skip_password_check && post_password_required( $post ) ) {
414 return false;
415 }
416
417 $statuses = self::get_trackable_statuses( $post );
418 return in_array( get_post_status( $post ), $statuses, true );
419 }
420
421 /**
422 * Deprecated. Please use the `Metadata` class instead.
423 *
424 * Creates parsely metadata object from post metadata.
425 *
426 * @deprecated 3.3.0
427 * @see \Parsely\Metadata::construct_metadata
428 *
429 * @param array<string, mixed> $parsely_options parsely_options array.
430 * @param WP_Post $post object.
431 * @return Metadata_Attributes
432 */
433 public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
434 _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
435 $metadata = new Metadata( $this );
436 return $metadata->construct_metadata( $post );
437 }
438
439 /**
440 * Calls Parse.ly's update metadata endpoint, sending the post's updated
441 * metadata.
442 *
443 * @param int $post_id The ID of the post to update.
444 * @return bool True if the metadata endpoint was called, false otherwise.
445 */
446 public function call_update_metadata_endpoint( int $post_id ): bool {
447 $options = $this->get_options();
448
449 if ( $this->site_id_is_missing() || '' === $options['metadata_secret'] ) {
450 return false;
451 }
452
453 $current_post_type = get_post_type( $post_id );
454 if ( false === $current_post_type ) {
455 return false;
456 }
457
458 $tracked_post_types = array_merge(
459 $options['track_post_types'],
460 $options['track_page_types']
461 );
462
463 // Check that the post's type is trackable.
464 if ( ! in_array( $current_post_type, $tracked_post_types, true ) ) {
465 return false;
466 }
467
468 // Check that the post's status is trackable.
469 if ( ! self::post_has_trackable_status( $post_id ) ) {
470 return false;
471 }
472
473 $post = get_post( $post_id );
474 if ( null === $post ) {
475 return false;
476 }
477
478 // Don't call the endpoint when integration tests are running, but
479 // signal that the above checks have passed.
480 if ( defined( 'INTEGRATION_TESTS_RUNNING' ) ) {
481 return true;
482 }
483
484 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
485
486 $endpoint_metadata = array(
487 'canonical_url' => $metadata['url'] ?? '',
488 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
489 'title' => $metadata['headline'] ?? '',
490 'image_url' => $metadata['image']['url'] ?? '',
491 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
492 'section' => $metadata['articleSection'] ?? '',
493 'authors' => $metadata['creator'] ?? '',
494 'tags' => $metadata['keywords'] ?? '',
495 );
496
497 $parsely_api_base_url = Content_API_Service::get_base_url();
498 $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
499 $parsely_metadata_secret = $options['metadata_secret'];
500
501 $headers = array( 'Content-Type' => 'application/json' );
502 $body = wp_json_encode(
503 array(
504 'secret' => $parsely_metadata_secret,
505 'apikey' => $this->get_site_id(),
506 'metadata' => $endpoint_metadata,
507 )
508 );
509
510 /**
511 * POST request options.
512 *
513 * @var WP_HTTP_Request_Args $request_options
514 */
515 $request_options = array(
516 'method' => 'POST',
517 'headers' => $headers,
518 'blocking' => false,
519 'body' => $body,
520 'data_format' => 'body',
521 );
522
523 $response = wp_remote_post( $parsely_api_endpoint, $request_options );
524
525 if ( is_wp_error( $response ) ) {
526 return false;
527 }
528
529 update_post_meta( $post_id, 'parsely_metadata_last_updated', time() );
530
531 return true;
532 }
533
534 /**
535 * Safely returns options for the plugin by assigning defaults contained in
536 * optionDefaults.
537 *
538 * As soon as actual options are saved, they override the defaults. This
539 * prevents us from having to do a lot of isset() checking on variables.
540 *
541 * @return Parsely_Options
542 */
543 public function get_options() {
544 /**
545 * Variable.
546 *
547 * @var Parsely_Options|null
548 */
549 $options = get_option( self::OPTIONS_KEY, null );
550
551 // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
552 if ( is_array( $options ) && ! isset( $options['full_metadata_in_non_posts'] ) ) {
553 // Existing plugin installation without full metadata option.
554 $this->set_default_full_metadata_in_non_posts();
555 }
556
557 // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
558 if ( is_array( $options ) && ! isset( $options['content_helper'] ) ) {
559 // Existing plugin installation without Content Helper options.
560 $this->set_default_content_helper_settings_values();
561 }
562
563 // New plugin installation that hasn't saved its options yet.
564 if ( ! is_array( $options ) ) {
565 $this->set_default_track_as_values();
566 $this->set_default_full_metadata_in_non_posts();
567 $options = $this->option_defaults;
568 }
569
570 /**
571 * Final options including managed credentials and options.
572 *
573 * @var Parsely_Options
574 */
575 return array_merge(
576 $this->option_defaults,
577 $options,
578 $this->get_managed_credentials(),
579 $this->managed_options
580 );
581 }
582
583 /**
584 * Returns the value of a nested option.
585 *
586 * @since 3.16.0
587 *
588 * @param string $option The option to get.
589 * @param Parsely_Options $options The options to get the value from.
590 * @return mixed The value of the nested option.
591 */
592 public static function get_nested_option_value( $option, $options ) {
593 $keys = explode( '[', str_replace( ']', '', $option ) );
594 $value = $options;
595
596 foreach ( $keys as $key ) {
597 if ( isset( $value[ $key ] ) ) {
598 $value = $value[ $key ];
599 }
600 }
601
602 return $value;
603 }
604
605 /**
606 * Sets the default values for the track_post_types and track_page_types
607 * options.
608 *
609 * @since 3.9.0
610 */
611 public function set_default_track_as_values(): void {
612 $this->option_defaults['track_page_types'] = array();
613 $this->option_defaults['track_post_types'] = array();
614
615 $post_types = get_post_types( array( 'public' => true ) );
616
617 foreach ( $post_types as $post_type ) {
618 if ( ! post_type_supports( $post_type, 'editor' ) ) {
619 continue;
620 }
621
622 if ( is_post_type_hierarchical( $post_type ) ) {
623 $this->option_defaults['track_page_types'][] = $post_type;
624 } else {
625 $this->option_defaults['track_post_types'][] = $post_type;
626 }
627 }
628 }
629
630 /**
631 * Sets the default value for the full_metadata_in_non_posts option.
632 *
633 * @since 3.14.0
634 */
635 public function set_default_full_metadata_in_non_posts(): void {
636 $this->option_defaults['full_metadata_in_non_posts'] = true;
637
638 // Usage of any of these filters will result in the setting being set
639 // to false.
640 $filter_tags = array(
641 'wp_parsely_metadata',
642 'wp_parsely_post_tags',
643 'wp_parsely_permalink',
644 'wp_parsely_post_category',
645 'wp_parsely_pre_authors',
646 'wp_parsely_post_authors',
647 'wp_parsely_custom_taxonomies',
648 'wp_parsely_post_type',
649 );
650
651 foreach ( $filter_tags as $filter_tag ) {
652 if ( has_filter( $filter_tag ) ) {
653 $this->option_defaults['full_metadata_in_non_posts'] = false;
654 break;
655 }
656 }
657 }
658
659 /**
660 * Sets the default values for Content Helper options.
661 *
662 * Gives PCH access to all users having the edit_posts capability, to keep
663 * consistent behavior with plugin versions prior to 3.16.0.
664 *
665 * @since 3.16.0
666 */
667 public function set_default_content_helper_settings_values(): void {
668 $this->option_defaults['content_helper'] =
669 Permissions::build_pch_permissions_settings_array(
670 true,
671 array_keys( Permissions::get_user_roles_with_edit_posts_cap() )
672 );
673 }
674
675 /**
676 * Gets the URL of the plugin's settings page.
677 *
678 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
679 * for context (Default null for current).
680 * @return string
681 */
682 public static function get_settings_url( ?int $_blog_id = null ): string {
683 return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
684 }
685
686 /**
687 * Returns the URL of the Parse.ly dashboard for a specific page. If a page
688 * is not specified, the home dashboard URL for the specified Site ID is
689 * returned.
690 *
691 * @since 3.7.0
692 *
693 * @param string $site_id The Site ID for which to get the URL.
694 * @param string $page_url Optional. The page for which to get the URL.
695 * @return string The complete dashboard URL.
696 */
697 public static function get_dash_url( string $site_id, string $page_url = '' ): string {
698 $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
699
700 if ( '' !== $page_url ) {
701 $page_url = self::get_url_with_itm_source( $page_url, null );
702 $result .= '?url=' . rawurlencode( $page_url );
703 }
704
705 return $result;
706 }
707
708 /**
709 * Adds or replaces the itm_source parameter in the URL. Removes the
710 * parameter if the passed value is null or an empty string.
711 *
712 * @since 3.9.0
713 *
714 * @param string $url The URL to modify.
715 * @param string|null $itm_source The value of the itm_source parameter.
716 * @return string The resulting URL.
717 */
718 public static function get_url_with_itm_source( string $url, $itm_source ): string {
719 if ( null === $itm_source || '' === $itm_source ) {
720 return remove_query_arg( 'itm_source', $url );
721 }
722
723 $itm_source = rawurlencode( $itm_source );
724
725 return add_query_arg( 'itm_source', $itm_source, $url );
726 }
727
728 /**
729 * Checks to see if the current user is a member of the current blog.
730 *
731 * @return bool
732 */
733 public function is_blog_member_logged_in(): bool {
734 // Can't use $blog_id here because it futzes with the global $blog_id.
735 $current_blog_id = get_current_blog_id();
736 $current_user_id = get_current_user_id();
737
738 return is_user_member_of_blog( $current_user_id, $current_blog_id );
739 }
740
741 /**
742 * Converts JSON-LD type to respective Parse.ly page type.
743 *
744 * If the JSON-LD type is one of the types Parse.ly supports as a "post",
745 * then "post" will be returned. Otherwise, for "non-posts" and unknown
746 * types, "index" is returned.
747 *
748 * @since 2.5.0
749 *
750 * @see https://docs.parse.ly/metatags/#h-field-description
751 *
752 * @param string $type JSON-LD type.
753 * @return string "post" or "index".
754 */
755 public function convert_jsonld_to_parsely_type( string $type ): string {
756 return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
757 }
758
759 /**
760 * Determines if a Site ID is saved in the options.
761 *
762 * @since 2.6.0
763 * @since 3.7.0 renamed from api_key_is_set.
764 *
765 * @return bool True is Site ID is set, false if it is missing.
766 */
767 public function site_id_is_set(): bool {
768 $options = $this->get_options();
769
770 return '' !== $options['apikey'];
771 }
772
773 /**
774 * Determines if a Site ID is not saved in the options.
775 *
776 * @since 2.6.0
777 * @since 3.7.0 renamed from api_key_is_missing.
778 *
779 * @return bool True if Site ID is missing, false if it is set.
780 */
781 public function site_id_is_missing(): bool {
782 return ! $this->site_id_is_set();
783 }
784
785 /**
786 * Gets the Site ID if set.
787 *
788 * @since 2.6.0
789 * @since 3.7.0 renamed from get_site_id.
790 *
791 * @return string Site ID if set, or empty string if not.
792 */
793 public function get_site_id(): string {
794 $options = $this->get_options();
795
796 return $this->site_id_is_set() ? $options['apikey'] : '';
797 }
798
799 /**
800 * Returns whether the API Secret is set in the plugin's options.
801 *
802 * @since 3.4.0
803 *
804 * @return bool True if the API Secret is set, false if not set.
805 */
806 public function api_secret_is_set(): bool {
807 $options = $this->get_options();
808
809 return '' !== $options['api_secret'];
810 }
811
812 /**
813 * Returns the API Secret stored in the plugin's options.
814 *
815 * @since 3.4.0
816 *
817 * @return string The API Secret, empty string if the API secret is not set.
818 */
819 public function get_api_secret(): string {
820 $options = $this->get_options();
821
822 return $this->api_secret_is_set() ? $options['api_secret'] : '';
823 }
824
825 /**
826 * Returns all supported post and non-post types.
827 *
828 * @since 3.7.0
829 *
830 * @return string[] all supported types
831 */
832 public function get_all_supported_types(): array {
833 return self::$all_supported_types;
834 }
835
836 /**
837 * Gets all tracked post types.
838 *
839 * @since 3.7.0
840 *
841 * @return array<string>
842 */
843 public function get_all_track_types(): array {
844 $options = $this->get_options();
845
846 return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
847 }
848
849 /**
850 * Gets default options.
851 *
852 * @since 3.8.0
853 *
854 * @return Parsely_Options
855 */
856 public function get_default_options() {
857 return $this->option_defaults;
858 }
859
860 /**
861 * Returns the credentials that are being managed at the platform level.
862 *
863 * @since 3.9.0
864 * @access private
865 *
866 * @return Parsely_Options|array<empty> The managed credentials.
867 */
868 private function get_managed_credentials() {
869 if ( true !== $this->are_credentials_managed ) {
870 return array();
871 }
872
873 $credentials = apply_filters( 'wp_parsely_credentials', array() );
874
875 if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
876 return array();
877 }
878
879 $result = array();
880
881 if ( isset( $credentials['site_id'] ) ) {
882 $result['apikey'] = $credentials['site_id'];
883 }
884
885 if ( isset( $credentials['api_secret'] ) ) {
886 $result['api_secret'] = $credentials['api_secret'];
887 }
888
889 if ( isset( $credentials['metadata_secret'] ) ) {
890 $result['metadata_secret'] = $credentials['metadata_secret'];
891 }
892
893 return $result;
894 }
895
896 /**
897 * Returns whether credentials are being managed at the platform level.
898 *
899 * @since 3.9.0
900 * @access private
901 *
902 * @return bool Whether credentials are being managed at the platform level.
903 */
904 private function are_credentials_managed(): bool {
905 $credentials = apply_filters( 'wp_parsely_credentials', array() );
906
907 if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
908 return false;
909 }
910
911 return $credentials['is_managed'] ?? false;
912 }
913
914 /**
915 * Sets the values of managed options.
916 *
917 * This function won't accept managing credentials or certain plugin options
918 * that are being managed through other means. For managing credentials,
919 * please use the `wp_parsely_credentials` filter.
920 *
921 * @since 3.9.0
922 * @access private
923 */
924 private function set_managed_options(): void {
925 $managed_options = apply_filters( 'wp_parsely_managed_options', false );
926
927 if ( ! is_array( $managed_options ) ) {
928 return;
929 }
930
931 // Don't allow certain options to be set as managed.
932 unset(
933 $managed_options['apikey'],
934 $managed_options['api_secret'],
935 $managed_options['metadata_secret'],
936 $managed_options['track_post_types'],
937 $managed_options['track_page_types'],
938 $managed_options['plugin_version']
939 );
940
941 if ( 0 === count( $managed_options ) ) {
942 return;
943 }
944
945 /**
946 * Current options.
947 *
948 * @var Parsely_Options $current_options
949 */
950 $current_options = get_option( self::OPTIONS_KEY, array() );
951
952 // Set managed options values.
953 foreach ( $managed_options as $key => $value ) {
954 $is_option_valid = isset( $this->option_defaults[ $key ] );
955
956 if ( $is_option_valid ) {
957 if ( null === $value ) {
958 // When null, the option gets its value from the database.
959 $this->managed_options[ $key ] =
960 $current_options[ $key ] ?? $this->option_defaults[ $key ];
961 } else {
962 $this->managed_options[ $key ] =
963 $this->sanitize_managed_option( $key, $value );
964 }
965 }
966 }
967 }
968
969 /**
970 * Sanitizes the value of the passed managed option.
971 *
972 * @since 3.9.0
973 * @access private
974 *
975 * @param string $option_id The option's ID.
976 * @param bool|string $value The option's value.
977 * @return bool|string The sanitized option value.
978 */
979 private function sanitize_managed_option( string $option_id, $value ) {
980 $option_value_type = gettype( $this->option_defaults[ $option_id ] );
981
982 if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
983 _doing_it_wrong(
984 __FUNCTION__,
985 esc_html(
986 sprintf( /* translators: 1: Option ID */
987 __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
988 $option_id
989 )
990 ),
991 ''
992 );
993
994 return false;
995 }
996
997 if ( 'string' === $option_value_type ) {
998 if ( ! is_string( $value ) ) {
999 _doing_it_wrong(
1000 __FUNCTION__,
1001 esc_html(
1002 sprintf( /* translators: 1: Option ID */
1003 __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
1004 $option_id
1005 )
1006 ),
1007 ''
1008 );
1009
1010 $value = strval( $value );
1011 }
1012
1013 // String options that are restricted to specific values.
1014 $restricted_value_options = array(
1015 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
1016 'meta_type' => array( 'json_ld', 'repeated_metas' ),
1017 );
1018
1019 // Verify that the above values are respected.
1020 foreach ( $restricted_value_options as $option_key => $valid_values ) {
1021 if ( $option_id === $option_key ) {
1022 if ( ! in_array( $value, $valid_values, true ) ) {
1023 _doing_it_wrong(
1024 __FUNCTION__,
1025 esc_html(
1026 sprintf( /* translators: 1: Option value 2: Option ID */
1027 __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
1028 $value,
1029 $option_id
1030 )
1031 ),
1032 ''
1033 );
1034
1035 $value = $this->option_defaults[ $option_id ];
1036 }
1037 }
1038 }
1039 }
1040
1041 return $value;
1042 }
1043
1044 /**
1045 * Allows remote requests to Parse.ly.
1046 *
1047 * This is needed for environments, such as wp-now, that block remote requests.
1048 *
1049 * @since 3.13.0
1050 * @access private
1051 */
1052 private function allow_parsely_remote_requests(): void {
1053 $allowed_urls = array(
1054 self::DASHBOARD_BASE_URL,
1055 Content_API_Service::get_base_url(),
1056 Suggestions_API_Service::get_base_url(),
1057 );
1058
1059 add_filter(
1060 'http_request_host_is_external',
1061 function ( $external, $host, $url ) use ( $allowed_urls ) {
1062 // Check if the URL matches any URLs on the allowed list.
1063 foreach ( $allowed_urls as $allowed_url ) {
1064 if ( Utils::str_starts_with( $url, $allowed_url ) ) {
1065 return true;
1066 }
1067 }
1068 return $external;
1069 },
1070 10,
1071 3
1072 );
1073 }
1074 }
1075