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
wp-parsely / src / rest-api / content-helper / class-endpoint-smart-linking.php

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

740 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Endpoint: Smart Linking
4 * Parse.ly Content Intelligence `/smart-linking` API endpoint class
5 *
6 * @package Parsely
7 * @since 3.17.0
8 */
9
10 declare(strict_types=1);
11
12 namespace Parsely\REST_API\Content_Helper;
13
14 use Parsely\Models\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 Parsely\Utils\Utils;
20 use WP_Error;
21 use WP_Post;
22 use WP_REST_Request;
23 use WP_REST_Response;
24
25 /**
26 * The Smart Linking API.
27 *
28 * Provides an endpoint for generating smart links for the given content.
29 *
30 * @since 3.17.0
31 */
32 class Endpoint_Smart_Linking extends Base_Endpoint {
33 use Content_Helper_Feature;
34 use Use_Post_ID_Parameter_Trait;
35
36 /**
37 * The Suggestions API service.
38 *
39 * @since 3.17.0
40 *
41 * @var Suggestions_API_Service $suggestions_api
42 */
43 protected $suggestions_api;
44
45 /**
46 * Initializes the class.
47 *
48 * @since 3.17.0
49 *
50 * @param Content_Helper_Controller $controller The Content Intelligence controller.
51 */
52 public function __construct( Content_Helper_Controller $controller ) {
53 parent::__construct( $controller );
54 $this->suggestions_api = $controller->get_parsely()->get_suggestions_api();
55 }
56
57 /**
58 * Returns the name of the endpoint.
59 *
60 * @since 3.17.0
61 *
62 * @return string The endpoint's name.
63 */
64 public static function get_endpoint_name(): string {
65 return 'smart-linking';
66 }
67
68 /**
69 * Returns the name of the feature associated with the current endpoint.
70 *
71 * @since 3.17.0
72 *
73 * @return string The feature's name.
74 */
75 public function get_pch_feature_name(): string {
76 return 'smart_linking';
77 }
78
79 /**
80 * Registers the routes for the endpoint.
81 *
82 * @since 3.17.0
83 */
84 public function register_routes(): void {
85 /**
86 * GET /smart-linking/generate
87 * Generates smart links for a post.
88 */
89 $this->register_rest_route(
90 'generate',
91 array( 'POST' ),
92 array( $this, 'generate_smart_links' ),
93 array(
94 'text' => array(
95 'required' => true,
96 'type' => 'string',
97 'description' => __( 'The text to generate Smart Links for.', 'wp-parsely' ),
98 ),
99 'max_links' => array(
100 'type' => 'integer',
101 'description' => __( 'The maximum number of Smart Links to generate.', 'wp-parsely' ),
102 'default' => 10,
103 ),
104 'url_exclusion_list' => array(
105 'type' => 'array',
106 'description' => __( 'The list of URLs to exclude from the Smart Links.', 'wp-parsely' ),
107 'validate_callback' => array( Validations\Validate_Url_Exclusion_List::class, 'validate' ),
108 'sanitize_callback' => array( Validations\Validate_Url_Exclusion_List::class, 'sanitize' ),
109 'default' => array(),
110 ),
111 )
112 );
113
114 /**
115 * GET /smart-linking/{post_id}/get
116 * Gets the smart links for a post.
117 */
118 $this->register_rest_route_with_post_id(
119 '/get',
120 array( 'GET' ),
121 array( $this, 'get_smart_links' )
122 );
123
124 /**
125 * POST /smart-linking/{post_id}/add
126 * Adds a smart link to a post.
127 */
128 $this->register_rest_route_with_post_id(
129 '/add',
130 array( 'POST' ),
131 array( $this, 'add_smart_link' ),
132 array(
133 'link' => array(
134 'required' => true,
135 'type' => 'object',
136 'description' => __( 'The Smart Link data to add.', 'wp-parsely' ),
137 'validate_callback' => array( $this, 'validate_smart_link_params' ),
138 ),
139 'update' => array(
140 'type' => 'boolean',
141 'description' => __( 'Whether to update the existing Smart Link.', 'wp-parsely' ),
142 'default' => false,
143 ),
144 )
145 );
146
147 /**
148 * POST /smart-linking/{post_id}/add-multiple
149 * Adds multiple smart links to a post.
150 */
151 $this->register_rest_route_with_post_id(
152 '/add-multiple',
153 array( 'POST' ),
154 array( $this, 'add_multiple_smart_links' ),
155 array(
156 'links' => array(
157 'required' => true,
158 'type' => 'array',
159 'description' => __( 'The multiple Smart Links data to add.', 'wp-parsely' ),
160 'validate_callback' => array( $this, 'validate_multiple_smart_links' ),
161 ),
162 'update' => array(
163 'type' => 'boolean',
164 'description' => __( 'Whether to update the existing Smart Links.', 'wp-parsely' ),
165 'default' => false,
166 ),
167 )
168 );
169
170 /**
171 * POST /smart-linking/{post_id}/set
172 * Updates the smart links of a given post and removes the ones that are not in the request.
173 */
174 $this->register_rest_route_with_post_id(
175 '/set',
176 array( 'POST' ),
177 array( $this, 'set_smart_links' ),
178 array(
179 'links' => array(
180 'required' => true,
181 'type' => 'array',
182 'description' => __( 'The Smart Links data to set.', 'wp-parsely' ),
183 'validate_callback' => array( $this, 'validate_multiple_smart_links' ),
184 ),
185 )
186 );
187
188 /**
189 * POST /smart-linking/get-post-meta-for-urls
190 * Gets the post meta information for the passed URLs.
191 */
192 $this->register_rest_route(
193 'get-post-meta-for-urls',
194 array( 'POST' ),
195 array( $this, 'get_post_meta_for_urls' ),
196 array(
197 'urls' => array(
198 'required' => true,
199 'type' => 'array',
200 'items' => array( 'type' => 'string' ),
201 'maxItems' => 500,
202 'description' => __( 'The URLs to get meta information for.', 'wp-parsely' ),
203 ),
204 )
205 );
206 }
207
208 /**
209 * API Endpoint: GET /smart-linking/generate.
210 *
211 * Generates smart links for a post.
212 *
213 * @since 3.16.0
214 *
215 * @param WP_REST_Request $request The request object.
216 * @return WP_REST_Response|WP_Error The response object.
217 */
218 public function generate_smart_links( WP_REST_Request $request ) {
219 /**
220 * The text to generate smart links for.
221 *
222 * @var string $post_content
223 */
224 $post_content = $request->get_param( 'text' );
225
226 /**
227 * The maximum number of smart links to generate.
228 *
229 * @var int $max_links
230 */
231 $max_links = $request->get_param( 'max_links' );
232
233 /**
234 * The URL exclusion list.
235 *
236 * @var array<string> $url_exclusion_list
237 */
238 $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array();
239
240 $response = $this->suggestions_api->get_smart_links(
241 $post_content,
242 array(
243 'max_items' => $max_links,
244 'url_exclusion_list' => $url_exclusion_list,
245 )
246 );
247
248 if ( is_wp_error( $response ) ) {
249 return $response;
250 }
251
252 $smart_links = array_map(
253 function ( Smart_Link $link ) {
254 // Set the context to Smart Linking.
255 $link->set_context( $this->get_pch_feature_name() );
256
257 return $link->to_array();
258 },
259 $response
260 );
261
262 return new WP_REST_Response( array( 'data' => $smart_links ), 200 );
263 }
264
265 /**
266 * API Endpoint: GET /smart-linking/{post_id}/get.
267 *
268 * Gets the smart links for a post.
269 *
270 * @since 3.16.0
271 * @since 3.24.2 Inbound links of non-visible source posts are omitted.
272 *
273 * @param WP_REST_Request $request The request object.
274 * @return WP_REST_Response|WP_Error The response object.
275 */
276 public function get_smart_links( WP_REST_Request $request ) {
277 $post = $this->get_request_post( $request );
278
279 if ( ! $post instanceof WP_Post ) {
280 return new WP_Error(
281 'invalid_post',
282 __( 'Invalid post.', 'wp-parsely' ),
283 array( 'status' => 404 )
284 );
285 }
286
287 $outbound_links = Smart_Link::get_outbound_smart_links( $post->ID, Smart_Link_Status::APPLIED );
288 $inbound_links = Smart_Link::get_inbound_smart_links( $post->ID, Smart_Link_Status::APPLIED );
289
290 $response = array(
291 'outbound' => $this->serialize_smart_links( $outbound_links ),
292 'inbound' => $this->serialize_visible_inbound_links( $inbound_links ),
293 );
294
295 return new WP_REST_Response( array( 'data' => $response ), 200 );
296 }
297
298 /**
299 * API Endpoint: POST /smart-linking/{post_id}/add.
300 *
301 * Adds a smart link to a post.
302 * If the update parameter is set to true, the existing smart link will be updated.
303 *
304 * @since 3.16.0
305 *
306 * @param WP_REST_Request $request The request object.
307 * @return WP_REST_Response The response object.
308 */
309 public function add_smart_link( WP_REST_Request $request ): WP_REST_Response {
310 /**
311 * The Smart Link model.
312 *
313 * @var Smart_Link $smart_link
314 */
315 $smart_link = $request->get_param( 'smart_link' );
316 $should_update = $request->get_param( 'update' ) === true;
317
318 if ( $smart_link->exists() && ! $should_update ) {
319 return new WP_REST_Response(
320 array(
321 'error' => array(
322 'name' => 'smart_link_exists',
323 'message' => __( 'Smart Link already exists.', 'wp-parsely' ),
324 ),
325 ),
326 409 // HTTP Conflict.
327 );
328 }
329
330 // Mark as applied.
331 $smart_link->set_status( Smart_Link_Status::APPLIED );
332
333 // If the context is not set, set it to Smart Linking.
334 if ( null === $smart_link->get_context() ) {
335 $smart_link->set_context( $this->get_pch_feature_name() );
336 }
337
338 // The smart link properties are set in the validate callback.
339 $saved = $smart_link->save();
340 if ( ! $saved ) {
341 return new WP_REST_Response(
342 array(
343 'error' => array(
344 'name' => 'add_smart_link_failed',
345 'message' => __( 'Failed to add the Smart Link.', 'wp-parsely' ),
346 ),
347 ),
348 500
349 );
350 }
351
352 // Clear the cache for the smart link.
353 $smart_link->flush_all_cache();
354
355 return new WP_REST_Response(
356 array(
357 'data' => json_decode( $smart_link->serialize() ),
358 ),
359 200
360 );
361 }
362
363 /**
364 * API Endpoint: POST /smart-linking/{post_id}/add_multiple.
365 *
366 * Adds multiple smart links to a post.
367 *
368 * @since 3.16.0
369 *
370 * @param WP_REST_Request $request The request object.
371 * @return WP_REST_Response The response object.
372 */
373 public function add_multiple_smart_links( WP_REST_Request $request ): WP_REST_Response {
374 /**
375 * Array of Smart Link models.
376 *
377 * @var Smart_Link[] $smart_links
378 */
379 $smart_links = $request->get_param( 'smart_links' );
380 $should_update = $request->get_param( 'update' ) === true;
381
382 $added_links = array();
383 $updated_links = array();
384 $failed_links = array();
385
386 foreach ( $smart_links as $smart_link ) {
387 if ( $smart_link->exists() && ! $should_update ) {
388 $failed_links[] = $smart_link;
389 continue;
390 }
391
392 $updated_link = $smart_link->exists() && $should_update;
393
394 // Mark as applied.
395 $smart_link->set_status( Smart_Link_Status::APPLIED );
396
397 // The smart link properties are set in the validate callback.
398 $saved = $smart_link->save();
399
400 if ( ! $saved ) {
401 $failed_links[] = $smart_link;
402 continue;
403 }
404
405 if ( $updated_link ) {
406 $updated_links[] = $smart_link;
407 } else {
408 $added_links[] = $smart_link;
409 }
410
411 // Clear the cache for the smart link.
412 $smart_link->flush_all_cache();
413 }
414
415 // If no link was added, return an error response.
416 if ( count( $added_links ) === 0 && count( $updated_links ) === 0 ) {
417 return new WP_REST_Response(
418 array(
419 'error' => array(
420 'name' => 'add_smart_link_failed',
421 'message' => __( 'Failed to add all the Smart Links.', 'wp-parsely' ),
422 ),
423 ),
424 500
425 );
426 }
427
428 $response = array();
429 if ( count( $added_links ) > 0 ) {
430 $response['added'] = $this->serialize_smart_links( $added_links );
431 }
432 if ( count( $failed_links ) > 0 ) {
433 $response['failed'] = $this->serialize_smart_links( $failed_links );
434 }
435 if ( count( $updated_links ) > 0 ) {
436 $response['updated'] = $this->serialize_smart_links( $updated_links );
437 }
438
439 return new WP_REST_Response( array( 'data' => $response ), 200 );
440 }
441
442 /**
443 * API Endpoint: POST /smart-linking/{post_id}/set.
444 *
445 * Updates the smart links of a given post and removes the ones that are not in the request.
446 *
447 * @since 3.16.0
448 *
449 * @param WP_REST_Request $request The request object.
450 * @return WP_REST_Response|WP_Error The response object.
451 */
452 public function set_smart_links( WP_REST_Request $request ) {
453 $post = $this->get_request_post( $request );
454
455 if ( ! $post instanceof WP_Post ) {
456 return new WP_Error(
457 'invalid_post',
458 __( 'Invalid post.', 'wp-parsely' ),
459 array( 'status' => 404 )
460 );
461 }
462
463 /**
464 * Array of Smart Link models provided in the request.
465 *
466 * @var Smart_Link[] $smart_links
467 */
468 $smart_links = $request->get_param( 'smart_links' );
469
470 // Get the current stored smart links.
471 $existing_links = Smart_Link::get_outbound_smart_links( $post->ID, Smart_Link_Status::APPLIED );
472 $removed_links = array();
473
474 foreach ( $existing_links as $existing_link ) {
475 $found = false;
476 foreach ( $smart_links as $smart_link ) {
477 if ( $smart_link->get_uid() === $existing_link->get_uid() ) {
478 $found = true;
479 break;
480 }
481 }
482
483 if ( ! $found ) {
484 $removed_links[] = $existing_link;
485 $existing_link->delete();
486
487 // Clear the cache for the smart link.
488 $existing_link->flush_all_cache();
489 }
490 }
491
492 $saved_links = array();
493 $failed_links = array();
494
495 foreach ( $smart_links as $smart_link ) {
496 // Mark as applied.
497 $smart_link->set_status( Smart_Link_Status::APPLIED );
498
499 // The smart link properties are set in the validate callback.
500 $saved = $smart_link->save();
501
502 if ( ! $saved ) {
503 $failed_links[] = $smart_link;
504 continue;
505 }
506
507 $saved_links[] = $smart_link;
508
509 // Clear the cache for the smart link.
510 $smart_link->flush_all_cache();
511 }
512
513 $response = array(
514 'saved' => $this->serialize_smart_links( $saved_links ),
515 'removed' => $this->serialize_smart_links( $removed_links ),
516 );
517
518 if ( count( $failed_links ) > 0 ) {
519 $response['failed'] = $this->serialize_smart_links( $failed_links );
520 }
521
522 return new WP_REST_Response( array( 'data' => $response ), 200 );
523 }
524
525 /**
526 * API Endpoint: POST /smart-linking/get-post-meta-for-urls.
527 *
528 * Returns post meta for the passed URLs.
529 *
530 * @since 3.18.0
531 *
532 * @param WP_REST_Request $request The request object.
533 * @return WP_REST_Response The response object.
534 */
535 public function get_post_meta_for_urls( WP_REST_Request $request ): WP_REST_Response {
536 $urls = $request->get_param( 'urls' );
537 $posts_meta = array();
538
539 foreach ( $urls as $url ) {
540 $post_id = Utils::get_post_id_by_url( $url );
541
542 if ( $post_id > 0 ) {
543 $post = get_post( $post_id );
544
545 // URLs resolve by slug, irrespective of the post's status.
546 if ( null !== $post && current_user_can( 'read_post', $post_id ) ) {
547 $post_type_obj = get_post_type_object( $post->post_type );
548 $post_type_name = $post_type_obj instanceof \WP_Post_Type ? $post_type_obj->labels->singular_name : '';
549
550 $posts_meta[] = array(
551 'author' => get_the_author_meta( 'display_name', intval( $post->post_author ) ),
552 'date' => get_the_date( '', $post ),
553 'id' => $post_id,
554 'title' => $post->post_title,
555 'thumbnail' => get_the_post_thumbnail_url( $post, 'thumbnail' ),
556 'type' => $post_type_name,
557 'url' => $url,
558 );
559 }
560 }
561 }
562
563 return new WP_REST_Response( array( 'data' => $posts_meta ), 200 );
564 }
565
566 /**
567 * Validates the smart link parameters.
568 *
569 * The callback sets the smart link object in the request object if the parameters are valid.
570 *
571 * @since 3.16.0
572 * @since 3.24.2 Rejects non-arrays.
573 * @access private
574 *
575 * @param mixed $params The raw parameter value, as validation runs before type checks.
576 * @param WP_REST_Request $request The request object.
577 * @return bool Whether the parameters are valid.
578 */
579 public function validate_smart_link_params( $params, WP_REST_Request $request ): bool {
580 if ( ! is_array( $params ) ) {
581 return false;
582 }
583
584 $required_params = array( 'uid', 'href', 'title', 'text', 'offset' );
585
586 foreach ( $required_params as $param ) {
587 if ( ! isset( $params[ $param ] ) ) {
588 return false;
589 }
590 }
591
592 $encoded_data = wp_json_encode( $params );
593 if ( false === $encoded_data ) {
594 return false;
595 }
596
597 $post_id = $request->get_param( 'post_id' );
598 if ( ! is_numeric( $post_id ) ) {
599 return false;
600 }
601
602 if ( ! is_string( $params['uid'] ) ) {
603 return false;
604 }
605
606 if ( ! is_array( $params['href'] ) ) {
607 return false;
608 }
609
610 if ( ! is_string( $params['href']['raw'] ) ) {
611 return false;
612 }
613
614 if ( ! Smart_Link::is_valid_href( $params['href']['raw'] ) ) {
615 return false;
616 }
617
618 // Try to get the smart link from the UID.
619 $smart_link = Smart_Link::get_smart_link( $params['uid'], intval( $post_id ) );
620 if ( $smart_link->exists() ) {
621 // Update the smart link with the new data.
622 $smart_link->set_href( $params['href']['raw'] );
623 $smart_link->title = $params['title'];
624 $smart_link->text = $params['text'];
625 $smart_link->offset = $params['offset'];
626 } else {
627 /**
628 * The Smart Link model.
629 *
630 * @var Smart_Link $smart_link
631 */
632 $smart_link = Smart_Link::deserialize( $encoded_data );
633 $smart_link->set_source_post_id( intval( $post_id ) );
634 }
635
636 // If the context is not set, set it to Smart Linking.
637 if ( null === $smart_link->get_context() ) {
638 $smart_link->set_context( $this->get_pch_feature_name() );
639 }
640
641 // Set the smart link attribute in the request.
642 $request->set_param( 'smart_link', $smart_link );
643
644 return true;
645 }
646
647 /**
648 * Validates the multiple smart link parameters.
649 *
650 * The callback sets the smart links object in the request object if the parameters are valid.
651 *
652 * @since 3.16.0
653 * @since 3.24.2 Rejects non-arrays.
654 * @access private
655 *
656 * @param mixed $param The raw parameter value, as validation runs before type checks.
657 * @param WP_REST_Request $request The request object.
658 * @return bool Whether the parameter is valid.
659 */
660 public function validate_multiple_smart_links( $param, WP_REST_Request $request ): bool {
661 if ( ! is_array( $param ) ) {
662 return false;
663 }
664
665 $smart_links = array();
666
667 foreach ( $param as $link ) {
668 if ( $this->validate_smart_link_params( $link, $request ) ) {
669 $smart_link = $request->get_param( 'smart_link' );
670 $smart_links[] = $smart_link;
671 } else {
672 return false;
673 }
674 }
675 $request->set_param( 'smart_link', null );
676 $request->set_param( 'smart_links', $smart_links );
677
678 return true;
679 }
680
681 /**
682 * Serializes inbound Smart Links, dropping the ones whose source post the
683 * current user cannot see.
684 *
685 * @since 3.24.2
686 *
687 * @param Smart_Link[] $links The inbound Smart Links to serialize.
688 * @return array<mixed> The serialized Smart Links.
689 */
690 private function serialize_visible_inbound_links( array $links ): array {
691 $serialized = array();
692
693 foreach ( $links as $link ) {
694 $source_post_id = $link->source_post_id;
695 $can_edit = current_user_can( 'edit_post', $source_post_id );
696
697 if ( ! $can_edit && ! is_post_publicly_viewable( $source_post_id ) ) {
698 continue;
699 }
700
701 $data = json_decode( $link->serialize(), true );
702
703 if ( ! is_array( $data ) ) {
704 continue;
705 }
706
707 // WordPress withholds a protected post's content; the anchor text
708 // and the paragraph are both drawn from it.
709 if ( ! $can_edit && post_password_required( $source_post_id ) ) {
710 $data['text'] = '';
711
712 if ( isset( $data['post_data'] ) && is_array( $data['post_data'] ) ) {
713 $data['post_data']['paragraph'] = '';
714 }
715 }
716
717 $serialized[] = $data;
718 }
719
720 return $serialized;
721 }
722
723 /**
724 * Serializes an array of Smart Links.
725 *
726 * @since 3.16.0
727 *
728 * @param Smart_Link[] $links The Smart Links to serialize.
729 * @return array<mixed> The serialized Smart Links.
730 */
731 private function serialize_smart_links( array $links ): array {
732 return array_map(
733 function ( Smart_Link $link ) {
734 return json_decode( $link->serialize(), true );
735 },
736 $links
737 );
738 }
739 }
740