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