*/ public static function kinds(): array { $kinds = Settings::postTypes(); if (Settings::includeAttachments()) { $kinds[] = 'attachment'; } return $kinds; } /** * WP_Query arguments for the eligible items of one kind. * * @return array */ public static function queryArgs(string $kind): array { $args = [ 'post_type' => $kind, 'post_status' => $kind === 'attachment' ? 'inherit' : Settings::statuses(), 'has_password' => false, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', // Every language: Polylang reads 'lang' => '' as "all"; WPML is switched to "all" around // the query (see run()). suppress_filters would also do it, but is not allowed on wordpress.org. 'lang' => '', 'ignore_sticky_posts' => true, 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- NOT EXISTS on one key; the opt-out is rare ['key' => self::EXCLUDE_META, 'compare' => 'NOT EXISTS'], ], ]; if ($kind === 'attachment') { $args['post_mime_type'] = 'image'; } if (Settings::after() !== '') { $args['date_query'] = [['after' => Settings::after(), 'inclusive' => true, 'column' => 'post_date_gmt']]; } $tax = self::taxQuery($kind); if ($tax !== []) { $args['tax_query'] = $tax; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } return $args; } /** * The taxonomy filter, limited to taxonomies this post type has; a type without * any of them is not filtered (pages are not dropped by a category filter). * * @return array */ private static function taxQuery(string $kind): array { $mode = Settings::taxMode(); if ($mode === 'none' || $kind === 'attachment') { return []; } $clauses = []; foreach (Settings::terms() as $taxonomy => $slugs) { if (!is_object_in_taxonomy($kind, $taxonomy)) { continue; } $clauses[] = [ 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => $slugs, 'operator' => $mode === 'include' ? 'IN' : 'NOT IN', ]; } if ($clauses === []) { return []; } return ['relation' => $mode === 'include' ? 'OR' : 'AND'] + $clauses; } /** * Eligible items per kind (before the aipcf_embed_should_index filter, which is per post). * * @return array */ public static function counts(): array { $out = []; foreach (self::kinds() as $kind) { $q = self::query(['posts_per_page' => 1, 'no_found_rows' => false] + self::queryArgs($kind)); $out[$kind] = (int) $q->found_posts; } return $out; } /** * A WP_Query over every language. WPML otherwise limits admin queries to the * language picked in the admin bar, which would silently skip the others. * * @param array $args */ public static function query(array $args): \WP_Query { $wpml = has_action('wpml_switch_language') !== false; if ($wpml) { $current = apply_filters('wpml_current_language', null); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own hook do_action('wpml_switch_language', 'all'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own hook } $q = new \WP_Query($args); if ($wpml) { do_action('wpml_switch_language', $current ?? null); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own hook } return $q; } /** * One page of eligible IDs of a kind, oldest ID first. * * @return list */ public static function ids(string $kind, int $page, int $perPage = 500, int $author = 0): array { $args = ['posts_per_page' => $perPage, 'paged' => max(1, $page), 'no_found_rows' => true] + self::queryArgs($kind); if ($author > 0) { $args['author'] = $author; } $q = self::query($args); return array_map('intval', $q->posts); } /** * Why this post is not embedded, or '' when it is. */ public static function whyNot(\WP_Post $post): string { $kind = $post->post_type; if (!in_array($kind, self::kinds(), true)) { return 'post type not selected'; } if ($kind === 'attachment') { if (strpos((string) $post->post_mime_type, 'image/') !== 0) { return 'not an image'; } if ($post->post_status !== 'inherit') { return 'not in the media library (e.g. trashed)'; } // An image uploaded to a post that is not public, or that the owner kept out, // would reveal it through its title, alt text and caption. $parent = $post->post_parent > 0 ? get_post($post->post_parent) : null; if ( $parent instanceof \WP_Post && ( !in_array($parent->post_status, Settings::statuses(), true) || $parent->post_password !== '' || get_post_meta($parent->ID, self::EXCLUDE_META, true) !== '' || !apply_filters('aipcf_embed_should_index', true, $parent) ) ) { return 'attached to content that is not selected (draft, private, password protected or left out)'; } } elseif (!in_array($post->post_status, Settings::statuses(), true)) { return sprintf('status "%s" not selected', $post->post_status); } if ($post->post_password !== '') { return 'password protected'; } if (get_post_meta($post->ID, self::EXCLUDE_META, true) !== '') { return 'excluded in the editor'; } if (Settings::after() !== '' && strtotime($post->post_date_gmt . ' UTC') < strtotime(Settings::after() . ' 00:00:00 UTC')) { return 'published before ' . Settings::after(); } $reason = self::taxReason($post); if ($reason !== '') { return $reason; } /** * Filters whether a post that matches the settings is embedded. * * @param bool $index Whether to embed it. * @param \WP_Post $post The post or attachment. */ if (!apply_filters('aipcf_embed_should_index', true, $post)) { return 'excluded by a filter'; } return ''; } public static function isEligible(\WP_Post $post): bool { return self::whyNot($post) === ''; } private static function taxReason(\WP_Post $post): string { $mode = Settings::taxMode(); if ($mode === 'none' || $post->post_type === 'attachment') { return ''; } $applies = false; $matches = false; foreach (Settings::terms() as $taxonomy => $slugs) { if (!is_object_in_taxonomy($post->post_type, $taxonomy)) { continue; } $applies = true; if (has_term($slugs, $taxonomy, $post)) { $matches = true; } } if (!$applies) { return ''; } if ($mode === 'include' && !$matches) { return 'not in an included category or tag'; } if ($mode === 'exclude' && $matches) { return 'in an excluded category or tag'; } return ''; } }