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

1,193 lines 32.6 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 Intelligence 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 Intelligence
571 // feature option.
572 /* @phpstan-ignore isset.offset */
573 if ( is_array( $options ) && isset( $options['content_helper'] ) ) {
574 /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options */
575 $pch_options = $options['content_helper'];
576
577 /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options_defaults */
578 $pch_options_defaults = $this->option_defaults['content_helper'];
579
580 if ( count( $pch_options ) !== count( $pch_options_defaults ) ) {
581 $new_keys = array_diff(
582 array_keys( $pch_options_defaults ),
583 array_keys( $pch_options )
584 );
585
586 foreach ( $new_keys as $key ) {
587 $options['content_helper'][ $key ] = $pch_options_defaults[ $key ];
588 }
589 }
590 }
591
592 // New plugin installation that hasn't saved its options yet.
593 if ( ! is_array( $options ) ) {
594 $this->set_default_track_as_values();
595 $this->set_default_full_metadata_in_non_posts();
596 $options = $this->option_defaults;
597 }
598
599 /**
600 * Final options including managed credentials and options.
601 *
602 * @var Parsely_Options
603 */
604 return array_merge(
605 $this->option_defaults,
606 $options,
607 $this->get_managed_credentials(),
608 $this->managed_options
609 );
610 }
611
612 /**
613 * Returns the value of a nested option.
614 *
615 * @since 3.16.0
616 *
617 * @param string $option The option to get.
618 * @param Parsely_Options $options The options to get the value from.
619 * @return mixed The value of the nested option.
620 */
621 public static function get_nested_option_value( $option, $options ) {
622 $keys = explode( '[', str_replace( ']', '', $option ) );
623 $value = $options;
624
625 foreach ( $keys as $key ) {
626 if ( isset( $value[ $key ] ) ) {
627 $value = $value[ $key ];
628 }
629 }
630
631 return $value;
632 }
633
634 /**
635 * Sets the default values for the track_post_types and track_page_types
636 * options.
637 *
638 * @since 3.9.0
639 */
640 public function set_default_track_as_values(): void {
641 $this->option_defaults['track_page_types'] = array();
642 $this->option_defaults['track_post_types'] = array();
643
644 $post_types = get_post_types( array( 'public' => true ) );
645
646 foreach ( $post_types as $post_type ) {
647 if ( ! post_type_supports( $post_type, 'editor' ) ) {
648 continue;
649 }
650
651 if ( is_post_type_hierarchical( $post_type ) ) {
652 $this->option_defaults['track_page_types'][] = $post_type;
653 } else {
654 $this->option_defaults['track_post_types'][] = $post_type;
655 }
656 }
657 }
658
659 /**
660 * Sets the default value for the full_metadata_in_non_posts option.
661 *
662 * @since 3.14.0
663 */
664 public function set_default_full_metadata_in_non_posts(): void {
665 $this->option_defaults['full_metadata_in_non_posts'] = true;
666
667 // Usage of any of these filters will result in the setting being set
668 // to false.
669 $filter_tags = array(
670 'wp_parsely_metadata',
671 'wp_parsely_post_tags',
672 'wp_parsely_permalink',
673 'wp_parsely_post_category',
674 'wp_parsely_pre_authors',
675 'wp_parsely_post_authors',
676 'wp_parsely_custom_taxonomies',
677 'wp_parsely_post_type',
678 );
679
680 foreach ( $filter_tags as $filter_tag ) {
681 if ( has_filter( $filter_tag ) ) {
682 $this->option_defaults['full_metadata_in_non_posts'] = false;
683 break;
684 }
685 }
686 }
687
688 /**
689 * Sets the default values for Content Intelligence options.
690 *
691 * Gives PCH access to all users having the edit_posts capability, to keep
692 * consistent behavior with plugin versions prior to 3.16.0.
693 *
694 * @since 3.16.0
695 */
696 public function set_default_content_helper_settings_values(): void {
697 $this->option_defaults['content_helper'] =
698 Permissions::build_pch_permissions_settings_array(
699 true,
700 array_keys( Permissions::get_user_roles_with_edit_posts_cap() )
701 );
702 }
703
704 /**
705 * Gets the URL of the plugin's settings page.
706 *
707 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
708 * for context (Default null for current).
709 * @return string
710 */
711 public static function get_settings_url( ?int $_blog_id = null ): string {
712 return get_admin_url( $_blog_id, 'admin.php?page=' . self::MENU_SLUG );
713 }
714
715 /**
716 * Returns the URL of the Parse.ly dashboard for a specific page. If a page
717 * is not specified, the home dashboard URL for the specified Site ID is
718 * returned.
719 *
720 * @since 3.7.0
721 *
722 * @param string $site_id The Site ID for which to get the URL.
723 * @param string $page_url Optional. The page for which to get the URL.
724 * @return string The complete dashboard URL.
725 */
726 public static function get_dash_url( string $site_id, string $page_url = '' ): string {
727 $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
728
729 if ( '' !== $page_url ) {
730 $page_url = self::get_url_with_itm_source( $page_url, null );
731 $result .= '?url=' . rawurlencode( $page_url );
732 }
733
734 return $result;
735 }
736
737 /**
738 * Adds or replaces the itm_source parameter in the URL. Removes the
739 * parameter if the passed value is null or an empty string.
740 *
741 * @since 3.9.0
742 *
743 * @param string $url The URL to modify.
744 * @param string|null $itm_source The value of the itm_source parameter.
745 * @return string The resulting URL.
746 */
747 public static function get_url_with_itm_source( string $url, $itm_source ): string {
748 if ( null === $itm_source || '' === $itm_source ) {
749 return remove_query_arg( 'itm_source', $url );
750 }
751
752 $itm_source = rawurlencode( $itm_source );
753
754 return add_query_arg( 'itm_source', $itm_source, $url );
755 }
756
757 /**
758 * Checks to see if the current user is a member of the current blog.
759 *
760 * @return bool
761 */
762 public function is_blog_member_logged_in(): bool {
763 // Can't use $blog_id here because it futzes with the global $blog_id.
764 $current_blog_id = get_current_blog_id();
765 $current_user_id = get_current_user_id();
766
767 return is_user_member_of_blog( $current_user_id, $current_blog_id );
768 }
769
770 /**
771 * Converts JSON-LD type to respective Parse.ly page type.
772 *
773 * If the JSON-LD type is one of the types Parse.ly supports as a "post",
774 * then "post" will be returned. Otherwise, for "non-posts" and unknown
775 * types, "index" is returned.
776 *
777 * @since 2.5.0
778 *
779 * @see https://docs.parse.ly/metatags/#h-field-description
780 *
781 * @param string $type JSON-LD type.
782 * @return string "post" or "index".
783 */
784 public function convert_jsonld_to_parsely_type( string $type ): string {
785 return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
786 }
787
788 /**
789 * Determines if a Site ID is saved in the options.
790 *
791 * @since 2.6.0
792 * @since 3.7.0 renamed from api_key_is_set.
793 *
794 * @return bool True is Site ID is set, false if it is missing.
795 */
796 public function site_id_is_set(): bool {
797 $options = $this->get_options();
798
799 return '' !== $options['apikey'];
800 }
801
802 /**
803 * Determines if a Site ID is not saved in the options.
804 *
805 * @since 2.6.0
806 * @since 3.7.0 renamed from api_key_is_missing.
807 *
808 * @return bool True if Site ID is missing, false if it is set.
809 */
810 public function site_id_is_missing(): bool {
811 return ! $this->site_id_is_set();
812 }
813
814 /**
815 * Gets the Site ID if set.
816 *
817 * @since 2.6.0
818 * @since 3.7.0 renamed from get_site_id.
819 *
820 * @return string Site ID if set, or empty string if not.
821 */
822 public function get_site_id(): string {
823 $options = $this->get_options();
824
825 return $this->site_id_is_set() ? $options['apikey'] : '';
826 }
827
828 /**
829 * Returns whether the API Secret is set in the plugin's options.
830 *
831 * @since 3.4.0
832 *
833 * @return bool True if the API Secret is set, false if not set.
834 */
835 public function api_secret_is_set(): bool {
836 $options = $this->get_options();
837
838 return '' !== $options['api_secret'];
839 }
840
841 /**
842 * Returns the API Secret stored in the plugin's options.
843 *
844 * @since 3.4.0
845 *
846 * @return string The API Secret, empty string if the API secret is not set.
847 */
848 public function get_api_secret(): string {
849 $options = $this->get_options();
850
851 return $this->api_secret_is_set() ? $options['api_secret'] : '';
852 }
853
854 /**
855 * Returns all supported post and non-post types.
856 *
857 * @since 3.7.0
858 *
859 * @return string[] all supported types
860 */
861 public function get_all_supported_types(): array {
862 return self::$all_supported_types;
863 }
864
865 /**
866 * Gets all tracked post types.
867 *
868 * @since 3.7.0
869 *
870 * @return array<string>
871 */
872 public function get_all_track_types(): array {
873 $options = $this->get_options();
874
875 return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
876 }
877
878 /**
879 * Gets default options.
880 *
881 * @since 3.8.0
882 *
883 * @return Parsely_Options
884 */
885 public function get_default_options() {
886 return $this->option_defaults;
887 }
888
889 /**
890 * Returns the credentials that are being managed at the platform level.
891 *
892 * @since 3.9.0
893 * @access private
894 *
895 * @return Parsely_Options|array<empty> The managed credentials.
896 */
897 private function get_managed_credentials() {
898 if ( true !== $this->are_credentials_managed ) {
899 return array();
900 }
901
902 $credentials = apply_filters( 'wp_parsely_credentials', array() );
903
904 if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
905 return array();
906 }
907
908 $result = array();
909
910 if ( isset( $credentials['site_id'] ) ) {
911 $result['apikey'] = $credentials['site_id'];
912 }
913
914 if ( isset( $credentials['api_secret'] ) ) {
915 $result['api_secret'] = $credentials['api_secret'];
916 }
917
918 if ( isset( $credentials['metadata_secret'] ) ) {
919 $result['metadata_secret'] = $credentials['metadata_secret'];
920 }
921
922 return $result;
923 }
924
925 /**
926 * Returns whether credentials are being managed at the platform level.
927 *
928 * @since 3.9.0
929 * @access private
930 *
931 * @return bool Whether credentials are being managed at the platform level.
932 */
933 private function are_credentials_managed(): bool {
934 $credentials = apply_filters( 'wp_parsely_credentials', array() );
935
936 if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
937 return false;
938 }
939
940 return $credentials['is_managed'] ?? false;
941 }
942
943 /**
944 * Sets the values of managed options.
945 *
946 * This function won't accept managing credentials or certain plugin options
947 * that are being managed through other means. For managing credentials,
948 * please use the `wp_parsely_credentials` filter.
949 *
950 * @since 3.9.0
951 * @access private
952 */
953 private function set_managed_options(): void {
954 $managed_options = apply_filters( 'wp_parsely_managed_options', false );
955
956 if ( ! is_array( $managed_options ) ) {
957 return;
958 }
959
960 // Don't allow certain options to be set as managed.
961 unset(
962 $managed_options['apikey'],
963 $managed_options['api_secret'],
964 $managed_options['metadata_secret'],
965 $managed_options['track_post_types'],
966 $managed_options['track_page_types'],
967 $managed_options['plugin_version']
968 );
969
970 if ( 0 === count( $managed_options ) ) {
971 return;
972 }
973
974 /**
975 * Current options.
976 *
977 * @var Parsely_Options $current_options
978 */
979 $current_options = get_option( self::OPTIONS_KEY, array() );
980
981 // Set managed options values.
982 foreach ( $managed_options as $key => $value ) {
983 $is_option_valid = isset( $this->option_defaults[ $key ] );
984
985 if ( $is_option_valid ) {
986 if ( null === $value ) {
987 // When null, the option gets its value from the database.
988 $this->managed_options[ $key ] =
989 $current_options[ $key ] ?? $this->option_defaults[ $key ];
990 } else {
991 $this->managed_options[ $key ] =
992 $this->sanitize_managed_option( $key, $value );
993 }
994 }
995 }
996 }
997
998 /**
999 * Gets the Parse.ly canonical URL for a given post.
1000 *
1001 * @since 3.19.0
1002 *
1003 * @param WP_Post|int $post The post ID or post object.
1004 * @return string The Parse.ly canonical URL.
1005 */
1006 public static function get_canonical_url_from_post( $post ): string {
1007 $post_id = is_int( $post ) ? $post : $post->ID;
1008 $canonical_url = get_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, true );
1009
1010 if ( null !== $canonical_url && is_string( $canonical_url ) && '' !== $canonical_url ) {
1011 return self::get_canonical_url( $canonical_url );
1012 }
1013
1014 $permalink = get_permalink( $post );
1015
1016 if ( false === $permalink ) {
1017 return __( 'no permalink', 'wp-parsely' );
1018 }
1019
1020 return self::get_canonical_url( $permalink );
1021 }
1022
1023 /**
1024 * Returns the canonical version of the passed URL.
1025 *
1026 * In this context, the canonical URL is the URL containing the Site ID as
1027 * its domain. If the Site ID differs from the real domain, the
1028 * `wp_parsely_canonical_url_domain` filter can be used to set it.
1029 *
1030 * @since 3.19.0
1031 * @since 3.20.4 Made the domain overridable.
1032 *
1033 * @param string $url The URL to get the canonical URL for.
1034 * @return string The canonical URL.
1035 */
1036 public static function get_canonical_url( string $url ): string {
1037 $canonical_url_domain = apply_filters(
1038 'wp_parsely_canonical_url_domain',
1039 null
1040 );
1041
1042 // Handle domain override.
1043 if ( is_string( $canonical_url_domain ) ) {
1044 // Get the canonical URL domain without protocol, trailing slashes
1045 // or accidental whitespace.
1046 $canonical_url_domain = rtrim( trim( $canonical_url_domain ), '/' );
1047 $canonical_url_domain = preg_replace( '#^https?://#', '', $canonical_url_domain );
1048
1049 if ( is_string( $canonical_url_domain ) && '' !== $canonical_url_domain ) {
1050 $url_domain = (string) wp_parse_url( $url, PHP_URL_HOST );
1051 return str_replace( $url_domain, $canonical_url_domain, $url );
1052 }
1053 }
1054
1055 $site_id = \Parsely\get_parsely()->get_site_id();
1056 $home_url_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
1057
1058 if ( $home_url_host === $site_id ) {
1059 // URL does not need to be modified.
1060 return $url;
1061 }
1062
1063 // Return the URL with the Site ID as the domain.
1064 return str_replace( $home_url_host, $site_id, $url );
1065 }
1066
1067 /**
1068 * Sets the Parse.ly canonical URL for a post.
1069 *
1070 * @since 3.19.0
1071 *
1072 * @param WP_Post|int $post The post object or post ID.
1073 * @param string $url The canonical URL.
1074 * @return bool True if the canonical URL was set, false otherwise.
1075 */
1076 public static function set_canonical_url( $post, string $url ): bool {
1077 $post_id = is_int( $post ) ? $post : $post->ID;
1078 $canonical_url = self::get_canonical_url( $url );
1079
1080 return false !== update_post_meta(
1081 $post_id,
1082 self::PARSELY_CANONICAL_URL_META_KEY,
1083 sanitize_url( $canonical_url, array( 'http', 'https' ) )
1084 );
1085 }
1086
1087 /**
1088 * Sanitizes the value of the passed managed option.
1089 *
1090 * @since 3.9.0
1091 * @access private
1092 *
1093 * @param string $option_id The option's ID.
1094 * @param bool|string $value The option's value.
1095 * @return bool|string The sanitized option value.
1096 */
1097 private function sanitize_managed_option( string $option_id, $value ) {
1098 $option_value_type = gettype( $this->option_defaults[ $option_id ] );
1099
1100 if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
1101 _doing_it_wrong(
1102 __FUNCTION__,
1103 esc_html(
1104 sprintf( /* translators: 1: Option ID */
1105 __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
1106 $option_id
1107 )
1108 ),
1109 ''
1110 );
1111
1112 return false;
1113 }
1114
1115 if ( 'string' === $option_value_type ) {
1116 if ( ! is_string( $value ) ) {
1117 _doing_it_wrong(
1118 __FUNCTION__,
1119 esc_html(
1120 sprintf( /* translators: 1: Option ID */
1121 __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
1122 $option_id
1123 )
1124 ),
1125 ''
1126 );
1127
1128 $value = strval( $value );
1129 }
1130
1131 // String options that are restricted to specific values.
1132 $restricted_value_options = array(
1133 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
1134 'meta_type' => array( 'json_ld', 'repeated_metas' ),
1135 );
1136
1137 // Verify that the above values are respected.
1138 foreach ( $restricted_value_options as $option_key => $valid_values ) {
1139 if ( $option_id === $option_key ) {
1140 if ( ! in_array( $value, $valid_values, true ) ) {
1141 _doing_it_wrong(
1142 __FUNCTION__,
1143 esc_html(
1144 sprintf( /* translators: 1: Option value 2: Option ID */
1145 __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
1146 $value,
1147 $option_id
1148 )
1149 ),
1150 ''
1151 );
1152
1153 $value = $this->option_defaults[ $option_id ];
1154 }
1155 }
1156 }
1157 }
1158
1159 return $value;
1160 }
1161
1162 /**
1163 * Allows remote requests to Parse.ly.
1164 *
1165 * This is needed for environments, such as wp-now, that block remote requests.
1166 *
1167 * @since 3.13.0
1168 * @access private
1169 */
1170 private function allow_parsely_remote_requests(): void {
1171 $allowed_urls = array(
1172 self::DASHBOARD_BASE_URL,
1173 Content_API_Service::get_base_url(),
1174 Suggestions_API_Service::get_base_url(),
1175 );
1176
1177 add_filter(
1178 'http_request_host_is_external',
1179 function ( bool $external, string $host, string $url ) use ( $allowed_urls ) {
1180 // Check if the URL matches any URLs on the allowed list.
1181 foreach ( $allowed_urls as $allowed_url ) {
1182 if ( Utils::str_starts_with( $url, $allowed_url ) ) {
1183 return true;
1184 }
1185 }
1186 return $external;
1187 },
1188 10,
1189 3
1190 );
1191 }
1192 }
1193