PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.2 3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 All 106 releases
wp-parsely / src / rest-api / content-helper / class-endpoint-traffic-boost.php

class-endpoint-traffic-boost.php in Parse.ly 3.24.2, at src/rest-api/content-helper/class-endpoint-traffic-boost.php

955 lines 25.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Endpoint: Traffic Boost
4 * Parse.ly Content Intelligence `/traffic-boost` API endpoint class
5 *
6 * @package Parsely
7 * @since 3.19.0
8 */
9
10 declare(strict_types=1);
11
12 namespace Parsely\REST_API\Content_Helper;
13
14 use Parsely\Models\Inbound_Smart_Link;
15 use Parsely\Models\Smart_Link_Status;
16 use Parsely\REST_API\Base_Endpoint;
17 use Parsely\REST_API\Use_Post_ID_Parameter_Trait;
18 use Parsely\Services\Suggestions_API\Suggestions_API_Service;
19 use WP_Error;
20 use WP_Post;
21 use WP_REST_Request;
22 use WP_REST_Response;
23
24 /**
25 * The Traffic Boost API.
26 *
27 * Provides an endpoint for getting traffic boost suggestions for a given post.
28 *
29 * @since 3.19.0
30 */
31 class Endpoint_Traffic_Boost extends Base_Endpoint {
32 use Content_Helper_Feature;
33 use Use_Post_ID_Parameter_Trait;
34
35 /**
36 * The Suggestions API service.
37 *
38 * @since 3.19.0
39 *
40 * @var Suggestions_API_Service $suggestions_api
41 */
42 protected $suggestions_api;
43
44 /**
45 * Initializes the class.
46 *
47 * @since 3.19.0
48 *
49 * @param Content_Helper_Controller $controller The Content Intelligence controller.
50 */
51 public function __construct( Content_Helper_Controller $controller ) {
52 parent::__construct( $controller );
53 $this->suggestions_api = $controller->get_parsely()->get_suggestions_api();
54 }
55
56 /**
57 * Returns the name of the endpoint.
58 *
59 * @since 3.19.0
60 *
61 * @return string The endpoint's name.
62 */
63 public static function get_endpoint_name(): string {
64 return 'traffic-boost';
65 }
66
67 /**
68 * Returns the name of the feature associated with the current endpoint.
69 *
70 * @since 3.19.0
71 *
72 * @return string The feature's name.
73 */
74 public function get_pch_feature_name(): string {
75 return 'traffic_boost';
76 }
77
78 /**
79 * Registers the routes for the endpoint.
80 *
81 * @since 3.19.0
82 */
83 public function register_routes(): void {
84 /**
85 * POST /traffic-boost/{post_id}/generate
86 * Gets traffic boost suggestions for a post.
87 */
88 $this->register_rest_route_with_post_id(
89 '/generate',
90 array( 'POST' ),
91 array( $this, 'generate_link_suggestions' ),
92 array(
93 'max_items' => array(
94 'type' => 'integer',
95 'description' => __( 'The maximum number of suggestions to return.', 'wp-parsely' ),
96 'default' => 10,
97 'required' => false,
98 ),
99 'save' => array(
100 'type' => 'boolean',
101 'description' => __( 'Whether to save the suggestions.', 'wp-parsely' ),
102 'default' => false,
103 'required' => false,
104 ),
105 'discard_previous' => array(
106 'type' => 'boolean',
107 'description' => __( 'Whether to discard the previous suggestions.', 'wp-parsely' ),
108 'default' => true,
109 'required' => false,
110 ),
111 'url_exclusion_list' => array(
112 'type' => 'array',
113 'description' => __( 'The URLs to exclude from the suggestions.', 'wp-parsely' ),
114 'required' => false,
115 'default' => array(),
116 'validate_callback' => array( Validations\Validate_Url_Exclusion_List::class, 'validate' ),
117 'sanitize_callback' => array( Validations\Validate_Url_Exclusion_List::class, 'sanitize' ),
118 ),
119 )
120 );
121
122 /**
123 * POST /traffic-boost/{post_id}/generate-placement/{source_post_id}.
124 * Suggests inbound link positions for a post.
125 */
126 $this->register_rest_route_with_post_id(
127 '/generate-placement/(?P<source_post_id>[0-9]+)',
128 array( 'POST' ),
129 array( $this, 'generate_placement_suggestions' ),
130 array(
131 'source_post_id' => array(
132 'type' => 'integer',
133 'description' => __( 'The ID of the source post.', 'wp-parsely' ),
134 'required' => true,
135 ),
136 'ignore_keywords' => array(
137 'type' => 'array',
138 'description' => __( 'The keywords to ignore.', 'wp-parsely' ),
139 'required' => false,
140 ),
141 'keyword_exclusion_list' => array(
142 'type' => 'array',
143 'description' => __( 'The keywords to exclude from the suggestions.', 'wp-parsely' ),
144 'required' => false,
145 'default' => array(),
146 ),
147 'allow_duplicate_links' => array(
148 'type' => 'boolean',
149 'description' => __( 'Whether to allow duplicate links.', 'wp-parsely' ),
150 'default' => false,
151 'required' => false,
152 ),
153 'save' => array(
154 'type' => 'boolean',
155 'description' => __( 'Whether to save the suggestion.', 'wp-parsely' ),
156 'default' => true,
157 'required' => false,
158 ),
159 )
160 );
161
162 /**
163 * GET /traffic-boost/{post_id}/get-suggestions.
164 * Gets the existing inbound smart links for a post.
165 */
166 $this->register_rest_route_with_post_id(
167 '/get-suggestions',
168 array( 'GET' ),
169 array( $this, 'get_existing_suggestions' )
170 );
171
172 /**
173 * GET /traffic-boost/{post_id}/get-inbound.
174 * Gets the inbound smart links for a post.
175 */
176 $this->register_rest_route_with_post_id(
177 '/get-inbound',
178 array( 'GET' ),
179 array( $this, 'get_inbound_smart_links' )
180 );
181
182 /**
183 * POST /traffic-boost/{post_id}/accept-suggestion/{smart_link_id}
184 * Accepts a specific suggestion for a post.
185 */
186 $this->register_rest_route_with_post_id(
187 '/accept-suggestion/(?P<smart_link_id>[0-9]+)',
188 array( 'POST' ),
189 array( $this, 'accept_suggestion' ),
190 array(
191 'smart_link_id' => array(
192 'type' => 'integer',
193 'description' => __( 'The ID of the Smart Link to accept.', 'wp-parsely' ),
194 'required' => true,
195 'validate_callback' => array( $this, 'validate_smart_link_id' ),
196 ),
197 'text' => array(
198 'type' => 'string',
199 'description' => __( 'The text of the Smart Link.', 'wp-parsely' ),
200 'required' => false,
201 ),
202 'offset' => array(
203 'type' => 'integer',
204 'description' => __( 'The offset of the Smart Link.', 'wp-parsely' ),
205 'required' => false,
206 ),
207 )
208 );
209
210 /**
211 * DELETE /traffic-boost/{post_id}/discard-suggestions.
212 * Discards all existing suggestions for a post.
213 */
214 $this->register_rest_route_with_post_id(
215 '/discard-suggestions',
216 array( 'DELETE' ),
217 array( $this, 'discard_suggestions' )
218 );
219
220 /**
221 * DELETE /traffic-boost/{post_id}/discard-suggestion/{smart_link_id}.
222 * Discards a specific suggestion for a post.
223 */
224 $this->register_rest_route_with_post_id(
225 '/discard-suggestion/(?P<smart_link_id>[0-9]+)',
226 array( 'DELETE' ),
227 array( $this, 'discard_suggestion' ),
228 array(
229 'smart_link_id' => array(
230 'type' => 'integer',
231 'description' => __( 'The ID of the Smart Link to discard.', 'wp-parsely' ),
232 'required' => true,
233 'validate_callback' => array( $this, 'validate_smart_link_id' ),
234 ),
235 )
236 );
237
238 /**
239 * DELETE /traffic-boost/{post_id}/delete-inbound/{smart_link_id}.
240 * Deletes an inbound smart link for a post.
241 */
242 $this->register_rest_route_with_post_id(
243 '/delete-inbound/(?P<smart_link_id>[0-9]+)',
244 array( 'DELETE' ),
245 array( $this, 'delete_inbound' ),
246 array(
247 'smart_link_id' => array(
248 'type' => 'integer',
249 'description' => __( 'The ID of the Smart Link to delete.', 'wp-parsely' ),
250 'required' => true,
251 'validate_callback' => array( $this, 'validate_smart_link_id' ),
252 ),
253 'restore_original' => array(
254 'type' => 'boolean',
255 'description' => __( 'Whether to restore the original link.', 'wp-parsely' ),
256 'default' => false,
257 'required' => false,
258 ),
259 )
260 );
261
262 /**
263 * POST /traffic-boost/{post_id}/update-inbound/{smart_link_id}.
264 * Updates an inbound smart link.
265 */
266 $this->register_rest_route_with_post_id(
267 '/update-inbound/(?P<smart_link_id>[0-9]+)',
268 array( 'POST' ),
269 array( $this, 'update_inbound' ),
270 array(
271 'smart_link_id' => array(
272 'type' => 'integer',
273 'description' => __( 'The ID of the Smart Link to update.', 'wp-parsely' ),
274 'required' => true,
275 'validate_callback' => array( $this, 'validate_smart_link_id' ),
276 ),
277 'text' => array(
278 'type' => 'string',
279 'description' => __( 'The text of the Smart Link.', 'wp-parsely' ),
280 'required' => false,
281 ),
282 'offset' => array(
283 'type' => 'integer',
284 'description' => __( 'The offset of the Smart Link.', 'wp-parsely' ),
285 'required' => false,
286 ),
287 'restore_original' => array(
288 'type' => 'boolean',
289 'description' => __( 'Whether to restore the original link.', 'wp-parsely' ),
290 'default' => false,
291 'required' => false,
292 ),
293 )
294 );
295 }
296
297 /**
298 * API Endpoint: POST /traffic-boost/{post_id}/generate.
299 *
300 * Gets traffic boost suggestions for a post.
301 *
302 * @since 3.19.0
303 *
304 * @param WP_REST_Request $request The request object.
305 * @return WP_REST_Response|WP_Error The response object.
306 */
307 public function generate_link_suggestions( WP_REST_Request $request ) {
308 $post = $this->get_request_post( $request );
309
310 if ( ! $post instanceof WP_Post ) {
311 return new WP_Error(
312 'invalid_post',
313 __( 'Invalid post.', 'wp-parsely' ),
314 array( 'status' => 404 )
315 );
316 }
317
318 /**
319 * The maximum number of suggestions to return.
320 *
321 * @var int $max_items
322 */
323 $max_items = (int) $request->get_param( 'max_items' );
324
325 /**
326 * Whether to save the suggestions.
327 *
328 * @var bool $save
329 */
330 $save = (bool) $request->get_param( 'save' );
331
332 /**
333 * Whether to discard the previous suggestions.
334 *
335 * @var bool $discard_previous
336 */
337 $discard_previous = (bool) $request->get_param( 'discard_previous' );
338
339 /**
340 * The URLs to exclude from the suggestions.
341 *
342 * @var array<string> $url_exclusion_list
343 */
344 $url_exclusion_list = $request->get_param( 'url_exclusion_list' );
345
346 $inbound_suggestions = $this->suggestions_api->get_inbound_links(
347 $post,
348 array(
349 'max_items' => $max_items,
350 'url_exclusion_list' => $url_exclusion_list,
351 )
352 );
353
354 if ( is_wp_error( $inbound_suggestions ) ) {
355 return $inbound_suggestions;
356 }
357
358 $inbound_suggestions = $this->filter_accessible_links(
359 $inbound_suggestions
360 );
361
362 $discard_result = null;
363
364 // If the discard_previous flag is set, discard the previous suggestions.
365 if ( $discard_previous ) {
366 $discard_result = Inbound_Smart_Link::delete_pending_suggestions( $post->ID );
367 }
368
369 $suggestions = array_map(
370 function ( Inbound_Smart_Link $link ) use ( $save ) {
371 $link->set_status( Smart_Link_Status::PENDING );
372
373 // Set the context to Traffic Boost.
374 $link->set_context( $this->get_pch_feature_name() );
375
376 // If the save flag is set, save the smart link.
377 if ( $save ) {
378 $link->save();
379 }
380
381 return $link->to_array();
382 },
383 $inbound_suggestions
384 );
385
386 $response = array(
387 'data' => $suggestions,
388 );
389
390 if ( null !== $discard_result ) {
391 $response['discarded'] = $discard_result;
392 }
393
394 return new WP_REST_Response( $response, 200 );
395 }
396
397 /**
398 * API Endpoint: POST /traffic-boost/{post_id}/generate-placement.
399 *
400 * Generates placement suggestions for a specific post.
401 *
402 * @since 3.19.0
403 *
404 * @param WP_REST_Request $request The request object.
405 * @return WP_REST_Response|WP_Error The response object.
406 */
407 public function generate_placement_suggestions( WP_REST_Request $request ) {
408 // The destination post, that the inbound link links to.
409 $destination_post = $this->get_request_post( $request );
410
411 if ( ! $destination_post instanceof WP_Post ) {
412 return new WP_Error(
413 'invalid_post',
414 __( 'Invalid post.', 'wp-parsely' ),
415 array( 'status' => 404 )
416 );
417 }
418
419 /**
420 * The source post ID, where the inbound link will be placed.
421 *
422 * @var int $source_post_id
423 */
424 $source_post_id = $request->get_param( 'source_post_id' );
425
426 /**
427 * The source post.
428 *
429 * @var WP_Post|null $source_post
430 */
431 $source_post = get_post( $source_post_id );
432
433 /**
434 * The keyword exclusion list.
435 *
436 * @var array<string> $keyword_exclusion_list
437 */
438 $keyword_exclusion_list = $request->get_param( 'keyword_exclusion_list' );
439
440 /**
441 * Whether to allow duplicate links.
442 *
443 * @var bool $allow_duplicate_links
444 */
445 $allow_duplicate_links = $request->get_param( 'allow_duplicate_links' );
446
447 /**
448 * Whether to save the suggestion.
449 *
450 * @var bool $save
451 */
452 $save = $request->get_param( 'save' );
453
454 if ( null === $source_post ) {
455 return new WP_Error(
456 'parsely_invalid_source_post',
457 __( 'Invalid source post.', 'wp-parsely' )
458 );
459 }
460
461 $source_post_access = $this->validate_source_post_access(
462 $source_post->ID
463 );
464 if ( is_wp_error( $source_post_access ) ) {
465 return $source_post_access;
466 }
467
468 $suggestions = $this->suggestions_api->get_inbound_link_positions(
469 $source_post,
470 $destination_post,
471 array(
472 'keyword_exclusion_list' => $keyword_exclusion_list,
473 )
474 );
475
476 if ( is_wp_error( $suggestions ) ) {
477 return $suggestions;
478 }
479
480 $valid_suggestion = null;
481 $errors = array();
482 // Try to find the first suggestion that has a valid placement.
483 foreach ( $suggestions as $suggestion ) {
484 // If the ignore keywords are set and the suggested text is in the ignore keywords, skip it.
485 if ( in_array( $suggestion->text, $keyword_exclusion_list, true ) ) {
486 continue;
487 }
488
489 /** @var WP_Error|bool $valid_placement Whether the suggestion has a valid placement. */
490 $valid_placement = $suggestion->has_valid_placement( true, $allow_duplicate_links );
491 if ( ! is_wp_error( $valid_placement ) ) {
492 $valid_suggestion = $suggestion;
493 break;
494 } else {
495 $errors[] = $valid_placement;
496 }
497 }
498
499 if ( null === $valid_suggestion ) {
500 return new WP_Error(
501 'parsely_no_valid_placement',
502 __( 'No valid placement found.', 'wp-parsely' ),
503 array(
504 'suggestions' => $suggestions,
505 'errors' => $errors,
506 )
507 );
508 }
509
510 // Set the context to Traffic Boost.
511 $valid_suggestion->set_context( $this->get_pch_feature_name() );
512
513 // Check if there's already a smart link with the same source and destination posts.
514 $existing_smart_link = Inbound_Smart_Link::get_smart_link_by_source_and_destination( $source_post_id, $destination_post->ID );
515
516 // If so, update the smart link with the new text and offset.
517 if ( false !== $existing_smart_link ) {
518 $existing_smart_link->text = $valid_suggestion->text;
519 $existing_smart_link->offset = $valid_suggestion->offset;
520
521 $valid_suggestion = $existing_smart_link;
522 }
523
524 // Save the suggestion to the database.
525 if ( $save ) {
526 $valid_suggestion->save();
527 }
528
529 return new WP_REST_Response( array( 'data' => $valid_suggestion->to_array() ), 200 );
530 }
531
532 /**
533 * API Endpoint: GET /traffic-boost/{post_id}/get-suggestions.
534 *
535 * Gets the existing inbound smart links for a post.
536 *
537 * @since 3.19.0
538 *
539 * @param WP_REST_Request $request The request object.
540 * @return WP_REST_Response The response object.
541 */
542 public function get_existing_suggestions( WP_REST_Request $request ) {
543 $post_id = $request->get_param( 'post_id' );
544
545 $suggestions = $this->filter_accessible_links(
546 Inbound_Smart_Link::get_existing_suggestions( $post_id )
547 );
548
549 // Convert the inbound smart links to an array.
550 $suggestions = array_map(
551 function ( Inbound_Smart_Link $link ) {
552 if ( ! (bool) $link->has_valid_placement() ) {
553 // Delete the link if it doesn't have a valid placement.
554 $link->delete();
555 return null;
556 }
557
558 return $link->to_array();
559 },
560 $suggestions
561 );
562
563 // Filter out null values.
564 $suggestions = array_values(
565 array_filter(
566 $suggestions,
567 function ( $suggestion ) {
568 return null !== $suggestion;
569 }
570 )
571 );
572
573 return new WP_REST_Response( array( 'data' => $suggestions ), 200 );
574 }
575
576 /**
577 * API Endpoint: GET /traffic-boost/{post_id}/get-inbound.
578 *
579 * Gets the inbound smart links for a post.
580 *
581 * @since 3.19.0
582 *
583 * @param WP_REST_Request $request The request object.
584 * @return WP_REST_Response The response object.
585 */
586 public function get_inbound_smart_links( WP_REST_Request $request ) {
587 $post_id = $request->get_param( 'post_id' );
588
589 // Get the inbound smart links for the post.
590 $inbound_links = $this->filter_accessible_links(
591 Inbound_Smart_Link::get_inbound_smart_links(
592 $post_id,
593 Smart_Link_Status::APPLIED
594 )
595 );
596
597 // Convert the inbound smart links to an array.
598 $inbound_links = array_map(
599 function ( Inbound_Smart_Link $link ) {
600 return $link->to_array();
601 },
602 $inbound_links
603 );
604
605 return new WP_REST_Response( array( 'data' => $inbound_links ), 200 );
606 }
607
608 /**
609 * API Endpoint: DELETE /traffic-boost/{post_id}/discard-suggestions.
610 *
611 * Discards all existing suggestions for a post.
612 *
613 * @since 3.19.0
614 *
615 * @param WP_REST_Request $request The request object.
616 * @return WP_REST_Response The response object.
617 */
618 public function discard_suggestions( WP_REST_Request $request ) {
619 $post_id = $request->get_param( 'post_id' );
620
621 $result = Inbound_Smart_Link::delete_pending_suggestions( $post_id );
622
623 return new WP_REST_Response( array( 'data' => $result ), 200 );
624 }
625
626 /**
627 * API Endpoint: DELETE /traffic-boost/{post_id}/discard-suggestion/{smart_link_id}.
628 *
629 * Discards a specific suggestion for a post.
630 *
631 * @since 3.19.0
632 *
633 * @param WP_REST_Request $request The request object.
634 * @return WP_REST_Response|WP_Error The response object.
635 */
636 public function discard_suggestion( WP_REST_Request $request ) {
637 /**
638 * The inbound smart link.
639 *
640 * @var Inbound_Smart_Link $inbound_link
641 */
642 $inbound_link = $request->get_param( 'inbound_link' );
643
644 $source_post_access = $this->validate_source_post_access(
645 $inbound_link->source_post_id
646 );
647 if ( is_wp_error( $source_post_access ) ) {
648 return $source_post_access;
649 }
650
651 $deleted = $inbound_link->delete();
652
653 return new WP_REST_Response( array( 'data' => array( 'success' => $deleted ) ), 200 );
654 }
655
656 /**
657 * API Endpoint: DELETE /traffic-boost/{post_id}/delete-inbound/{smart_link_id}.
658 *
659 * Deletes an inbound smart link for a post.
660 *
661 * @since 3.19.0
662 *
663 * @param WP_REST_Request $request The request object.
664 * @return WP_REST_Response|WP_Error The response object.
665 */
666 public function delete_inbound( WP_REST_Request $request ) {
667 /**
668 * The inbound smart link.
669 *
670 * @var Inbound_Smart_Link $inbound_link
671 */
672 $inbound_link = $request->get_param( 'inbound_link' );
673
674 /**
675 * Whether to restore the original link.
676 *
677 * @var bool $restore_original_link
678 */
679 $restore_original_link = $request->get_param( 'restore_original' );
680
681 $source_post_access = $this->validate_source_post_access(
682 $inbound_link->source_post_id
683 );
684 if ( is_wp_error( $source_post_access ) ) {
685 return $source_post_access;
686 }
687
688 $deleted = $inbound_link->remove( $restore_original_link );
689
690 if ( is_wp_error( $deleted ) ) {
691 return $deleted;
692 }
693
694 return new WP_REST_Response(
695 array(
696 'data' => array(
697 'success' => $deleted,
698 'restore_original' => $restore_original_link,
699 ),
700 ),
701 200
702 );
703 }
704
705 /**
706 * API Endpoint: POST /traffic-boost/{post_id}/update-inbound/{smart_link_id}.
707 *
708 * Updates an inbound smart link.
709 *
710 * @since 3.19.0
711 *
712 * @param WP_REST_Request $request The request object.
713 * @return WP_REST_Response|WP_Error The response object.
714 */
715 public function update_inbound( WP_REST_Request $request ) {
716 /**
717 * The inbound smart link.
718 *
719 * @var Inbound_Smart_Link $inbound_link
720 */
721 $inbound_link = $request->get_param( 'inbound_link' );
722
723 /**
724 * The text of the smart link.
725 *
726 * @var string $text
727 */
728 $text = $request->get_param( 'text' );
729
730 /**
731 * The offset of the smart link.
732 *
733 * @var int $offset
734 */
735 $offset = $request->get_param( 'offset' );
736
737 /**
738 * Whether to restore the original link.
739 *
740 * @var bool $restore_original_link
741 */
742 $restore_original_link = (bool) $request->get_param( 'restore_original' );
743
744 $source_post_access = $this->validate_source_post_access(
745 $inbound_link->source_post_id
746 );
747 if ( is_wp_error( $source_post_access ) ) {
748 return $source_post_access;
749 }
750
751 $updated = $inbound_link->update_link_text( $text, $offset, $restore_original_link );
752
753 $post = get_post( $inbound_link->source_post_id );
754 if ( null === $post ) {
755 return new WP_Error(
756 'parsely_post_not_found',
757 __( 'Source post not found.', 'wp-parsely' )
758 );
759 }
760
761 if ( is_wp_error( $updated ) ) {
762 return $updated;
763 }
764
765 return new WP_REST_Response(
766 array(
767 'data' => array(
768 'success' => $updated,
769 'smart_link' => $inbound_link->to_array(),
770 'restore_original' => $restore_original_link,
771 'did_replace_link' => $inbound_link->did_replace_link(),
772 'post_content' => $post->post_content,
773 ),
774 ),
775 200
776 );
777 }
778
779 /**
780 * API Endpoint: POST /traffic-boost/{post_id}/accept-suggestion/{smart_link_id}.
781 *
782 * Accepts a specific suggestion for a post.
783 *
784 * @since 3.19.0
785 *
786 * @param WP_REST_Request $request The request object.
787 * @return WP_REST_Response|WP_Error The response object.
788 */
789 public function accept_suggestion( WP_REST_Request $request ) {
790 /**
791 * The inbound smart link.
792 *
793 * @var Inbound_Smart_Link $inbound_link
794 */
795 $inbound_link = $request->get_param( 'inbound_link' );
796
797 /**
798 * The text of the smart link.
799 *
800 * @var string|null $text
801 */
802 $text = $request->get_param( 'text' );
803
804 /**
805 * The offset of the smart link.
806 *
807 * @var int|null $offset
808 */
809 $offset = $request->get_param( 'offset' );
810
811 // If the text is set and the offset is not, or the other way around, return an error.
812 if ( ( null !== $text && null === $offset ) || ( null !== $offset && null === $text ) ) {
813 return new WP_Error(
814 'parsely_invalid_smart_link_override',
815 __( 'If you provide a text, you must also provide an offset.', 'wp-parsely' )
816 );
817 } elseif ( null !== $text && null !== $offset ) {
818 $inbound_link->text = $text;
819 $inbound_link->offset = $offset;
820 }
821
822 if ( $inbound_link->is_applied() ) {
823 return new WP_Error(
824 'parsely_smart_link_already_applied',
825 __( 'Smart Link already applied.', 'wp-parsely' )
826 );
827 }
828
829 $source_post_access = $this->validate_source_post_access(
830 $inbound_link->source_post_id
831 );
832 if ( is_wp_error( $source_post_access ) ) {
833 return $source_post_access;
834 }
835
836 $applied = $inbound_link->apply();
837
838 if ( is_wp_error( $applied ) ) {
839 return $applied;
840 }
841
842 $post = get_post( $inbound_link->source_post_id );
843 if ( null === $post ) {
844 return new WP_Error(
845 'parsely_post_not_found',
846 __( 'Source post not found.', 'wp-parsely' )
847 );
848 }
849
850 // Clear the cache for the smart link.
851 $inbound_link->flush_all_cache();
852
853 return new WP_REST_Response(
854 array(
855 'data' => array(
856 'success' => $applied,
857 'did_replace_link' => $inbound_link->did_replace_link(),
858 'post_content' => $post->post_content,
859 ),
860 ),
861 200
862 );
863 }
864
865 /**
866 * Removes Smart Links whose source post the current user cannot use the
867 * feature on.
868 *
869 * Also stops the list endpoints from returning data about source posts the
870 * user has no access to.
871 *
872 * @since 3.23.6
873 *
874 * @param array<Inbound_Smart_Link> $links The Smart Links to filter.
875 * @return array<Inbound_Smart_Link> The accessible Smart Links.
876 */
877 private function filter_accessible_links( array $links ): array {
878 return array_values(
879 array_filter(
880 $links,
881 function ( Inbound_Smart_Link $link ): bool {
882 return $this->is_pch_feature_enabled_for_user(
883 $link->source_post_id
884 );
885 }
886 )
887 );
888 }
889
890 /**
891 * Validates that the current user can use the feature on the source post.
892 *
893 * The source post is the post that gets read and modified when a placement
894 * is generated or accepted. It is not always known at permission callback
895 * time, as it can be derived from a stored Smart Link.
896 *
897 * @since 3.23.6
898 *
899 * @param int $source_post_id The ID of the source post.
900 * @return bool|WP_Error True if access is allowed, WP_Error otherwise.
901 */
902 private function validate_source_post_access( int $source_post_id ) {
903 if ( $this->is_pch_feature_enabled_for_user( $source_post_id ) ) {
904 return true;
905 }
906
907 return new WP_Error(
908 'parsely_cannot_access_source_post',
909 __( 'You are not allowed to modify the source post.', 'wp-parsely' ),
910 array( 'status' => 403 )
911 );
912 }
913
914 /**
915 * Validates a smart link ID.
916 *
917 * @since 3.19.0
918 *
919 * @param int $smart_link_id The smart link ID.
920 * @param WP_REST_Request $request The request object.
921 * @return bool|WP_Error True if the smart link ID is valid, WP_Error on failure.
922 */
923 public function validate_smart_link_id( int $smart_link_id, WP_REST_Request $request ) {
924 /** @var Inbound_Smart_Link|false $inbound_link */
925 $inbound_link = Inbound_Smart_Link::get_smart_link_by_id( $smart_link_id );
926
927 if ( false === $inbound_link ) {
928 return new WP_Error(
929 'parsely_smart_link_not_found',
930 __( 'Smart Link not found', 'wp-parsely' )
931 );
932 }
933
934 // If the context is not set, set it to Traffic Boost.
935 if ( null === $inbound_link->get_context() ) {
936 $inbound_link->set_context( $this->get_pch_feature_name() );
937 }
938
939 // Set the inbound link in the request.
940 $request->set_param( 'inbound_link', $inbound_link );
941
942 // Validate if the smart link is associated with the post.
943 $post_id = intval( $request->get_param( 'post_id' ) );
944
945 if ( $inbound_link->destination_post_id !== $post_id ) {
946 return new WP_Error(
947 'parsely_invalid_smart_link',
948 __( 'Smart Link is not associated with this post', 'wp-parsely' )
949 );
950 }
951
952 return true;
953 }
954 }
955