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

446 lines 13.0 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 $metadata = new Metadata( $this );
243 return $metadata->construct_metadata( $post );
244 }
245
246 /**
247 * Updates the Parsely metadata endpoint with the new metadata of the post.
248 *
249 * @param int $post_id id of the post to update.
250 */
251 public function update_metadata_endpoint( int $post_id ): void {
252 $parsely_options = $this->get_options();
253 if ( $this->api_key_is_missing() || empty( $parsely_options['metadata_secret'] ) ) {
254 return;
255 }
256
257 $post = get_post( $post_id );
258 if ( null === $post ) {
259 return;
260 }
261
262 $metadata = ( new Metadata( $this ) )->construct_metadata( $post );
263
264 $endpoint_metadata = array(
265 'canonical_url' => $metadata['url'],
266 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ),
267 'title' => $metadata['headline'],
268 'image_url' => $metadata['image']['url'],
269 'pub_date_tmsp' => $metadata['datePublished'],
270 'section' => $metadata['articleSection'],
271 'authors' => $metadata['creator'],
272 'tags' => $metadata['keywords'],
273 );
274
275 $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts';
276 $parsely_metadata_secret = $parsely_options['metadata_secret'];
277 $headers = array(
278 'Content-Type' => 'application/json',
279 );
280 $body = wp_json_encode(
281 array(
282 'secret' => $parsely_metadata_secret,
283 'apikey' => $parsely_options['apikey'],
284 'metadata' => $endpoint_metadata,
285 )
286 );
287 $response = wp_remote_post(
288 $parsely_api_endpoint,
289 array(
290 'method' => 'POST',
291 'headers' => $headers,
292 'blocking' => false,
293 'body' => $body,
294 'data_format' => 'body',
295 )
296 );
297
298 if ( ! is_wp_error( $response ) ) {
299 $current_timestamp = time();
300 update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp );
301 }
302 }
303
304 /**
305 * Updates posts with Parsely metadata api in bulk.
306 */
307 public function bulk_update_posts(): void {
308 global $wpdb;
309 $parsely_options = $this->get_options();
310 $allowed_types = array_merge( $parsely_options['track_post_types'], $parsely_options['track_page_types'] );
311 $allowed_types_string = implode(
312 ', ',
313 array_map(
314 function( $v ) {
315 return "'" . esc_sql( $v ) . "'";
316 },
317 $allowed_types
318 )
319 );
320 $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
321 if ( false === $ids ) {
322 $ids = array();
323 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
324 $results = $wpdb->get_results(
325 $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 ),
326 ARRAY_N
327 );
328 foreach ( $results as $result ) {
329 $ids[] = $result[0];
330 }
331 wp_cache_set( 'parsely_post_ids_need_meta_updating', $ids, '', 86400 );
332 }
333
334 for ( $i = 0; $i < 100; $i++ ) {
335 $post_id = array_pop( $ids );
336 if ( null === $post_id ) {
337 wp_clear_scheduled_hook( 'parsely_bulk_metas_update' );
338 break;
339 }
340 $this->update_metadata_endpoint( $post_id );
341 }
342 }
343
344 /**
345 * Safely returns options for the plugin by assigning defaults contained in
346 * optionDefaults.
347 *
348 * As soon as actual options are saved, they override the defaults. This
349 * prevents us from having to do a lot of isset() checking on variables.
350 *
351 * @return array<string, mixed>
352 */
353 public function get_options(): array {
354 $options = get_option( self::OPTIONS_KEY, $this->option_defaults );
355
356 if ( ! is_array( $options ) ) {
357 return $this->option_defaults;
358 }
359
360 return array_merge( $this->option_defaults, $options );
361 }
362
363 /**
364 * Gets the URL of the plugin's settings page.
365 *
366 * @param int|null $_blog_id The Blog ID for the multisite subsite to use
367 * for context (Default null for current).
368 *
369 * @return string
370 */
371 public static function get_settings_url( int $_blog_id = null ): string {
372 return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
373 }
374
375 /**
376 * Checks to see if Parse.ly user is logged in.
377 *
378 * @return bool
379 */
380 public function parsely_is_user_logged_in(): bool {
381 // can't use $blog_id here because it futzes with the global $blog_id.
382 $current_blog_id = get_current_blog_id();
383 $current_user_id = get_current_user_id();
384 return is_user_member_of_blog( $current_user_id, $current_blog_id );
385 }
386
387 /**
388 * Converts JSON-LD type to respective Parse.ly page type.
389 *
390 * If the JSON-LD type is one of the types Parse.ly supports as a "post",
391 * then "post" will be returned. Otherwise, for "non-posts" and unknown
392 * types, "index" is returned.
393 *
394 * @since 2.5.0
395 *
396 * @see https://www.parse.ly/help/integration/metatags#field-description
397 *
398 * @param string $type JSON-LD type.
399 * @return string "post" or "index".
400 */
401 public function convert_jsonld_to_parsely_type( string $type ): string {
402 return in_array( $type, self::SUPPORTED_JSONLD_POST_TYPES, true ) ? 'post' : 'index';
403 }
404
405 /**
406 * Determines if an API key is saved in the options.
407 *
408 * @since 2.6.0
409 *
410 * @return bool True is API key is set, false if it is missing.
411 */
412 public function api_key_is_set(): bool {
413 $options = $this->get_options();
414
415 return (
416 isset( $options['apikey'] ) &&
417 is_string( $options['apikey'] ) &&
418 '' !== $options['apikey']
419 );
420 }
421
422 /**
423 * Determines if an API key is not saved in the options.
424 *
425 * @since 2.6.0
426 *
427 * @return bool True if API key is missing, false if it is set.
428 */
429 public function api_key_is_missing(): bool {
430 return ! $this->api_key_is_set();
431 }
432
433 /**
434 * Gets the API key if set.
435 *
436 * @since 2.6.0
437 *
438 * @return string API key if set, or empty string if not.
439 */
440 public function get_api_key(): string {
441 $options = $this->get_options();
442
443 return $this->api_key_is_set() ? $options['apikey'] : '';
444 }
445 }
446