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

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

479 lines 13.9 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 class Parsely {
23 /**
24 * Declare our constants
25 */
26 public const VERSION = PARSELY_VERSION;
27 public const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php.
28 public const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database.
29 public const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings.
30 public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
31
32 /**
33 * Declare some class properties
34 *
35 * @var array<string, mixed> $option_defaults The defaults we need for the class.
36 */
37 private $option_defaults = array(
38 'apikey' => '',
39 'content_id_prefix' => '',
40 'api_secret' => '',
41 'use_top_level_cats' => false,
42 'custom_taxonomy_section' => 'category',
43 'cats_as_tags' => false,
44 'track_authenticated_users' => true,
45 'lowercase_tags' => true,
46 'force_https_canonicals' => false,
47 'track_post_types' => array( 'post' ),
48 'track_page_types' => array( 'page' ),
49 'disable_javascript' => false,
50 'disable_amp' => false,
51 'meta_type' => 'json_ld',
52 'logo' => '',
53 'metadata_secret' => '',
54 'parsely_wipe_metadata_cache' => false,
55 'disable_autotrack' => false,
56 );
57
58 /**
59 * Declare post types that Parse.ly will process as "posts".
60 *
61 * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
62 *
63 * @since 2.5.0
64 * @var string[]
65 */
66 public const SUPPORTED_JSONLD_POST_TYPES = array(
67 'NewsArticle',
68 'Article',
69 'TechArticle',
70 'BlogPosting',
71 'LiveBlogPosting',
72 'Report',
73 'Review',
74 'CreativeWork',
75 );
76
77 /**
78 * Declare post types that Parse.ly will process as "non-posts".
79 *
80 * @link https://www.parse.ly/help/integration/jsonld#distinguishing-between-posts-and-pages
81 *
82 * @since 2.5.0
83 * @var string[]
84 */
85 public const SUPPORTED_JSONLD_NON_POST_TYPES = array(
86 'WebPage',
87 'Event',
88 'Hotel',
89 'Restaurant',
90 'Movie',
91 );
92
93 /**
94 * Registers action and filter hook callbacks, and immediately upgrades
95 * options if needed.
96 */
97 public function run(): void {
98 // Run upgrade options if they exist for the version currently defined.
99 $options = $this->get_options();
100 if ( empty( $options['plugin_version'] ) || self::VERSION !== $options['plugin_version'] ) {
101 $method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
102 if ( method_exists( $this, $method ) ) {
103 call_user_func_array( array( $this, $method ), array( $options ) );
104 }
105 // Update our version info.
106 $options['plugin_version'] = self::VERSION;
107 update_option( self::OPTIONS_KEY, $options );
108 }
109
110 // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval
111 add_filter( 'cron_schedules', array( $this, 'wpparsely_add_cron_interval' ) );
112 add_action( 'parsely_bulk_metas_update', array( $this, 'bulk_update_posts' ) );
113 add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) );
114 }
115
116 /**
117 * Adds 10 minute cron interval.
118 *
119 * @param array $schedules WP schedules array.
120 * @return array
121 */
122 public function wpparsely_add_cron_interval( array $schedules ): array {
123 $schedules['everytenminutes'] = array(
124 'interval' => 600, // time in seconds.
125 'display' => __( 'Every 10 Minutes', 'wp-parsely' ),
126 );
127 return $schedules;
128 }
129
130 /**
131 * Gets the full URL of the JavaScript tracker file for the site. If an API
132 * key is not set, return an empty string.
133 *
134 * @since 3.2.0
135 *
136 * @return string
137 */
138 public function get_tracker_url(): string {
139 if ( $this->api_key_is_set() ) {
140 $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_api_key() . '/p.js';
141 return esc_url( $tracker_url );
142 }
143 return '';
144 }
145
146 /**
147 * Deprecated.
148 * Inserts the code for the <meta name='parsely-page'> parameter within the
149 * head tag.
150 *
151 * @since 3.2.0
152 * @deprecated 3.3.0
153 * @see Metadata_Renderer::render_metadata
154 *
155 * @param string $meta_type `json_ld` or `repeated_metas`.
156 */
157 public function render_metadata( string $meta_type ): void {
158 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
159 $metadata_renderer = new Metadata_Renderer( $this );
160 $metadata_renderer->render_metadata( $meta_type );
161 }
162
163 /**
164 * Deprecated.
165 * Insert the code for the <meta name='parsely-page'> parameter within the
166 * head tag.
167 *
168 * @since 3.0.0
169 * @deprecated 3.3.0
170 * @see Metadata_Renderer::render_metadata
171 */
172 public function insert_page_header_metadata(): void {
173 _deprecated_function( __FUNCTION__, '3.3', 'Metadata_Renderer::render_metadata()' );
174 $parsely_options = $this->get_options();
175 $metadata_renderer = new Metadata_Renderer( $this );
176 $metadata_renderer->render_metadata( $parsely_options['meta_type'] );
177 }
178
179 /**
180 * Compares the post_status key against an allowed list.
181 *
182 * By default, only 'publish'ed content includes tracking data.
183 *
184 * @since 2.5.0
185 *
186 * @param int|WP_Post $post Which post object or ID to check.
187 * @return bool Should the post status be tracked for the provided post's post_type.
188 * By default,only 'publish' is allowed.
189 */
190 public static function post_has_trackable_status( $post ): bool {
191 static $cache = array();
192 $post_id = is_int( $post ) ? $post : $post->ID;
193 if ( isset( $cache[ $post_id ] ) ) {
194 return $cache[ $post_id ];
195 }
196
197 /**
198 * Filters whether the post password check should be skipped when getting
199 * the post trackable status.
200 *
201 * @since 3.0.1
202 *
203 * @param bool $skip True if the password check should be skipped.
204 * @param int|WP_Post $post Which post object or ID is being checked.
205 *
206 * @returns bool
207 */
208 $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post );
209 if ( ! $skip_password_check && post_password_required( $post ) ) {
210 $cache[ $post_id ] = false;
211 return false;
212 }
213
214 /**
215 * Filters the statuses that are permitted to be tracked.
216 *
217 * By default, the only status tracked is 'publish'. Use this filter if
218 * you have other published content that has a different (custom) status.
219 *
220 * @since 2.5.0
221 *
222 * @param string[] $trackable_statuses The list of post statuses that are allowed to be tracked.
223 * @param int|WP_Post $post Which post object or ID is being checked.
224 */
225 $statuses = apply_filters( 'wp_parsely_trackable_statuses', array( 'publish' ), $post );
226 $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true );
227 return $cache[ $post_id ];
228 }
229
230 /**
231 * Deprecated. Please use the `Metadata` class instead.
232 *
233 * Creates parsely metadata object from post metadata.
234 *
235 * @deprecated 3.3.0
236 * @see \Parsely\Metadata::construct_metadata
237 *
238 * @param array<string, mixed> $parsely_options parsely_options array.
239 * @param WP_Post $post object.
240 * @return array<string, mixed>
241 */
242 public function construct_parsely_metadata( array $parsely_options, WP_Post $post ): array {
243 _deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
244 $metadata = new Metadata( $this );
245 return $metadata->construct_metadata( $post );
246 }
247
248 /**
249 * Updates the Parsely metadata endpoint with the new metadata of the post.
250 *
251 * @param int $post_id id of the post to update.
252 */
253 public function update_metadata_endpoint( int $post_id ): void {
254 $parsely_options = $this->get_options();
255 if ( $this->api_key_is_missing() || empty( $parsely_options['metadata_secret'] ) ) {
256 return;
257 }
258
259 $post = get_post( $post_id );
260 if ( null === $post ) {
261 return;
262 }
263
264 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
265
266 $endpoint_metadata = array(
267 'canonical_url' => $metadata['url'],
268 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ),
269 'title' => $metadata['headline'],
270 'image_url' => $metadata['image']['url'],
271 'pub_date_tmsp' => $metadata['datePublished'],
272 'section' => $metadata['articleSection'],
273 'authors' => $metadata['creator'],
274 'tags' => $metadata['keywords'],
275 );
276
277 $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts';
278 $parsely_metadata_secret = $parsely_options['metadata_secret'];
279 $headers = array(
280 'Content-Type' => 'application/json',
281 );
282 $body = wp_json_encode(
283 array(
284 'secret' => $parsely_metadata_secret,
285 'apikey' => $parsely_options['apikey'],
286 'metadata' => $endpoint_metadata,
287 )
288 );
289 $response = wp_remote_post(
290 $parsely_api_endpoint,
291 array(
292 'method' => 'POST',
293 'headers' => $headers,
294 'blocking' => false,
295 'body' => $body,
296 'data_format' => 'body',
297 )
298 );
299
300 if ( ! is_wp_error( $response ) ) {
301 $current_timestamp = time();
302 update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
303 }
304 }
305
306 /**
307 * Updates posts with Parsely metadata API in bulk.
308 */
309 public function bulk_update_posts(): void {
310 global $wpdb;
311 $parsely_options = $this->get_options();
312 $allowed_types = array_merge( $parsely_options['track_post_types'], $parsely_options['track_page_types'] );
313 $allowed_types_string = implode(
314 ', ',
315 array_map(
316 function( $v ) {
317 return "'" . esc_sql( $v ) . "'";
318 },
319 $allowed_types
320 )
321 );
322 $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
323 if ( false === $ids ) {
324 $ids = array();
325 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
326 $results = $wpdb->get_results(
327 $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 ),
328 ARRAY_N
329 );
330 foreach ( $results as $result ) {
331 $ids[] = $result[0];
332 }
333 wp_cache_set( 'parsely_post_ids_need_meta_updating', $ids, '', 86400 );
334 }
335
336 for ( $i = 0; $i < 100; $i++ ) {
337 $post_id = array_pop( $ids );
338 if ( null === $post_id ) {
339 wp_clear_scheduled_hook( 'parsely_bulk_metas_update' );
340 break;
341 }
342 $this->update_metadata_endpoint( $post_id );
343 }
344 }
345
346 /**
347 * Safely returns options for the plugin by assigning defaults contained in
348 * optionDefaults.
349 *
350 * As soon as actual options are saved, they override the defaults. This
351 * prevents us from having to do a lot of isset() checking on variables.
352 *
353 * @return array<string, mixed>
354 */
355 public function get_options(): array {
356 $options = get_option( self::OPTIONS_KEY, $this->option_defaults );
357
358 if ( ! is_array( $options ) ) {
359 return $this->option_defaults;
360 }
361
362 return array_merge( $this->option_defaults, $options );
363 }
364
365 /**
366 * Gets the URL of the plugin's settings page.
367 *
368 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
369 * for context (Default null for current).
370 *
371 * @return string
372 */
373 public static function get_settings_url( int $_blog_id = null ): string {
374 return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
375 }
376
377 /**
378 * Checks to see if the current user is a member of the current blog.
379 *
380 * @return bool
381 */
382 public function is_blog_member_logged_in(): bool {
383 // Can't use $blog_id here because it futzes with the global $blog_id.
384 $current_blog_id = get_current_blog_id();
385 $current_user_id = get_current_user_id();
386
387 return is_user_member_of_blog( $current_user_id, $current_blog_id );
388 }
389
390 /**
391 * Converts JSON-LD type to respective Parse.ly page type.
392 *
393 * If the JSON-LD type is one of the types Parse.ly supports as a "post",
394 * then "post" will be returned. Otherwise, for "non-posts" and unknown
395 * types, "index" is returned.
396 *
397 * @since 2.5.0
398 *
399 * @see https://www.parse.ly/help/integration/metatags#field-description
400 *
401 * @param string $type JSON-LD type.
402 * @return string "post" or "index".
403 */
404 public function convert_jsonld_to_parsely_type( string $type ): string {
405 return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
406 }
407
408 /**
409 * Determines if an API key is saved in the options.
410 *
411 * @since 2.6.0
412 *
413 * @return bool True is API key is set, false if it is missing.
414 */
415 public function api_key_is_set(): bool {
416 $options = $this->get_options();
417
418 return (
419 isset( $options['apikey'] ) &&
420 is_string( $options['apikey'] ) &&
421 '' !== $options['apikey']
422 );
423 }
424
425 /**
426 * Determines if an API key is not saved in the options.
427 *
428 * @since 2.6.0
429 *
430 * @return bool True if API key is missing, false if it is set.
431 */
432 public function api_key_is_missing(): bool {
433 return ! $this->api_key_is_set();
434 }
435
436 /**
437 * Gets the API key if set.
438 *
439 * @since 2.6.0
440 *
441 * @return string API key if set, or empty string if not.
442 */
443 public function get_api_key(): string {
444 $options = $this->get_options();
445
446 return $this->api_key_is_set() ? $options['apikey'] : '';
447 }
448
449 /**
450 * Returns whether the API Secret is set in the plugin's options.
451 *
452 * @since 3.4.0
453 *
454 * @return bool True if the API Secret is set, false if not set.
455 */
456 public function api_secret_is_set(): bool {
457 $options = $this->get_options();
458
459 return (
460 isset( $options['api_secret'] ) &&
461 is_string( $options['api_secret'] ) &&
462 '' !== $options['api_secret']
463 );
464 }
465
466 /**
467 * Returns the API Secret stored in the plugin's options.
468 *
469 * @since 3.4.0
470 *
471 * @return string The API Secret, empty string if the API secret is not set.
472 */
473 public function get_api_secret(): string {
474 $options = $this->get_options();
475
476 return $this->api_secret_is_set() ? $options['api_secret'] : '';
477 }
478 }
479