PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.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 All 106 releases
wp-parsely / src / Models / class-smart-link.php

class-smart-link.php in Parse.ly 3.24.2, at src/Models/class-smart-link.php

1,310 lines 36.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Smart Link model: Represents a smart link suggestion returned by the Smart
4 * Linking API
5 *
6 * @package Parsely
7 * @since 3.16.0
8 */
9
10 declare(strict_types=1);
11
12 namespace Parsely\Models;
13
14 use InvalidArgumentException;
15 use Parsely\Parsely;
16 use Parsely\Utils\Utils;
17 use WP_Post;
18
19 use const Parsely\PARSELY_CACHE_GROUP;
20
21 /**
22 * Smart Link class.
23 *
24 * Represents a smart link suggestion returned by the Smart Linking API.
25 *
26 * @since 3.16.0
27 */
28 class Smart_Link extends Base_Model {
29 /**
30 * The internal ID of the smart link custom post type object.
31 *
32 * @since 3.16.0
33 * @var int The ID of the smart link.
34 */
35 protected $smart_link_id = 0;
36
37 /**
38 * The post ID of the suggested link (link source).
39 *
40 * @since 3.16.0
41 * @var int The post ID of the suggested link, 0 if not set.
42 */
43 public $source_post_id = 0;
44
45 /**
46 * The context of the smart link.
47 *
48 * For example, 'traffic_boost' or 'smart_linking'.
49 *
50 * @since 3.19.0
51 * @var string|null The context of the smart link.
52 */
53 protected $context = null;
54
55 /**
56 * The source post object.
57 *
58 * @since 3.19.0
59 *
60 * @var WP_Post|null The source post.
61 */
62 protected $source_post;
63
64 /**
65 * The post ID of the link destination.
66 *
67 * @since 3.16.0
68 * @var int The post ID of the link destination, 0 if not set.
69 */
70 public $destination_post_id = 0;
71
72 /**
73 * The post type of the destination post.
74 *
75 * @since 3.16.0
76 * @var string The post type of the destination post.
77 */
78 public $destination_post_type = 'external';
79
80 /**
81 * The post type of the source post.
82 *
83 * @since 3.19.0
84 * @var string The post type of the source post.
85 */
86 public $source_post_type = 'unknown';
87
88 /**
89 * The URL of the suggested link.
90 *
91 * @since 3.16.0
92 * @var string The URL of the suggested link.
93 */
94 protected $href = '';
95
96 /**
97 * The title of the suggested link.
98 *
99 * @since 3.16.0
100 * @var string The title of the suggested link.
101 */
102 public $title;
103
104 /**
105 * The text of the suggested link.
106 *
107 * @since 3.16.0
108 * @var string The text of the suggested link.
109 */
110 public $text;
111
112 /**
113 * The offset/position for the suggested link.
114 *
115 * @since 3.16.0
116 * @var int The offset/position for the suggested link.
117 */
118 public $offset;
119
120 /**
121 * The unique ID of the suggested link.
122 *
123 * @since 3.16.0
124 * @var string The unique ID of the suggested link.
125 */
126 public $uid;
127
128 /**
129 * The status of the smart link.
130 *
131 * @since 3.19.0
132 * @var string|null The status of the smart link.
133 */
134 protected $status = null;
135
136 /**
137 * Whether the smart link exists on the database.
138 *
139 * @since 3.16.0
140 * @var bool Whether the link exists.
141 */
142 private $exists = false;
143
144 /**
145 * The post meta of the smart link object.
146 *
147 * @since 3.19.0
148 * @var array<string,array<int,mixed>> The post meta of the smart link.
149 */
150 private $smart_link_post_meta = array();
151
152 /**
153 * Smart Link constructor.
154 *
155 * @since 3.16.0
156 *
157 * @param string $href The URL of the suggested link.
158 * @param string $title The title of the suggested link.
159 * @param string $text The text of the suggested link.
160 * @param int $offset The offset/position for the suggested link.
161 * @param int $post_id The post ID of the suggested link.
162 */
163 public function __construct(
164 string $href,
165 string $title,
166 string $text,
167 int $offset,
168 int $post_id = 0
169 ) {
170 if ( '' !== $href ) {
171 $this->set_href( $href );
172 }
173
174 // Set the title to be the destination post title if the destination post ID is set.
175 if ( 0 !== $this->destination_post_id ) {
176 $this->title = get_the_title( $this->destination_post_id );
177 } else {
178 $this->title = $title;
179 }
180
181 $this->text = $text;
182 $this->offset = $offset;
183 $this->set_source_post_id( $post_id );
184
185 parent::__construct();
186 }
187
188 /**
189 * Gets the smart link post object by UID.
190 *
191 * @since 3.16.0
192 *
193 * @param string $uid The UID of the smart link.
194 * @return int The ID of the smart link post object.
195 */
196 private function get_smart_link_object_by_uid( string $uid ): int {
197 $cache_key = self::get_uid_to_smart_link_cache_key( $uid );
198 $cached = wp_cache_get( $cache_key, PARSELY_CACHE_GROUP );
199
200 if ( false !== $cached && is_numeric( $cached ) ) {
201 return (int) $cached;
202 }
203
204 $smart_links = new \WP_Query(
205 array(
206 'post_type' => 'parsely_smart_link',
207 'fields' => 'ids', // Only get the post IDs to improve performance.
208 'posts_per_page' => 1,
209 'title' => $uid,
210 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
211 'tax_query' => array(
212 array(
213 'taxonomy' => 'smart_link_source',
214 'include_children' => false, // Performance optimization.
215 'field' => 'slug',
216 'terms' => (string) $this->source_post_id,
217 ),
218 ),
219 )
220 );
221
222 if ( $smart_links->have_posts() && is_int( $smart_links->posts[0] ) ) {
223 wp_cache_set(
224 $cache_key,
225 $smart_links->posts[0],
226 PARSELY_CACHE_GROUP,
227 WEEK_IN_SECONDS
228 );
229 return $smart_links->posts[0];
230 }
231
232 return 0;
233 }
234
235 /**
236 * Loads the smart link post object.
237 *
238 * @since 3.16.0
239 *
240 * @return bool True if the smart link was loaded successfully, false otherwise.
241 */
242 private function load(): bool {
243 if ( 0 === $this->smart_link_id ) {
244 // Try to get the smart link id from the UID.
245 $this->smart_link_id = $this->get_smart_link_object_by_uid( $this->uid );
246 if ( 0 === $this->smart_link_id ) {
247 $this->exists = false;
248 return false;
249 }
250 }
251
252 $smart_link = get_post( $this->smart_link_id );
253
254 if ( null === $smart_link || 'parsely_smart_link' !== $smart_link->post_type ) {
255 $this->exists = false;
256 return false;
257 }
258
259 $this->exists = true;
260
261 $this->uid = $smart_link->post_title;
262
263 // Load the Smart Link properties from the post meta.
264 $this->load_post_meta();
265
266 $this->title = $this->get_string_meta( '_smart_link_title' );
267 $this->href = $this->get_string_meta( '_smart_link_href' );
268 $this->text = $this->get_string_meta( '_smart_link_text' );
269 $this->offset = $this->get_int_meta( '_smart_link_offset' );
270
271 $this->status = $this->get_status();
272
273 // Load the context of the smart link, if it exists.
274 if ( isset( $this->smart_link_post_meta['_smart_link_context'] ) ) {
275 $this->context = $this->get_string_meta( '_smart_link_context' );
276 }
277
278 // Load the source post ID.
279 $source_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_source' );
280 if ( ! is_wp_error( $source_terms ) && count( $source_terms ) > 0 ) {
281 $source_term = $source_terms[0];
282 $this->source_post_id = (int) $source_term->name;
283 }
284
285 // Load the destination post ID.
286 $destination_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_destination' );
287 if ( ! is_wp_error( $destination_terms ) && count( $destination_terms ) > 0 ) {
288 $destination_term = $destination_terms[0];
289 if ( 'external' !== $destination_term->slug ) {
290 $this->destination_post_id = (int) $destination_term->name;
291 }
292 }
293
294 // If the destination post ID is not set, try to get it from the URL.
295 if ( 0 === $this->destination_post_id ) {
296 $this->destination_post_id = Utils::get_post_id_by_url( $this->href );
297 }
298
299 // Get the post type of the destination post.
300 $post_type = get_post_type( $this->destination_post_id );
301 if ( false !== $post_type ) {
302 $post_type_object = get_post_type_object( $post_type );
303 if ( null !== $post_type_object ) {
304 $this->destination_post_type = $post_type_object->labels->singular_name;
305 }
306 } else {
307 $this->destination_post_type = 'external';
308 }
309
310 return true;
311 }
312
313 /**
314 * Saves the smart link to the post meta.
315 *
316 * @since 3.16.0
317 *
318 * @return bool True if the smart link was saved successfully, false otherwise.
319 */
320 public function save(): bool {
321 if ( 0 === $this->source_post_id ) {
322 return false;
323 }
324
325 $did_update = false;
326 if ( $this->exists() ) {
327 // If it exists, try to update the existing post.
328 $updated = wp_update_post(
329 array(
330 'ID' => $this->smart_link_id,
331 'post_title' => $this->uid,
332 ),
333 true // Return WP_Error if the post is not updated.
334 );
335
336 if ( is_wp_error( $updated ) ) {
337 // If the post is not updated, there is an invalid post ID cached.
338 // Flush the cache to avoid future errors.
339 $this->flush_cache();
340 } else {
341 $did_update = true;
342 }
343 }
344
345 // If the smart link does not exist, or if the post was not updated, create a new post.
346 if ( ! $did_update || ! $this->exists ) {
347 // Create the post object.
348 $post_id = wp_insert_post(
349 array(
350 'post_type' => 'parsely_smart_link',
351 'post_title' => $this->uid,
352 'post_status' => 'publish',
353 )
354 );
355
356 if ( 0 === $post_id ) {
357 return false;
358 }
359
360 $this->smart_link_id = $post_id;
361 $this->exists = true;
362 wp_cache_set(
363 self::get_uid_to_smart_link_cache_key( $this->uid ),
364 $post_id,
365 PARSELY_CACHE_GROUP,
366 WEEK_IN_SECONDS
367 );
368 }
369
370 // Update the smart link meta.
371 $meta = array(
372 '_smart_link_title' => $this->title,
373 '_smart_link_href' => $this->href,
374 '_smart_link_text' => $this->text,
375 '_smart_link_offset' => $this->offset,
376 );
377
378 if ( null !== $this->context ) {
379 $meta['_smart_link_context'] = $this->context;
380 }
381
382 foreach ( $meta as $key => $value ) {
383 update_post_meta( $this->smart_link_id, $key, $value );
384 }
385
386 // Add the source term.
387 wp_set_post_terms( $this->smart_link_id, (string) $this->source_post_id, 'smart_link_source' );
388
389 // Add the destination term.
390 if ( 0 !== $this->destination_post_id ) {
391 wp_set_post_terms( $this->smart_link_id, (string) $this->destination_post_id, 'smart_link_destination' );
392 } else {
393 wp_set_post_terms( $this->smart_link_id, 'external', 'smart_link_destination' );
394 }
395
396 // Update the status term.
397 if ( null !== $this->status && Smart_Link_Status::is_valid_status( $this->status ) ) {
398 wp_set_post_terms( $this->smart_link_id, $this->status, 'smart_link_status' );
399 } else {
400 wp_set_post_terms( $this->smart_link_id, Smart_Link_Status::PENDING, 'smart_link_status' );
401 }
402
403 // Flush all the associated cache on the source and destination posts.
404 $this->flush_all_cache();
405
406 return true;
407 }
408
409 /**
410 * Removes the smart link from the database.
411 *
412 * @since 3.19.0
413 *
414 * @return bool True if the smart link was removed successfully, false otherwise.
415 */
416 public function delete(): bool {
417 if ( 0 === $this->smart_link_id ) {
418 return false;
419 }
420
421 // Delete the post object.
422 $deleted = wp_delete_post( $this->smart_link_id, true );
423
424 if ( $deleted instanceof WP_Post ) {
425 $this->smart_link_id = 0;
426 $this->exists = false;
427 $this->status = null;
428 $this->flush_all_cache();
429
430 return true;
431 }
432
433 return false;
434 }
435
436 /**
437 * Checks if the smart link is saved in the database.
438 *
439 * @since 3.16.0
440 *
441 * @return bool True if the smart link exists, false otherwise.
442 */
443 public function exists(): bool {
444 if ( $this->exists ) {
445 return true;
446 }
447
448 // Try to find a smart link with the same UID.
449 $smart_link_id = $this->get_smart_link_object_by_uid( $this->uid );
450
451 if ( 0 !== $smart_link_id ) {
452 $this->exists = true;
453 $this->smart_link_id = $smart_link_id;
454 return true;
455 }
456
457 $this->exists = false;
458 $this->smart_link_id = 0;
459 return false;
460 }
461
462 /**
463 * Updates the UID of the smart link.
464 *
465 * @since 3.19.0
466 */
467 public function update_uid(): void {
468 $this->uid = $this->generate_uid();
469 }
470
471 /**
472 * Returns the href of the smart link with ITM parameters appended.
473 *
474 * @since 3.19.0
475 *
476 * @param bool $skip_utm_params Whether to skip the ITM parameters.
477 * @return string The href of the smart link with ITM parameters appended.
478 */
479 public function get_link_href( $skip_utm_params = false ): string {
480 if ( $skip_utm_params ) {
481 return $this->href;
482 }
483
484 $params = array(
485 'campaign' => 'wp-parsely',
486 'medium' => 'smart-link',
487 'term' => $this->uid,
488 );
489
490 // If the context is set, add it to the params as the source.
491 if ( null !== $this->get_context() ) {
492 // Replace underscores with hyphens, for consistency with the ITM parameters.
493 $params['source'] = str_replace( '_', '-', $this->get_context() );
494 }
495
496 return Utils::append_itm_params( $this->href, $params );
497 }
498
499 /**
500 * Returns the context of the smart link.
501 *
502 * @since 3.19.0
503 *
504 * @return string|null The context of the smart link.
505 */
506 public function get_context() {
507 return $this->context;
508 }
509
510 /**
511 * Gets the status of the smart link.
512 *
513 * If the smart link does not have a valid status, it is pending.
514 *
515 * @since 3.19.0
516 *
517 * @return string The status of the smart link.
518 */
519 public function get_status(): string {
520 if ( null !== $this->status && Smart_Link_Status::is_valid_status( $this->status ) ) {
521 return $this->status;
522 }
523
524 $status_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_status' );
525
526 if ( is_wp_error( $status_terms ) || count( $status_terms ) === 0 ) {
527 return Smart_Link_Status::PENDING;
528 }
529
530 $term = $status_terms[0]->slug;
531
532 if ( ! Smart_Link_Status::is_valid_status( $term ) ) {
533 return Smart_Link_Status::PENDING;
534 }
535
536 $this->status = $term;
537 return $term;
538 }
539
540 /**
541 * Checks if the smart link is applied.
542 *
543 * @since 3.19.0
544 *
545 * @return bool True if the smart link is applied, false otherwise.
546 */
547 public function is_applied(): bool {
548 return $this->get_status() === Smart_Link_Status::APPLIED;
549 }
550
551 /**
552 * Sets the status of the smart link.
553 *
554 * @since 3.19.0
555 *
556 * @param string $status The status to set.
557 * @param bool $save Whether to save the status to the database.
558 * @throws \InvalidArgumentException If the status is invalid.
559 */
560 public function set_status( string $status, bool $save = false ): void {
561 if ( ! Smart_Link_Status::is_valid_status( $status ) ) {
562 throw new \InvalidArgumentException( 'Invalid status' );
563 }
564
565 if ( $save && null !== $this->smart_link_id ) {
566 wp_set_post_terms( $this->smart_link_id, $status, 'smart_link_status' );
567 }
568
569 $this->status = $status;
570 }
571
572 /**
573 * Loads the post meta of the smart link object.
574 *
575 * @since 3.19.0
576 */
577 private function load_post_meta(): void {
578 $post_meta = get_post_meta( $this->smart_link_id );
579 /** @var array<string,array<int,mixed>> $post_meta */
580 $this->smart_link_post_meta = $post_meta;
581 }
582
583 /**
584 * Gets a string meta value from the smart link post.
585 *
586 * @since 3.16.0
587 *
588 * @param string $meta_key The meta key to get the value for.
589 * @param string $default_value The default value to return if the meta value is not a string.
590 * @return string The meta value.
591 */
592 private function get_string_meta( string $meta_key, string $default_value = '' ): string {
593 if ( ! isset( $this->smart_link_post_meta[ $meta_key ] ) ) {
594 return $default_value;
595 }
596
597 $meta_value = $this->smart_link_post_meta[ $meta_key ][0];
598 return is_string( $meta_value ) ? $meta_value : $default_value;
599 }
600
601 /**
602 * Gets an integer meta value from the smart link post.
603 *
604 * @since 3.16.0
605 *
606 * @param string $meta_key The meta key to get the value for.
607 * @param int $default_value The default value to return if the meta value is not an integer.
608 * @return int The meta value.
609 */
610 private function get_int_meta( string $meta_key, int $default_value = 0 ): int {
611 if ( ! isset( $this->smart_link_post_meta[ $meta_key ] ) ) {
612 return $default_value;
613 }
614
615 $value = $this->smart_link_post_meta[ $meta_key ][0];
616 if ( ! is_numeric( $value ) ) {
617 return $default_value;
618 }
619
620 return (int) $value;
621 }
622
623 /**
624 * Sets the source post from a post object.
625 *
626 * This method is an alias for Smart_Link::set_source_post_id().
627 *
628 * @since 3.19.0
629 *
630 * @see Smart_Link::set_source_post_id()
631 * @param WP_Post $post The source post.
632 * @param string|null $canonical_url The canonical URL for the source post, to be set if it is not already set.
633 */
634 public function set_source_post( WP_Post $post, $canonical_url = null ): void {
635 $this->source_post = $post;
636 $this->set_source_post_id( $post->ID, $canonical_url );
637 }
638
639 /**
640 * Sets the source post ID.
641 *
642 * @since 3.16.0
643 *
644 * @param int $source_post_id The source post ID.
645 * @param string|null $canonical_url The canonical URL for the source post, to be set if it is not already set.
646 */
647 public function set_source_post_id( int $source_post_id, $canonical_url = null ): void {
648 if ( 0 === $source_post_id ) {
649 return;
650 }
651
652 $this->source_post_id = $source_post_id;
653 if ( null === $this->source_post ) {
654 $this->source_post = get_post( $source_post_id );
655 }
656
657 // Get the post type of the source post.
658 $post_type = get_post_type( $this->source_post_id );
659 if ( false !== $post_type ) {
660 $post_type_object = get_post_type_object( $post_type );
661 if ( null !== $post_type_object ) {
662 $this->source_post_type = $post_type_object->labels->singular_name;
663 }
664 } else {
665 $this->source_post_type = 'unknown';
666 }
667
668 // Update the canonical URL for the source post.
669 if ( null !== $canonical_url ) {
670 Parsely::set_canonical_url( $this->source_post_id, $canonical_url );
671 }
672 }
673
674 /**
675 * Sets the destination post.
676 *
677 * @since 3.19.0
678 *
679 * @param WP_Post $post The destination post.
680 * @param string|null $canonical_url The canonical URL for the destination post, to be set if it is not already set.
681 */
682 public function set_destination_post( WP_Post $post, $canonical_url = null ): void {
683 $this->destination_post_id = $post->ID;
684 $this->href = get_permalink( $post );
685
686 // Get the post type of the destination post.
687 $post_type = get_post_type( $this->destination_post_id );
688 if ( false !== $post_type ) {
689 $post_type_object = get_post_type_object( $post_type );
690 if ( null !== $post_type_object ) {
691 $this->destination_post_type = $post_type_object->labels->singular_name;
692 }
693 } else {
694 $this->destination_post_type = 'external';
695 }
696
697 // Update the canonical URL for the destination post.
698 if ( null !== $canonical_url ) {
699 Parsely::set_canonical_url( $this->destination_post_id, $canonical_url );
700 }
701 }
702
703 /**
704 * Sets the destination post ID.
705 *
706 * @since 3.19.0
707 *
708 * @see Smart_Link::set_destination_post()
709 * @param int $destination_post_id The destination post ID.
710 * @param string|null $canonical_url The canonical URL for the destination post, to be set if it is not already set.
711 */
712 public function set_destination_post_id( int $destination_post_id, $canonical_url = null ): void {
713 $post = get_post( $destination_post_id );
714 if ( null === $post ) {
715 return;
716 }
717
718 $this->set_destination_post( $post, $canonical_url );
719 }
720
721 /**
722 * Sets the UID of the smart link.
723 *
724 * @since 3.19.0
725 *
726 * @param string $uid The UID of the smart link.
727 */
728 public function set_uid( string $uid ): void {
729 $this->uid = $uid;
730 }
731
732 /**
733 * Determines whether an href is valid for a Smart Link.
734 *
735 * Only web URLs are accepted. Scheme-less references such as `/path/` or
736 * `#anchor` also pass, unless they contain a colon, which WordPress reads
737 * as a scheme delimiter. `esc_url_raw()` is used instead of a scheme
738 * comparison, as it also catches case and whitespace variations.
739 *
740 * @since 3.24.2
741 *
742 * @param string $href The href to check.
743 * @return bool Whether the href is valid.
744 */
745 public static function is_valid_href( string $href ): bool {
746 return '' !== esc_url_raw( $href, array( 'http', 'https' ) );
747 }
748
749 /**
750 * Sets the href of the smart link.
751 *
752 * Invalid hrefs are ignored. Callers accepting user input should validate
753 * with `is_valid_href()` first, to report the rejection.
754 *
755 * No canonical URL is stored, as this runs during REST argument
756 * validation, which must not have side effects.
757 *
758 * @since 3.16.0
759 * @since 3.24.2 Invalid hrefs are ignored, and no canonical URL is stored.
760 *
761 * @param string $href The href of the smart link.
762 */
763 public function set_href( string $href ): void {
764 if ( ! self::is_valid_href( $href ) ) {
765 return;
766 }
767
768 $this->href = $href;
769 $destination_post_id = Utils::get_post_id_by_url( $href );
770
771 if ( 0 !== $destination_post_id ) {
772 $this->set_destination_post_id( $destination_post_id );
773 }
774 }
775
776 /**
777 * Sets the context of the smart link.
778 *
779 * @since 3.19.0
780 *
781 * @param string $context The context of the smart link.
782 */
783 public function set_context( string $context ): void {
784 $this->context = $context;
785 }
786
787 /**
788 * Generates a unique ID for the suggested link.
789 *
790 * It takes the href, title, text, and offset properties and concatenates
791 * them to create a unique ID. This ID is hashed to ensure it is unique.
792 *
793 * @since 3.16.0
794 *
795 * @return string The unique ID.
796 */
797 protected function generate_uid(): string {
798 return md5( $this->source_post_id . $this->destination_post_id . $this->href . $this->title . $this->text . $this->offset );
799 }
800
801 /**
802 * Serializes the model to a JSON string.
803 *
804 * @since 3.16.0
805 *
806 * @return array<mixed> The serialized model.
807 */
808 public function to_array(): array {
809 return array(
810 'smart_link_id' => $this->smart_link_id,
811 'uid' => $this->uid,
812 'href' => array(
813 'raw' => $this->href,
814 'itm' => $this->get_link_href(),
815 ),
816 'title' => $this->title,
817 'text' => $this->text,
818 'offset' => $this->offset,
819 'context' => $this->context,
820 'status' => $this->status,
821 'applied' => $this->is_applied(),
822 'source' => array(
823 'post_type' => $this->source_post_type,
824 'post_id' => $this->source_post_id,
825 'canonical_url' => Parsely::get_canonical_url_from_post( $this->source_post_id ),
826 ),
827 'destination' => array(
828 'post_type' => $this->destination_post_type,
829 'post_id' => $this->destination_post_id,
830 'canonical_url' => Parsely::get_canonical_url_from_post( $this->destination_post_id ),
831 ),
832 );
833 }
834
835 /**
836 * Deserializes a JSON string to a model.
837 *
838 * @since 3.16.0
839 *
840 * @throws InvalidArgumentException If the JSON data is invalid.
841 *
842 * @param string $json The JSON string to deserialize.
843 * @return Base_Model The deserialized model.
844 */
845 public static function deserialize( string $json ): Base_Model {
846 $data = json_decode( $json, true );
847
848 // Validate the JSON data.
849 if ( ! is_array( $data ) ) {
850 throw new InvalidArgumentException( 'Invalid JSON data' );
851 }
852
853 // If the UID has been provided, set it on the model.
854 $smart_link = new Smart_Link( $data['href']['raw'], $data['title'], $data['text'], $data['offset'] );
855
856 if ( isset( $data['uid'] ) ) {
857 $smart_link->set_uid( $data['uid'] );
858
859 if ( $smart_link->exists() ) {
860 $smart_link->load();
861 // Update the fields.
862 $smart_link->set_href( $data['href']['raw'] );
863 $smart_link->title = $data['title'];
864 $smart_link->text = $data['text'];
865 $smart_link->offset = $data['offset'];
866 }
867 }
868
869 return $smart_link;
870 }
871
872 /**
873 * Gets a smart link by UID.
874 *
875 * @since 3.16.0
876 *
877 * @param string $uid The UID of the smart link.
878 * @param int $post_id The post ID of the smart link.
879 * @return Smart_Link The smart link object.
880 */
881 public static function get_smart_link( string $uid, int $post_id ): Smart_Link {
882 $smart_link = new Smart_Link( '', '', '', 0 );
883 $smart_link->uid = $uid;
884 $smart_link->source_post_id = $post_id;
885 $smart_link->load();
886 return $smart_link;
887 }
888
889 /**
890 * Gets a smart link by post object ID.
891 *
892 * @since 3.16.0
893 *
894 * @param int $smart_link_id The ID of the smart link.
895 * @return Smart_Link|false The smart link object, or false if it does not exist.
896 */
897 public static function get_smart_link_by_id( int $smart_link_id ) {
898 $smart_link = new Smart_Link( '', '', '', 0 );
899 $smart_link->smart_link_id = $smart_link_id;
900 if ( $smart_link->load() ) {
901 return $smart_link;
902 }
903
904 return false;
905 }
906
907 /**
908 * Gets smart links based on the specified parameters.
909 *
910 * @since 3.19.0
911 *
912 * @param int $post_id The post ID to get the smart links for.
913 * @param string $type The type of smart links to get (outbound or inbound or all).
914 * @param string $status The status of the smart links to get (all or pending or applied).
915 * @param array<string,mixed> $args WP_Query arguments to pass to the query.
916 * @param callable(Smart_Link):(Smart_Link|Inbound_Smart_Link|false|null)|null $process_smart_link_callback A callback to process each individual smart link.
917 * @return array<Smart_Link> The smart links.
918 */
919 public static function get_smart_links( int $post_id, string $type, string $status, array $args = array(), $process_smart_link_callback = null ): array {
920 if ( ! Smart_Link_Status::is_valid_status( $status ) ) {
921 $status = 'all';
922 _doing_it_wrong( __METHOD__, 'Invalid status, defaulting to all.', '3.19.0' );
923 }
924
925 if ( ! in_array( $type, array( 'outbound', 'inbound', 'all' ), true ) ) {
926 _doing_it_wrong( __METHOD__, 'Invalid type, defaulting to outbound.', '3.19.0' );
927 $type = 'outbound';
928 }
929
930 $skip_cache = isset( $args['skip_cache'] ) && true === $args['skip_cache'];
931 $cache_key = self::get_smart_links_for_post_cache_key( $type, $status );
932 $cache_group = self::get_smart_links_post_cache_group( $post_id );
933 $smart_link_ids = false;
934
935 // If the cache is not being skipped, get the smart links from the cache.
936 if ( ! $skip_cache ) {
937 /** @var array<int>|false $smart_link_ids */
938 $smart_link_ids = wp_cache_get( $cache_key, $cache_group );
939 }
940
941 if ( false === $smart_link_ids ) {
942 $tax_query = array();
943
944 // Add the tax query for the type of smart links to get.
945 if ( 'outbound' === $type ) {
946 $tax_query[] = array(
947 'taxonomy' => 'smart_link_source',
948 'include_children' => false, // Performance optimization.
949 'field' => 'slug',
950 'terms' => (string) $post_id,
951 );
952 } elseif ( 'inbound' === $type ) {
953 $tax_query[] = array(
954 'taxonomy' => 'smart_link_destination',
955 'include_children' => false, // Performance optimization.
956 'field' => 'slug',
957 'terms' => (string) $post_id,
958 );
959 }
960
961 // Add the tax query for the status of the smart links to get.
962 if ( Smart_Link_Status::ALL === $status ) {
963 $tax_query[] = array(
964 'taxonomy' => 'smart_link_status',
965 'include_children' => false,
966 'field' => 'slug',
967 'terms' => Smart_Link_Status::get_all_statuses(),
968 );
969 } else {
970 $tax_query[] = array(
971 'taxonomy' => 'smart_link_status',
972 'include_children' => false,
973 'field' => 'slug',
974 'terms' => array( $status ),
975 );
976 }
977 // Build the query arguments.
978 $query_args = array(
979 'post_type' => 'parsely_smart_link',
980 'posts_per_page' => -1, // phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.posts_per_page_posts_per_page
981 'fields' => 'ids', // Only get the post IDs to improve performance.
982 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
983 'tax_query' => array_merge( array( 'relation' => 'AND' ), $tax_query ),
984 );
985
986 // Merge the query arguments with the additional arguments.
987 $query_args = array_merge( $query_args, $args );
988
989 // Get the smart links post objects.
990 $smart_links_query = new \WP_Query( $query_args );
991
992 // Cache the queried IDs.
993 $smart_link_ids = $smart_links_query->posts;
994 wp_cache_set( $cache_key, $smart_link_ids, $cache_group, DAY_IN_SECONDS );
995 }
996
997 // Create and process the smart links.
998 $smart_links = array();
999 foreach ( $smart_link_ids as $smart_link_id ) {
1000 /** @var int $smart_link_id */
1001 $smart_link = self::get_smart_link_by_id( $smart_link_id );
1002
1003 if ( false === $smart_link ) {
1004 continue;
1005 }
1006
1007 if ( is_callable( $process_smart_link_callback ) ) {
1008 /**
1009 * The processed smart link after it has been processed by the callback.
1010 *
1011 * This callback is used to modify the smart link before it is added to the array,
1012 * or false if the smart link should be skipped.
1013 *
1014 * @since 3.19.0
1015 *
1016 * @var Smart_Link|Inbound_Smart_Link|false|null $smart_link
1017 */
1018 $smart_link = $process_smart_link_callback( $smart_link );
1019 }
1020
1021 if ( false === $smart_link || null === $smart_link ) {
1022 continue;
1023 }
1024
1025 $smart_links[] = $smart_link;
1026 }
1027
1028 return $smart_links;
1029 }
1030
1031 /**
1032 * Gets the outbound smart links in a post.
1033 *
1034 * Outbound smart links are smart links that link to other posts.
1035 *
1036 * @since 3.16.0
1037 * @since 3.19.0 Added status parameter.
1038 *
1039 * @param int $post_id The post ID to get the smart links for.
1040 * @param string $status The status of the smart links to get.
1041 * @return array<Smart_Link> The smart links in the post.
1042 */
1043 public static function get_outbound_smart_links( int $post_id, string $status = Smart_Link_Status::ALL ): array {
1044 /** @var array<Smart_Link> */
1045 return self::get_smart_links(
1046 $post_id,
1047 'outbound',
1048 $status,
1049 array(
1050 'orderby' => 'date',
1051 'order' => 'ASC',
1052 )
1053 );
1054 }
1055
1056 /**
1057 * Gets the inbound smart links in a post.
1058 *
1059 * Inbound smart links are links on other posts that link to the post.
1060 *
1061 * @since 3.16.0
1062 * @since 3.19.0 Added status parameter.
1063 *
1064 * @param int $post_id The post ID to get the smart links for.
1065 * @param string $status The status of the smart links to get.
1066 * @return array<Inbound_Smart_Link> The smart links in the post.
1067 */
1068 public static function get_inbound_smart_links( int $post_id, string $status = Smart_Link_Status::ALL ): array {
1069 /** @var array<Inbound_Smart_Link> */
1070 return self::get_smart_links(
1071 $post_id,
1072 'inbound',
1073 $status,
1074 array(
1075 'orderby' => 'date modified',
1076 'order' => 'ASC',
1077 ),
1078 /**
1079 * Process the smart link to convert it to an inbound smart link.
1080 *
1081 * @param Smart_Link $smart_link The smart link to process.
1082 * @return Inbound_Smart_Link|false The processed smart link.
1083 */
1084 function ( Smart_Link $smart_link ) {
1085 $smart_link = Inbound_Smart_Link::from_smart_link( $smart_link );
1086 $is_linked = $smart_link->is_linked();
1087 $status = $smart_link->get_status();
1088
1089 // If the smart link is linked and the status is pending, set the status to applied.
1090 // This is to ensure backwards compatibility with Parse.ly < 3.18.0.
1091 if ( $is_linked && Smart_Link_Status::PENDING === $status ) {
1092 $smart_link->set_status( Smart_Link_Status::APPLIED, true );
1093 $status = Smart_Link_Status::APPLIED;
1094 }
1095
1096 // Check if this inbound smart link is still linked to a post.
1097 // If not, do not add it to the array, and instead remove it.
1098 if ( Smart_Link_Status::APPLIED === $status && ! $is_linked ) {
1099 $smart_link->delete();
1100 return false;
1101 }
1102
1103 /** @var Inbound_Smart_Link */
1104 return $smart_link;
1105 }
1106 );
1107 }
1108
1109 /**
1110 * Gets the link counts for a post.
1111 *
1112 * @since 3.19.0
1113 *
1114 * @param int $post_id The post ID to get the link counts for.
1115 * @param string $status The status of the smart links to get.
1116 * @return array<string,int> The link counts.
1117 */
1118 public static function get_link_counts( int $post_id, string $status = Smart_Link_Status::ALL ): array {
1119 if ( ! Smart_Link_Status::is_valid_status( $status ) ) {
1120 $status = Smart_Link_Status::ALL;
1121 _doing_it_wrong( __METHOD__, 'Invalid status, defaulting to all.', '3.19.0' );
1122 }
1123
1124 $cache_key = self::get_smart_link_counts_cache_key( $status );
1125 $cache_group = self::get_smart_links_post_cache_group( $post_id );
1126 $link_counts = wp_cache_get( $cache_key, $cache_group );
1127
1128 if ( false !== $link_counts && is_array( $link_counts ) ) {
1129 return $link_counts;
1130 }
1131
1132 $base_query_args = array(
1133 'post_type' => 'parsely_smart_link',
1134 'posts_per_page' => 0,
1135 'fields' => 'ids',
1136 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1137 'tax_query' => array(),
1138 );
1139
1140 // Build the tax query for the status.
1141 if ( Smart_Link_Status::ALL !== $status ) {
1142 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1143 $base_query_args['tax_query'] = array(
1144 array(
1145 'taxonomy' => 'smart_link_status',
1146 'field' => 'slug',
1147 'terms' => array( $status ),
1148 ),
1149 );
1150 }
1151
1152 // Build the query arguments for the inbound links.
1153 $inbound_query_args = $base_query_args;
1154 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1155 $inbound_query_args['tax_query'] = array_merge(
1156 $inbound_query_args['tax_query'],
1157 array(
1158 array(
1159 'taxonomy' => 'smart_link_destination',
1160 'field' => 'slug',
1161 'terms' => $post_id,
1162 ),
1163 )
1164 );
1165
1166 // Build the query arguments for the outbound links.
1167 $outbound_query_args = array_merge(
1168 $base_query_args,
1169 array(
1170 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1171 'tax_query' => array_merge(
1172 $base_query_args['tax_query'],
1173 array(
1174 array(
1175 'taxonomy' => 'smart_link_source',
1176 'field' => 'slug',
1177 'terms' => $post_id,
1178 ),
1179 )
1180 ),
1181 )
1182 );
1183
1184 // Get the inbound links.
1185 $inbound_links = new \WP_Query( $inbound_query_args );
1186
1187 // Get the outbound links.
1188 $outbound_links = new \WP_Query( $outbound_query_args );
1189
1190 $link_counts = array(
1191 'inbound' => $inbound_links->found_posts,
1192 'outbound' => $outbound_links->found_posts,
1193 );
1194
1195 wp_cache_set( $cache_key, $link_counts, $cache_group, WEEK_IN_SECONDS );
1196
1197 return $link_counts;
1198 }
1199
1200 /**
1201 * Flushes the cache for a single smart link.
1202 *
1203 * @since 3.19.0
1204 */
1205 protected function flush_cache(): void {
1206 // Delete the cache for the smart link UID to post ID association.
1207 $cache_key = self::get_uid_to_smart_link_cache_key( $this->uid );
1208 wp_cache_delete( $cache_key, PARSELY_CACHE_GROUP );
1209 }
1210
1211 /**
1212 * Flushes the cache for all smart links in a post.
1213 *
1214 * @since 3.19.0
1215 */
1216 public function flush_all_cache(): void {
1217 $this->flush_cache();
1218
1219 if ( $this->source_post_id > 0 ) {
1220 static::flush_cache_by_post_id( $this->source_post_id );
1221 }
1222
1223 if ( $this->destination_post_id > 0 ) {
1224 static::flush_cache_by_post_id( $this->destination_post_id );
1225 }
1226 }
1227
1228 /**
1229 * Generates a cache key for the smart link.
1230 *
1231 * @since 3.19.0
1232 *
1233 * @param string $uid The unique identifier for the cache key.
1234 * @return string The cache key.
1235 */
1236 protected static function get_uid_to_smart_link_cache_key( string $uid ): string {
1237 return sprintf( 'smart-link-uid-map-%s', $uid );
1238 }
1239
1240 /**
1241 * Gets the cache key for all smart links in a post.
1242 *
1243 * @since 3.19.0
1244 *
1245 * @param string $type The type of smart links ('outbound', 'inbound', or 'all').
1246 * @param string $status The status of the smart links ('all', 'pending', or 'applied').
1247 *
1248 * @return string The cache key.
1249 */
1250 protected static function get_smart_links_for_post_cache_key( string $type, string $status ): string {
1251 return sprintf( 'smart-links-post-id-map-%s-%s', $type, $status );
1252 }
1253
1254 /**
1255 * Gets the cache key for all counts of smart links in a post.
1256 *
1257 * @since 3.19.0
1258 *
1259 * @param string $status The status of the smart links ('all', 'pending', or 'applied').
1260 *
1261 * @return string The cache key.
1262 */
1263 protected static function get_smart_link_counts_cache_key( string $status ): string {
1264 return sprintf( 'smart-link-counts-%s', $status );
1265 }
1266
1267 /**
1268 * Generates a cache group for smart links on a post. Useful with wp_cache_flush_group()
1269 * to flush all smart link caches on a post.
1270 *
1271 * @since 3.19.0
1272 *
1273 * @param int $post_id The post ID.
1274 * @return string The cache group.
1275 */
1276 protected static function get_smart_links_post_cache_group( int $post_id ): string {
1277 return sprintf( '%s-smart-links-%d', PARSELY_CACHE_GROUP, $post_id );
1278 }
1279
1280 /**
1281 * Flushes the cache for all smart links associated with a given post.
1282 *
1283 * @since 3.19.0
1284 *
1285 * @param int $post_id The post ID to flush the cache for.
1286 */
1287 protected static function flush_cache_by_post_id( int $post_id ): void {
1288 $cache_group = self::get_smart_links_post_cache_group( $post_id );
1289
1290 if ( function_exists( 'wp_cache_flush_group' ) && wp_cache_supports( 'flush_group' ) ) {
1291 wp_cache_flush_group( $cache_group );
1292 } else {
1293 $statuses = Smart_Link_Status::get_all_statuses();
1294 $types = array( 'outbound', 'inbound', 'all' );
1295
1296 foreach ( $statuses as $status ) {
1297 // Delete smart link count cache.
1298 $smart_link_counts_cache_key = self::get_smart_link_counts_cache_key( $status );
1299 wp_cache_delete( $smart_link_counts_cache_key, $cache_group );
1300
1301 // Delete smart links caches.
1302 foreach ( $types as $type ) {
1303 $cache_key = self::get_smart_links_for_post_cache_key( $type, $status );
1304 wp_cache_delete( $cache_key, $cache_group );
1305 }
1306 }
1307 }
1308 }
1309 }
1310