PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.2 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 All 106 releases
← All changes | src/class-parsely.php +343 -66 3.16.0 → 3.24.2 View file →
@@ -9,8 +9,12 @@
9 9 declare(strict_types=1);
10 10
11 11 namespace Parsely;
12 12
13 +use Parsely\Content_Helper\Suggestion_Defaults;
14 +use Parsely\REST_API\REST_API_Controller;
15 +use Parsely\Services\Content_API\Content_API_Service;
16 +use Parsely\Services\Suggestions_API\Suggestions_API_Service;
13 17 use Parsely\UI\Metadata_Renderer;
14 18 use Parsely\UI\Settings_Page;
15 19 use WP_Post;
16 20
@@ -27,8 +31,9 @@
27 31 * use_top_level_cats: bool,
28 32 * custom_taxonomy_section: string,
29 33 * cats_as_tags: bool,
30 34 * content_helper: Parsely_Options_Content_Helper,
35 + * headline_testing: Parsely_Options_Headline_Testing,
31 36 * track_authenticated_users: bool,
32 37 * lowercase_tags: bool,
33 38 * force_https_canonicals: bool,
34 39 * track_post_types: string[],
@@ -48,22 +53,35 @@
48 53 * ai_features_enabled: bool,
49 54 * smart_linking: Parsely_Options_Content_Helper_Feature,
50 55 * title_suggestions: Parsely_Options_Content_Helper_Feature,
51 56 * excerpt_suggestions: Parsely_Options_Content_Helper_Feature,
57 + * traffic_boost: Parsely_Options_Content_Helper_Feature,
52 58 * }
53 59 *
54 60 * @phpstan-type Parsely_Options_Content_Helper_Feature array{
55 61 * enabled: bool,
56 62 * allowed_user_roles: string[],
63 + * default_length?: int,
64 + * default_tone?: string,
65 + * default_persona?: string,
57 66 * }
58 67 *
68 + * @phpstan-type Parsely_Options_Headline_Testing array{
69 + * enabled: bool,
70 + * installation_method: string,
71 + * enable_flicker_control: bool,
72 + * enable_live_updates: bool,
73 + * live_update_timeout: int,
74 + * allow_after_content_load: bool,
75 + * }
76 + *
59 77 * @phpstan-type WP_HTTP_Request_Args array{
60 - * method: string,
61 - * timeout: float,
62 - * blocking: bool,
63 - * headers: array<string, string>,
64 - * body: string,
65 - * data_format: string,
78 + * method?: string,
79 + * timeout?: float,
80 + * blocking?: bool,
81 + * headers?: array<string, string>,
82 + * body?: string,
83 + * data_format?: string,
66 84 * }
67 85 *
68 86 * @phpstan-import-type Metadata_Attributes from Metadata
69 87 */
@@ -70,17 +88,38 @@
70 88 class Parsely {
71 89 /**
72 90 * Declare our constants
73 91 */
74 - public const VERSION = PARSELY_VERSION;
75 - public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php.
76 - public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
77 - public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
78 - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
79 - public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2';
80 - public const PUBLIC_SUGGESTIONS_API_BASE_URL = 'https://content-suggestions-api.parsely.net/prod';
92 + public const VERSION = PARSELY_VERSION;
93 + public const MENU_SLUG = 'parsely-settings'; // The page param passed to admin.php.
94 + public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
95 + public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
96 + public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
81 97
98 + private const PARSELY_CANONICAL_URL_META_KEY = '_parsely_canonical_url';
99 +
82 100 /**
101 + * The Content API service.
102 + *
103 + * @var ?Content_API_Service $content_api_service
104 + */
105 + private $content_api_service;
106 +
107 + /**
108 + * The Suggestions API service.
109 + *
110 + * @var ?Suggestions_API_Service $suggestions_api_service
111 + */
112 + private $suggestions_api_service;
113 +
114 + /**
115 + * The Parse.ly internal REST API controller.
116 + *
117 + * @var REST_API_Controller|null $rest_api_controller
118 + */
119 + private $rest_api_controller;
120 +
121 + /**
83 122 * Declare some class properties
84 123 *
85 124 * @var Parsely_Options $option_defaults The defaults we need for the class.
86 125 */
@@ -99,14 +138,31 @@
99 138 ),
100 139 'title_suggestions' => array(
101 140 'enabled' => true,
102 141 'allowed_user_roles' => array( 'administrator' ),
142 + 'default_tone' => Suggestion_Defaults::DEFAULT_TONE,
143 + 'default_persona' => Suggestion_Defaults::DEFAULT_PERSONA,
103 144 ),
104 145 'excerpt_suggestions' => array(
105 146 'enabled' => true,
106 147 'allowed_user_roles' => array( 'administrator' ),
148 + 'default_length' => Suggestion_Defaults::DEFAULT_LENGTH,
149 + 'default_tone' => Suggestion_Defaults::DEFAULT_TONE,
150 + 'default_persona' => Suggestion_Defaults::DEFAULT_PERSONA,
107 151 ),
152 + 'traffic_boost' => array(
153 + 'enabled' => true,
154 + 'allowed_user_roles' => array( 'administrator' ),
155 + ),
108 156 ),
157 + 'headline_testing' => array(
158 + 'enabled' => false,
159 + 'installation_method' => 'one_line',
160 + 'enable_flicker_control' => false,
161 + 'enable_live_updates' => false,
162 + 'live_update_timeout' => 30000,
163 + 'allow_after_content_load' => false,
164 + ),
109 165 'track_authenticated_users' => false,
110 166 'lowercase_tags' => true,
111 167 'force_https_canonicals' => false,
112 168 'track_post_types' => array(),
@@ -209,8 +265,34 @@
209 265 $this->allow_parsely_remote_requests();
210 266 }
211 267
212 268 /**
269 + * Gets the allowed post statuses for tracking.
270 + *
271 + * Uses the `wp_parsely_trackable_statuses` filter to determine which post statuses are allowed to be tracked.
272 + *
273 + * @since 3.17.0
274 + *
275 + * @param WP_Post|int|null $post The post object.
276 + * @return array<string> The allowed post statuses.
277 + */
278 + public static function get_trackable_statuses( $post = null ): array {
279 + /**
280 + * Filters the statuses that are permitted to be tracked.
281 + *
282 + * By default, the only status tracked is 'publish'. Use this filter if
283 + * you have other published content that has a different (custom) status.
284 + *
285 + * @since 2.5.0
286 + * @since 3.17.0 Filter extracted to a separate method.
287 + *
288 + * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
289 + * @param WP_Post|int|null $post Which post object or ID is being checked.
290 + */
291 + return apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
292 + }
293 +
294 + /**
213 295 * Registers action and filter hook callbacks, and immediately upgrades
214 296 * options if needed.
215 297 */
216 298 public function run(): void {
@@ -232,12 +314,64 @@
232 314 $options['plugin_version'] = self::VERSION;
233 315 update_option( self::OPTIONS_KEY, $options );
234 316 }
235 317
236 - add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
318 + // @phpstan-ignore return.void
319 + add_action( 'save_post', array( $this, 'call_update_metadata_endpoint' ) );
237 320 }
238 321
239 322 /**
323 + * Returns the Content API service.
324 + *
325 + * This method returns the Content API service, which is used to interact with the Parse.ly Content API.
326 + *
327 + * @since 3.17.0
328 + *
329 + * @return Content_API_Service
330 + */
331 + public function get_content_api(): Content_API_Service {
332 + if ( ! isset( $this->content_api_service ) ) {
333 + $this->content_api_service = new Content_API_Service( $this );
334 + }
335 +
336 + return $this->content_api_service;
337 + }
338 +
339 + /**
340 + * Returns the Suggestions API service.
341 + *
342 + * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
343 + *
344 + * @since 3.17.0
345 + *
346 + * @return Suggestions_API_Service
347 + */
348 + public function get_suggestions_api(): Suggestions_API_Service {
349 + if ( ! isset( $this->suggestions_api_service ) ) {
350 + $this->suggestions_api_service = new Suggestions_API_Service( $this );
351 + }
352 +
353 + return $this->suggestions_api_service;
354 + }
355 +
356 + /**
357 + * Gets the REST API controller.
358 + *
359 + * If the controller is not set, a new instance is created.
360 + *
361 + * @since 3.17.0
362 + *
363 + * @return REST_API_Controller
364 + */
365 + public function get_rest_api_controller(): REST_API_Controller {
366 + if ( ! isset( $this->rest_api_controller ) ) {
367 + $this->rest_api_controller = new REST_API_Controller( $this );
368 + }
369 +
370 + return $this->rest_api_controller;
371 + }
372 +
373 + /**
240 374 * Gets the full URL of the JavaScript tracker file for the site. If an API
241 375 * key is not set, return an empty string.
242 376 *
243 377 * @since 3.2.0
@@ -246,10 +380,21 @@
246 380 */
247 381 public function get_tracker_url(): string {
248 382 if ( $this->site_id_is_set() ) {
249 383 $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
250 - return esc_url( $tracker_url );
384 +
385 + /**
386 + * Filters the URL of the Parse.ly tracker script.
387 + *
388 + * The filtered value gets sanitized with {@see esc_url_raw()}.
389 + *
390 + * @since 3.23.0
391 + *
392 + * @param string $tracker_url The URL of the tracker script.
393 + */
394 + return esc_url_raw( apply_filters( 'wp_parsely_tracker_url', $tracker_url ) );
251 395 }
396 +
252 397 return '';
253 398 }
254 399
255 400 /**
@@ -296,14 +441,8 @@
296 441 * @return bool Should the post status be tracked for the provided post's post_type.
297 442 * By default,only 'publish' is allowed.
298 443 */
299 444 public static function post_has_trackable_status( $post ): bool {
300 - static $cache = array();
301 - $post_id = is_int( $post ) ? $post : $post->ID;
302 - if ( isset( $cache[ $post_id ] ) ) {
303 - return $cache[ $post_id ];
304 - }
305 -
306 445 /**
307 446 * Filters whether the post password check should be skipped when getting
308 447 * the post trackable status.
309 448 *
@@ -315,26 +454,13 @@
315 454 * @return bool
316 455 */
317 456 $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
318 457 if ( ! $skip_password_check && post_password_required( $post ) ) {
319 - $cache[ $post_id ] = false;
320 458 return false;
321 459 }
322 460
323 - /**
324 - * Filters the statuses that are permitted to be tracked.
325 - *
326 - * By default, the only status tracked is 'publish'. Use this filter if
327 - * you have other published content that has a different (custom) status.
328 - *
329 - * @since 2.5.0
330 - *
331 - * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
332 - * @param int|WP_Post $post Which post object or ID is being checked.
333 - */
334 - $statuses = apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
335 - $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true );
336 - return $cache[ $post_id ];
461 + $statuses = self::get_trackable_statuses( $post );
462 + return in_array( get_post_status( $post ), $statuses, true );
337 463 }
338 464
339 465 /**
340 466 * Deprecated. Please use the `Metadata` class instead.
@@ -354,23 +480,52 @@
354 480 return $metadata->construct_metadata( $post );
355 481 }
356 482
357 483 /**
358 - * Updates the Parsely metadata endpoint with the new metadata of the post.
484 + * Calls Parse.ly's update metadata endpoint, sending the post's updated
485 + * metadata.
359 486 *
360 - * @param int $post_id id of the post to update.
487 + * @param int $post_id The ID of the post to update.
488 + * @return bool True if the metadata endpoint was called, false otherwise.
361 489 */
362 - public function update_metadata_endpoint( int $post_id ): void {
363 - $parsely_options = $this->get_options();
364 - if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) {
365 - return;
490 + public function call_update_metadata_endpoint( int $post_id ): bool {
491 + $options = $this->get_options();
492 +
493 + if ( $this->site_id_is_missing() || '' === $options['metadata_secret'] ) {
494 + return false;
366 495 }
367 496
497 + $current_post_type = get_post_type( $post_id );
498 + if ( false === $current_post_type ) {
499 + return false;
500 + }
501 +
502 + $tracked_post_types = array_merge(
503 + $options['track_post_types'],
504 + $options['track_page_types']
505 + );
506 +
507 + // Check that the post's type is trackable.
508 + if ( ! in_array( $current_post_type, $tracked_post_types, true ) ) {
509 + return false;
510 + }
511 +
512 + // Check that the post's status is trackable.
513 + if ( ! self::post_has_trackable_status( $post_id ) ) {
514 + return false;
515 + }
516 +
368 517 $post = get_post( $post_id );
369 518 if ( null === $post ) {
370 - return;
519 + return false;
371 520 }
372 521
522 + // Don't call the endpoint when integration tests are running, but
523 + // signal that the above checks have passed.
524 + if ( defined( 'INTEGRATION_TESTS_RUNNING' ) ) {
525 + return true;
526 + }
527 +
373 528 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
374 529
375 530 $endpoint_metadata = array(
376 531 'canonical_url' => $metadata['url'] ?? '',
@@ -382,10 +537,11 @@
382 537 'authors' => $metadata['creator'] ?? '',
383 538 'tags' => $metadata['keywords'] ?? '',
384 539 );
385 540
386 - $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts';
387 - $parsely_metadata_secret = $parsely_options['metadata_secret'];
541 + $parsely_api_base_url = Content_API_Service::get_base_url();
542 + $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
543 + $parsely_metadata_secret = $options['metadata_secret'];
388 544
389 545 $headers = array( 'Content-Type' => 'application/json' );
390 546 $body = wp_json_encode(
391 547 array(
@@ -397,11 +553,11 @@
397 553
398 554 /**
399 555 * POST request options.
400 556 *
401 - * @var WP_HTTP_Request_Args $options
557 + * @var WP_HTTP_Request_Args $request_options
402 558 */
403 - $options = array(
559 + $request_options = array(
404 560 'method' => 'POST',
405 561 'headers' => $headers,
406 562 'blocking' => false,
407 563 'body' => $body,
@@ -407,14 +563,17 @@
407 563 'body' => $body,
408 564 'data_format' => 'body',
409 565 );
410 566
411 - $response = wp_remote_post( $parsely_api_endpoint, $options );
567 + $response = wp_remote_post( $parsely_api_endpoint, $request_options );
412 568
413 - if ( ! is_wp_error( $response ) ) {
414 - $current_timestamp = time();
415 - update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
569 + if ( is_wp_error( $response ) ) {
570 + return false;
416 571 }
572 +
573 + update_post_meta( $post_id, 'parsely_metadata_last_updated', time() );
574 +
575 + return true;
417 576 }
418 577
419 578 /**
420 579 * Safely returns options for the plugin by assigning defaults contained in
@@ -432,20 +591,42 @@
432 591 * @var Parsely_Options|null
433 592 */
434 593 $options = get_option( self::OPTIONS_KEY, null );
435 594
436 - // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
595 + // Existing plugin installation without full metadata option.
596 + /* @phpstan-ignore isset.offset, booleanAnd.alwaysFalse */
437 597 if ( is_array( $options ) && ! isset( $options['full_metadata_in_non_posts'] ) ) {
438 - // Existing plugin installation without full metadata option.
439 598 $this->set_default_full_metadata_in_non_posts();
440 599 }
441 600
442 - // @phpstan-ignore isset.offset, booleanAnd.alwaysFalse
601 + // Existing plugin installation without Content Intelligence options.
602 + /* @phpstan-ignore isset.offset, booleanAnd.alwaysFalse */
443 603 if ( is_array( $options ) && ! isset( $options['content_helper'] ) ) {
444 - // Existing plugin installation without Content Helper options.
445 604 $this->set_default_content_helper_settings_values();
446 605 }
447 606
607 + // Existing plugin installation that's missing a Content Intelligence
608 + // feature option.
609 + /* @phpstan-ignore isset.offset */
610 + if ( is_array( $options ) && isset( $options['content_helper'] ) ) {
611 + /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options */
612 + $pch_options = $options['content_helper'];
613 +
614 + /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options_defaults */
615 + $pch_options_defaults = $this->option_defaults['content_helper'];
616 +
617 + if ( count( $pch_options ) !== count( $pch_options_defaults ) ) {
618 + $new_keys = array_diff(
619 + array_keys( $pch_options_defaults ),
620 + array_keys( $pch_options )
621 + );
622 +
623 + foreach ( $new_keys as $key ) {
624 + $options['content_helper'][ $key ] = $pch_options_defaults[ $key ];
625 + }
626 + }
627 + }
628 +
448 629 // New plugin installation that hasn't saved its options yet.
449 630 if ( ! is_array( $options ) ) {
450 631 $this->set_default_track_as_values();
451 632 $this->set_default_full_metadata_in_non_posts();
@@ -541,9 +722,9 @@
541 722 }
542 723 }
543 724
544 725 /**
545 - * Sets the default values for Content Helper options.
726 + * Sets the default values for Content Intelligence options.
546 727 *
547 728 * Gives PCH access to all users having the edit_posts capability, to keep
548 729 * consistent behavior with plugin versions prior to 3.16.0.
549 730 *
@@ -563,10 +744,10 @@
563 744 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
564 745 * for context (Default null for current).
565 746 * @return string
566 747 */
567 - public static function get_settings_url( int $_blog_id = null ): string {
568 - return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
748 + public static function get_settings_url( ?int $_blog_id = null ): string {
749 + return get_admin_url( $_blog_id, 'admin.php?page=' . self::MENU_SLUG );
569 750 }
570 751
571 752 /**
572 753 * Returns the URL of the Parse.ly dashboard for a specific page. If a page
@@ -851,8 +1032,97 @@
851 1032 }
852 1033 }
853 1034
854 1035 /**
1036 + * Gets the Parse.ly canonical URL for a given post.
1037 + *
1038 + * @since 3.19.0
1039 + *
1040 + * @param WP_Post|int $post The post ID or post object.
1041 + * @return string The Parse.ly canonical URL.
1042 + */
1043 + public static function get_canonical_url_from_post( $post ): string {
1044 + $post_id = is_int( $post ) ? $post : $post->ID;
1045 + $canonical_url = get_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, true );
1046 +
1047 + if ( null !== $canonical_url && is_string( $canonical_url ) && '' !== $canonical_url ) {
1048 + return self::get_canonical_url( $canonical_url );
1049 + }
1050 +
1051 + $permalink = get_permalink( $post );
1052 +
1053 + if ( false === $permalink ) {
1054 + return __( 'no permalink', 'wp-parsely' );
1055 + }
1056 +
1057 + return self::get_canonical_url( $permalink );
1058 + }
1059 +
1060 + /**
1061 + * Returns the canonical version of the passed URL.
1062 + *
1063 + * In this context, the canonical URL is the URL containing the Site ID as
1064 + * its domain. If the Site ID differs from the real domain, the
1065 + * `wp_parsely_canonical_url_domain` filter can be used to set it.
1066 + *
1067 + * @since 3.19.0
1068 + * @since 3.20.4 Made the domain overridable.
1069 + *
1070 + * @param string $url The URL to get the canonical URL for.
1071 + * @return string The canonical URL.
1072 + */
1073 + public static function get_canonical_url( string $url ): string {
1074 + $canonical_url_domain = apply_filters(
1075 + 'wp_parsely_canonical_url_domain',
1076 + null
1077 + );
1078 +
1079 + // Handle domain override.
1080 + if ( is_string( $canonical_url_domain ) ) {
1081 + // Get the canonical URL domain without protocol, trailing slashes
1082 + // or accidental whitespace.
1083 + $canonical_url_domain = rtrim( trim( $canonical_url_domain ), '/' );
1084 + $canonical_url_domain = preg_replace( '#^https?://#', '', $canonical_url_domain );
1085 +
1086 + if ( is_string( $canonical_url_domain ) && '' !== $canonical_url_domain ) {
1087 + $url_domain = (string) wp_parse_url( $url, PHP_URL_HOST );
1088 + return str_replace( $url_domain, $canonical_url_domain, $url );
1089 + }
1090 + }
1091 +
1092 + $site_id = \Parsely\get_parsely()->get_site_id();
1093 + $home_url_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
1094 +
1095 + if ( $home_url_host === $site_id ) {
1096 + // URL does not need to be modified.
1097 + return $url;
1098 + }
1099 +
1100 + // Return the URL with the Site ID as the domain.
1101 + return str_replace( $home_url_host, $site_id, $url );
1102 + }
1103 +
1104 + /**
1105 + * Sets the Parse.ly canonical URL for a post.
1106 + *
1107 + * @since 3.19.0
1108 + *
1109 + * @param WP_Post|int $post The post object or post ID.
1110 + * @param string $url The canonical URL.
1111 + * @return bool True if the canonical URL was set, false otherwise.
1112 + */
1113 + public static function set_canonical_url( $post, string $url ): bool {
1114 + $post_id = is_int( $post ) ? $post : $post->ID;
1115 + $canonical_url = self::get_canonical_url( $url );
1116 +
1117 + return false !== update_post_meta(
1118 + $post_id,
1119 + self::PARSELY_CANONICAL_URL_META_KEY,
1120 + sanitize_url( $canonical_url, array( 'http', 'https' ) )
1121 + );
1122 + }
1123 +
1124 + /**
855 1125 * Sanitizes the value of the passed managed option.
856 1126 *
857 1127 * @since 3.9.0
858 1128 * @access private
@@ -931,26 +1201,33 @@
931 1201 *
932 1202 * This is needed for environments, such as wp-now, that block remote requests.
933 1203 *
934 1204 * @since 3.13.0
1205 + * @since 3.24.2 Matches the Parse.ly hosts exactly, over HTTPS.
935 1206 * @access private
936 1207 */
937 1208 private function allow_parsely_remote_requests(): void {
938 1209 $allowed_urls = array(
939 1210 self::DASHBOARD_BASE_URL,
940 - self::PUBLIC_API_BASE_URL,
941 - self::PUBLIC_SUGGESTIONS_API_BASE_URL,
1211 + Content_API_Service::get_base_url(),
1212 + Suggestions_API_Service::get_base_url(),
942 1213 );
943 1214
944 1215 add_filter(
945 1216 'http_request_host_is_external',
946 - function ( $external, $host, $url ) use ( $allowed_urls ) {
947 - // Check if the URL matches any URLs on the allowed list.
948 - foreach ( $allowed_urls as $allowed_url ) {
949 - if ( \Parsely\Utils\str_starts_with( $url, $allowed_url ) ) {
950 - return true;
951 - }
1217 + function ( bool $external, string $host, string $url ) use ( $allowed_urls ) {
1218 + $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
1219 + $allowed_hosts = array_map(
1220 + static function ( string $allowed_url ): string {
1221 + return (string) wp_parse_url( $allowed_url, PHP_URL_HOST );
1222 + },
1223 + $allowed_urls
1224 + );
1225 +
1226 + if ( 'https' === $scheme && in_array( strtolower( $host ), $allowed_hosts, true ) ) {
1227 + return true;
952 1228 }
1229 +
953 1230 return $external;
954 1231 },
955 1232 10,
956 1233 3