Post IDs as keys. */ private static function featured_image_posts(array $ids, int $limit): array { global $wpdb; $placeholders = implode(',', array_fill(0, count($ids), '%d')); $statuses = self::status_placeholders(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT pm.post_id FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_thumbnail_id' AND pm.meta_value IN ({$placeholders}) AND p.post_status IN ({$statuses}) LIMIT %d", array_merge($ids, self::CACHEABLE_STATUSES, [$limit]) ) ); // phpcs:enable return self::flip(array_map('intval', (array) $rows)); } /** * Posts whose builder tree mentions one of the attachments. * * Every builder stores its tree as JSON or as serialised PHP, so the ID * cannot be matched exactly in SQL. The query narrows on `meta_key`, which * is indexed, and PHP then confirms each candidate. * * @param int[] $ids Attachment IDs. * @param int $limit Row cap. * @return int[] Post IDs. */ private static function meta_reference_posts(array $ids, int $limit): array { global $wpdb; $keys = Builder_Content::builder_meta_keys(); if ([] === $keys) { return []; } $key_placeholders = implode(',', array_fill(0, count($keys), '%s')); $like_clauses = []; $like_values = []; foreach ($ids as $id) { $like_clauses[] = 'pm.meta_value LIKE %s'; $like_values[] = '%' . $wpdb->esc_like((string) $id) . '%'; } // Candidates only: the LIKE matches 531 inside 5310 and inside any // unrelated number. verify() below is what decides. $like_sql = implode(' OR ', $like_clauses); $statuses = self::status_placeholders(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key IN ({$key_placeholders}) AND ({$like_sql}) AND p.post_status IN ({$statuses}) LIMIT %d", array_merge($keys, $like_values, self::CACHEABLE_STATUSES, [$limit * 4]) ) ); // phpcs:enable $posts = []; foreach ((array) $rows as $row) { if (self::mentions_any($ids, (string) $row->meta_value)) { $posts[] = (int) $row->post_id; } } return $posts; } /** * Posts whose `post_content` references one of the attachments. * * Covers the block and classic editors, whose image markup carries the ID * in a `wp-image-` class, a block attribute or a gallery shortcode. * * @param int[] $ids Attachment IDs. * @param int $limit Row cap. * @return int[] Post IDs. */ private static function content_reference_posts(array $ids, int $limit): array { global $wpdb; $like_clauses = []; $like_values = []; foreach ($ids as $id) { $like_clauses[] = 'p.post_content LIKE %s'; $like_values[] = '%' . $wpdb->esc_like((string) $id) . '%'; } $like_sql = implode(' OR ', $like_clauses); $statuses = self::status_placeholders(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT p.ID, p.post_content FROM {$wpdb->posts} p WHERE p.post_type != 'attachment' AND p.post_status IN ({$statuses}) AND ({$like_sql}) LIMIT %d", array_merge(self::CACHEABLE_STATUSES, $like_values, [$limit * 4]) ) ); // phpcs:enable $posts = []; foreach ((array) $rows as $row) { if (self::content_mentions_any($ids, (string) $row->post_content)) { $posts[] = (int) $row->ID; } } return $posts; } /** * `%s` placeholders for CACHEABLE_STATUSES, for an IN () clause. * * @return string */ private static function status_placeholders(): string { return implode(',', array_fill(0, count(self::CACHEABLE_STATUSES), '%s')); } /** * Whether a builder tree really refers to one of the attachments. * * A number surrounded by digits is a different number, which is the one * false positive worth ruling out. Beyond that this stays loose on purpose: * an ID that appears as some other setting's value costs one extra cache * purge, while a missed reference leaves a visitor looking at stale markup, * which is the bug being fixed. * * @param int[] $ids Attachment IDs. * @param string $value Stored builder tree. * @return bool */ private static function mentions_any(array $ids, string $value): bool { foreach ($ids as $id) { if (1 === preg_match('/(? */ private static function flip(array $ids): array { $set = []; foreach ($ids as $id) { $set[(int) $id] = true; } return $set; } }