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/rest-api/content-helper/class-endpoint-smart-linking.php +192 -118 3.17.0 → 3.24.2 View file →
@@ -1,8 +1,8 @@
1 1 <?php
2 2 /**
3 3 * Endpoint: Smart Linking
4 - * Parse.ly Content Helper `/smart-linking` API endpoint class
4 + * Parse.ly Content Intelligence `/smart-linking` API endpoint class
5 5 *
6 6 * @package Parsely
7 7 * @since 3.17.0
8 8 */
@@ -11,11 +11,13 @@
11 11
12 12 namespace Parsely\REST_API\Content_Helper;
13 13
14 14 use Parsely\Models\Smart_Link;
15 +use Parsely\Models\Smart_Link_Status;
15 16 use Parsely\REST_API\Base_Endpoint;
16 17 use Parsely\REST_API\Use_Post_ID_Parameter_Trait;
17 18 use Parsely\Services\Suggestions_API\Suggestions_API_Service;
19 +use Parsely\Utils\Utils;
18 20 use WP_Error;
19 21 use WP_Post;
20 22 use WP_REST_Request;
21 23 use WP_REST_Response;
@@ -44,9 +46,9 @@
44 46 * Initializes the class.
45 47 *
46 48 * @since 3.17.0
47 49 *
48 - * @param Content_Helper_Controller $controller The content helper controller.
50 + * @param Content_Helper_Controller $controller The Content Intelligence controller.
49 51 */
50 52 public function __construct( Content_Helper_Controller $controller ) {
51 53 parent::__construct( $controller );
52 54 $this->suggestions_api = $controller->get_parsely()->get_suggestions_api();
@@ -56,9 +58,9 @@
56 58 * Returns the name of the endpoint.
57 59 *
58 60 * @since 3.17.0
59 61 *
60 - * @return string The endpoint name.
62 + * @return string The endpoint's name.
61 63 */
62 64 public static function get_endpoint_name(): string {
63 65 return 'smart-linking';
64 66 }
@@ -67,9 +69,9 @@
67 69 * Returns the name of the feature associated with the current endpoint.
68 70 *
69 71 * @since 3.17.0
70 72 *
71 - * @return string The feature name.
73 + * @return string The feature's name.
72 74 */
73 75 public function get_pch_feature_name(): string {
74 76 return 'smart_linking';
75 77 }
@@ -91,19 +93,20 @@
91 93 array(
92 94 'text' => array(
93 95 'required' => true,
94 96 'type' => 'string',
95 - 'description' => __( 'The text to generate smart links for.', 'wp-parsely' ),
97 + 'description' => __( 'The text to generate Smart Links for.', 'wp-parsely' ),
96 98 ),
97 99 'max_links' => array(
98 100 'type' => 'integer',
99 - 'description' => __( 'The maximum number of smart links to generate.', 'wp-parsely' ),
101 + 'description' => __( 'The maximum number of Smart Links to generate.', 'wp-parsely' ),
100 102 'default' => 10,
101 103 ),
102 104 'url_exclusion_list' => array(
103 105 'type' => 'array',
104 - 'description' => __( 'The list of URLs to exclude from the smart links.', 'wp-parsely' ),
105 - 'validate_callback' => array( $this, 'validate_url_exclusion_list' ),
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' ),
106 109 'default' => array(),
107 110 ),
108 111 )
109 112 );
@@ -129,14 +132,14 @@
129 132 array(
130 133 'link' => array(
131 134 'required' => true,
132 135 'type' => 'object',
133 - 'description' => __( 'The smart link data to add.', 'wp-parsely' ),
136 + 'description' => __( 'The Smart Link data to add.', 'wp-parsely' ),
134 137 'validate_callback' => array( $this, 'validate_smart_link_params' ),
135 138 ),
136 139 'update' => array(
137 140 'type' => 'boolean',
138 - 'description' => __( 'Whether to update the existing smart link.', 'wp-parsely' ),
141 + 'description' => __( 'Whether to update the existing Smart Link.', 'wp-parsely' ),
139 142 'default' => false,
140 143 ),
141 144 )
142 145 );
@@ -152,14 +155,14 @@
152 155 array(
153 156 'links' => array(
154 157 'required' => true,
155 158 'type' => 'array',
156 - 'description' => __( 'The multiple smart links data to add.', 'wp-parsely' ),
159 + 'description' => __( 'The multiple Smart Links data to add.', 'wp-parsely' ),
157 160 'validate_callback' => array( $this, 'validate_multiple_smart_links' ),
158 161 ),
159 162 'update' => array(
160 163 'type' => 'boolean',
161 - 'description' => __( 'Whether to update the existing smart links.', 'wp-parsely' ),
164 + 'description' => __( 'Whether to update the existing Smart Links.', 'wp-parsely' ),
162 165 'default' => false,
163 166 ),
164 167 )
165 168 );
@@ -175,9 +178,9 @@
175 178 array(
176 179 'links' => array(
177 180 'required' => true,
178 181 'type' => 'array',
179 - 'description' => __( 'The smart links data to set.', 'wp-parsely' ),
182 + 'description' => __( 'The Smart Links data to set.', 'wp-parsely' ),
180 183 'validate_callback' => array( $this, 'validate_multiple_smart_links' ),
181 184 ),
182 185 )
183 186 );
@@ -182,15 +185,24 @@
182 185 )
183 186 );
184 187
185 188 /**
186 - * POST /smart-linking/url-to-post-type
187 - * Converts a URL to a post type.
189 + * POST /smart-linking/get-post-meta-for-urls
190 + * Gets the post meta information for the passed URLs.
188 191 */
189 192 $this->register_rest_route(
190 - 'url-to-post-type',
193 + 'get-post-meta-for-urls',
191 194 array( 'POST' ),
192 - array( $this, 'url_to_post_type' )
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 + )
193 205 );
194 206 }
195 207
196 208 /**
@@ -227,11 +239,11 @@
227 239
228 240 $response = $this->suggestions_api->get_smart_links(
229 241 $post_content,
230 242 array(
231 - 'max_items' => $max_links,
232 - ),
233 - $url_exclusion_list
243 + 'max_items' => $max_links,
244 + 'url_exclusion_list' => $url_exclusion_list,
245 + )
234 246 );
235 247
236 248 if ( is_wp_error( $response ) ) {
237 249 return $response;
@@ -238,8 +250,11 @@
238 250 }
239 251
240 252 $smart_links = array_map(
241 253 function ( Smart_Link $link ) {
254 + // Set the context to Smart Linking.
255 + $link->set_context( $this->get_pch_feature_name() );
256 +
242 257 return $link->to_array();
243 258 },
244 259 $response
245 260 );
@@ -252,32 +267,35 @@
252 267 *
253 268 * Gets the smart links for a post.
254 269 *
255 270 * @since 3.16.0
271 + * @since 3.24.2 Inbound links of non-visible source posts are omitted.
256 272 *
257 273 * @param WP_REST_Request $request The request object.
258 - * @return WP_REST_Response The response object.
274 + * @return WP_REST_Response|WP_Error The response object.
259 275 */
260 - public function get_smart_links( WP_REST_Request $request ): WP_REST_Response {
261 - /**
262 - * The post object.
263 - *
264 - * @var WP_Post $post
265 - */
266 - $post = $request->get_param( 'post' );
276 + public function get_smart_links( WP_REST_Request $request ) {
277 + $post = $this->get_request_post( $request );
267 278
268 - $outbound_links = Smart_Link::get_outbound_smart_links( $post->ID );
269 - $inbound_links = Smart_Link::get_inbound_smart_links( $post->ID );
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 + }
270 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 +
271 290 $response = array(
272 291 'outbound' => $this->serialize_smart_links( $outbound_links ),
273 - 'inbound' => $this->serialize_smart_links( $inbound_links ),
292 + 'inbound' => $this->serialize_visible_inbound_links( $inbound_links ),
274 293 );
275 294
276 295 return new WP_REST_Response( array( 'data' => $response ), 200 );
277 296 }
278 297
279 -
280 298 /**
281 299 * API Endpoint: POST /smart-linking/{post_id}/add.
282 300 *
283 301 * Adds a smart link to a post.
@@ -301,9 +319,9 @@
301 319 return new WP_REST_Response(
302 320 array(
303 321 'error' => array(
304 322 'name' => 'smart_link_exists',
305 - 'message' => __( 'Smart link already exists.', 'wp-parsely' ),
323 + 'message' => __( 'Smart Link already exists.', 'wp-parsely' ),
306 324 ),
307 325 ),
308 326 409 // HTTP Conflict.
309 327 );
@@ -308,8 +326,16 @@
308 326 409 // HTTP Conflict.
309 327 );
310 328 }
311 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 +
312 338 // The smart link properties are set in the validate callback.
313 339 $saved = $smart_link->save();
314 340 if ( ! $saved ) {
315 341 return new WP_REST_Response(
@@ -315,9 +341,9 @@
315 341 return new WP_REST_Response(
316 342 array(
317 343 'error' => array(
318 344 'name' => 'add_smart_link_failed',
319 - 'message' => __( 'Failed to add the smart link.', 'wp-parsely' ),
345 + 'message' => __( 'Failed to add the Smart Link.', 'wp-parsely' ),
320 346 ),
321 347 ),
322 348 500
323 349 );
@@ -322,8 +348,11 @@
322 348 500
323 349 );
324 350 }
325 351
352 + // Clear the cache for the smart link.
353 + $smart_link->flush_all_cache();
354 +
326 355 return new WP_REST_Response(
327 356 array(
328 357 'data' => json_decode( $smart_link->serialize() ),
329 358 ),
@@ -361,8 +390,11 @@
361 390 }
362 391
363 392 $updated_link = $smart_link->exists() && $should_update;
364 393
394 + // Mark as applied.
395 + $smart_link->set_status( Smart_Link_Status::APPLIED );
396 +
365 397 // The smart link properties are set in the validate callback.
366 398 $saved = $smart_link->save();
367 399
368 400 if ( ! $saved ) {
@@ -374,8 +406,11 @@
374 406 $updated_links[] = $smart_link;
375 407 } else {
376 408 $added_links[] = $smart_link;
377 409 }
410 +
411 + // Clear the cache for the smart link.
412 + $smart_link->flush_all_cache();
378 413 }
379 414
380 415 // If no link was added, return an error response.
381 416 if ( count( $added_links ) === 0 && count( $updated_links ) === 0 ) {
@@ -382,9 +417,9 @@
382 417 return new WP_REST_Response(
383 418 array(
384 419 'error' => array(
385 420 'name' => 'add_smart_link_failed',
386 - 'message' => __( 'Failed to add all the smart links.', 'wp-parsely' ),
421 + 'message' => __( 'Failed to add all the Smart Links.', 'wp-parsely' ),
387 422 ),
388 423 ),
389 424 500
390 425 );
@@ -411,18 +446,21 @@
411 446 *
412 447 * @since 3.16.0
413 448 *
414 449 * @param WP_REST_Request $request The request object.
415 - * @return WP_REST_Response The response object.
450 + * @return WP_REST_Response|WP_Error The response object.
416 451 */
417 - public function set_smart_links( WP_REST_Request $request ): WP_REST_Response {
418 - /**
419 - * The post object.
420 - *
421 - * @var WP_Post $post
422 - */
423 - $post = $request->get_param( 'post' );
452 + public function set_smart_links( WP_REST_Request $request ) {
453 + $post = $this->get_request_post( $request );
424 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 +
425 463 /**
426 464 * Array of Smart Link models provided in the request.
427 465 *
428 466 * @var Smart_Link[] $smart_links
@@ -429,9 +467,9 @@
429 467 */
430 468 $smart_links = $request->get_param( 'smart_links' );
431 469
432 470 // Get the current stored smart links.
433 - $existing_links = Smart_Link::get_outbound_smart_links( $post->ID );
471 + $existing_links = Smart_Link::get_outbound_smart_links( $post->ID, Smart_Link_Status::APPLIED );
434 472 $removed_links = array();
435 473
436 474 foreach ( $existing_links as $existing_link ) {
437 475 $found = false;
@@ -444,8 +482,11 @@
444 482
445 483 if ( ! $found ) {
446 484 $removed_links[] = $existing_link;
447 485 $existing_link->delete();
486 +
487 + // Clear the cache for the smart link.
488 + $existing_link->flush_all_cache();
448 489 }
449 490 }
450 491
451 492 $saved_links = array();
@@ -451,8 +492,11 @@
451 492 $saved_links = array();
452 493 $failed_links = array();
453 494
454 495 foreach ( $smart_links as $smart_link ) {
496 + // Mark as applied.
497 + $smart_link->set_status( Smart_Link_Status::APPLIED );
498 +
455 499 // The smart link properties are set in the validate callback.
456 500 $saved = $smart_link->save();
457 501
458 502 if ( ! $saved ) {
@@ -460,8 +504,11 @@
460 504 continue;
461 505 }
462 506
463 507 $saved_links[] = $smart_link;
508 +
509 + // Clear the cache for the smart link.
510 + $smart_link->flush_all_cache();
464 511 }
465 512
466 513 $response = array(
467 514 'saved' => $this->serialize_smart_links( $saved_links ),
@@ -474,104 +521,67 @@
474 521
475 522 return new WP_REST_Response( array( 'data' => $response ), 200 );
476 523 }
477 524
478 -
479 525 /**
480 - * API Endpoint: POST /smart-linking/url-to-post-type.
526 + * API Endpoint: POST /smart-linking/get-post-meta-for-urls.
481 527 *
482 - * Converts a URL to a post type.
528 + * Returns post meta for the passed URLs.
483 529 *
484 - * @since 3.16.0
530 + * @since 3.18.0
485 531 *
486 532 * @param WP_REST_Request $request The request object.
487 533 * @return WP_REST_Response The response object.
488 534 */
489 - public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response {
490 - $url = $request->get_param( 'url' );
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();
491 538
492 - if ( ! is_string( $url ) ) {
493 - return new WP_REST_Response(
494 - array(
495 - 'error' => array(
496 - 'name' => 'invalid_request',
497 - 'message' => __( 'Invalid request body.', 'wp-parsely' ),
498 - ),
499 - ),
500 - 400
501 - );
502 - }
539 + foreach ( $urls as $url ) {
540 + $post_id = Utils::get_post_id_by_url( $url );
503 541
504 - $post_id = 0;
505 - $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' );
542 + if ( $post_id > 0 ) {
543 + $post = get_post( $post_id );
506 544
507 - if ( is_integer( $cache ) ) {
508 - $post_id = $cache;
509 - } elseif ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
510 - $post_id = wpcom_vip_url_to_postid( $url );
511 - } else {
512 - // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
513 - $post_id = url_to_postid( $url );
514 - wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' );
515 - }
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 : '';
516 549
517 - $response = array(
518 - 'data' => array(
519 - 'post_id' => false,
520 - 'post_type' => false,
521 - ),
522 - );
523 -
524 - if ( 0 !== $post_id ) {
525 - $response['data']['post_id'] = $post_id;
526 - $response['data']['post_type'] = get_post_type( $post_id );
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 + }
527 561 }
528 562
529 - return new WP_REST_Response( $response, 200 );
563 + return new WP_REST_Response( array( 'data' => $posts_meta ), 200 );
530 564 }
531 565
532 566 /**
533 - * Validates the URL exclusion list parameter.
534 - *
535 - * The callback sets the URL exclusion list in the request object if the parameter is valid.
536 - *
537 - * @since 3.17.0
538 - * @access private
539 - *
540 - * @param mixed $param The parameter value.
541 - * @param WP_REST_Request $request The request object.
542 - * @return true|WP_Error Whether the parameter is valid.
543 - */
544 - public function validate_url_exclusion_list( $param, WP_REST_Request $request ) {
545 - if ( ! is_array( $param ) ) {
546 - return new WP_Error( 'invalid_url_exclusion_list', __( 'The URL exclusion list must be an array.', 'wp-parsely' ) );
547 - }
548 -
549 - $valid_urls = array_filter(
550 - $param,
551 - function ( $url ) {
552 - return is_string( $url ) && false !== filter_var( $url, FILTER_VALIDATE_URL );
553 - }
554 - );
555 -
556 - $request->set_param( 'url_exclusion_list', $valid_urls );
557 -
558 - return true;
559 - }
560 -
561 - /**
562 567 * Validates the smart link parameters.
563 568 *
564 569 * The callback sets the smart link object in the request object if the parameters are valid.
565 570 *
566 571 * @since 3.16.0
572 + * @since 3.24.2 Rejects non-arrays.
567 573 * @access private
568 574 *
569 - * @param array<mixed> $params The parameters.
575 + * @param mixed $params The raw parameter value, as validation runs before type checks.
570 576 * @param WP_REST_Request $request The request object.
571 577 * @return bool Whether the parameters are valid.
572 578 */
573 - public function validate_smart_link_params( array $params, WP_REST_Request $request ): bool {
579 + public function validate_smart_link_params( $params, WP_REST_Request $request ): bool {
580 + if ( ! is_array( $params ) ) {
581 + return false;
582 + }
583 +
574 584 $required_params = array( 'uid', 'href', 'title', 'text', 'offset' );
575 585
576 586 foreach ( $required_params as $param ) {
577 587 if ( ! isset( $params[ $param ] ) ) {
@@ -592,13 +602,25 @@
592 602 if ( ! is_string( $params['uid'] ) ) {
593 603 return false;
594 604 }
595 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 +
596 618 // Try to get the smart link from the UID.
597 619 $smart_link = Smart_Link::get_smart_link( $params['uid'], intval( $post_id ) );
598 620 if ( $smart_link->exists() ) {
599 621 // Update the smart link with the new data.
600 - $smart_link->set_href( $params['href'] );
622 + $smart_link->set_href( $params['href']['raw'] );
601 623 $smart_link->title = $params['title'];
602 624 $smart_link->text = $params['text'];
603 625 $smart_link->offset = $params['offset'];
604 626 } else {
@@ -610,8 +632,13 @@
610 632 $smart_link = Smart_Link::deserialize( $encoded_data );
611 633 $smart_link->set_source_post_id( intval( $post_id ) );
612 634 }
613 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 +
614 641 // Set the smart link attribute in the request.
615 642 $request->set_param( 'smart_link', $smart_link );
616 643
617 644 return true;
@@ -622,15 +649,20 @@
622 649 *
623 650 * The callback sets the smart links object in the request object if the parameters are valid.
624 651 *
625 652 * @since 3.16.0
653 + * @since 3.24.2 Rejects non-arrays.
626 654 * @access private
627 655 *
628 - * @param array<array<mixed>> $param The parameter value.
629 - * @param WP_REST_Request $request The request object.
656 + * @param mixed $param The raw parameter value, as validation runs before type checks.
657 + * @param WP_REST_Request $request The request object.
630 658 * @return bool Whether the parameter is valid.
631 659 */
632 - public function validate_multiple_smart_links( array $param, WP_REST_Request $request ): bool {
660 + public function validate_multiple_smart_links( $param, WP_REST_Request $request ): bool {
661 + if ( ! is_array( $param ) ) {
662 + return false;
663 + }
664 +
633 665 $smart_links = array();
634 666
635 667 foreach ( $param as $link ) {
636 668 if ( $this->validate_smart_link_params( $link, $request ) ) {
@@ -643,8 +675,50 @@
643 675 $request->set_param( 'smart_link', null );
644 676 $request->set_param( 'smart_links', $smart_links );
645 677
646 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;
647 721 }
648 722
649 723 /**
650 724 * Serializes an array of Smart Links.