PluginProbe
Parse.ly / 3.8.4
Parse.ly v3.8.4
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
← All changes | src/class-parsely.php +145 -777 3.23.33.8.4 View file →
@@ -9,14 +9,9 @@
9 9 declare(strict_types=1);
10 10
11 11 namespace Parsely;
12 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 13 use Parsely\UI\Metadata_Renderer;
17 -use Parsely\UI\Settings_Page;
18 -use Parsely\Utils\Utils;
19 14 use WP_Post;
20 15
21 16 /**
22 17 * Holds most of the logic for the plugin.
@@ -30,10 +25,8 @@
30 25 * api_secret: string,
31 26 * use_top_level_cats: bool,
32 27 * custom_taxonomy_section: string,
33 28 * cats_as_tags: bool,
34 - * content_helper: Parsely_Options_Content_Helper,
35 - * headline_testing: Parsely_Options_Headline_Testing,
36 29 * track_authenticated_users: bool,
37 30 * lowercase_tags: bool,
38 31 * force_https_canonicals: bool,
39 32 * track_post_types: string[],
@@ -38,49 +31,18 @@
38 31 * force_https_canonicals: bool,
39 32 * track_post_types: string[],
40 33 * track_page_types: string[],
41 34 * track_post_types_as?: array<string, string>,
42 - * full_metadata_in_non_posts: bool,
43 35 * disable_javascript: bool,
44 36 * disable_amp: bool,
45 37 * meta_type: string,
46 38 * logo: string,
47 39 * metadata_secret: string,
40 + * parsely_wipe_metadata_cache: bool,
48 41 * disable_autotrack: bool,
49 42 * plugin_version: string,
50 43 * }
51 44 *
52 - * @phpstan-type Parsely_Options_Content_Helper array{
53 - * ai_features_enabled: bool,
54 - * smart_linking: Parsely_Options_Content_Helper_Feature,
55 - * title_suggestions: Parsely_Options_Content_Helper_Feature,
56 - * excerpt_suggestions: Parsely_Options_Content_Helper_Feature,
57 - * traffic_boost: Parsely_Options_Content_Helper_Feature,
58 - * }
59 - *
60 - * @phpstan-type Parsely_Options_Content_Helper_Feature array{
61 - * enabled: bool,
62 - * allowed_user_roles: string[],
63 - * }
64 - *
65 - * @phpstan-type Parsely_Options_Headline_Testing array{
66 - * enabled: bool,
67 - * installation_method: string,
68 - * enable_flicker_control: bool,
69 - * enable_live_updates: bool,
70 - * live_update_timeout: int,
71 - * allow_after_content_load: bool,
72 - * }
73 - *
74 - * @phpstan-type WP_HTTP_Request_Args array{
75 - * method?: string,
76 - * timeout?: float,
77 - * blocking?: bool,
78 - * headers?: array<string, string>,
79 - * body?: string,
80 - * data_format?: string,
81 - * }
82 - *
83 45 * @phpstan-import-type Metadata_Attributes from Metadata
84 46 */
85 47 class Parsely {
86 48 /**
@@ -85,98 +47,49 @@
85 47 class Parsely {
86 48 /**
87 49 * Declare our constants
88 50 */
89 - public const VERSION = PARSELY_VERSION;
90 - public const MENU_SLUG = 'parsely-settings'; // The page param passed to admin.php.
91 - public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database.
92 - public const CAPABILITY = 'manage_options'; // The capability required to administer settings.
93 - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
51 + public const VERSION = PARSELY_VERSION;
52 + public const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php.
53 + public const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database.
54 + public const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings.
55 + public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
56 + public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2';
94 57
95 - private const PARSELY_CANONICAL_URL_META_KEY = '_parsely_canonical_url';
96 -
97 58 /**
98 - * The Content API service.
99 - *
100 - * @var ?Content_API_Service $content_api_service
101 - */
102 - private $content_api_service;
103 -
104 - /**
105 - * The Suggestions API service.
106 - *
107 - * @var ?Suggestions_API_Service $suggestions_api_service
108 - */
109 - private $suggestions_api_service;
110 -
111 - /**
112 - * The Parse.ly internal REST API controller.
113 - *
114 - * @var REST_API_Controller|null $rest_api_controller
115 - */
116 - private $rest_api_controller;
117 -
118 - /**
119 59 * Declare some class properties
120 60 *
121 61 * @var Parsely_Options $option_defaults The defaults we need for the class.
122 62 */
123 63 private $option_defaults = array(
124 - 'apikey' => '',
125 - 'content_id_prefix' => '',
126 - 'api_secret' => '',
127 - 'use_top_level_cats' => false,
128 - 'custom_taxonomy_section' => 'category',
129 - 'cats_as_tags' => false,
130 - 'content_helper' => array(
131 - 'ai_features_enabled' => true,
132 - 'smart_linking' => array(
133 - 'enabled' => true,
134 - 'allowed_user_roles' => array( 'administrator' ),
135 - ),
136 - 'title_suggestions' => array(
137 - 'enabled' => true,
138 - 'allowed_user_roles' => array( 'administrator' ),
139 - ),
140 - 'excerpt_suggestions' => array(
141 - 'enabled' => true,
142 - 'allowed_user_roles' => array( 'administrator' ),
143 - ),
144 - 'traffic_boost' => array(
145 - 'enabled' => true,
146 - 'allowed_user_roles' => array( 'administrator' ),
147 - ),
148 - ),
149 - 'headline_testing' => array(
150 - 'enabled' => false,
151 - 'installation_method' => 'one_line',
152 - 'enable_flicker_control' => false,
153 - 'enable_live_updates' => false,
154 - 'live_update_timeout' => 30000,
155 - 'allow_after_content_load' => false,
156 - ),
157 - 'track_authenticated_users' => false,
158 - 'lowercase_tags' => true,
159 - 'force_https_canonicals' => false,
160 - 'track_post_types' => array(),
161 - 'track_page_types' => array(),
162 - 'full_metadata_in_non_posts' => true,
163 - 'disable_javascript' => false,
164 - 'disable_amp' => false,
165 - 'meta_type' => 'json_ld',
166 - 'logo' => '',
167 - 'metadata_secret' => '',
168 - 'disable_autotrack' => false,
169 - 'plugin_version' => self::VERSION,
64 + 'apikey' => '',
65 + 'content_id_prefix' => '',
66 + 'api_secret' => '',
67 + 'use_top_level_cats' => false,
68 + 'custom_taxonomy_section' => 'category',
69 + 'cats_as_tags' => false,
70 + 'track_authenticated_users' => false,
71 + 'lowercase_tags' => true,
72 + 'force_https_canonicals' => false,
73 + 'track_post_types' => array( 'post' ),
74 + 'track_page_types' => array( 'page' ),
75 + 'disable_javascript' => false,
76 + 'disable_amp' => false,
77 + 'meta_type' => 'json_ld',
78 + 'logo' => '',
79 + 'metadata_secret' => '',
80 + 'parsely_wipe_metadata_cache' => false,
81 + 'disable_autotrack' => false,
82 + 'plugin_version' => '',
170 83 );
171 84
172 85 /**
173 86 * Declare post types that Parse.ly will process as "posts".
174 87 *
88 + * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
89 + *
175 90 * @since 2.5.0
176 91 * @var string[]
177 - *
178 - * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
179 92 */
180 93 public const SUPPORTED_JSONLD_POST_TYPES = array(
181 94 'NewsArticle',
182 95 'Article',
@@ -185,26 +98,17 @@
185 98 'LiveBlogPosting',
186 99 'Report',
187 100 'Review',
188 101 'CreativeWork',
189 - 'OpinionNewsArticle',
190 - 'AnalysisNewsArticle',
191 - 'BackgroundNewsArticle',
192 - 'ReviewNewsArticle',
193 - 'ReportageNewsArticle',
194 - 'Recipe',
195 - 'AdvertiserContentArticle',
196 - 'MedicalWebPage',
197 - 'PodcastEpisode',
198 102 );
199 103
200 104 /**
201 105 * Declare post types that Parse.ly will process as "non-posts".
202 106 *
107 + * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
108 + *
203 109 * @since 2.5.0
204 110 * @var string[]
205 - *
206 - * @link https://docs.parse.ly/metadata-jsonld/#distinguishing-between-posts-and-non-posts-pages
207 111 */
208 112 public const SUPPORTED_JSONLD_NON_POST_TYPES = array(
209 113 'WebPage',
210 114 'Event',
@@ -221,70 +125,15 @@
221 125 */
222 126 private static $all_supported_types;
223 127
224 128 /**
225 - * Returns whether credentials are being managed at the platform level.
226 - *
227 - * This allows hosting providers to provide a more customized experience for
228 - * the plugin by handling credentials automatically.
229 - *
230 - * @since 3.9.0
231 - * @access private
232 - * @var bool
233 - */
234 - public $are_credentials_managed;
235 -
236 - /**
237 - * Holds the managed options and their values.
238 - *
239 - * This allows hosting providers to provide a more customized experience for
240 - * the plugin by handling options automatically.
241 - *
242 - * @since 3.9.0
243 - * @access private
244 - * @var array<empty>|array<string, bool|string|null>
245 - */
246 - public $managed_options = array();
247 -
248 - /**
249 129 * Constructor.
250 130 */
251 131 public function __construct() {
252 132 self::$all_supported_types = array_merge( self::SUPPORTED_JSONLD_POST_TYPES, self::SUPPORTED_JSONLD_NON_POST_TYPES );
253 -
254 - $this->are_credentials_managed = $this->are_credentials_managed();
255 - $this->set_managed_options();
256 -
257 - $this->allow_parsely_remote_requests();
258 133 }
259 134
260 135 /**
261 - * Gets the allowed post statuses for tracking.
262 - *
263 - * Uses the `wp_parsely_trackable_statuses` filter to determine which post statuses are allowed to be tracked.
264 - *
265 - * @since 3.17.0
266 - *
267 - * @param WP_Post|int|null $post The post object.
268 - * @return array<string> The allowed post statuses.
269 - */
270 - public static function get_trackable_statuses( $post = null ): array {
271 - /**
272 - * Filters the statuses that are permitted to be tracked.
273 - *
274 - * By default, the only status tracked is 'publish'. Use this filter if
275 - * you have other published content that has a different (custom) status.
276 - *
277 - * @since 2.5.0
278 - * @since 3.17.0 Filter extracted to a separate method.
279 - *
280 - * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
281 - * @param WP_Post|int|null $post Which post object or ID is being checked.
282 - */
283 - return apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
284 - }
285 -
286 - /**
287 136 * Registers action and filter hook callbacks, and immediately upgrades
288 137 * options if needed.
289 138 */
290 139 public function run(): void {
@@ -300,70 +149,35 @@
300 149 */
301 150 $callable = array( $this, $method );
302 151 call_user_func_array( $callable, array( $options ) );
303 152 }
304 -
305 153 // Update our version info.
306 154 $options['plugin_version'] = self::VERSION;
307 155 update_option( self::OPTIONS_KEY, $options );
308 156 }
309 157
310 - // @phpstan-ignore return.void
311 - add_action( 'save_post', array( $this, 'call_update_metadata_endpoint' ) );
158 + // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval
159 + add_filter( 'cron_schedules', array( $this, 'wpparsely_add_cron_interval' ) );
160 + add_action( 'parsely_bulk_metas_update', array( $this, 'bulk_update_posts' ) );
161 + add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
312 162 }
313 163
314 164 /**
315 - * Returns the Content API service.
165 + * Adds 10 minute cron interval.
316 166 *
317 - * This method returns the Content API service, which is used to interact with the Parse.ly Content API.
167 + * @param array<string, mixed> $schedules WP schedules array.
318 168 *
319 - * @since 3.17.0
320 - *
321 - * @return Content_API_Service
169 + * @return array<string, mixed>
322 170 */
323 - public function get_content_api(): Content_API_Service {
324 - if ( ! isset( $this->content_api_service ) ) {
325 - $this->content_api_service = new Content_API_Service( $this );
326 - }
327 -
328 - return $this->content_api_service;
171 + public function wpparsely_add_cron_interval( array $schedules ): array {
172 + $schedules['everytenminutes'] = array(
173 + 'interval' => 600, // time in seconds.
174 + 'display' => __( 'Every 10 Minutes', 'wp-parsely' ),
175 + );
176 + return $schedules;
329 177 }
330 178
331 179 /**
332 - * Returns the Suggestions API service.
333 - *
334 - * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API.
335 - *
336 - * @since 3.17.0
337 - *
338 - * @return Suggestions_API_Service
339 - */
340 - public function get_suggestions_api(): Suggestions_API_Service {
341 - if ( ! isset( $this->suggestions_api_service ) ) {
342 - $this->suggestions_api_service = new Suggestions_API_Service( $this );
343 - }
344 -
345 - return $this->suggestions_api_service;
346 - }
347 -
348 - /**
349 - * Gets the REST API controller.
350 - *
351 - * If the controller is not set, a new instance is created.
352 - *
353 - * @since 3.17.0
354 - *
355 - * @return REST_API_Controller
356 - */
357 - public function get_rest_api_controller(): REST_API_Controller {
358 - if ( ! isset( $this->rest_api_controller ) ) {
359 - $this->rest_api_controller = new REST_API_Controller( $this );
360 - }
361 -
362 - return $this->rest_api_controller;
363 - }
364 -
365 - /**
366 180 * Gets the full URL of the JavaScript tracker file for the site. If an API
367 181 * key is not set, return an empty string.
368 182 *
369 183 * @since 3.2.0
@@ -372,21 +186,10 @@
372 186 */
373 187 public function get_tracker_url(): string {
374 188 if ( $this->site_id_is_set() ) {
375 189 $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
376 -
377 - /**
378 - * Filters the URL of the Parse.ly tracker script.
379 - *
380 - * The filtered value gets sanitized with {@see esc_url_raw()}.
381 - *
382 - * @since 3.23.0
383 - *
384 - * @param string $tracker_url The URL of the tracker script.
385 - */
386 - return esc_url_raw( apply_filters( 'wp_parsely_tracker_url', $tracker_url ) );
190 + return esc_url( $tracker_url );
387 191 }
388 -
389 192 return '';
390 193 }
391 194
392 195 /**
@@ -433,8 +236,14 @@
433 236 * @return bool Should the post status be tracked for the provided post's post_type.
434 237 * By default,only 'publish' is allowed.
435 238 */
436 239 public static function post_has_trackable_status( $post ): bool {
240 + static $cache = array();
241 + $post_id = is_int( $post ) ? $post : $post->ID;
242 + if ( isset( $cache[ $post_id ] ) ) {
243 + return $cache[ $post_id ];
244 + }
245 +
437 246 /**
438 247 * Filters whether the post password check should be skipped when getting
439 248 * the post trackable status.
440 249 *
@@ -442,17 +251,30 @@
442 251 *
443 252 * @param bool $skip True if the password check should be skipped.
444 253 * @param int|WP_Post $post Which post object or ID is being checked.
445 254 *
446 - * @return bool
255 + * @returns bool
447 256 */
448 257 $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
449 258 if ( ! $skip_password_check && post_password_required( $post ) ) {
259 + $cache[ $post_id ] = false;
450 260 return false;
451 261 }
452 262
453 - $statuses = self::get_trackable_statuses( $post );
454 - return in_array( get_post_status( $post ), $statuses, true );
263 + /**
264 + * Filters the statuses that are permitted to be tracked.
265 + *
266 + * By default, the only status tracked is 'publish'. Use this filter if
267 + * you have other published content that has a different (custom) status.
268 + *
269 + * @since 2.5.0
270 + *
271 + * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
272 + * @param int|WP_Post $post Which post object or ID is being checked.
273 + */
274 + $statuses = apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
275 + $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true );
276 + return $cache[ $post_id ];
455 277 }
456 278
457 279 /**
458 280 * Deprecated. Please use the `Metadata` class instead.
@@ -463,8 +285,9 @@
463 285 * @see \Parsely\Metadata::construct_metadata
464 286 *
465 287 * @param array<string, mixed> $parsely_options parsely_options array.
466 288 * @param WP_Post $post object.
289 + *
467 290 * @return Metadata_Attributes
468 291 */
469 292 public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
470 293 _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
@@ -472,52 +295,23 @@
472 295 return $metadata->construct_metadata( $post );
473 296 }
474 297
475 298 /**
476 - * Calls Parse.ly's update metadata endpoint, sending the post's updated
477 - * metadata.
299 + * Updates the Parsely metadata endpoint with the new metadata of the post.
478 300 *
479 - * @param int $post_id The ID of the post to update.
480 - * @return bool True if the metadata endpoint was called, false otherwise.
301 + * @param int $post_id id of the post to update.
481 302 */
482 - public function call_update_metadata_endpoint( int $post_id ): bool {
483 - $options = $this->get_options();
484 -
485 - if ( $this->site_id_is_missing() || '' === $options['metadata_secret'] ) {
486 - return false;
303 + public function update_metadata_endpoint( int $post_id ): void {
304 + $parsely_options = $this->get_options();
305 + if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) {
306 + return;
487 307 }
488 308
489 - $current_post_type = get_post_type( $post_id );
490 - if ( false === $current_post_type ) {
491 - return false;
492 - }
493 -
494 - $tracked_post_types = array_merge(
495 - $options['track_post_types'],
496 - $options['track_page_types']
497 - );
498 -
499 - // Check that the post's type is trackable.
500 - if ( ! in_array( $current_post_type, $tracked_post_types, true ) ) {
501 - return false;
502 - }
503 -
504 - // Check that the post's status is trackable.
505 - if ( ! self::post_has_trackable_status( $post_id ) ) {
506 - return false;
507 - }
508 -
509 309 $post = get_post( $post_id );
510 310 if ( null === $post ) {
511 - return false;
311 + return;
512 312 }
513 313
514 - // Don't call the endpoint when integration tests are running, but
515 - // signal that the above checks have passed.
516 - if ( defined( 'INTEGRATION_TESTS_RUNNING' ) ) {
517 - return true;
518 - }
519 -
520 314 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
521 315
522 316 $endpoint_metadata = array(
523 317 'canonical_url' => $metadata['url'] ?? '',
@@ -529,14 +323,14 @@
529 323 'authors' => $metadata['creator'] ?? '',
530 324 'tags' => $metadata['keywords'] ?? '',
531 325 );
532 326
533 - $parsely_api_base_url = Content_API_Service::get_base_url();
534 - $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts';
535 - $parsely_metadata_secret = $options['metadata_secret'];
536 -
537 - $headers = array( 'Content-Type' => 'application/json' );
538 - $body = wp_json_encode(
327 + $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts';
328 + $parsely_metadata_secret = $parsely_options['metadata_secret'];
329 + $headers = array(
330 + 'Content-Type' => 'application/json',
331 + );
332 + $body = wp_json_encode(
539 333 array(
540 334 'secret' => $parsely_metadata_secret,
541 335 'apikey' => $this->get_site_id(),
542 336 'metadata' => $endpoint_metadata,
@@ -541,31 +335,68 @@
541 335 'apikey' => $this->get_site_id(),
542 336 'metadata' => $endpoint_metadata,
543 337 )
544 338 );
339 + $response = wp_remote_post(
340 + $parsely_api_endpoint,
341 + array(
342 + 'method' => 'POST',
343 + 'headers' => $headers,
344 + 'blocking' => false,
345 + 'body' => $body,
346 + 'data_format' => 'body',
347 + )
348 + );
545 349
350 + if ( ! is_wp_error( $response ) ) {
351 + $current_timestamp = time();
352 + update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
353 + }
354 + }
355 +
356 + /**
357 + * Updates posts with Parsely metadata API in bulk.
358 + */
359 + public function bulk_update_posts(): void {
360 + global $wpdb;
361 + $allowed_types = $this->get_all_track_types();
362 + $allowed_types_string = implode(
363 + ', ',
364 + array_map(
365 + function( $v ) {
366 + return "'" . esc_sql( $v ) . "'";
367 + },
368 + $allowed_types
369 + )
370 + );
371 +
546 372 /**
547 - * POST request options.
373 + * Variable.
548 374 *
549 - * @var WP_HTTP_Request_Args $request_options
375 + * @var int[]|false
550 376 */
551 - $request_options = array(
552 - 'method' => 'POST',
553 - 'headers' => $headers,
554 - 'blocking' => false,
555 - 'body' => $body,
556 - 'data_format' => 'body',
557 - );
377 + $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
378 + if ( false === $ids ) {
379 + $ids = array();
380 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
381 + $results = $wpdb->get_results(
382 + $wpdb->prepare( "SELECT DISTINCT(id) FROM {$wpdb->posts} WHERE post_type IN (\" . %s . \") AND id NOT IN (SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'parsely_metadata_last_updated');", $allowed_types_string ),
383 + ARRAY_N
384 + );
385 + foreach ( $results as $result ) {
386 + $ids[] = $result[0];
387 + }
388 + wp_cache_set( 'parsely_post_ids_need_meta_updating', $ids, '', 86400 );
389 + }
558 390
559 - $response = wp_remote_post( $parsely_api_endpoint, $request_options );
560 -
561 - if ( is_wp_error( $response ) ) {
562 - return false;
391 + for ( $i = 0; $i < 100; $i++ ) {
392 + $post_id = array_pop( $ids );
393 + if ( null === $post_id ) {
394 + wp_clear_scheduled_hook( 'parsely_bulk_metas_update' );
395 + break;
396 + }
397 + $this->update_metadata_endpoint( $post_id );
563 398 }
564 -
565 - update_post_meta( $post_id, 'parsely_metadata_last_updated', time() );
566 -
567 - return true;
568 399 }
569 400
570 401 /**
571 402 * Safely returns options for the plugin by assigning defaults contained in
@@ -581,165 +412,27 @@
581 412 * Variable.
582 413 *
583 414 * @var Parsely_Options|null
584 415 */
585 - $options = get_option( self::OPTIONS_KEY, null );
416 + $options = get_option( self::OPTIONS_KEY, $this->option_defaults );
586 417
587 - // Existing plugin installation without full metadata option.
588 - /* @phpstan-ignore isset.offset, booleanAnd.alwaysFalse */
589 - if ( is_array( $options ) && ! isset( $options['full_metadata_in_non_posts'] ) ) {
590 - $this->set_default_full_metadata_in_non_posts();
591 - }
592 -
593 - // Existing plugin installation without Content Intelligence options.
594 - /* @phpstan-ignore isset.offset, booleanAnd.alwaysFalse */
595 - if ( is_array( $options ) && ! isset( $options['content_helper'] ) ) {
596 - $this->set_default_content_helper_settings_values();
597 - }
598 -
599 - // Existing plugin installation that's missing a Content Intelligence
600 - // feature option.
601 - /* @phpstan-ignore isset.offset */
602 - if ( is_array( $options ) && isset( $options['content_helper'] ) ) {
603 - /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options */
604 - $pch_options = $options['content_helper'];
605 -
606 - /** @var array<string,Parsely_Options_Content_Helper_Feature> $pch_options_defaults */
607 - $pch_options_defaults = $this->option_defaults['content_helper'];
608 -
609 - if ( count( $pch_options ) !== count( $pch_options_defaults ) ) {
610 - $new_keys = array_diff(
611 - array_keys( $pch_options_defaults ),
612 - array_keys( $pch_options )
613 - );
614 -
615 - foreach ( $new_keys as $key ) {
616 - $options['content_helper'][ $key ] = $pch_options_defaults[ $key ];
617 - }
618 - }
619 - }
620 -
621 - // New plugin installation that hasn't saved its options yet.
622 418 if ( ! is_array( $options ) ) {
623 - $this->set_default_track_as_values();
624 - $this->set_default_full_metadata_in_non_posts();
625 - $options = $this->option_defaults;
419 + return $this->option_defaults;
626 420 }
627 421
628 - /**
629 - * Final options including managed credentials and options.
630 - *
631 - * @var Parsely_Options
632 - */
633 - return array_merge(
634 - $this->option_defaults,
635 - $options,
636 - $this->get_managed_credentials(),
637 - $this->managed_options
638 - );
422 + return array_merge( $this->option_defaults, $options );
639 423 }
640 424
641 425 /**
642 - * Returns the value of a nested option.
643 - *
644 - * @since 3.16.0
645 - *
646 - * @param string $option The option to get.
647 - * @param Parsely_Options $options The options to get the value from.
648 - * @return mixed The value of the nested option.
649 - */
650 - public static function get_nested_option_value( $option, $options ) {
651 - $keys = explode( '[', str_replace( ']', '', $option ) );
652 - $value = $options;
653 -
654 - foreach ( $keys as $key ) {
655 - if ( isset( $value[ $key ] ) ) {
656 - $value = $value[ $key ];
657 - }
658 - }
659 -
660 - return $value;
661 - }
662 -
663 - /**
664 - * Sets the default values for the track_post_types and track_page_types
665 - * options.
666 - *
667 - * @since 3.9.0
668 - */
669 - public function set_default_track_as_values(): void {
670 - $this->option_defaults['track_page_types'] = array();
671 - $this->option_defaults['track_post_types'] = array();
672 -
673 - $post_types = get_post_types( array( 'public' => true ) );
674 -
675 - foreach ( $post_types as $post_type ) {
676 - if ( ! post_type_supports( $post_type, 'editor' ) ) {
677 - continue;
678 - }
679 -
680 - if ( is_post_type_hierarchical( $post_type ) ) {
681 - $this->option_defaults['track_page_types'][] = $post_type;
682 - } else {
683 - $this->option_defaults['track_post_types'][] = $post_type;
684 - }
685 - }
686 - }
687 -
688 - /**
689 - * Sets the default value for the full_metadata_in_non_posts option.
690 - *
691 - * @since 3.14.0
692 - */
693 - public function set_default_full_metadata_in_non_posts(): void {
694 - $this->option_defaults['full_metadata_in_non_posts'] = true;
695 -
696 - // Usage of any of these filters will result in the setting being set
697 - // to false.
698 - $filter_tags = array(
699 - 'wp_parsely_metadata',
700 - 'wp_parsely_post_tags',
701 - 'wp_parsely_permalink',
702 - 'wp_parsely_post_category',
703 - 'wp_parsely_pre_authors',
704 - 'wp_parsely_post_authors',
705 - 'wp_parsely_custom_taxonomies',
706 - 'wp_parsely_post_type',
707 - );
708 -
709 - foreach ( $filter_tags as $filter_tag ) {
710 - if ( has_filter( $filter_tag ) ) {
711 - $this->option_defaults['full_metadata_in_non_posts'] = false;
712 - break;
713 - }
714 - }
715 - }
716 -
717 - /**
718 - * Sets the default values for Content Intelligence options.
719 - *
720 - * Gives PCH access to all users having the edit_posts capability, to keep
721 - * consistent behavior with plugin versions prior to 3.16.0.
722 - *
723 - * @since 3.16.0
724 - */
725 - public function set_default_content_helper_settings_values(): void {
726 - $this->option_defaults['content_helper'] =
727 - Permissions::build_pch_permissions_settings_array(
728 - true,
729 - array_keys( Permissions::get_user_roles_with_edit_posts_cap() )
730 - );
731 - }
732 -
733 - /**
734 426 * Gets the URL of the plugin's settings page.
735 427 *
736 428 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
737 429 * for context (Default null for current).
430 + *
738 431 * @return string
739 432 */
740 - public static function get_settings_url( ?int $_blog_id = null ): string {
741 - return get_admin_url( $_blog_id, 'admin.php?page=' . self::MENU_SLUG );
433 + public static function get_settings_url( int $_blog_id = null ): string {
434 + return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
742 435 }
743 436
744 437 /**
745 438 * Returns the URL of the Parse.ly dashboard for a specific page. If a page
@@ -755,10 +448,9 @@
755 448 public static function get_dash_url( string $site_id, string $page_url = '' ): string {
756 449 $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
757 450
758 451 if ( '' !== $page_url ) {
759 - $page_url = self::get_url_with_itm_source( $page_url, null );
760 - $result .= '?url=' . rawurlencode( $page_url );
452 + $result .= '?url=' . rawurlencode( $page_url );
761 453 }
762 454
763 455 return $result;
764 456 }
@@ -763,28 +455,8 @@
763 455 return $result;
764 456 }
765 457
766 458 /**
767 - * Adds or replaces the itm_source parameter in the URL. Removes the
768 - * parameter if the passed value is null or an empty string.
769 - *
770 - * @since 3.9.0
771 - *
772 - * @param string $url The URL to modify.
773 - * @param string|null $itm_source The value of the itm_source parameter.
774 - * @return string The resulting URL.
775 - */
776 - public static function get_url_with_itm_source( string $url, $itm_source ): string {
777 - if ( null === $itm_source || '' === $itm_source ) {
778 - return remove_query_arg( 'itm_source', $url );
779 - }
780 -
781 - $itm_source = rawurlencode( $itm_source );
782 -
783 - return add_query_arg( 'itm_source', $itm_source, $url );
784 - }
785 -
786 - /**
787 459 * Checks to see if the current user is a member of the current blog.
788 460 *
789 461 * @return bool
790 462 */
@@ -804,9 +476,9 @@
804 476 * types, "index" is returned.
805 477 *
806 478 * @since 2.5.0
807 479 *
808 - * @see https://docs.parse.ly/metatags/#h-field-description
480 + * @see https://www.parse.ly/help/integration/metatags#field-description
809 481 *
810 482 * @param string $type JSON-LD type.
811 483 * @return string "post" or "index".
812 484 */
@@ -817,9 +489,9 @@
817 489 /**
818 490 * Determines if a Site ID is saved in the options.
819 491 *
820 492 * @since 2.6.0
821 - * @since 3.7.0 renamed from api_key_is_set.
493 + * @since 3.7.0 renamed from api_key_is_set
822 494 *
823 495 * @return bool True is Site ID is set, false if it is missing.
824 496 */
825 497 public function site_id_is_set(): bool {
@@ -831,9 +503,9 @@
831 503 /**
832 504 * Determines if a Site ID is not saved in the options.
833 505 *
834 506 * @since 2.6.0
835 - * @since 3.7.0 renamed from api_key_is_missing.
507 + * @since 3.7.0 renamed from api_key_is_missing
836 508 *
837 509 * @return bool True if Site ID is missing, false if it is set.
838 510 */
839 511 public function site_id_is_missing(): bool {
@@ -843,9 +515,9 @@
843 515 /**
844 516 * Gets the Site ID if set.
845 517 *
846 518 * @since 2.6.0
847 - * @since 3.7.0 renamed from get_site_id.
519 + * @since 3.7.0 renamed from get_site_id
848 520 *
849 521 * @return string Site ID if set, or empty string if not.
850 522 */
851 523 public function get_site_id(): string {
@@ -912,310 +584,6 @@
912 584 * @return Parsely_Options
913 585 */
914 586 public function get_default_options() {
915 587 return $this->option_defaults;
916 - }
917 -
918 - /**
919 - * Returns the credentials that are being managed at the platform level.
920 - *
921 - * @since 3.9.0
922 - * @access private
923 - *
924 - * @return Parsely_Options|array<empty> The managed credentials.
925 - */
926 - private function get_managed_credentials() {
927 - if ( true !== $this->are_credentials_managed ) {
928 - return array();
929 - }
930 -
931 - $credentials = apply_filters( 'wp_parsely_credentials', array() );
932 -
933 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
934 - return array();
935 - }
936 -
937 - $result = array();
938 -
939 - if ( isset( $credentials['site_id'] ) ) {
940 - $result['apikey'] = $credentials['site_id'];
941 - }
942 -
943 - if ( isset( $credentials['api_secret'] ) ) {
944 - $result['api_secret'] = $credentials['api_secret'];
945 - }
946 -
947 - if ( isset( $credentials['metadata_secret'] ) ) {
948 - $result['metadata_secret'] = $credentials['metadata_secret'];
949 - }
950 -
951 - return $result;
952 - }
953 -
954 - /**
955 - * Returns whether credentials are being managed at the platform level.
956 - *
957 - * @since 3.9.0
958 - * @access private
959 - *
960 - * @return bool Whether credentials are being managed at the platform level.
961 - */
962 - private function are_credentials_managed(): bool {
963 - $credentials = apply_filters( 'wp_parsely_credentials', array() );
964 -
965 - if ( ! is_array( $credentials ) || 0 === count( $credentials ) ) {
966 - return false;
967 - }
968 -
969 - return $credentials['is_managed'] ?? false;
970 - }
971 -
972 - /**
973 - * Sets the values of managed options.
974 - *
975 - * This function won't accept managing credentials or certain plugin options
976 - * that are being managed through other means. For managing credentials,
977 - * please use the `wp_parsely_credentials` filter.
978 - *
979 - * @since 3.9.0
980 - * @access private
981 - */
982 - private function set_managed_options(): void {
983 - $managed_options = apply_filters( 'wp_parsely_managed_options', false );
984 -
985 - if ( ! is_array( $managed_options ) ) {
986 - return;
987 - }
988 -
989 - // Don't allow certain options to be set as managed.
990 - unset(
991 - $managed_options['apikey'],
992 - $managed_options['api_secret'],
993 - $managed_options['metadata_secret'],
994 - $managed_options['track_post_types'],
995 - $managed_options['track_page_types'],
996 - $managed_options['plugin_version']
997 - );
998 -
999 - if ( 0 === count( $managed_options ) ) {
1000 - return;
1001 - }
1002 -
1003 - /**
1004 - * Current options.
1005 - *
1006 - * @var Parsely_Options $current_options
1007 - */
1008 - $current_options = get_option( self::OPTIONS_KEY, array() );
1009 -
1010 - // Set managed options values.
1011 - foreach ( $managed_options as $key => $value ) {
1012 - $is_option_valid = isset( $this->option_defaults[ $key ] );
1013 -
1014 - if ( $is_option_valid ) {
1015 - if ( null === $value ) {
1016 - // When null, the option gets its value from the database.
1017 - $this->managed_options[ $key ] =
1018 - $current_options[ $key ] ?? $this->option_defaults[ $key ];
1019 - } else {
1020 - $this->managed_options[ $key ] =
1021 - $this->sanitize_managed_option( $key, $value );
1022 - }
1023 - }
1024 - }
1025 - }
1026 -
1027 - /**
1028 - * Gets the Parse.ly canonical URL for a given post.
1029 - *
1030 - * @since 3.19.0
1031 - *
1032 - * @param WP_Post|int $post The post ID or post object.
1033 - * @return string The Parse.ly canonical URL.
1034 - */
1035 - public static function get_canonical_url_from_post( $post ): string {
1036 - $post_id = is_int( $post ) ? $post : $post->ID;
1037 - $canonical_url = get_post_meta( $post_id, self::PARSELY_CANONICAL_URL_META_KEY, true );
1038 -
1039 - if ( null !== $canonical_url && is_string( $canonical_url ) && '' !== $canonical_url ) {
1040 - return self::get_canonical_url( $canonical_url );
1041 - }
1042 -
1043 - $permalink = get_permalink( $post );
1044 -
1045 - if ( false === $permalink ) {
1046 - return __( 'no permalink', 'wp-parsely' );
1047 - }
1048 -
1049 - return self::get_canonical_url( $permalink );
1050 - }
1051 -
1052 - /**
1053 - * Returns the canonical version of the passed URL.
1054 - *
1055 - * In this context, the canonical URL is the URL containing the Site ID as
1056 - * its domain. If the Site ID differs from the real domain, the
1057 - * `wp_parsely_canonical_url_domain` filter can be used to set it.
1058 - *
1059 - * @since 3.19.0
1060 - * @since 3.20.4 Made the domain overridable.
1061 - *
1062 - * @param string $url The URL to get the canonical URL for.
1063 - * @return string The canonical URL.
1064 - */
1065 - public static function get_canonical_url( string $url ): string {
1066 - $canonical_url_domain = apply_filters(
1067 - 'wp_parsely_canonical_url_domain',
1068 - null
1069 - );
1070 -
1071 - // Handle domain override.
1072 - if ( is_string( $canonical_url_domain ) ) {
1073 - // Get the canonical URL domain without protocol, trailing slashes
1074 - // or accidental whitespace.
1075 - $canonical_url_domain = rtrim( trim( $canonical_url_domain ), '/' );
1076 - $canonical_url_domain = preg_replace( '#^https?://#', '', $canonical_url_domain );
1077 -
1078 - if ( is_string( $canonical_url_domain ) && '' !== $canonical_url_domain ) {
1079 - $url_domain = (string) wp_parse_url( $url, PHP_URL_HOST );
1080 - return str_replace( $url_domain, $canonical_url_domain, $url );
1081 - }
1082 - }
1083 -
1084 - $site_id = \Parsely\get_parsely()->get_site_id();
1085 - $home_url_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
1086 -
1087 - if ( $home_url_host === $site_id ) {
1088 - // URL does not need to be modified.
1089 - return $url;
1090 - }
1091 -
1092 - // Return the URL with the Site ID as the domain.
1093 - return str_replace( $home_url_host, $site_id, $url );
1094 - }
1095 -
1096 - /**
1097 - * Sets the Parse.ly canonical URL for a post.
1098 - *
1099 - * @since 3.19.0
1100 - *
1101 - * @param WP_Post|int $post The post object or post ID.
1102 - * @param string $url The canonical URL.
1103 - * @return bool True if the canonical URL was set, false otherwise.
1104 - */
1105 - public static function set_canonical_url( $post, string $url ): bool {
1106 - $post_id = is_int( $post ) ? $post : $post->ID;
1107 - $canonical_url = self::get_canonical_url( $url );
1108 -
1109 - return false !== update_post_meta(
1110 - $post_id,
1111 - self::PARSELY_CANONICAL_URL_META_KEY,
1112 - sanitize_url( $canonical_url, array( 'http', 'https' ) )
1113 - );
1114 - }
1115 -
1116 - /**
1117 - * Sanitizes the value of the passed managed option.
1118 - *
1119 - * @since 3.9.0
1120 - * @access private
1121 - *
1122 - * @param string $option_id The option's ID.
1123 - * @param bool|string $value The option's value.
1124 - * @return bool|string The sanitized option value.
1125 - */
1126 - private function sanitize_managed_option( string $option_id, $value ) {
1127 - $option_value_type = gettype( $this->option_defaults[ $option_id ] );
1128 -
1129 - if ( 'boolean' === $option_value_type && ! is_bool( $value ) ) {
1130 - _doing_it_wrong(
1131 - __FUNCTION__,
1132 - esc_html(
1133 - sprintf( /* translators: 1: Option ID */
1134 - __( 'The value of the managed option `%1$s` must be of type `boolean`.', 'wp-parsely' ),
1135 - $option_id
1136 - )
1137 - ),
1138 - ''
1139 - );
1140 -
1141 - return false;
1142 - }
1143 -
1144 - if ( 'string' === $option_value_type ) {
1145 - if ( ! is_string( $value ) ) {
1146 - _doing_it_wrong(
1147 - __FUNCTION__,
1148 - esc_html(
1149 - sprintf( /* translators: 1: Option ID */
1150 - __( 'The value of the managed option `%1$s` must be of type `string`.', 'wp-parsely' ),
1151 - $option_id
1152 - )
1153 - ),
1154 - ''
1155 - );
1156 -
1157 - $value = strval( $value );
1158 - }
1159 -
1160 - // String options that are restricted to specific values.
1161 - $restricted_value_options = array(
1162 - 'custom_taxonomy_section' => Settings_Page::get_section_taxonomies(),
1163 - 'meta_type' => array( 'json_ld', 'repeated_metas' ),
1164 - );
1165 -
1166 - // Verify that the above values are respected.
1167 - foreach ( $restricted_value_options as $option_key => $valid_values ) {
1168 - if ( $option_id === $option_key ) {
1169 - if ( ! in_array( $value, $valid_values, true ) ) {
1170 - _doing_it_wrong(
1171 - __FUNCTION__,
1172 - esc_html(
1173 - sprintf( /* translators: 1: Option value 2: Option ID */
1174 - __( 'The value `%1$s` is not allowed for the managed option `%2$s`.', 'wp-parsely' ),
1175 - $value,
1176 - $option_id
1177 - )
1178 - ),
1179 - ''
1180 - );
1181 -
1182 - $value = $this->option_defaults[ $option_id ];
1183 - }
1184 - }
1185 - }
1186 - }
1187 -
1188 - return $value;
1189 - }
1190 -
1191 - /**
1192 - * Allows remote requests to Parse.ly.
1193 - *
1194 - * This is needed for environments, such as wp-now, that block remote requests.
1195 - *
1196 - * @since 3.13.0
1197 - * @access private
1198 - */
1199 - private function allow_parsely_remote_requests(): void {
1200 - $allowed_urls = array(
1201 - self::DASHBOARD_BASE_URL,
1202 - Content_API_Service::get_base_url(),
1203 - Suggestions_API_Service::get_base_url(),
1204 - );
1205 -
1206 - add_filter(
1207 - 'http_request_host_is_external',
1208 - function ( bool $external, string $host, string $url ) use ( $allowed_urls ) {
1209 - // Check if the URL matches any URLs on the allowed list.
1210 - foreach ( $allowed_urls as $allowed_url ) {
1211 - if ( Utils::str_starts_with( $url, $allowed_url ) ) {
1212 - return true;
1213 - }
1214 - }
1215 - return $external;
1216 - },
1217 - 10,
1218 - 3
1219 - );
1220 588 }
1221 589 }