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/Utils/class-utils.php +180 -10 3.18.0 → 3.24.2 View file →
@@ -15,8 +15,9 @@
15 15
16 16 use WP_Post;
17 17 use WP_Error;
18 18
19 +use const Parsely\PARSELY_CACHE_GROUP;
19 20 use const Parsely\PARSELY_FILE;
20 21
21 22 /**
22 23 * Utils Class.
@@ -21,8 +22,16 @@
21 22 /**
22 23 * Utils Class.
23 24 *
24 25 * @since 3.17.0
26 + *
27 + * @phpstan-type ItmParams array{
28 + * campaign: string,
29 + * source?: string,
30 + * medium?: string,
31 + * content?: string,
32 + * term?: string,
33 + * }
25 34 */
26 35 class Utils {
27 36 const DATE_UTC_FORMAT = 'Y-m-d';
28 37 const WP_DATE_TIME_FORMAT = 'Y-m-d H:i:s';
@@ -270,9 +279,9 @@
270 279
271 280 return esc_html(
272 281 sprintf(
273 282 /* translators: 1: Number of seconds */
274 - __( '%1$d sec.', 'wp-parsely' ),
283 + __( '%1$ds', 'wp-parsely' ),
275 284 round( $seconds )
276 285 )
277 286 );
278 287 }
@@ -410,18 +419,24 @@
410 419
411 420 /**
412 421 * Returns the post ID for the passed URL.
413 422 *
423 + * Posts the current user can neither view publicly nor read resolve to 0.
424 + *
414 425 * @since 3.16.0
415 426 * @since 3.18.0 Moved from `Models/class-smart-link.php`.
427 + * @since 3.24.2 Resolves only to posts visible to the current user, and
428 + * matches slugs only of this site's URLs, unambiguously.
416 429 *
417 430 * @param string $url The URL to get the post ID for.
418 431 * @return int The post ID of the URL, 0 if not found.
419 432 */
420 433 public static function get_post_id_by_url( string $url ): int {
421 - $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' );
422 - if ( is_integer( $cache ) ) {
423 - return $cache;
434 + $cache_key = sprintf( 'url-to-postid-%s', hash( 'sha256', $url ) );
435 + $cache = wp_cache_get( $cache_key, PARSELY_CACHE_GROUP );
436 +
437 + if ( false !== $cache && is_numeric( $cache ) ) {
438 + return self::get_visible_post_id( (int) $cache );
424 439 }
425 440
426 441 if ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
427 442 $post_id = wpcom_vip_url_to_postid( $url );
@@ -427,16 +442,21 @@
427 442 $post_id = wpcom_vip_url_to_postid( $url );
428 443 } else {
429 444 // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
430 445 $post_id = url_to_postid( $url );
431 - wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' );
446 + wp_cache_set( $cache_key, $post_id, PARSELY_CACHE_GROUP, WEEK_IN_SECONDS );
432 447 }
433 448
434 449 // A post ID was found, return it.
435 450 if ( 0 !== $post_id ) {
436 - return $post_id;
451 + return self::get_visible_post_id( $post_id );
437 452 }
438 453
454 + // A slug can only identify a post in this site's URLs.
455 + if ( ! self::is_site_url( $url ) ) {
456 + return 0;
457 + }
458 +
439 459 // No post ID was found, try to find it from the slug.
440 460 $clean_url = preg_replace( '/\?.*$/', '', $url ); // Remove the query string from the URL.
441 461 if ( null === $clean_url ) {
442 462 $clean_url = $url;
@@ -442,8 +462,43 @@
442 462 $clean_url = $url;
443 463 }
444 464 $post_slug = basename( $clean_url );
445 465
466 + // Public posts take precedence over other posts with the same slug.
467 + $post_ids = array_values(
468 + array_filter(
469 + self::get_top_level_post_ids_by_slug( $post_slug, array( 'publish', 'inherit' ) ),
470 + 'is_post_publicly_viewable'
471 + )
472 + );
473 +
474 + // Visibility is checked on return, so the resolution can be cached for all users.
475 + if ( 0 === count( $post_ids ) ) {
476 + $post_ids = self::get_top_level_post_ids_by_slug( $post_slug, array( 'any' ) );
477 + }
478 +
479 + // A slug shared by several posts identifies none of them.
480 + if ( 1 === count( $post_ids ) ) {
481 + wp_cache_set( $cache_key, $post_ids[0], PARSELY_CACHE_GROUP, WEEK_IN_SECONDS );
482 + return self::get_visible_post_id( $post_ids[0] );
483 + }
484 +
485 + return 0;
486 + }
487 +
488 + /**
489 + * Returns up to 2 IDs of top-level posts of public post types with the
490 + * passed slug, enough to tell whether the slug is ambiguous.
491 + *
492 + * Like get_page_by_path() given a single slug, child posts don't match.
493 + *
494 + * @since 3.24.2
495 + *
496 + * @param string $slug The post slug.
497 + * @param array<string> $post_statuses The post statuses to match.
498 + * @return array<int> The post IDs.
499 + */
500 + private static function get_top_level_post_ids_by_slug( string $slug, array $post_statuses ): array {
446 501 $public_post_types = get_post_types(
447 502 array(
448 503 'public' => true,
449 504 'show_in_rest' => true,
@@ -448,14 +503,129 @@
448 503 'public' => true,
449 504 'show_in_rest' => true,
450 505 )
451 506 );
452 - $post = get_page_by_path( $post_slug, OBJECT, array_keys( $public_post_types ) );
453 507
454 - if ( null !== $post ) {
455 - wp_cache_set( $url, $post->ID, 'wp_parsely_smart_link_url_to_postid' );
456 - return $post->ID;
508 + $query = new \WP_Query(
509 + array(
510 + 'name' => $slug,
511 + 'post_type' => array_keys( $public_post_types ),
512 + 'post_status' => $post_statuses,
513 + 'post_parent' => 0,
514 + 'fields' => 'ids',
515 + 'posts_per_page' => 2,
516 + 'no_found_rows' => true,
517 + )
518 + );
519 +
520 + $post_ids = array();
521 + foreach ( $query->posts as $post_id ) {
522 + if ( is_int( $post_id ) ) {
523 + $post_ids[] = $post_id;
524 + }
457 525 }
458 526
527 + return $post_ids;
528 + }
529 +
530 + /**
531 + * Returns the passed post ID, or 0 if the current user can neither view
532 + * the post publicly nor read it.
533 + *
534 + * @since 3.24.2
535 + *
536 + * @param int $post_id The post ID.
537 + * @return int The post ID, or 0.
538 + */
539 + private static function get_visible_post_id( int $post_id ): int {
540 + if ( is_post_publicly_viewable( $post_id ) || current_user_can( 'read_post', $post_id ) ) {
541 + return $post_id;
542 + }
543 +
459 544 return 0;
545 + }
546 +
547 + /**
548 + * Returns whether the passed URL is relative, or has the host of the home
549 + * URL, the site URL, the Site ID or the canonical URL domain.
550 + *
551 + * A leading `www.` is ignored, as `url_to_postid()` does.
552 + *
553 + * @since 3.24.2
554 + *
555 + * @param string $url The URL to check.
556 + * @return bool Whether the URL belongs to this site.
557 + */
558 + private static function is_site_url( string $url ): bool {
559 + $url_host = wp_parse_url( $url, PHP_URL_HOST );
560 +
561 + if ( ! is_string( $url_host ) ) {
562 + return true;
563 + }
564 +
565 + $site_addresses = array(
566 + home_url(),
567 + site_url(),
568 + \Parsely\get_parsely()->get_site_id(),
569 + apply_filters( 'wp_parsely_canonical_url_domain', null ),
570 + );
571 +
572 + foreach ( $site_addresses as $site_address ) {
573 + if ( ! is_string( $site_address ) || '' === trim( $site_address ) ) {
574 + continue;
575 + }
576 +
577 + // Domain-only addresses need a scheme to be parsed.
578 + $site_host = wp_parse_url(
579 + 'http://' . preg_replace( '#^https?://#i', '', trim( $site_address ) ),
580 + PHP_URL_HOST
581 + );
582 +
583 + if ( is_string( $site_host ) && self::strip_www( $site_host ) === self::strip_www( $url_host ) ) {
584 + return true;
585 + }
586 + }
587 +
588 + return false;
589 + }
590 +
591 + /**
592 + * Lowercases a host and removes its leading `www.`.
593 + *
594 + * @since 3.24.2
595 + *
596 + * @param string $host The host.
597 + * @return string The host, without its leading `www.`.
598 + */
599 + private static function strip_www( string $host ): string {
600 + return (string) preg_replace( '/^www\./', '', strtolower( $host ) );
601 + }
602 +
603 + /**
604 + * Appends ITM parameters to a URL.
605 + *
606 + * @since 3.19.0
607 + *
608 + * @param string $url The URL to append the ITM parameters to.
609 + * @param ItmParams $params The ITM parameters to append.
610 + * @return string The URL with the ITM parameters appended.
611 + */
612 + public static function append_itm_params( string $url, $params ): string {
613 + // Convert the params array to the correct format.
614 + $mapping = array(
615 + 'campaign' => 'itm_campaign',
616 + 'source' => 'itm_source',
617 + 'medium' => 'itm_medium',
618 + 'content' => 'itm_content',
619 + 'term' => 'itm_term',
620 + );
621 +
622 + $itm_params = array();
623 + foreach ( $params as $key => $value ) {
624 + if ( array_key_exists( $key, $mapping ) ) {
625 + $itm_params[ $mapping[ $key ] ] = $value;
626 + }
627 + }
628 +
629 + return add_query_arg( $itm_params, $url );
460 630 }
461 631 }