PluginProbe
Parse.ly / 3.20.1
Parse.ly v3.20.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 / Models / class-smart-link.php

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

1,283 lines 35.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 * Sets the href of the smart link.
734 *
735 * @since 3.16.0
736 *
737 * @param string $href The href of the smart link.
738 */
739 public function set_href( string $href ): void {
740 $this->href = $href;
741 $destination_post_id = Utils::get_post_id_by_url( $href );
742
743 if ( 0 !== $destination_post_id ) {
744 // Set the destination post ID, and update the canonical URL.
745 $this->set_destination_post_id( $destination_post_id, $href );
746 }
747 }
748
749 /**
750 * Sets the context of the smart link.
751 *
752 * @since 3.19.0
753 *
754 * @param string $context The context of the smart link.
755 */
756 public function set_context( string $context ): void {
757 $this->context = $context;
758 }
759
760 /**
761 * Generates a unique ID for the suggested link.
762 *
763 * It takes the href, title, text, and offset properties and concatenates
764 * them to create a unique ID. This ID is hashed to ensure it is unique.
765 *
766 * @since 3.16.0
767 *
768 * @return string The unique ID.
769 */
770 protected function generate_uid(): string {
771 return md5( $this->source_post_id . $this->destination_post_id . $this->href . $this->title . $this->text . $this->offset );
772 }
773
774 /**
775 * Serializes the model to a JSON string.
776 *
777 * @since 3.16.0
778 *
779 * @return array<mixed> The serialized model.
780 */
781 public function to_array(): array {
782 return array(
783 'smart_link_id' => $this->smart_link_id,
784 'uid' => $this->uid,
785 'href' => array(
786 'raw' => $this->href,
787 'itm' => $this->get_link_href(),
788 ),
789 'title' => $this->title,
790 'text' => $this->text,
791 'offset' => $this->offset,
792 'context' => $this->context,
793 'status' => $this->status,
794 'applied' => $this->is_applied(),
795 'source' => array(
796 'post_type' => $this->source_post_type,
797 'post_id' => $this->source_post_id,
798 'canonical_url' => Parsely::get_canonical_url_from_post( $this->source_post_id ),
799 ),
800 'destination' => array(
801 'post_type' => $this->destination_post_type,
802 'post_id' => $this->destination_post_id,
803 'canonical_url' => Parsely::get_canonical_url_from_post( $this->destination_post_id ),
804 ),
805 );
806 }
807
808 /**
809 * Deserializes a JSON string to a model.
810 *
811 * @since 3.16.0
812 *
813 * @throws InvalidArgumentException If the JSON data is invalid.
814 *
815 * @param string $json The JSON string to deserialize.
816 * @return Base_Model The deserialized model.
817 */
818 public static function deserialize( string $json ): Base_Model {
819 $data = json_decode( $json, true );
820
821 // Validate the JSON data.
822 if ( ! is_array( $data ) ) {
823 throw new InvalidArgumentException( 'Invalid JSON data' );
824 }
825
826 // If the UID has been provided, set it on the model.
827 $smart_link = new Smart_Link( $data['href']['raw'], $data['title'], $data['text'], $data['offset'] );
828
829 if ( isset( $data['uid'] ) ) {
830 $smart_link->set_uid( $data['uid'] );
831
832 if ( $smart_link->exists() ) {
833 $smart_link->load();
834 // Update the fields.
835 $smart_link->set_href( $data['href']['raw'] );
836 $smart_link->title = $data['title'];
837 $smart_link->text = $data['text'];
838 $smart_link->offset = $data['offset'];
839 }
840 }
841
842 return $smart_link;
843 }
844
845 /**
846 * Gets a smart link by UID.
847 *
848 * @since 3.16.0
849 *
850 * @param string $uid The UID of the smart link.
851 * @param int $post_id The post ID of the smart link.
852 * @return Smart_Link The smart link object.
853 */
854 public static function get_smart_link( string $uid, int $post_id ): Smart_Link {
855 $smart_link = new Smart_Link( '', '', '', 0 );
856 $smart_link->uid = $uid;
857 $smart_link->source_post_id = $post_id;
858 $smart_link->load();
859 return $smart_link;
860 }
861
862 /**
863 * Gets a smart link by post object ID.
864 *
865 * @since 3.16.0
866 *
867 * @param int $smart_link_id The ID of the smart link.
868 * @return Smart_Link|false The smart link object, or false if it does not exist.
869 */
870 public static function get_smart_link_by_id( int $smart_link_id ) {
871 $smart_link = new Smart_Link( '', '', '', 0 );
872 $smart_link->smart_link_id = $smart_link_id;
873 if ( $smart_link->load() ) {
874 return $smart_link;
875 }
876
877 return false;
878 }
879
880 /**
881 * Gets smart links based on the specified parameters.
882 *
883 * @since 3.19.0
884 *
885 * @param int $post_id The post ID to get the smart links for.
886 * @param string $type The type of smart links to get (outbound or inbound or all).
887 * @param string $status The status of the smart links to get (all or pending or applied).
888 * @param array<string,mixed> $args WP_Query arguments to pass to the query.
889 * @param callable(Smart_Link):(Smart_Link|Inbound_Smart_Link|false|null)|null $process_smart_link_callback A callback to process each individual smart link.
890 * @return array<Smart_Link> The smart links.
891 */
892 public static function get_smart_links( int $post_id, string $type, string $status, array $args = array(), $process_smart_link_callback = null ): array {
893 if ( ! Smart_Link_Status::is_valid_status( $status ) ) {
894 $status = 'all';
895 _doing_it_wrong( __METHOD__, 'Invalid status, defaulting to all.', '3.19.0' );
896 }
897
898 if ( ! in_array( $type, array( 'outbound', 'inbound', 'all' ), true ) ) {
899 _doing_it_wrong( __METHOD__, 'Invalid type, defaulting to outbound.', '3.19.0' );
900 $type = 'outbound';
901 }
902
903 $skip_cache = isset( $args['skip_cache'] ) && true === $args['skip_cache'];
904 $cache_key = self::get_smart_links_for_post_cache_key( $type, $status );
905 $cache_group = self::get_smart_links_post_cache_group( $post_id );
906 $smart_link_ids = false;
907
908 // If the cache is not being skipped, get the smart links from the cache.
909 if ( ! $skip_cache ) {
910 /** @var array<int>|false $smart_link_ids */
911 $smart_link_ids = wp_cache_get( $cache_key, $cache_group );
912 }
913
914 if ( false === $smart_link_ids ) {
915 $tax_query = array();
916
917 // Add the tax query for the type of smart links to get.
918 if ( 'outbound' === $type ) {
919 $tax_query[] = array(
920 'taxonomy' => 'smart_link_source',
921 'include_children' => false, // Performance optimization.
922 'field' => 'slug',
923 'terms' => (string) $post_id,
924 );
925 } elseif ( 'inbound' === $type ) {
926 $tax_query[] = array(
927 'taxonomy' => 'smart_link_destination',
928 'include_children' => false, // Performance optimization.
929 'field' => 'slug',
930 'terms' => (string) $post_id,
931 );
932 }
933
934 // Add the tax query for the status of the smart links to get.
935 if ( Smart_Link_Status::ALL === $status ) {
936 $tax_query[] = array(
937 'taxonomy' => 'smart_link_status',
938 'include_children' => false,
939 'field' => 'slug',
940 'terms' => Smart_Link_Status::get_all_statuses(),
941 );
942 } else {
943 $tax_query[] = array(
944 'taxonomy' => 'smart_link_status',
945 'include_children' => false,
946 'field' => 'slug',
947 'terms' => array( $status ),
948 );
949 }
950 // Build the query arguments.
951 $query_args = array(
952 'post_type' => 'parsely_smart_link',
953 'posts_per_page' => -1,
954 'fields' => 'ids', // Only get the post IDs to improve performance.
955 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
956 'tax_query' => array_merge( array( 'relation' => 'AND' ), $tax_query ),
957 );
958
959 // Merge the query arguments with the additional arguments.
960 $query_args = array_merge( $query_args, $args );
961
962 // Get the smart links post objects.
963 $smart_links_query = new \WP_Query( $query_args );
964
965 // Cache the queried IDs.
966 $smart_link_ids = $smart_links_query->posts;
967 wp_cache_set( $cache_key, $smart_link_ids, $cache_group, DAY_IN_SECONDS );
968 }
969
970 // Create and process the smart links.
971 $smart_links = array();
972 foreach ( $smart_link_ids as $smart_link_id ) {
973 /** @var int $smart_link_id */
974 $smart_link = self::get_smart_link_by_id( $smart_link_id );
975
976 if ( false === $smart_link ) {
977 continue;
978 }
979
980 if ( is_callable( $process_smart_link_callback ) ) {
981 /**
982 * The processed smart link after it has been processed by the callback.
983 *
984 * This callback is used to modify the smart link before it is added to the array,
985 * or false if the smart link should be skipped.
986 *
987 * @since 3.19.0
988 *
989 * @var Smart_Link|Inbound_Smart_Link|false|null $smart_link
990 */
991 $smart_link = $process_smart_link_callback( $smart_link );
992 }
993
994 if ( false === $smart_link || null === $smart_link ) {
995 continue;
996 }
997
998 $smart_links[] = $smart_link;
999 }
1000
1001 return $smart_links;
1002 }
1003
1004 /**
1005 * Gets the outbound smart links in a post.
1006 *
1007 * Outbound smart links are smart links that link to other posts.
1008 *
1009 * @since 3.16.0
1010 * @since 3.19.0 Added status parameter.
1011 *
1012 * @param int $post_id The post ID to get the smart links for.
1013 * @param string $status The status of the smart links to get.
1014 * @return array<Smart_Link> The smart links in the post.
1015 */
1016 public static function get_outbound_smart_links( int $post_id, string $status = Smart_Link_Status::ALL ): array {
1017 /** @var array<Smart_Link> */
1018 return self::get_smart_links(
1019 $post_id,
1020 'outbound',
1021 $status,
1022 array(
1023 'orderby' => 'date',
1024 'order' => 'ASC',
1025 )
1026 );
1027 }
1028
1029 /**
1030 * Gets the inbound smart links in a post.
1031 *
1032 * Inbound smart links are links on other posts that link to the post.
1033 *
1034 * @since 3.16.0
1035 * @since 3.19.0 Added status parameter.
1036 *
1037 * @param int $post_id The post ID to get the smart links for.
1038 * @param string $status The status of the smart links to get.
1039 * @return array<Inbound_Smart_Link> The smart links in the post.
1040 */
1041 public static function get_inbound_smart_links( int $post_id, string $status = Smart_Link_Status::ALL ): array {
1042 /** @var array<Inbound_Smart_Link> */
1043 return self::get_smart_links(
1044 $post_id,
1045 'inbound',
1046 $status,
1047 array(
1048 'orderby' => 'date modified',
1049 'order' => 'ASC',
1050 ),
1051 /**
1052 * Process the smart link to convert it to an inbound smart link.
1053 *
1054 * @param Smart_Link $smart_link The smart link to process.
1055 * @return Inbound_Smart_Link|false The processed smart link.
1056 */
1057 function ( Smart_Link $smart_link ) {
1058 $smart_link = Inbound_Smart_Link::from_smart_link( $smart_link );
1059 $is_linked = $smart_link->is_linked();
1060 $status = $smart_link->get_status();
1061
1062 // If the smart link is linked and the status is pending, set the status to applied.
1063 // This is to ensure backwards compatibility with Parse.ly < 3.18.0.
1064 if ( $is_linked && Smart_Link_Status::PENDING === $status ) {
1065 $smart_link->set_status( Smart_Link_Status::APPLIED, true );
1066 $status = Smart_Link_Status::APPLIED;
1067 }
1068
1069 // Check if this inbound smart link is still linked to a post.
1070 // If not, do not add it to the array, and instead remove it.
1071 if ( Smart_Link_Status::APPLIED === $status && ! $is_linked ) {
1072 $smart_link->delete();
1073 return false;
1074 }
1075
1076 /** @var Inbound_Smart_Link */
1077 return $smart_link;
1078 }
1079 );
1080 }
1081
1082 /**
1083 * Gets the link counts for a post.
1084 *
1085 * @since 3.19.0
1086 *
1087 * @param int $post_id The post ID to get the link counts for.
1088 * @param string $status The status of the smart links to get.
1089 * @return array<string,int> The link counts.
1090 */
1091 public static function get_link_counts( int $post_id, string $status = Smart_Link_Status::ALL ): array {
1092 if ( ! Smart_Link_Status::is_valid_status( $status ) ) {
1093 $status = Smart_Link_Status::ALL;
1094 _doing_it_wrong( __METHOD__, 'Invalid status, defaulting to all.', '3.19.0' );
1095 }
1096
1097 $cache_key = self::get_smart_link_counts_cache_key( $status );
1098 $cache_group = self::get_smart_links_post_cache_group( $post_id );
1099 $link_counts = wp_cache_get( $cache_key, $cache_group );
1100
1101 if ( false !== $link_counts && is_array( $link_counts ) ) {
1102 return $link_counts;
1103 }
1104
1105 $base_query_args = array(
1106 'post_type' => 'parsely_smart_link',
1107 'posts_per_page' => 0,
1108 'fields' => 'ids',
1109 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1110 'tax_query' => array(),
1111 );
1112
1113 // Build the tax query for the status.
1114 if ( Smart_Link_Status::ALL !== $status ) {
1115 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1116 $base_query_args['tax_query'] = array(
1117 array(
1118 'taxonomy' => 'smart_link_status',
1119 'field' => 'slug',
1120 'terms' => array( $status ),
1121 ),
1122 );
1123 }
1124
1125 // Build the query arguments for the inbound links.
1126 $inbound_query_args = $base_query_args;
1127 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1128 $inbound_query_args['tax_query'] = array_merge(
1129 $inbound_query_args['tax_query'],
1130 array(
1131 array(
1132 'taxonomy' => 'smart_link_destination',
1133 'field' => 'slug',
1134 'terms' => $post_id,
1135 ),
1136 )
1137 );
1138
1139 // Build the query arguments for the outbound links.
1140 $outbound_query_args = array_merge(
1141 $base_query_args,
1142 array(
1143 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1144 'tax_query' => array_merge(
1145 $base_query_args['tax_query'],
1146 array(
1147 array(
1148 'taxonomy' => 'smart_link_source',
1149 'field' => 'slug',
1150 'terms' => $post_id,
1151 ),
1152 )
1153 ),
1154 )
1155 );
1156
1157 // Get the inbound links.
1158 $inbound_links = new \WP_Query( $inbound_query_args );
1159
1160 // Get the outbound links.
1161 $outbound_links = new \WP_Query( $outbound_query_args );
1162
1163 $link_counts = array(
1164 'inbound' => $inbound_links->found_posts,
1165 'outbound' => $outbound_links->found_posts,
1166 );
1167
1168 wp_cache_set( $cache_key, $link_counts, $cache_group, WEEK_IN_SECONDS );
1169
1170 return $link_counts;
1171 }
1172
1173 /**
1174 * Flushes the cache for a single smart link.
1175 *
1176 * @since 3.19.0
1177 */
1178 protected function flush_cache(): void {
1179 // Delete the cache for the smart link UID to post ID association.
1180 $cache_key = self::get_uid_to_smart_link_cache_key( $this->uid );
1181 wp_cache_delete( $cache_key, PARSELY_CACHE_GROUP );
1182 }
1183
1184 /**
1185 * Flushes the cache for all smart links in a post.
1186 *
1187 * @since 3.19.0
1188 */
1189 public function flush_all_cache(): void {
1190 $this->flush_cache();
1191
1192 if ( $this->source_post_id > 0 ) {
1193 static::flush_cache_by_post_id( $this->source_post_id );
1194 }
1195
1196 if ( $this->destination_post_id > 0 ) {
1197 static::flush_cache_by_post_id( $this->destination_post_id );
1198 }
1199 }
1200
1201 /**
1202 * Generates a cache key for the smart link.
1203 *
1204 * @since 3.19.0
1205 *
1206 * @param string $uid The unique identifier for the cache key.
1207 * @return string The cache key.
1208 */
1209 protected static function get_uid_to_smart_link_cache_key( string $uid ): string {
1210 return sprintf( 'smart-link-uid-map-%s', $uid );
1211 }
1212
1213 /**
1214 * Gets the cache key for all smart links in a post.
1215 *
1216 * @since 3.19.0
1217 *
1218 * @param string $type The type of smart links ('outbound', 'inbound', or 'all').
1219 * @param string $status The status of the smart links ('all', 'pending', or 'applied').
1220 *
1221 * @return string The cache key.
1222 */
1223 protected static function get_smart_links_for_post_cache_key( string $type, string $status ): string {
1224 return sprintf( 'smart-links-post-id-map-%s-%s', $type, $status );
1225 }
1226
1227 /**
1228 * Gets the cache key for all counts of smart links in a post.
1229 *
1230 * @since 3.19.0
1231 *
1232 * @param string $status The status of the smart links ('all', 'pending', or 'applied').
1233 *
1234 * @return string The cache key.
1235 */
1236 protected static function get_smart_link_counts_cache_key( string $status ): string {
1237 return sprintf( 'smart-link-counts-%s', $status );
1238 }
1239
1240 /**
1241 * Generates a cache group for smart links on a post. Useful with wp_cache_flush_group()
1242 * to flush all smart link caches on a post.
1243 *
1244 * @since 3.19.0
1245 *
1246 * @param int $post_id The post ID.
1247 * @return string The cache group.
1248 */
1249 protected static function get_smart_links_post_cache_group( int $post_id ): string {
1250 return sprintf( '%s-smart-links-%d', PARSELY_CACHE_GROUP, $post_id );
1251 }
1252
1253 /**
1254 * Flushes the cache for all smart links associated with a given post.
1255 *
1256 * @since 3.19.0
1257 *
1258 * @param int $post_id The post ID to flush the cache for.
1259 */
1260 protected static function flush_cache_by_post_id( int $post_id ): void {
1261 $cache_group = self::get_smart_links_post_cache_group( $post_id );
1262
1263 if ( function_exists( 'wp_cache_flush_group' ) && wp_cache_supports( 'flush_group' ) ) {
1264 wp_cache_flush_group( $cache_group );
1265 } else {
1266 $statuses = Smart_Link_Status::get_all_statuses();
1267 $types = array( 'outbound', 'inbound', 'all' );
1268
1269 foreach ( $statuses as $status ) {
1270 // Delete smart link count cache.
1271 $smart_link_counts_cache_key = self::get_smart_link_counts_cache_key( $status );
1272 wp_cache_delete( $smart_link_counts_cache_key, $cache_group );
1273
1274 // Delete smart links caches.
1275 foreach ( $types as $type ) {
1276 $cache_key = self::get_smart_links_for_post_cache_key( $type, $status );
1277 wp_cache_delete( $cache_key, $cache_group );
1278 }
1279 }
1280 }
1281 }
1282 }
1283