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

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