| @@ -279,9 +279,9 @@ | ||
| 279 | 279 | |
| 280 | 280 | return esc_html( |
| 281 | 281 | sprintf( |
| 282 | 282 | /* translators: 1: Number of seconds */ |
| 283 | - __( '%1$d sec.', 'wp-parsely' ), | |
| 283 | + __( '%1$ds', 'wp-parsely' ), | |
| 284 | 284 | round( $seconds ) |
| 285 | 285 | ) |
| 286 | 286 | ); |
| 287 | 287 | } |
| @@ -419,10 +419,14 @@ | ||
| 419 | 419 | |
| 420 | 420 | /** |
| 421 | 421 | * Returns the post ID for the passed URL. |
| 422 | 422 | * |
| 423 | + * Posts the current user can neither view publicly nor read resolve to 0. | |
| 424 | + * | |
| 423 | 425 | * @since 3.16.0 |
| 424 | 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. | |
| 425 | 429 | * |
| 426 | 430 | * @param string $url The URL to get the post ID for. |
| 427 | 431 | * @return int The post ID of the URL, 0 if not found. |
| 428 | 432 | */ |
| @@ -429,10 +433,10 @@ | ||
| 429 | 433 | public static function get_post_id_by_url( string $url ): int { |
| 430 | 434 | $cache_key = sprintf( 'url-to-postid-%s', hash( 'sha256', $url ) ); |
| 431 | 435 | $cache = wp_cache_get( $cache_key, PARSELY_CACHE_GROUP ); |
| 432 | 436 | |
| 433 | - if ( is_integer( $cache ) ) { | |
| 434 | - return $cache; | |
| 437 | + if ( false !== $cache && is_numeric( $cache ) ) { | |
| 438 | + return self::get_visible_post_id( (int) $cache ); | |
| 435 | 439 | } |
| 436 | 440 | |
| 437 | 441 | if ( function_exists( 'wpcom_vip_url_to_postid' ) ) { |
| 438 | 442 | $post_id = wpcom_vip_url_to_postid( $url ); |
| @@ -443,11 +447,16 @@ | ||
| 443 | 447 | } |
| 444 | 448 | |
| 445 | 449 | // A post ID was found, return it. |
| 446 | 450 | if ( 0 !== $post_id ) { |
| 447 | - return $post_id; | |
| 451 | + return self::get_visible_post_id( $post_id ); | |
| 448 | 452 | } |
| 449 | 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 | + | |
| 450 | 459 | // No post ID was found, try to find it from the slug. |
| 451 | 460 | $clean_url = preg_replace( '/\?.*$/', '', $url ); // Remove the query string from the URL. |
| 452 | 461 | if ( null === $clean_url ) { |
| 453 | 462 | $clean_url = $url; |
| @@ -453,8 +462,43 @@ | ||
| 453 | 462 | $clean_url = $url; |
| 454 | 463 | } |
| 455 | 464 | $post_slug = basename( $clean_url ); |
| 456 | 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 { | |
| 457 | 501 | $public_post_types = get_post_types( |
| 458 | 502 | array( |
| 459 | 503 | 'public' => true, |
| 460 | 504 | 'show_in_rest' => true, |
| @@ -459,16 +503,102 @@ | ||
| 459 | 503 | 'public' => true, |
| 460 | 504 | 'show_in_rest' => true, |
| 461 | 505 | ) |
| 462 | 506 | ); |
| 463 | - $post = get_page_by_path( $post_slug, OBJECT, array_keys( $public_post_types ) ); | |
| 464 | 507 | |
| 465 | - if ( null !== $post ) { | |
| 466 | - wp_cache_set( $cache_key, $post->ID, PARSELY_CACHE_GROUP, WEEK_IN_SECONDS ); | |
| 467 | - 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 | + } | |
| 468 | 525 | } |
| 469 | 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 | + | |
| 470 | 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 ) ); | |
| 471 | 601 | } |
| 472 | 602 | |
| 473 | 603 | /** |
| 474 | 604 | * Appends ITM parameters to a URL. |