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