PluginProbe
Parse.ly / 3.7.1
Parse.ly v3.7.1
3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 3.20.3 All 105 releases
wp-parsely / src / class-parsely.php

class-parsely.php in Parse.ly 3.7.1, at src/class-parsely.php

579 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Parsely class
4 *
5 * @package Parsely
6 * @since 2.5.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely;
12
13 use Parsely\UI\Metadata_Renderer;
14 use WP_Post;
15
16 /**
17 * Holds most of the logic for the plugin.
18 *
19 * @since 1.0.0
20 * @since 2.5.0 Moved from plugin root file to this file.
21 *
22 * @phpstan-type Parsely_Options array{
23 * apikey: string,
24 * content_id_prefix: string,
25 * api_secret: string,
26 * use_top_level_cats: bool,
27 * custom_taxonomy_section: string,
28 * cats_as_tags: bool,
29 * track_authenticated_users: bool,
30 * lowercase_tags: bool,
31 * force_https_canonicals: bool,
32 * track_post_types: string[],
33 * track_page_types: string[],
34 * track_post_types_as?: array<string, string>,
35 * disable_javascript: bool,
36 * disable_amp: bool,
37 * meta_type: string,
38 * logo: string,
39 * metadata_secret: string,
40 * parsely_wipe_metadata_cache: bool,
41 * disable_autotrack: bool,
42 * plugin_version: string,
43 * }
44 *
45 * @phpstan-import-type Metadata_Attributes from Metadata
46 */
47 class Parsely {
48 /**
49 * Declare our constants
50 */
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';
57
58 /**
59 * Declare some class properties
60 *
61 * @var Parsely_Options $option_defaults The defaults we need for the class.
62 */
63 private $option_defaults = array(
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' => true,
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' => '',
83 );
84
85 /**
86 * Declare post types that Parse.ly will process as "posts".
87 *
88 * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
89 *
90 * @since 2.5.0
91 * @var string[]
92 */
93 public const SUPPORTED_JSONLD_POST_TYPES = array(
94 'NewsArticle',
95 'Article',
96 'TechArticle',
97 'BlogPosting',
98 'LiveBlogPosting',
99 'Report',
100 'Review',
101 'CreativeWork',
102 );
103
104 /**
105 * Declare post types that Parse.ly will process as "non-posts".
106 *
107 * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
108 *
109 * @since 2.5.0
110 * @var string[]
111 */
112 public const SUPPORTED_JSONLD_NON_POST_TYPES = array(
113 'WebPage',
114 'Event',
115 'Hotel',
116 'Restaurant',
117 'Movie',
118 );
119
120 /**
121 * Declare all supported types (both post and non-post types).
122 *
123 * @since 3.7.0
124 * @var string[]
125 */
126 private static $all_supported_types;
127
128 /**
129 * Constructor.
130 */
131 public function __construct() {
132 self::$all_supported_types = array_merge( self::SUPPORTED_JSONLD_POST_TYPES, self::SUPPORTED_JSONLD_NON_POST_TYPES );
133 }
134
135 /**
136 * Registers action and filter hook callbacks, and immediately upgrades
137 * options if needed.
138 */
139 public function run(): void {
140 // Run upgrade options if they exist for the version currently defined.
141 $options = $this->get_options();
142 if ( self::VERSION !== $options['plugin_version'] ) {
143 $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
144 if ( method_exists( $this, $method ) ) {
145 /**
146 * Variable.
147 *
148 * @var callable
149 */
150 $callable = array( $this, $method );
151 call_user_func_array( $callable, array( $options ) );
152 }
153 // Update our version info.
154 $options['plugin_version'] = self::VERSION;
155 update_option( self::OPTIONS_KEY, $options );
156 }
157
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' ) );
162 }
163
164 /**
165 * Adds 10 minute cron interval.
166 *
167 * @param array<string, mixed> $schedules WP schedules array.
168 *
169 * @return array<string, mixed>
170 */
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;
177 }
178
179 /**
180 * Gets the full URL of the JavaScript tracker file for the site. If an API
181 * key is not set, return an empty string.
182 *
183 * @since 3.2.0
184 *
185 * @return string
186 */
187 public function get_tracker_url(): string {
188 if ( $this->site_id_is_set() ) {
189 $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
190 return esc_url( $tracker_url );
191 }
192 return '';
193 }
194
195 /**
196 * Deprecated.
197 * Inserts the code for the <meta name='parsely-page'> parameter within the
198 * head tag.
199 *
200 * @since 3.2.0
201 * @deprecated 3.3.0
202 * @see Metadata_Renderer::render_metadata
203 *
204 * @param string $meta_type `json_ld` or `repeated_metas`.
205 */
206 public function render_metadata( string $meta_type ): void {
207 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
208 $metadata_renderer = new Metadata_Renderer( $this );
209 $metadata_renderer->render_metadata( $meta_type );
210 }
211
212 /**
213 * Deprecated.
214 * Insert the code for the <meta name='parsely-page'> parameter within the
215 * head tag.
216 *
217 * @since 3.0.0
218 * @deprecated 3.3.0
219 * @see Metadata_Renderer::render_metadata
220 */
221 public function insert_page_header_metadata(): void {
222 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
223 $parsely_options = $this->get_options();
224 $metadata_renderer = new Metadata_Renderer( $this );
225 $metadata_renderer->render_metadata( $parsely_options['meta_type'] );
226 }
227
228 /**
229 * Compares the post_status key against an allowed list.
230 *
231 * By default, only 'publish'ed content includes tracking data.
232 *
233 * @since 2.5.0
234 *
235 * @param int|WP_Post $post Which post object or ID to check.
236 * @return bool Should the post status be tracked for the provided post's post_type.
237 * By default,only 'publish' is allowed.
238 */
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
246 /**
247 * Filters whether the post password check should be skipped when getting
248 * the post trackable status.
249 *
250 * @since 3.0.1
251 *
252 * @param bool $skip True if the password check should be skipped.
253 * @param int|WP_Post $post Which post object or ID is being checked.
254 *
255 * @returns bool
256 */
257 $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
258 if ( ! $skip_password_check && post_password_required( $post ) ) {
259 $cache[ $post_id ] = false;
260 return false;
261 }
262
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 ];
277 }
278
279 /**
280 * Deprecated. Please use the `Metadata` class instead.
281 *
282 * Creates parsely metadata object from post metadata.
283 *
284 * @deprecated 3.3.0
285 * @see \Parsely\Metadata::construct_metadata
286 *
287 * @param array<string, mixed> $parsely_options parsely_options array.
288 * @param WP_Post $post object.
289 *
290 * @return Metadata_Attributes
291 */
292 public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
293 _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
294 $metadata = new Metadata( $this );
295 return $metadata->construct_metadata( $post );
296 }
297
298 /**
299 * Updates the Parsely metadata endpoint with the new metadata of the post.
300 *
301 * @param int $post_id id of the post to update.
302 */
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;
307 }
308
309 $post = get_post( $post_id );
310 if ( null === $post ) {
311 return;
312 }
313
314 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
315
316 $endpoint_metadata = array(
317 'canonical_url' => $metadata['url'] ?? '',
318 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
319 'title' => $metadata['headline'] ?? '',
320 'image_url' => isset( $metadata['image']['url'] ) ? $metadata['image']['url'] : '',
321 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
322 'section' => $metadata['articleSection'] ?? '',
323 'authors' => $metadata['creator'] ?? '',
324 'tags' => $metadata['keywords'] ?? '',
325 );
326
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(
333 array(
334 'secret' => $parsely_metadata_secret,
335 'apikey' => $this->get_site_id(),
336 'metadata' => $endpoint_metadata,
337 )
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 );
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
372 /**
373 * Variable.
374 *
375 * @var int[]|false
376 */
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 }
390
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 );
398 }
399 }
400
401 /**
402 * Safely returns options for the plugin by assigning defaults contained in
403 * optionDefaults.
404 *
405 * As soon as actual options are saved, they override the defaults. This
406 * prevents us from having to do a lot of isset() checking on variables.
407 *
408 * @return Parsely_Options
409 */
410 public function get_options() {
411 /**
412 * Variable.
413 *
414 * @var Parsely_Options|null
415 */
416 $options = get_option( self::OPTIONS_KEY, $this->option_defaults );
417
418 if ( ! is_array( $options ) ) {
419 return $this->option_defaults;
420 }
421
422 return array_merge( $this->option_defaults, $options );
423 }
424
425 /**
426 * Gets the URL of the plugin's settings page.
427 *
428 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
429 * for context (Default null for current).
430 *
431 * @return string
432 */
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 );
435 }
436
437 /**
438 * Returns the URL of the Parse.ly dashboard for a specific page. If a page
439 * is not specified, the home dashboard URL for the specified Site ID is
440 * returned.
441 *
442 * @since 3.7.0
443 *
444 * @param string $site_id The Site ID for which to get the URL.
445 * @param string $page_url Optional. The page for which to get the URL.
446 * @return string The complete dashboard URL.
447 */
448 public static function get_dash_url( string $site_id, string $page_url = '' ): string {
449 $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
450
451 if ( '' !== $page_url ) {
452 $result .= '?url=' . rawurlencode( $page_url );
453 }
454
455 return $result;
456 }
457
458 /**
459 * Checks to see if the current user is a member of the current blog.
460 *
461 * @return bool
462 */
463 public function is_blog_member_logged_in(): bool {
464 // Can't use $blog_id here because it futzes with the global $blog_id.
465 $current_blog_id = get_current_blog_id();
466 $current_user_id = get_current_user_id();
467
468 return is_user_member_of_blog( $current_user_id, $current_blog_id );
469 }
470
471 /**
472 * Converts JSON-LD type to respective Parse.ly page type.
473 *
474 * If the JSON-LD type is one of the types Parse.ly supports as a "post",
475 * then "post" will be returned. Otherwise, for "non-posts" and unknown
476 * types, "index" is returned.
477 *
478 * @since 2.5.0
479 *
480 * @see https://www.parse.ly/help/integration/metatags#field-description
481 *
482 * @param string $type JSON-LD type.
483 * @return string "post" or "index".
484 */
485 public function convert_jsonld_to_parsely_type( string $type ): string {
486 return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
487 }
488
489 /**
490 * Determines if a Site ID is saved in the options.
491 *
492 * @since 2.6.0
493 * @since 3.7.0 renamed from api_key_is_set
494 *
495 * @return bool True is Site ID is set, false if it is missing.
496 */
497 public function site_id_is_set(): bool {
498 $options = $this->get_options();
499
500 return '' !== $options['apikey'];
501 }
502
503 /**
504 * Determines if a Site ID is not saved in the options.
505 *
506 * @since 2.6.0
507 * @since 3.7.0 renamed from api_key_is_missing
508 *
509 * @return bool True if Site ID is missing, false if it is set.
510 */
511 public function site_id_is_missing(): bool {
512 return ! $this->site_id_is_set();
513 }
514
515 /**
516 * Gets the Site ID if set.
517 *
518 * @since 2.6.0
519 * @since 3.7.0 renamed from get_site_id
520 *
521 * @return string Site ID if set, or empty string if not.
522 */
523 public function get_site_id(): string {
524 $options = $this->get_options();
525
526 return $this->site_id_is_set() ? $options['apikey'] : '';
527 }
528
529 /**
530 * Returns whether the API Secret is set in the plugin's options.
531 *
532 * @since 3.4.0
533 *
534 * @return bool True if the API Secret is set, false if not set.
535 */
536 public function api_secret_is_set(): bool {
537 $options = $this->get_options();
538
539 return '' !== $options['api_secret'];
540 }
541
542 /**
543 * Returns the API Secret stored in the plugin's options.
544 *
545 * @since 3.4.0
546 *
547 * @return string The API Secret, empty string if the API secret is not set.
548 */
549 public function get_api_secret(): string {
550 $options = $this->get_options();
551
552 return $this->api_secret_is_set() ? $options['api_secret'] : '';
553 }
554
555 /**
556 * Returns all supported post and non-post types.
557 *
558 * @since 3.7.0
559 *
560 * @return string[] all supported types
561 */
562 public function get_all_supported_types(): array {
563 return self::$all_supported_types;
564 }
565
566 /**
567 * Gets all tracked post types.
568 *
569 * @since 3.7.0
570 *
571 * @return array<string>
572 */
573 public function get_all_track_types(): array {
574 $options = $this->get_options();
575
576 return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
577 }
578 }
579