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
← All changes | src/Models/class-smart-link.php +777 -112 3.18.0 → 3.24.2 View file →
@@ -11,10 +11,14 @@
11 11
12 12 namespace Parsely\Models;
13 13
14 14 use InvalidArgumentException;
15 +use Parsely\Parsely;
15 16 use Parsely\Utils\Utils;
17 +use WP_Post;
16 18
19 +use const Parsely\PARSELY_CACHE_GROUP;
20 +
17 21 /**
18 22 * Smart Link class.
19 23 *
20 24 * Represents a smart link suggestion returned by the Smart Linking API.
@@ -27,9 +31,9 @@
27 31 *
28 32 * @since 3.16.0
29 33 * @var int The ID of the smart link.
30 34 */
31 - private $smart_link_id = 0;
35 + protected $smart_link_id = 0;
32 36
33 37 /**
34 38 * The post ID of the suggested link (link source).
35 39 *
@@ -38,8 +42,27 @@
38 42 */
39 43 public $source_post_id = 0;
40 44
41 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 + /**
42 65 * The post ID of the link destination.
43 66 *
44 67 * @since 3.16.0
45 68 * @var int The post ID of the link destination, 0 if not set.
@@ -46,22 +69,30 @@
46 69 */
47 70 public $destination_post_id = 0;
48 71
49 72 /**
50 - * The post type of the suggested link.
73 + * The post type of the destination post.
51 74 *
52 75 * @since 3.16.0
53 - * @var string The post type of the suggested link.
76 + * @var string The post type of the destination post.
54 77 */
55 78 public $destination_post_type = 'external';
56 79
57 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 + /**
58 89 * The URL of the suggested link.
59 90 *
60 91 * @since 3.16.0
61 92 * @var string The URL of the suggested link.
62 93 */
63 - protected $href;
94 + protected $href = '';
64 95
65 96 /**
66 97 * The title of the suggested link.
67 98 *
@@ -94,14 +125,14 @@
94 125 */
95 126 public $uid;
96 127
97 128 /**
98 - * Whether the link has been applied.
129 + * The status of the smart link.
99 130 *
100 - * @since 3.16.0
101 - * @var bool Whether the link has been applied.
131 + * @since 3.19.0
132 + * @var string|null The status of the smart link.
102 133 */
103 - public $applied = false;
134 + protected $status = null;
104 135
105 136 /**
106 137 * Whether the smart link exists on the database.
107 138 *
@@ -110,8 +141,16 @@
110 141 */
111 142 private $exists = false;
112 143
113 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 + /**
114 153 * Smart Link constructor.
115 154 *
116 155 * @since 3.16.0
117 156 *
@@ -138,11 +177,11 @@
138 177 } else {
139 178 $this->title = $title;
140 179 }
141 180
142 - $this->text = $text;
143 - $this->offset = $offset;
144 - $this->source_post_id = $post_id;
181 + $this->text = $text;
182 + $this->offset = $offset;
183 + $this->set_source_post_id( $post_id );
145 184
146 185 parent::__construct();
147 186 }
148 187
@@ -154,11 +193,13 @@
154 193 * @param string $uid The UID of the smart link.
155 194 * @return int The ID of the smart link post object.
156 195 */
157 196 private function get_smart_link_object_by_uid( string $uid ): int {
158 - $cached = wp_cache_get( $uid . $this->source_post_id, 'wp_parsely_smart_link_id' );
159 - if ( is_int( $cached ) && 0 !== $cached ) {
160 - return $cached;
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;
161 202 }
162 203
163 204 $smart_links = new \WP_Query(
164 205 array(
@@ -170,9 +211,9 @@
170 211 'tax_query' => array(
171 212 array(
172 213 'taxonomy' => 'smart_link_source',
173 214 'include_children' => false, // Performance optimization.
174 - 'field' => 'name',
215 + 'field' => 'slug',
175 216 'terms' => (string) $this->source_post_id,
176 217 ),
177 218 ),
178 219 )
@@ -179,11 +220,12 @@
179 220 );
180 221
181 222 if ( $smart_links->have_posts() && is_int( $smart_links->posts[0] ) ) {
182 223 wp_cache_set(
183 - $uid . $this->source_post_id,
224 + $cache_key,
184 225 $smart_links->posts[0],
185 - 'wp_parsely_smart_link_id'
226 + PARSELY_CACHE_GROUP,
227 + WEEK_IN_SECONDS
186 228 );
187 229 return $smart_links->posts[0];
188 230 }
189 231
@@ -208,24 +250,32 @@
208 250 }
209 251
210 252 $smart_link = get_post( $this->smart_link_id );
211 253
212 - if ( null === $smart_link ) {
254 + if ( null === $smart_link || 'parsely_smart_link' !== $smart_link->post_type ) {
213 255 $this->exists = false;
214 256 return false;
215 257 }
216 258
217 - $this->exists = true;
218 - $this->applied = true;
259 + $this->exists = true;
219 260
220 261 $this->uid = $smart_link->post_title;
221 262
222 263 // Load the Smart Link properties from the post meta.
264 + $this->load_post_meta();
265 +
223 266 $this->title = $this->get_string_meta( '_smart_link_title' );
224 267 $this->href = $this->get_string_meta( '_smart_link_href' );
225 268 $this->text = $this->get_string_meta( '_smart_link_text' );
226 269 $this->offset = $this->get_int_meta( '_smart_link_offset' );
227 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 +
228 278 // Load the source post ID.
229 279 $source_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_source' );
230 280 if ( ! is_wp_error( $source_terms ) && count( $source_terms ) > 0 ) {
231 281 $source_term = $source_terms[0];
@@ -271,9 +321,30 @@
271 321 if ( 0 === $this->source_post_id ) {
272 322 return false;
273 323 }
274 324
275 - if ( ! $this->exists() ) {
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 ) {
276 347 // Create the post object.
277 348 $post_id = wp_insert_post(
278 349 array(
279 350 'post_type' => 'parsely_smart_link',
@@ -286,19 +357,17 @@
286 357 return false;
287 358 }
288 359
289 360 $this->smart_link_id = $post_id;
290 - wp_cache_set( $this->uid . $this->source_post_id, $post_id, 'wp_parsely_smart_link_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 + );
291 368 }
292 369
293 - // Update UID.
294 - wp_update_post(
295 - array(
296 - 'ID' => $this->smart_link_id,
297 - 'post_title' => $this->uid,
298 - )
299 - );
300 -
301 370 // Update the smart link meta.
302 371 $meta = array(
303 372 '_smart_link_title' => $this->title,
304 373 '_smart_link_href' => $this->href,
@@ -304,8 +373,13 @@
304 373 '_smart_link_href' => $this->href,
305 374 '_smart_link_text' => $this->text,
306 375 '_smart_link_offset' => $this->offset,
307 376 );
377 +
378 + if ( null !== $this->context ) {
379 + $meta['_smart_link_context'] = $this->context;
380 + }
381 +
308 382 foreach ( $meta as $key => $value ) {
309 383 update_post_meta( $this->smart_link_id, $key, $value );
310 384 }
311 385
@@ -318,11 +392,18 @@
318 392 } else {
319 393 wp_set_post_terms( $this->smart_link_id, 'external', 'smart_link_destination' );
320 394 }
321 395
322 - $this->applied = true;
323 - $this->exists = true;
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 + }
324 402
403 + // Flush all the associated cache on the source and destination posts.
404 + $this->flush_all_cache();
405 +
325 406 return true;
326 407 }
327 408
328 409 /**
@@ -327,9 +408,9 @@
327 408
328 409 /**
329 410 * Removes the smart link from the database.
330 411 *
331 - * @since 3.16.0
412 + * @since 3.19.0
332 413 *
333 414 * @return bool True if the smart link was removed successfully, false otherwise.
334 415 */
335 416 public function delete(): bool {
@@ -339,12 +420,14 @@
339 420
340 421 // Delete the post object.
341 422 $deleted = wp_delete_post( $this->smart_link_id, true );
342 423
343 - if ( false !== $deleted && null !== $deleted && is_a( $deleted, 'WP_Post' ) ) {
424 + if ( $deleted instanceof WP_Post ) {
344 425 $this->smart_link_id = 0;
345 426 $this->exists = false;
346 - wp_cache_delete( $this->uid . $this->source_post_id, 'wp_parsely_smart_link_id' );
427 + $this->status = null;
428 + $this->flush_all_cache();
429 +
347 430 return true;
348 431 }
349 432
350 433 return false;
@@ -376,8 +459,129 @@
376 459 return false;
377 460 }
378 461
379 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 + /**
380 584 * Gets a string meta value from the smart link post.
381 585 *
382 586 * @since 3.16.0
383 587 *
@@ -385,9 +589,13 @@
385 589 * @param string $default_value The default value to return if the meta value is not a string.
386 590 * @return string The meta value.
387 591 */
388 592 private function get_string_meta( string $meta_key, string $default_value = '' ): string {
389 - $meta_value = get_post_meta( $this->smart_link_id, $meta_key, true );
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];
390 598 return is_string( $meta_value ) ? $meta_value : $default_value;
391 599 }
392 600
393 601 /**
@@ -399,27 +607,122 @@
399 607 * @param int $default_value The default value to return if the meta value is not an integer.
400 608 * @return int The meta value.
401 609 */
402 610 private function get_int_meta( string $meta_key, int $default_value = 0 ): int {
403 - $meta_value = get_post_meta( $this->smart_link_id, $meta_key, true );
404 - return is_int( $meta_value ) ? $meta_value : $default_value;
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;
405 621 }
406 622
407 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 + /**
408 640 * Sets the source post ID.
409 641 *
410 642 * @since 3.16.0
411 643 *
412 - * @param int $source_post_id The source post ID.
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.
413 646 */
414 - public function set_source_post_id( int $source_post_id ): void {
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 +
415 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 + }
416 672 }
417 673
418 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 + /**
419 722 * Sets the UID of the smart link.
420 723 *
421 - * @since 3.16.0
724 + * @since 3.19.0
422 725 *
423 726 * @param string $uid The UID of the smart link.
424 727 */
425 728 public function set_uid( string $uid ): void {
@@ -426,25 +729,63 @@
426 729 $this->uid = $uid;
427 730 }
428 731
429 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 + /**
430 750 * Sets the href of the smart link.
431 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 + *
432 758 * @since 3.16.0
759 + * @since 3.24.2 Invalid hrefs are ignored, and no canonical URL is stored.
433 760 *
434 761 * @param string $href The href of the smart link.
435 762 */
436 763 public function set_href( string $href ): void {
437 - $this->href = $href;
438 - $this->destination_post_id = Utils::get_post_id_by_url( $href );
764 + if ( ! self::is_valid_href( $href ) ) {
765 + return;
766 + }
439 767
440 - if ( 0 !== $this->destination_post_id ) {
441 - $post_type = get_post_type( $this->destination_post_id );
442 - $this->destination_post_type = false !== $post_type ? $post_type : 'external';
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 );
443 773 }
444 774 }
445 775
446 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 + /**
447 788 * Generates a unique ID for the suggested link.
448 789 *
449 790 * It takes the href, title, text, and offset properties and concatenates
450 791 * them to create a unique ID. This ID is hashed to ensure it is unique.
@@ -453,9 +794,9 @@
453 794 *
454 795 * @return string The unique ID.
455 796 */
456 797 protected function generate_uid(): string {
457 - return md5( $this->href . $this->title . $this->text . $this->offset );
798 + return md5( $this->source_post_id . $this->destination_post_id . $this->href . $this->title . $this->text . $this->offset );
458 799 }
459 800
460 801 /**
461 802 * Serializes the model to a JSON string.
@@ -467,20 +808,27 @@
467 808 public function to_array(): array {
468 809 return array(
469 810 'smart_link_id' => $this->smart_link_id,
470 811 'uid' => $this->uid,
471 - 'href' => $this->href,
812 + 'href' => array(
813 + 'raw' => $this->href,
814 + 'itm' => $this->get_link_href(),
815 + ),
472 816 'title' => $this->title,
473 817 'text' => $this->text,
474 818 'offset' => $this->offset,
475 - 'applied' => $this->applied,
819 + 'context' => $this->context,
820 + 'status' => $this->status,
821 + 'applied' => $this->is_applied(),
476 822 'source' => array(
477 - 'post_type' => get_post_type( $this->source_post_id ),
478 - 'post_id' => $this->source_post_id,
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 ),
479 826 ),
480 827 'destination' => array(
481 - 'post_type' => $this->destination_post_type,
482 - 'post_id' => $this->destination_post_id,
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 ),
483 831 ),
484 832 );
485 833 }
486 834
@@ -502,9 +850,9 @@
502 850 throw new InvalidArgumentException( 'Invalid JSON data' );
503 851 }
504 852
505 853 // If the UID has been provided, set it on the model.
506 - $smart_link = new Smart_Link( $data['href'], $data['title'], $data['text'], $data['offset'] );
854 + $smart_link = new Smart_Link( $data['href']['raw'], $data['title'], $data['text'], $data['offset'] );
507 855
508 856 if ( isset( $data['uid'] ) ) {
509 857 $smart_link->set_uid( $data['uid'] );
510 858
@@ -510,9 +858,9 @@
510 858
511 859 if ( $smart_link->exists() ) {
512 860 $smart_link->load();
513 861 // Update the fields.
514 - $smart_link->set_href( $data['href'] );
862 + $smart_link->set_href( $data['href']['raw'] );
515 863 $smart_link->title = $data['title'];
516 864 $smart_link->text = $data['text'];
517 865 $smart_link->offset = $data['offset'];
518 866 }
@@ -543,55 +891,167 @@
543 891 *
544 892 * @since 3.16.0
545 893 *
546 894 * @param int $smart_link_id The ID of the smart link.
547 - * @return Smart_Link The smart link object.
895 + * @return Smart_Link|false The smart link object, or false if it does not exist.
548 896 */
549 - private static function get_smart_link_by_id( int $smart_link_id ): Smart_Link {
897 + public static function get_smart_link_by_id( int $smart_link_id ) {
550 898 $smart_link = new Smart_Link( '', '', '', 0 );
551 899 $smart_link->smart_link_id = $smart_link_id;
552 - $smart_link->load();
553 - return $smart_link;
900 + if ( $smart_link->load() ) {
901 + return $smart_link;
902 + }
903 +
904 + return false;
554 905 }
555 906
556 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 + /**
557 1032 * Gets the outbound smart links in a post.
558 1033 *
559 1034 * Outbound smart links are smart links that link to other posts.
560 1035 *
561 1036 * @since 3.16.0
1037 + * @since 3.19.0 Added status parameter.
562 1038 *
563 - * @param int $post_id The post ID to get the smart links for.
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.
564 1041 * @return array<Smart_Link> The smart links in the post.
565 1042 */
566 - public static function get_outbound_smart_links( int $post_id ): array {
567 - $smart_links = new \WP_Query(
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,
568 1049 array(
569 - 'post_type' => 'parsely_smart_link',
570 - 'posts_per_page' => -1,
571 - 'fields' => 'ids', // Only get the post IDs to improve performance.
572 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
573 - 'tax_query' => array(
574 - array(
575 - 'taxonomy' => 'smart_link_source',
576 - 'include_children' => false, // Performance optimization.
577 - 'field' => 'name',
578 - 'terms' => (string) $post_id,
579 - ),
580 - ),
1050 + 'orderby' => 'date',
1051 + 'order' => 'ASC',
581 1052 )
582 1053 );
583 -
584 - $links = array();
585 - foreach ( $smart_links->posts as $smart_link_id ) {
586 - if ( ! is_int( $smart_link_id ) ) {
587 - continue;
588 - }
589 - $smart_link = self::get_smart_link_by_id( $smart_link_id );
590 - $links[] = $smart_link;
591 - }
592 -
593 - return $links;
594 1054 }
595 1055
596 1056 /**
597 1057 * Gets the inbound smart links in a post.
@@ -598,47 +1058,252 @@
598 1058 *
599 1059 * Inbound smart links are links on other posts that link to the post.
600 1060 *
601 1061 * @since 3.16.0
1062 + * @since 3.19.0 Added status parameter.
602 1063 *
603 - * @param int $post_id The post ID to get the smart links for.
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.
604 1066 * @return array<Inbound_Smart_Link> The smart links in the post.
605 1067 */
606 - public static function get_inbound_smart_links( int $post_id ): array {
607 - $smart_links = new \WP_Query(
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,
608 1074 array(
609 - 'post_type' => 'parsely_smart_link',
610 - 'posts_per_page' => -1,
611 - 'fields' => 'ids', // Only get the post IDs to improve performance.
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(
612 1170 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
613 - 'tax_query' => array(
1171 + 'tax_query' => array_merge(
1172 + $base_query_args['tax_query'],
614 1173 array(
615 - 'taxonomy' => 'smart_link_destination',
616 - 'include_children' => false, // Performance optimization.
617 - 'field' => 'name',
618 - 'terms' => (string) $post_id,
619 - ),
1174 + array(
1175 + 'taxonomy' => 'smart_link_source',
1176 + 'field' => 'slug',
1177 + 'terms' => $post_id,
1178 + ),
1179 + )
620 1180 ),
621 1181 )
622 1182 );
623 1183
624 - $links = array();
625 - foreach ( $smart_links->posts as $smart_link_id ) {
626 - if ( ! is_int( $smart_link_id ) ) {
627 - continue;
628 - }
629 - $smart_link = self::get_smart_link_by_id( $smart_link_id );
630 - $smart_link = Inbound_Smart_Link::from_smart_link( $smart_link );
1184 + // Get the inbound links.
1185 + $inbound_links = new \WP_Query( $inbound_query_args );
631 1186
632 - // Check if this inbound smart link is still linked to a post.
633 - // If not, do not add it to the array, and instead remove it.
634 - if ( ! $smart_link->is_linked() ) {
635 - $smart_link->delete();
636 - continue;
637 - }
1187 + // Get the outbound links.
1188 + $outbound_links = new \WP_Query( $outbound_query_args );
638 1189
639 - $links[] = $smart_link;
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 );
640 1221 }
641 1222
642 - return $links;
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 + }
643 1308 }
644 1309 }