$urls Absolute or relative internal URLs. * * @return array URL to post id; 0 where nothing matched. */ public function resolve( array $urls ): array { $urls = array_values( array_unique( array_filter( $urls, static fn( $u ): bool => is_string( $u ) && '' !== $u ) ) ); if ( array() === $urls ) { return array(); } $expected_depth = self::permalink_depth(); $slug_by_url = array(); foreach ( $urls as $url ) { $slug = self::slug_from_url( $url, $expected_depth ); if ( '' !== $slug ) { $slug_by_url[ $url ] = $slug; } } $id_by_slug = $this->ids_for_slugs( array_values( array_unique( $slug_by_url ) ) ); $out = array(); foreach ( $urls as $url ) { $slug = $slug_by_url[ $url ] ?? ''; if ( '' !== $slug && isset( $id_by_slug[ $slug ] ) ) { $out[ $url ] = $id_by_slug[ $slug ]; continue; } // Permalink shapes the slug heuristic cannot cover (?p=, dates // with a colliding tail, attachments) still resolve correctly. $out[ $url ] = function_exists( 'url_to_postid' ) ? (int) url_to_postid( $url ) : 0; } return $out; } /** * Looks up every slug in one query. * * @since 1.14.0 * * @param array $slugs Post slugs. * * @return array Slug to post id. */ private function ids_for_slugs( array $slugs ): array { if ( array() === $slugs ) { return array(); } global $wpdb; if ( ! isset( $wpdb ) ) { return array(); } $placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- $placeholders is built from %s only; the dynamic IN() list is the standard WP pattern, and this reflects link state that changes as the scan runs. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT ID, post_name FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_name IN ({$placeholders})", $slugs ) ); // phpcs:enable $out = array(); if ( is_array( $rows ) ) { foreach ( $rows as $row ) { $name = (string) ( $row->post_name ?? '' ); $id = (int) ( $row->ID ?? 0 ); if ( '' !== $name && $id > 0 ) { $out[ $name ] = $id; } } } return $out; } /** * Returns the post slug a URL ends in, when the URL could be a permalink. * * The depth check is not an optimisation, it is the correctness condition. * Matching on the last segment alone resolved `/tag/sex/` to a post whose * slug happened to be "sex" — recording a link to a post the author never * linked to, and corrupting the link graph rather than merely being slow. * A tag, category, author, or paginated URL is a different depth from a * post permalink, so comparing depth rules them all out at once without * needing to know what any of them look like. * * @since 1.14.0 * * @param string $url Internal URL. * @param int $expected_depth Path segments a post permalink has, or 0 * when the structure has no usable slug. * * @return string Slug, or '' when this URL cannot be a post permalink. */ private static function slug_from_url( string $url, int $expected_depth ): string { if ( $expected_depth < 1 ) { return ''; } $path = (string) wp_parse_url( $url, PHP_URL_PATH ); if ( '' === $path ) { return ''; } $segments = array_values( array_filter( explode( '/', trim( $path, '/' ) ) ) ); if ( count( $segments ) !== $expected_depth ) { return ''; } return rawurldecode( (string) end( $segments ) ); } /** * How many path segments a post permalink has on this site. * * Read from the site's own permalink structure rather than assumed, since * `/%postname%/` and `/%category%/%postname%/` put posts at different * depths. Returns 0 when the structure does not end in `%postname%` — * plain permalinks, or an id- or date-terminated structure — because then * a trailing path segment is not a post slug and the shortcut has nothing * to offer. * * A structure with a nested category can put a post deeper than the * structure itself suggests. Those URLs simply miss the depth check and * take the reliable route, which costs a query rather than an answer. * * @since 1.14.0 * * @return int */ private static function permalink_depth(): int { $structure = function_exists( 'get_option' ) ? (string) get_option( 'permalink_structure', '' ) : ''; if ( '' === $structure ) { return 0; } $segments = array_values( array_filter( explode( '/', trim( $structure, '/' ) ) ) ); if ( array() === $segments ) { return 0; } if ( '%postname%' !== end( $segments ) ) { return 0; } return count( $segments ); } }