Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.33% covered (warning)
57.33%
43 / 75
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Search_Stats
57.33% covered (warning)
57.33%
43 / 75
16.67% covered (danger)
16.67%
1 / 6
51.07
0.00% covered (danger)
0.00%
0 / 1
 get_stats_from_wpcom
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 queue_post_count_query_from_wpcom
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 estimate_count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_post_type_breakdown
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
3.01
 get_post_type_breakdown_with
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 get_raw_post_type_breakdown
63.64% covered (warning)
63.64%
14 / 22
0.00% covered (danger)
0.00%
0 / 1
6.20
1<?php
2/**
3 * Get search stats for use in the wp-admin dashboard.
4 *
5 * @package my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack\Products;
9
10use Automattic\Jetpack\Connection\Client;
11use Jetpack_Options;
12
13/**
14 * Search stats (e.g. post count, post type breakdown)
15 */
16class Search_Stats {
17    const EXCLUDED_POST_TYPES = array(
18        'elementor_library', // Used by Elementor.
19        'jp_sitemap', // Used by Jetpack.
20        'revision',
21        'vip-legacy-redirect',
22        'scheduled-action',
23        'nbcs_video_lookup',
24        'reply', // bbpress, these get included in the topic
25        'product_variation', // woocommerce, not really public
26        'nav_menu_item',
27        'shop_order', // woocommerce, not really public
28        'redirect_rule', // Used by the Safe Redirect plugin.
29    );
30
31    const DO_NOT_EXCLUDE_POST_TYPES = array(
32        'topic', // bbpress
33        'forum', // bbpress
34    );
35
36    const CACHE_EXPIRY                  = 1 * MINUTE_IN_SECONDS;
37    const CACHE_GROUP                   = 'jetpack_search';
38    const POST_TYPE_BREAKDOWN_CACHE_KEY = 'post_type_break_down';
39    const TOTAL_POSTS_COUNT_CACHE_KEY   = 'total-post-count';
40    const POST_COUNT_QUERY_LIMIT        = 1e5;
41
42    /**
43     * Get stats from the WordPress.com API for the current blog ID.
44     */
45    public function get_stats_from_wpcom() {
46        $blog_id = Jetpack_Options::get_option( 'id' );
47
48        if ( ! is_numeric( $blog_id ) ) {
49            return null;
50        }
51
52        $response = Client::wpcom_json_api_request_as_blog(
53            '/sites/' . (int) $blog_id . '/jetpack-search/stats',
54            '2',
55            array(),
56            null,
57            'wpcom'
58        );
59
60        return $response;
61    }
62
63    /**
64     * Queue querying the post type breakdown from WordPress.com API for the current blog ID.
65     */
66    public function queue_post_count_query_from_wpcom() {
67        $blog_id = Jetpack_Options::get_option( 'id' );
68
69        if ( ! is_numeric( $blog_id ) ) {
70            return null;
71        }
72
73        Client::wpcom_json_api_request_as_blog(
74            '/sites/' . (int) $blog_id . '/jetpack-search/queue-post-count',
75            '2',
76            array(),
77            null,
78            'wpcom'
79        );
80    }
81
82    /**
83     * Estimate record counts via a local database query.
84     */
85    public static function estimate_count() {
86        return array_sum( static::get_post_type_breakdown() );
87    }
88
89    /**
90     * Calculate breakdown of post types for the site.
91     */
92    public static function get_post_type_breakdown() {
93        $indexable_post_types   = get_post_types(
94            array(
95                'public'              => true,
96                'exclude_from_search' => false,
97            )
98        );
99        $indexable_status_array = get_post_stati(
100            array(
101                'public'              => true,
102                'exclude_from_search' => false,
103            )
104        );
105        $raw_posts_counts       = static::get_raw_post_type_breakdown();
106        if ( ! $raw_posts_counts || is_wp_error( $raw_posts_counts ) ) {
107            return array();
108        }
109        $posts_counts = static::get_post_type_breakdown_with( $raw_posts_counts, $indexable_post_types, $indexable_status_array );
110
111        return $posts_counts;
112    }
113
114    /**
115     * Calculate breakdown of post types with passed in indexable post types and statuses.
116     * The function is going to be used from WPCOM as well for consistency.
117     *
118     * @param array $raw_posts_counts Array of post types with counts as value.
119     * @param array $indexable_post_types Array of indexable post types.
120     * @param array $indexable_status_array Array of indexable post statuses.
121     */
122    public static function get_post_type_breakdown_with( $raw_posts_counts, $indexable_post_types, $indexable_status_array ) {
123        $posts_counts = array();
124        foreach ( $raw_posts_counts as $row ) {
125            // ignore if status is not public.
126            if ( ! in_array( $row['post_status'], $indexable_status_array, true ) ) {
127                continue;
128            }
129            // ignore if post type is in excluded post types.
130            if ( in_array( $row['post_type'], self::EXCLUDED_POST_TYPES, true ) ) {
131                continue;
132            }
133            // ignore if post type is not public and is not explicitly included.
134            if ( ! in_array( $row['post_type'], $indexable_post_types, true ) &&
135                ! in_array( $row['post_type'], self::DO_NOT_EXCLUDE_POST_TYPES, true )
136            ) {
137                continue;
138            }
139            // add up post type counts of potentially multiple post_status.
140            if ( ! isset( $posts_counts[ $row['post_type'] ] ) ) {
141                $posts_counts[ $row['post_type'] ] = 0;
142            }
143            $posts_counts[ $row['post_type'] ] += intval( $row['num_posts'] );
144        }
145
146        arsort( $posts_counts, SORT_NUMERIC );
147        return $posts_counts;
148    }
149
150    /**
151     * Get raw post type breakdown from the database or a remote request if posts count is high.
152     */
153    protected static function get_raw_post_type_breakdown() {
154        global $wpdb;
155
156        $results = wp_cache_get( self::POST_TYPE_BREAKDOWN_CACHE_KEY, self::CACHE_GROUP );
157        if ( false !== $results ) {
158            return $results;
159        }
160
161        $total_posts_count = wp_cache_get( self::TOTAL_POSTS_COUNT_CACHE_KEY, self::CACHE_GROUP );
162        if ( false === $total_posts_count ) {
163            // phpcs:ignore WordPress.DB.DirectDatabaseQuery */
164            $total_posts_counts = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts}" );
165            wp_cache_set( self::TOTAL_POSTS_COUNT_CACHE_KEY, $total_posts_counts, self::CACHE_GROUP, self::CACHE_EXPIRY );
166        }
167
168        // Get post type breakdown from a remote request if the post count is high
169        if ( $total_posts_count > self::POST_COUNT_QUERY_LIMIT ) {
170            $search_stats = new Search_Stats();
171            $search_stats->queue_post_count_query_from_wpcom();
172            $wpcom_stats = json_decode( wp_remote_retrieve_body( $search_stats->get_stats_from_wpcom() ), true );
173            if ( ! empty( $wpcom_stats['raw_post_type_breakdown'] ) ) {
174                $results = $wpcom_stats['raw_post_type_breakdown'];
175                wp_cache_set( self::POST_TYPE_BREAKDOWN_CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_EXPIRY );
176                return $results;
177            } else {
178                return array();
179            }
180        }
181
182        $query = "SELECT post_type, post_status, COUNT( * ) AS num_posts
183        FROM {$wpdb->posts}
184        WHERE post_password = ''
185        GROUP BY post_type, post_status";
186
187        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery
188        $results = $wpdb->get_results( $query, ARRAY_A );
189        wp_cache_set( self::POST_TYPE_BREAKDOWN_CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_EXPIRY );
190        return $results;
191    }
192}