PluginProbe
Parse.ly / 3.5.0
Parse.ly v3.5.0
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.5.0, at src/class-parsely.php

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