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 +781 -131 3.16.0 → 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,14 +166,23 @@
126 166 string $text,
127 167 int $offset,
128 168 int $post_id = 0
129 169 ) {
130 - $this->set_href( $href );
131 - $this->title = $title;
132 - $this->text = $text;
133 - $this->offset = $offset;
134 - $this->source_post_id = $post_id;
170 + if ( '' !== $href ) {
171 + $this->set_href( $href );
172 + }
135 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 +
136 185 parent::__construct();
137 186 }
138 187
139 188 /**
@@ -144,11 +193,13 @@
144 193 * @param string $uid The UID of the smart link.
145 194 * @return int The ID of the smart link post object.
146 195 */
147 196 private function get_smart_link_object_by_uid( string $uid ): int {
148 - $cached = wp_cache_get( $uid . $this->source_post_id, 'wp_parsely_smart_link_id' );
149 - if ( is_int( $cached ) && 0 !== $cached ) {
150 - 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;
151 202 }
152 203
153 204 $smart_links = new \WP_Query(
154 205 array(
@@ -160,9 +211,9 @@
160 211 'tax_query' => array(
161 212 array(
162 213 'taxonomy' => 'smart_link_source',
163 214 'include_children' => false, // Performance optimization.
164 - 'field' => 'name',
215 + 'field' => 'slug',
165 216 'terms' => (string) $this->source_post_id,
166 217 ),
167 218 ),
168 219 )
@@ -169,11 +220,12 @@
169 220 );
170 221
171 222 if ( $smart_links->have_posts() && is_int( $smart_links->posts[0] ) ) {
172 223 wp_cache_set(
173 - $uid . $this->source_post_id,
224 + $cache_key,
174 225 $smart_links->posts[0],
175 - 'wp_parsely_smart_link_id'
226 + PARSELY_CACHE_GROUP,
227 + WEEK_IN_SECONDS
176 228 );
177 229 return $smart_links->posts[0];
178 230 }
179 231
@@ -198,24 +250,32 @@
198 250 }
199 251
200 252 $smart_link = get_post( $this->smart_link_id );
201 253
202 - if ( null === $smart_link ) {
254 + if ( null === $smart_link || 'parsely_smart_link' !== $smart_link->post_type ) {
203 255 $this->exists = false;
204 256 return false;
205 257 }
206 258
207 - $this->exists = true;
208 - $this->applied = true;
259 + $this->exists = true;
209 260
210 261 $this->uid = $smart_link->post_title;
211 262
212 263 // Load the Smart Link properties from the post meta.
264 + $this->load_post_meta();
265 +
213 266 $this->title = $this->get_string_meta( '_smart_link_title' );
214 267 $this->href = $this->get_string_meta( '_smart_link_href' );
215 268 $this->text = $this->get_string_meta( '_smart_link_text' );
216 269 $this->offset = $this->get_int_meta( '_smart_link_offset' );
217 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 +
218 278 // Load the source post ID.
219 279 $source_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_source' );
220 280 if ( ! is_wp_error( $source_terms ) && count( $source_terms ) > 0 ) {
221 281 $source_term = $source_terms[0];
@@ -232,9 +292,9 @@
232 292 }
233 293
234 294 // If the destination post ID is not set, try to get it from the URL.
235 295 if ( 0 === $this->destination_post_id ) {
236 - $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 );
237 297 }
238 298
239 299 // Get the post type of the destination post.
240 300 $post_type = get_post_type( $this->destination_post_id );
@@ -261,9 +321,30 @@
261 321 if ( 0 === $this->source_post_id ) {
262 322 return false;
263 323 }
264 324
265 - 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 ) {
266 347 // Create the post object.
267 348 $post_id = wp_insert_post(
268 349 array(
269 350 'post_type' => 'parsely_smart_link',
@@ -276,19 +357,17 @@
276 357 return false;
277 358 }
278 359
279 360 $this->smart_link_id = $post_id;
280 - 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 + );
281 368 }
282 369
283 - // Update UID.
284 - wp_update_post(
285 - array(
286 - 'ID' => $this->smart_link_id,
287 - 'post_title' => $this->uid,
288 - )
289 - );
290 -
291 370 // Update the smart link meta.
292 371 $meta = array(
293 372 '_smart_link_title' => $this->title,
294 373 '_smart_link_href' => $this->href,
@@ -294,8 +373,13 @@
294 373 '_smart_link_href' => $this->href,
295 374 '_smart_link_text' => $this->text,
296 375 '_smart_link_offset' => $this->offset,
297 376 );
377 +
378 + if ( null !== $this->context ) {
379 + $meta['_smart_link_context'] = $this->context;
380 + }
381 +
298 382 foreach ( $meta as $key => $value ) {
299 383 update_post_meta( $this->smart_link_id, $key, $value );
300 384 }
301 385
@@ -308,11 +392,18 @@
308 392 } else {
309 393 wp_set_post_terms( $this->smart_link_id, 'external', 'smart_link_destination' );
310 394 }
311 395
312 - $this->applied = true;
313 - $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 + }
314 402
403 + // Flush all the associated cache on the source and destination posts.
404 + $this->flush_all_cache();
405 +
315 406 return true;
316 407 }
317 408
318 409 /**
@@ -317,9 +408,9 @@
317 408
318 409 /**
319 410 * Removes the smart link from the database.
320 411 *
321 - * @since 3.16.0
412 + * @since 3.19.0
322 413 *
323 414 * @return bool True if the smart link was removed successfully, false otherwise.
324 415 */
325 416 public function delete(): bool {
@@ -329,12 +420,14 @@
329 420
330 421 // Delete the post object.
331 422 $deleted = wp_delete_post( $this->smart_link_id, true );
332 423
333 - if ( false !== $deleted && null !== $deleted && is_a( $deleted, 'WP_Post' ) ) {
424 + if ( $deleted instanceof WP_Post ) {
334 425 $this->smart_link_id = 0;
335 426 $this->exists = false;
336 - wp_cache_delete( $this->uid . $this->source_post_id, 'wp_parsely_smart_link_id' );
427 + $this->status = null;
428 + $this->flush_all_cache();
429 +
337 430 return true;
338 431 }
339 432
340 433 return false;
@@ -366,8 +459,129 @@
366 459 return false;
367 460 }
368 461
369 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 + /**
370 584 * Gets a string meta value from the smart link post.
371 585 *
372 586 * @since 3.16.0
373 587 *
@@ -375,9 +589,13 @@
375 589 * @param string $default_value The default value to return if the meta value is not a string.
376 590 * @return string The meta value.
377 591 */
378 592 private function get_string_meta( string $meta_key, string $default_value = '' ): string {
379 - $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];
380 598 return is_string( $meta_value ) ? $meta_value : $default_value;
381 599 }
382 600
383 601 /**
@@ -389,52 +607,122 @@
389 607 * @param int $default_value The default value to return if the meta value is not an integer.
390 608 * @return int The meta value.
391 609 */
392 610 private function get_int_meta( string $meta_key, int $default_value = 0 ): int {
393 - $meta_value = get_post_meta( $this->smart_link_id, $meta_key, true );
394 - 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;
395 621 }
396 622
397 623 /**
398 - * Gets the post ID by URL.
624 + * Sets the source post from a post object.
399 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 + *
400 642 * @since 3.16.0
401 643 *
402 - * @param string $url The URL to get the post ID for.
403 - * @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.
404 646 */
405 - private function get_post_id_by_url( string $url ): int {
406 - $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' );
407 - if ( is_integer( $cache ) ) {
408 - 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;
409 650 }
410 651
411 - if ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
412 - $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 + }
413 664 } else {
414 - // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
415 - $post_id = url_to_postid( $url );
416 - wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' );
665 + $this->source_post_type = 'unknown';
417 666 }
418 667
419 - 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 + }
420 672 }
421 673
422 674 /**
423 - * Sets the source post ID.
675 + * Sets the destination post.
424 676 *
425 - * @since 3.16.0
677 + * @since 3.19.0
426 678 *
427 - * @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.
428 681 */
429 - public function set_source_post_id( int $source_post_id ): void {
430 - $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 + }
431 701 }
432 702
433 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 + /**
434 722 * Sets the UID of the smart link.
435 723 *
436 - * @since 3.16.0
724 + * @since 3.19.0
437 725 *
438 726 * @param string $uid The UID of the smart link.
439 727 */
440 728 public function set_uid( string $uid ): void {
@@ -441,25 +729,63 @@
441 729 $this->uid = $uid;
442 730 }
443 731
444 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 + /**
445 750 * Sets the href of the smart link.
446 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 + *
447 758 * @since 3.16.0
759 + * @since 3.24.2 Invalid hrefs are ignored, and no canonical URL is stored.
448 760 *
449 761 * @param string $href The href of the smart link.
450 762 */
451 763 public function set_href( string $href ): void {
452 - $this->href = $href;
453 - $this->destination_post_id = $this->get_post_id_by_url( $href );
764 + if ( ! self::is_valid_href( $href ) ) {
765 + return;
766 + }
454 767
455 - if ( 0 !== $this->destination_post_id ) {
456 - $post_type = get_post_type( $this->destination_post_id );
457 - $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 );
458 773 }
459 774 }
460 775
461 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 + /**
462 788 * Generates a unique ID for the suggested link.
463 789 *
464 790 * It takes the href, title, text, and offset properties and concatenates
465 791 * them to create a unique ID. This ID is hashed to ensure it is unique.
@@ -468,9 +794,9 @@
468 794 *
469 795 * @return string The unique ID.
470 796 */
471 797 protected function generate_uid(): string {
472 - 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 );
473 799 }
474 800
475 801 /**
476 802 * Serializes the model to a JSON string.
@@ -482,20 +808,27 @@
482 808 public function to_array(): array {
483 809 return array(
484 810 'smart_link_id' => $this->smart_link_id,
485 811 'uid' => $this->uid,
486 - 'href' => $this->href,
812 + 'href' => array(
813 + 'raw' => $this->href,
814 + 'itm' => $this->get_link_href(),
815 + ),
487 816 'title' => $this->title,
488 817 'text' => $this->text,
489 818 'offset' => $this->offset,
490 - 'applied' => $this->applied,
819 + 'context' => $this->context,
820 + 'status' => $this->status,
821 + 'applied' => $this->is_applied(),
491 822 'source' => array(
492 - 'post_type' => get_post_type( $this->source_post_id ),
493 - '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 ),
494 826 ),
495 827 'destination' => array(
496 - 'post_type' => $this->destination_post_type,
497 - '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 ),
498 831 ),
499 832 );
500 833 }
501 834
@@ -517,9 +850,9 @@
517 850 throw new InvalidArgumentException( 'Invalid JSON data' );
518 851 }
519 852
520 853 // If the UID has been provided, set it on the model.
521 - $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'] );
522 855
523 856 if ( isset( $data['uid'] ) ) {
524 857 $smart_link->set_uid( $data['uid'] );
525 858
@@ -525,9 +858,9 @@
525 858
526 859 if ( $smart_link->exists() ) {
527 860 $smart_link->load();
528 861 // Update the fields.
529 - $smart_link->set_href( $data['href'] );
862 + $smart_link->set_href( $data['href']['raw'] );
530 863 $smart_link->title = $data['title'];
531 864 $smart_link->text = $data['text'];
532 865 $smart_link->offset = $data['offset'];
533 866 }
@@ -558,55 +891,167 @@
558 891 *
559 892 * @since 3.16.0
560 893 *
561 894 * @param int $smart_link_id The ID of the smart link.
562 - * @return Smart_Link The smart link object.
895 + * @return Smart_Link|false The smart link object, or false if it does not exist.
563 896 */
564 - 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 ) {
565 898 $smart_link = new Smart_Link( '', '', '', 0 );
566 899 $smart_link->smart_link_id = $smart_link_id;
567 - $smart_link->load();
568 - return $smart_link;
900 + if ( $smart_link->load() ) {
901 + return $smart_link;
902 + }
903 +
904 + return false;
569 905 }
570 906
571 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 + /**
572 1032 * Gets the outbound smart links in a post.
573 1033 *
574 1034 * Outbound smart links are smart links that link to other posts.
575 1035 *
576 1036 * @since 3.16.0
1037 + * @since 3.19.0 Added status parameter.
577 1038 *
578 - * @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.
579 1041 * @return array<Smart_Link> The smart links in the post.
580 1042 */
581 - public static function get_outbound_smart_links( int $post_id ): array {
582 - $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,
583 1049 array(
584 - 'post_type' => 'parsely_smart_link',
585 - 'posts_per_page' => -1,
586 - 'fields' => 'ids', // Only get the post IDs to improve performance.
587 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
588 - 'tax_query' => array(
589 - array(
590 - 'taxonomy' => 'smart_link_source',
591 - 'include_children' => false, // Performance optimization.
592 - 'field' => 'name',
593 - 'terms' => (string) $post_id,
594 - ),
595 - ),
1050 + 'orderby' => 'date',
1051 + 'order' => 'ASC',
596 1052 )
597 1053 );
598 -
599 - $links = array();
600 - foreach ( $smart_links->posts as $smart_link_id ) {
601 - if ( ! is_int( $smart_link_id ) ) {
602 - continue;
603 - }
604 - $smart_link = self::get_smart_link_by_id( $smart_link_id );
605 - $links[] = $smart_link;
606 - }
607 -
608 - return $links;
609 1054 }
610 1055
611 1056 /**
612 1057 * Gets the inbound smart links in a post.
@@ -613,47 +1058,252 @@
613 1058 *
614 1059 * Inbound smart links are links on other posts that link to the post.
615 1060 *
616 1061 * @since 3.16.0
1062 + * @since 3.19.0 Added status parameter.
617 1063 *
618 - * @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.
619 1066 * @return array<Inbound_Smart_Link> The smart links in the post.
620 1067 */
621 - public static function get_inbound_smart_links( int $post_id ): array {
622 - $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,
623 1074 array(
624 - 'post_type' => 'parsely_smart_link',
625 - 'posts_per_page' => -1,
626 - '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(
627 1170 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
628 - 'tax_query' => array(
1171 + 'tax_query' => array_merge(
1172 + $base_query_args['tax_query'],
629 1173 array(
630 - 'taxonomy' => 'smart_link_destination',
631 - 'include_children' => false, // Performance optimization.
632 - 'field' => 'name',
633 - 'terms' => (string) $post_id,
634 - ),
1174 + array(
1175 + 'taxonomy' => 'smart_link_source',
1176 + 'field' => 'slug',
1177 + 'terms' => $post_id,
1178 + ),
1179 + )
635 1180 ),
636 1181 )
637 1182 );
638 1183
639 - $links = array();
640 - foreach ( $smart_links->posts as $smart_link_id ) {
641 - if ( ! is_int( $smart_link_id ) ) {
642 - continue;
643 - }
644 - $smart_link = self::get_smart_link_by_id( $smart_link_id );
645 - $smart_link = Inbound_Smart_Link::from_smart_link( $smart_link );
1184 + // Get the inbound links.
1185 + $inbound_links = new \WP_Query( $inbound_query_args );
646 1186
647 - // Check if this inbound smart link is still linked to a post.
648 - // If not, do not add it to the array, and instead remove it.
649 - if ( ! $smart_link->is_linked() ) {
650 - $smart_link->delete();
651 - continue;
652 - }
1187 + // Get the outbound links.
1188 + $outbound_links = new \WP_Query( $outbound_query_args );
653 1189
654 - $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 );
655 1221 }
656 1222
657 - 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 + }
658 1308 }
659 1309 }