Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.16% covered (danger)
4.16%
23 / 553
25.00% covered (danger)
25.00%
11 / 44
CRAP
n/a
0 / 0
stats_load
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
3.05
jetpack_is_dnt_enabled
n/a
0 / 0
n/a
0 / 0
1
stats_ignore_db_version
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
stats_map_meta_caps
n/a
0 / 0
n/a
0 / 0
1
stats_template_redirect
n/a
0 / 0
n/a
0 / 0
1
stats_build_view_data
n/a
0 / 0
n/a
0 / 0
1
stats_get_options
n/a
0 / 0
n/a
0 / 0
1
stats_get_option
n/a
0 / 0
n/a
0 / 0
1
stats_set_option
n/a
0 / 0
n/a
0 / 0
1
stats_set_options
n/a
0 / 0
n/a
0 / 0
1
stats_upgrade_options
n/a
0 / 0
n/a
0 / 0
1
stats_admin_menu
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
132
stats_admin_path
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
stats_reports_load
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
stats_script_dismiss_nudge_handler
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
stats_reports_css
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
stats_js_remove_stnojs_cookie
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_admin_ui_stats_report_page_wrapper
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
stats_reports_page
0.00% covered (danger)
0.00%
0 / 137
0.00% covered (danger)
0.00%
0 / 1
992
stats_convert_image_urls
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_stats_convert_chart_urls_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
stats_convert_chart_urls
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
stats_convert_post_titles
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
stats_convert_post_title
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
stats_hide_smile_css
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
stats_admin_bar_head
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
stats_get_image_chart_src
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
stats_admin_bar_menu
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
stats_add_link_to_admin_bar_site_menu
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
stats_get_blog
n/a
0 / 0
n/a
0 / 0
1
stats_dashboard_widget_options
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
stats_dashboard_widget_control
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
stats_dashboard_widget_controls_handle_submission
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
stats_dashboard_widget_controls_html
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
stats_jetpack_dashboard_widget
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
stats_dashboard_widget_content
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 1
462
stats_print_wp_remote_error
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
stats_get_csv
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
156
stats_get_remote_csv
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
stats_str_getcsv
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_stats_api_path
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
stats_get_from_restapi
n/a
0 / 0
n/a
0 / 0
5
filter_stats_array_add_jp_version
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
convert_stats_array_to_object
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Module Name: Jetpack Stats
4 * Module Description: Clear, concise traffic insights right in your WordPress dashboard.
5 * Sort Order: 1
6 * Recommendation Order: 2
7 * First Introduced: 1.1
8 * Requires Connection: Yes
9 * Auto Activate: Yes
10 * Module Tags: Jetpack Stats, Site Stats, Recommended
11 * Feature: Engagement
12 * Additional Search Queries: statistics, tracking, analytics, views, traffic, stats
13 *
14 * @package automattic/jetpack
15 */
16
17use Automattic\Jetpack\Admin_UI\Admin_Menu;
18use Automattic\Jetpack\Connection\Client;
19use Automattic\Jetpack\Redirect;
20use Automattic\Jetpack\Stats\Main as Stats;
21use Automattic\Jetpack\Stats\Options as Stats_Options;
22use Automattic\Jetpack\Stats\Tracking_Pixel as Stats_Tracking_Pixel;
23use Automattic\Jetpack\Stats\WPCOM_Stats;
24use Automattic\Jetpack\Stats\XMLRPC_Provider as Stats_XMLRPC;
25use Automattic\Jetpack\Stats_Admin\Admin_Post_List_Column;
26use Automattic\Jetpack\Stats_Admin\Dashboard as Stats_Dashboard;
27use Automattic\Jetpack\Stats_Admin\Main as Stats_Main;
28use Automattic\Jetpack\Status\Host;
29use Automattic\Jetpack\Tracking;
30
31if ( ! defined( 'ABSPATH' ) ) {
32    exit( 0 );
33}
34
35if ( defined( 'STATS_DASHBOARD_SERVER' ) ) {
36    return;
37}
38
39define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
40
41/**
42 * Stats content markers.
43 * Used to test for content vs script when parsing server-generated HTML.
44 */
45const STATS_BODY_MARKER    = '<div id="statchart"';
46const STATS_CONTENT_MARKER = '<div class="gotonewdash">';
47
48add_action( 'jetpack_modules_loaded', 'stats_load' );
49
50/**
51 * Load Stats.
52 *
53 * @access public
54 * @return void
55 */
56function stats_load() {
57    Jetpack::enable_module_configurable( __FILE__ );
58
59    // Only run the callback for those who can see the stats.
60    if ( is_user_logged_in() && current_user_can( 'view_stats' ) ) {
61        add_action( 'admin_head', 'stats_admin_bar_head', 100 );
62        add_action( 'wp_head', 'stats_admin_bar_head', 100 );
63    }
64
65    Admin_Post_List_Column::register();
66
67    add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
68    add_action( 'admin_bar_menu', 'stats_add_link_to_admin_bar_site_menu', 40 );
69
70    add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
71
72    // Filter for adding the Jetpack plugin version to tracking stats.
73    add_filter( 'stats_array', 'filter_stats_array_add_jp_version' );
74
75    require_once __DIR__ . '/stats/class-jetpack-stats-upgrade-nudges.php';
76    add_action( 'updating_jetpack_version', array( 'Jetpack_Stats_Upgrade_Nudges', 'unset_nudges_setting' ) );
77}
78
79/**
80 * Checks if filter is set and dnt is enabled.
81 *
82 * @deprecated 11.5
83 * @return bool
84 */
85function jetpack_is_dnt_enabled() {
86    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Main::jetpack_is_dnt_enabled' );
87    return Stats::jetpack_is_dnt_enabled();
88}
89
90/**
91 * Prevent sparkline img requests being redirected to upgrade.php.
92 * See wp-admin/admin.php where it checks $wp_db_version.
93 *
94 * @access public
95 * @param mixed $version Version.
96 * @return string $version.
97 */
98function stats_ignore_db_version( $version ) {
99    if (
100        is_admin() &&
101        isset( $_GET['page'] ) && 'stats' === $_GET['page'] && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
102        isset( $_GET['chart'] ) && strpos( $_GET['chart'], 'admin-bar-hours' ) === 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
103    ) {
104        global $wp_db_version;
105        return $wp_db_version;
106    }
107    return $version;
108}
109
110/**
111 * Maps view_stats cap to read cap as needed.
112 *
113 * @deprecated 11.5
114 *
115 * @access public
116 * @param mixed $caps Caps.
117 * @param mixed $cap Cap.
118 * @param mixed $user_id User ID.
119 * @return array Possibly mapped capabilities for meta capability.
120 */
121function stats_map_meta_caps( $caps, $cap, $user_id ) {
122    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Main::map_meta_caps' );
123    return Stats::map_meta_caps( $caps, $cap, $user_id );
124}
125
126/**
127 * Stats Template Redirect.
128 *
129 * @deprecated 11.5
130 * @access public
131 * @return void
132 */
133function stats_template_redirect() {
134    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Main::template_redirect' );
135    Stats::template_redirect();
136}
137
138/**
139 * Stats Build View Data.
140 *
141 * @deprecated 11.5
142 * @access public
143 * @return array
144 */
145function stats_build_view_data() {
146    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Tracking_Pixel::build_view_data' );
147    return Stats_Tracking_Pixel::build_view_data();
148}
149
150/**
151 * Stats Get Options.
152 *
153 * @deprecated 11.5
154 *
155 * @access public
156 * @return array
157 */
158function stats_get_options() {
159    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Options::get_options' );
160    return Stats_Options::get_options();
161}
162
163/**
164 * Get Stats Options.
165 *
166 * @deprecated 11.5
167 *
168 * @access public
169 * @param mixed $option Option.
170 * @return mixed|null
171 */
172function stats_get_option( $option ) {
173    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Options::get_option' );
174    return Stats_Options::get_option( $option );
175}
176
177/**
178 * Stats Set Options.
179 *
180 * @deprecated 11.5
181 *
182 * @access public
183 * @param mixed $option Option.
184 * @param mixed $value Value.
185 * @return bool
186 */
187function stats_set_option( $option, $value ) {
188    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Options::set_option' );
189    return Stats_Options::set_option( $option, $value );
190}
191
192/**
193 * Stats Set Options.
194 *
195 * @deprecated 11.5
196 *
197 * @access public
198 * @param mixed $options Options.
199 * @return bool
200 */
201function stats_set_options( $options ) {
202    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Options::set_options' );
203    return Stats_Options::set_options( $options );
204}
205
206/**
207 * Stats Upgrade Options.
208 *
209 * @deprecated 11.5
210 *
211 * @access public
212 * @param mixed $options Options.
213 * @return array|bool
214 */
215function stats_upgrade_options( $options ) {
216    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Automattic\Jetpack\Stats\Options::upgrade_options' );
217    return Stats_Options::upgrade_options( $options );
218}
219
220/**
221 * Admin Pages.
222 *
223 * @access public
224 * @return void
225 */
226function stats_admin_menu() {
227    global $pagenow;
228
229    // If we're at an old Stats URL, redirect to the new one.
230    // Don't even bother with caps, menu_page_url(), etc.  Just do it.
231    if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
232        $redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : null );
233        $relative_pos = strpos( $redirect_url, '/wp-admin/' );
234        if ( false !== $relative_pos ) {
235            wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
236            exit( 0 );
237        }
238    }
239
240    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
241    if ( ! ( new Host() )->is_woa_site() && isset( $_GET['enable_new_stats'] ) && '1' === $_GET['enable_new_stats'] ) {
242        // Passing true enables Odyssey Stats.
243        // We're ignorning the return value for now.
244        Stats_Main::update_new_stats_status( true );
245    }
246
247    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
248    if ( ! Stats_Options::get_option( 'enable_odyssey_stats' ) || isset( $_GET['noheader'] ) ) {
249        // Show old Jetpack Stats interface for:
250        // - When the "enable_odyssey_stats" option is disabled.
251        // - When being shown in the adminbar outside of wp-admin.
252        $hook = Admin_Menu::add_menu( __( 'Stats', 'jetpack' ), __( 'Stats', 'jetpack' ), 'view_stats', 'stats', 'jetpack_admin_ui_stats_report_page_wrapper' );
253        add_action( "load-$hook", 'stats_reports_load' );
254    } else {
255        // Enable the new Odyssey Stats experience.
256        Stats_Dashboard::init();
257    }
258}
259
260/**
261 * Stats Admin Path.
262 *
263 * @access public
264 * @return string
265 */
266function stats_admin_path() {
267    return Jetpack::module_configuration_url( __FILE__ );
268}
269
270/**
271 * Stats Reports Load.
272 *
273 * @access public
274 * @return void
275 */
276function stats_reports_load() {
277    require_once __DIR__ . '/stats/class-jetpack-stats-upgrade-nudges.php';
278    Jetpack_Stats_Upgrade_Nudges::init();
279
280    wp_enqueue_script( 'jquery' );
281    wp_enqueue_script( 'postbox' );
282    wp_enqueue_script( 'underscore' );
283
284    Jetpack_Admin_Page::load_wrapper_styles();
285    add_action( 'admin_print_styles', 'stats_reports_css' );
286
287    if ( ! empty( $_GET['nojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
288        $parsed = wp_parse_url( admin_url() );
289        // Remember user doesn't want JS.
290        setcookie( 'stnojs', '1', time() + 172800, $parsed['path'], COOKIE_DOMAIN, is_ssl(), true ); // 2 days.
291    }
292
293    if ( ! empty( $_COOKIE['stnojs'] ) ) {
294        // Detect if JS is on.  If so, remove cookie so next page load is via JS.
295        add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
296    } elseif ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
297        // Normal page load.  Load page content via JS.
298        add_action( 'admin_print_footer_scripts', 'stats_script_dismiss_nudge_handler' );
299    }
300}
301
302/**
303 * JavaScript to dismiss the Odyssey nudge.
304 *
305 * @access public
306 * @return void
307 */
308function stats_script_dismiss_nudge_handler() {
309    ?>
310    <script type="text/javascript">
311    function stats_odyssey_dismiss_nudge() {
312        // Hide the nudge UI, effectively dismissing it.
313        var element = document.getElementById( "stats-odyssey-nudge-main" );
314        element.classList.toggle( "is-hidden" );
315        // Send an AJAX request.
316        // Note we can provide a 'postponed_for' parameter to set the delay.
317        // Without a parameter it defaults to 30 days which is what we want here.
318        let nonce = <?php echo wp_json_encode( wp_create_nonce( 'wp_rest' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
319        let url = <?php echo wp_json_encode( rest_url( '/jetpack/v4/stats-app/stats/notices' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
320        let data = {
321            id: 'opt_in_new_stats',
322            status: 'postponed',
323        };
324        jQuery.ajax({
325            type: "POST",
326            url: url,
327            data: data,
328            headers: { "x-wp-nonce": nonce },
329        });
330    }
331    </script>
332    <?php
333}
334
335/**
336 * Stats Reports CSS.
337 *
338 * @access public
339 * @return void
340 */
341function stats_reports_css() {
342    ?>
343<style type="text/css">
344#jp-stats-wrap, #jp-stats-report-bottom {
345    max-width: 1040px;
346    margin: 0 auto;
347    overflow: hidden;
348}
349</style>
350    <?php
351}
352
353/**
354 * Detect if JS is on.  If so, remove cookie so next page load is via JS.
355 *
356 * @access public
357 * @return void
358 */
359function stats_js_remove_stnojs_cookie() {
360    $parsed = wp_parse_url( admin_url() );
361    ?>
362<script type="text/javascript">
363/* <![CDATA[ */
364document.cookie = <?php echo wp_json_encode( 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=' . $parsed['path'], JSON_UNESCAPED_SLASHES | JSON_HEX_TAG ); ?>;
365/* ]]> */
366</script>
367    <?php
368}
369
370/**
371 * Jetpack Admin Page Wrapper.
372 */
373function jetpack_admin_ui_stats_report_page_wrapper() {
374    if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
375        Jetpack_Admin_Page::wrap_ui( 'stats_reports_page', array( 'is-wide' => true ) );
376    } else {
377        stats_reports_page();
378    }
379}
380
381/**
382 * Stats Report Page.
383 *
384 * @access public
385 * @param bool $main_chart_only (default: false) Main Chart Only.
386 */
387function stats_reports_page( $main_chart_only = false ) {
388    if ( isset( $_GET['dashboard'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
389        stats_dashboard_widget_content();
390        exit( 0 ); // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though stats_dashboard_widget_content() never returns.
391    }
392
393    $blog_id               = Stats_Options::get_option( 'blog_id' );
394    $learn_url             = Redirect::get_url( 'jetpack-stats-learn-more' );
395    $redirect_url          = admin_url( 'admin.php?page=stats&enable_new_stats=1' );
396    $stats_bg_url          = plugins_url( 'images/odyssey-upgrade/background.png', JETPACK__PLUGIN_FILE );
397    $stats_bg_gradient_url = plugins_url( 'images/odyssey-upgrade/gradient.png', JETPACK__PLUGIN_FILE );
398
399    if ( ! $main_chart_only && ! isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
400        ?>
401
402    <style>
403        .stats-odyssey-notice {
404            display: flex;
405            font-size: var( --font-body );
406
407            border: 1px solid var( --jp-gray-5 );
408            border-left-color: var( --jp-black );
409            border-left-width: 6px;
410            border-radius: 4px;
411
412            margin-top: 24px;
413            background: white;
414            position: relative;
415        }
416        .stats-odyssey-notice--content__highlighted {
417            border-left-color: var( --jp-red );
418        }
419        .stats-odyssey-notice--content {
420            padding: 24px 0 24px 30px;
421            font-size: 2em;
422            width: 100%;
423        }
424        .stats-odyssey-notice--content-header {
425            font-size: 24px;
426            line-height: 32px;
427            margin: 0;
428            margin-bottom: 8px;
429        }
430        .stats-odyssey-notice--content-text {
431            font-size: 16px;
432            margin: 0;
433        }
434        .stats-odyssey-notice--image-container {
435            background-image: url("<?php echo esc_url( $stats_bg_url ); ?>"), url("<?php echo esc_url( $stats_bg_gradient_url ); ?>");
436            background-size: cover;
437            padding-right: 28px;
438            width: 100%;
439        }
440        .stats-odyssey-notice--close-button {
441            position: absolute;
442            top: 1rem;
443            right: 1rem;
444            background-color: transparent;
445            border: none;
446            cursor: pointer;
447        }
448        .stats-odyssey-notice--action-bar {
449            display: flex;
450            align-items: center;
451            margin-top: 24px;
452        }
453        .stats-odyssey-notice--primary-button {
454            margin-right: 18px;
455            padding-left: 20px;
456            padding-right: 20px;
457            font-size: 16px;
458            border-color: black;
459            background-color: black;
460        }
461        .stats-odyssey-notice--primary-button:hover {
462            border-color: #3c434a;
463            background-color: #3c434a;
464        }
465        .is-primary-link {
466            color: white;
467            text-decoration: none;
468        }
469        .is-primary-link:active {
470            color: white;
471        }
472        .is-primary-link:focus {
473            color: white;
474            box-shadow: none;
475            outline: none;
476        }
477        .is-primary-link:hover {
478            color: white;
479        }
480        .is-secondary-link {
481            color: black;
482            font-size: var( --font-body );
483        }
484        .is-secondary-link:hover {
485            color: black;
486        }
487        .is-hidden {
488            display: none;
489        }
490    </style>
491    <div id="jp-stats-wrap">
492        <div class="wrap">
493            <h1><?php esc_html_e( 'Jetpack Stats', 'jetpack' ); ?>
494            <?php
495            if ( current_user_can( 'jetpack_manage_modules' ) ) :
496                $i18n_headers = jetpack_get_module_i18n( 'stats' );
497                ?>
498                <a
499                    style="font-size:13px;"
500                    href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack#/settings?term=' . rawurlencode( $i18n_headers['name'] ?? '' ) ) ); ?>"
501                >
502                <?php esc_html_e( 'Configure', 'jetpack' ); ?>
503                </a>
504                <?php
505                endif;
506
507            ?>
508            </h2>
509        </div>
510        <div class="wrap">
511            <div class="stats-odyssey-notice stats-odyssey-notice--content__highlighted">
512                <div class="stats-odyssey-notice--content">
513                    <h2 class="stats-odyssey-notice--content-header"><?php esc_html_e( 'Deprecated Jetpack Stats Experience', 'jetpack' ); ?></h2>
514                    <p class="stats-odyssey-notice--content-text"><?php esc_html_e( 'The old Jetpack Stats has been deprecated. Please click the button to enable the new experience.', 'jetpack' ); ?></p>
515                    <div class="stats-odyssey-notice--action-bar">
516                        <button class="dops-button stats-odyssey-notice--primary-button">
517                            <a class="is-primary-link" href="<?php echo esc_url( $redirect_url ); ?>"><?php esc_html_e( 'Switch to new Stats', 'jetpack' ); ?></a>
518                        </button>
519                        <a class="is-secondary-link" href="<?php echo esc_url( $learn_url ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'Learn about Stats', 'jetpack' ); ?> <svg xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle;" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg></a>
520                    </div>
521                </div>
522                <div class="stats-odyssey-notice--image-container"></div>
523            </div>
524        </div>
525        <p></p>
526    </div>
527        <?php
528        return;
529    }
530
531    $day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
532    $q   = array(
533        'noheader' => 'true',
534        'proxy'    => '',
535        'page'     => 'stats',
536        'day'      => $day,
537        'blog'     => $blog_id,
538        'charset'  => get_option( 'blog_charset' ),
539        'color'    => get_user_option( 'admin_color' ),
540        'ssl'      => is_ssl(),
541        'j'        => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
542    );
543    if ( get_locale() !== 'en_US' ) {
544        $q['jp_lang'] = get_locale();
545    }
546    // Only show the main chart, without extra header data, or metaboxes.
547    $q['main_chart_only'] = $main_chart_only;
548    $args                 = array(
549        'view'                => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
550        'numdays'             => 'int',
551        'day'                 => 'date',
552        'unit'                => array( '1', '7', '31', 'human' ),
553        'humanize'            => array( 'true' ),
554        'num'                 => 'int',
555        'summarize'           => null,
556        'post'                => 'int',
557        'width'               => 'int',
558        'height'              => 'int',
559        'data'                => 'data',
560        'blog_subscribers'    => 'int',
561        'comment_subscribers' => null,
562        'type'                => array( 'wpcom', 'email', 'pending' ),
563        'pagenum'             => 'int',
564        'masterbar'           => null,
565    );
566    foreach ( $args as $var => $vals ) {
567        if ( ! isset( $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
568            continue;
569        }
570        $val = wp_unslash( $_REQUEST[ $var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
571        if ( is_array( $vals ) ) {
572            if ( in_array( $val, $vals, true ) ) {
573                $q[ $var ] = $val;
574            }
575        } elseif ( 'int' === $vals ) {
576            $q[ $var ] = (int) $val;
577        } elseif ( 'date' === $vals ) {
578            if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $val ) ) {
579                $q[ $var ] = $val;
580            }
581        } elseif ( null === $vals ) {
582            $q[ $var ] = '';
583        } elseif ( 'data' === $vals ) {
584            if ( str_starts_with( $val, 'index.php' ) ) {
585                $q[ $var ] = $val;
586            }
587        }
588    }
589
590    $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
591    if ( isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
592        if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
593            $chart = sanitize_title( $_GET['chart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
594            $url   = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
595        }
596    }
597
598    $url     = add_query_arg( $q, $url );
599    $method  = 'GET';
600    $timeout = 90;
601    $user_id = 0; // Means use the blog token.
602
603    $get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
604    $get_code = wp_remote_retrieve_response_code( $get );
605    if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
606        stats_print_wp_remote_error( $get, $url );
607    } elseif ( ! empty( $get['headers']['content-type'] ) ) {
608        $type = $get['headers']['content-type'];
609        if ( str_starts_with( $type, 'image' ) ) {
610            $img = $get['body'];
611            header( 'Content-Type: ' . $type );
612            header( 'Content-Length: ' . strlen( $img ) );
613            echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
614            die( 0 );
615        }
616    }
617
618    if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
619        $tracking = new Tracking();
620        $tracking->record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
621    }
622
623    if ( isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
624        die( 0 );
625    }
626}
627
628/**
629 * Stats Convert Image URLs.
630 *
631 * @access public
632 * @param mixed $html HTML.
633 * @return string
634 */
635function stats_convert_image_urls( $html ) {
636    $url  = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
637    $html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
638    return $html;
639}
640
641/**
642 * Callback for preg_replace_callback used in stats_convert_chart_urls()
643 *
644 * @since 5.6.0
645 *
646 * @param  array $matches The matches resulting from the preg_replace_callback call.
647 * @return string          The admin url for the chart.
648 */
649function jetpack_stats_convert_chart_urls_callback( $matches ) {
650    // If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
651    return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
652}
653
654/**
655 * Stats Convert Chart URLs.
656 *
657 * @access public
658 * @param mixed $html HTML.
659 * @return string
660 */
661function stats_convert_chart_urls( $html ) {
662    $html = preg_replace_callback(
663        '|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
664        'jetpack_stats_convert_chart_urls_callback',
665        $html
666    );
667    return $html;
668}
669
670/**
671 * Stats Convert Post Title HTML
672 *
673 * @access public
674 * @param mixed $html HTML.
675 * @return string
676 */
677function stats_convert_post_titles( $html ) {
678    global $stats_posts;
679    $pattern = "<span class='post-(\d+)-link'>.*?</span>";
680    if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
681        return $html;
682    }
683    $posts = get_posts(
684        array(
685            'include'          => implode( ',', $matches[1] ),
686            'post_type'        => 'any',
687            'post_status'      => 'any',
688            'numberposts'      => -1,
689            'suppress_filters' => false,
690        )
691    );
692    foreach ( $posts as $post ) {
693        $stats_posts[ $post->ID ] = $post;
694    }
695    $html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
696    return $html;
697}
698
699/**
700 * Stats Convert Post Title Matches.
701 *
702 * @access public
703 * @param mixed $matches Matches.
704 * @return string
705 */
706function stats_convert_post_title( $matches ) {
707    global $stats_posts;
708    $post_id = $matches[1];
709    if ( isset( $stats_posts[ $post_id ] ) ) {
710        return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
711    }
712    return $matches[0];
713}
714
715/**
716 * CSS to hide the tracking pixel smiley.
717 * It is now hidden for everyone (used to be visible if you had set the hide_smile option).
718 *
719 * @access public
720 * @return void
721 */
722function stats_hide_smile_css() {
723    ?>
724<style>img#wpstats{display:none}</style>
725    <?php
726}
727
728/**
729 * Stats Admin Bar Head.
730 *
731 * @access public
732 * @return void
733 */
734function stats_admin_bar_head() {
735    // Let's not show the stats admin bar to users who are not logged in.
736    if ( ! is_user_logged_in() ) {
737        return;
738    }
739
740    if ( ! Stats_Options::get_option( 'admin_bar' ) ) {
741        return;
742    }
743
744    if ( ! current_user_can( 'view_stats' ) ) {
745        return;
746    }
747
748    if ( ! is_admin_bar_showing() ) {
749        return;
750    }
751
752    add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
753    ?>
754
755<style data-ampdevmode type='text/css'>
756#wpadminbar .quicklinks li#wp-admin-bar-stats {
757    height: 32px;
758}
759#wpadminbar .quicklinks li#wp-admin-bar-stats a {
760    height: 32px;
761    padding: 0;
762}
763#wpadminbar .quicklinks li#wp-admin-bar-stats a div {
764    height: 32px;
765    width: 95px;
766    overflow: hidden;
767    margin: 0 10px;
768}
769#wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
770    width: auto;
771    margin: 0 8px 0 10px;
772}
773#wpadminbar .quicklinks li#wp-admin-bar-stats a img {
774    height: 24px;
775    margin: 4px 0;
776    max-width: none;
777    border: none;
778}
779</style>
780    <?php
781}
782
783/**
784 * Gets the image source of the given stats chart.
785 *
786 * @param string $chart Name of the chart.
787 * @param array  $args Extra list of argument to use in the image source.
788 * @return string An image source.
789 */
790function stats_get_image_chart_src( $chart, $args = array() ) {
791    $url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) );
792
793    return add_query_arg(
794        array_merge(
795            array(
796                'noheader' => '',
797                'proxy'    => '',
798                'chart'    => $chart,
799            ),
800            $args
801        ),
802        $url
803    );
804}
805
806/**
807 * Stats AdminBar.
808 *
809 * @access public
810 * @param mixed $wp_admin_bar WPAdminBar.
811 * @return void
812 */
813function stats_admin_bar_menu( &$wp_admin_bar ) {
814    $img_src    = esc_attr( stats_get_image_chart_src( 'admin-bar-hours-scale' ) );
815    $img_src_2x = esc_attr( stats_get_image_chart_src( 'admin-bar-hours-scale-2x' ) );
816    $alt        = esc_attr( __( 'Stats', 'jetpack' ) );
817    $title      = esc_attr( __( 'Views over 48 hours. Click for more Jetpack Stats.', 'jetpack' ) );
818
819    $menu = array(
820        'id'    => 'stats',
821        'href'  => add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ), // no menu_page_url() blog-side.
822        'title' => "<div><img fetchpriority='low' loading='lazy' decoding='async' src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
823    );
824
825    $wp_admin_bar->add_menu( $menu );
826}
827
828/**
829 * Adds a Stats link to the site-name admin bar submenu.
830 *
831 * @access public
832 * @param WP_Admin_Bar|null $admin_bar WP_Admin_Bar instance.
833 * @return void
834 */
835function stats_add_link_to_admin_bar_site_menu( $admin_bar = null ) {
836    global $wp_admin_bar;
837
838    if ( null === $admin_bar ) {
839        $admin_bar = $wp_admin_bar;
840    }
841
842    if (
843        ! is_object( $admin_bar ) ||
844        ( ! $admin_bar->get_node( 'dashboard' ) && ! $admin_bar->get_node( 'view-site' ) ) ||
845        ! current_user_can( 'view_stats' ) ||
846        ( new Host() )->is_wpcom_platform()
847    ) {
848        return;
849    }
850
851    $admin_bar->add_node(
852        array(
853            'parent' => 'site-name',
854            'id'     => 'jetpack-stats',
855            'title'  => __( 'Stats', 'jetpack' ),
856            'href'   => admin_url( 'admin.php?page=stats' ),
857        )
858    );
859}
860
861/**
862 * Stats Get Blog.
863 *
864 * @deprecated 11.5
865 *
866 * @access public
867 * @return string
868 */
869function stats_get_blog() {
870    _deprecated_function( __METHOD__, 'jetpack-11.5' );
871    return Stats_XMLRPC::init()->get_blog();
872}
873
874/**
875 * Stats Dashboard Widget Options.
876 *
877 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
878 *
879 * @access public
880 * @return array
881 */
882function stats_dashboard_widget_options() {
883    $defaults = array(
884        'chart'  => 1,
885        'top'    => 1,
886        'search' => 7,
887    );
888    $options  = get_option( 'stats_dashboard_widget' );
889    if ( ( ! $options ) || ! is_array( $options ) ) {
890        $options = array();
891    }
892
893    // Ignore obsolete option values.
894    $intervals = array( 1, 7, 31, 90, 365 );
895    foreach ( array( 'top', 'search' ) as $key ) {
896        if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
897            unset( $options[ $key ] );
898        }
899    }
900
901    return array_merge( $defaults, $options );
902}
903
904/**
905 * Stats Dashboard Widget Control.
906 *
907 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
908 *
909 * @access public
910 * @return void
911 */
912function stats_dashboard_widget_control() {
913    stats_dashboard_widget_controls_handle_submission();
914    $periods   = array(
915        '1'  => __( 'day', 'jetpack' ),
916        '7'  => __( 'week', 'jetpack' ),
917        '31' => __( 'month', 'jetpack' ),
918    );
919    $intervals = array(
920        '1'   => __( 'the past day', 'jetpack' ),
921        '7'   => __( 'the past week', 'jetpack' ),
922        '31'  => __( 'the past month', 'jetpack' ),
923        '90'  => __( 'the past quarter', 'jetpack' ),
924        '365' => __( 'the past year', 'jetpack' ),
925    );
926    stats_dashboard_widget_controls_html( $intervals, $periods, stats_dashboard_widget_options() );
927}
928
929/**
930 * Handle widget controls form submission.
931 *
932 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
933 *
934 * @access public
935 * @return void
936 */
937function stats_dashboard_widget_controls_handle_submission() {
938    $options  = stats_dashboard_widget_options();
939    $defaults = array(
940        'top'    => 1,
941        'search' => 7,
942    );
943
944    // Check if the correct form was submitted.
945    if ( isset( $_POST['stats_id'] ) && 'dashboard_stats' === $_POST['stats_id'] ) {
946        // Perform nonce verification.
947        if (
948            isset( $_POST['dashboard-widget-nonce'] ) &&
949            wp_verify_nonce( filter_var( wp_unslash( $_POST['dashboard-widget-nonce'] ) ), 'edit-dashboard-widget_dashboard_stats' )
950        ) {
951            // Update options.
952            $options['chart'] = isset( $_POST['chart'] ) ? (int) $_POST['chart'] : 1;
953            foreach ( array( 'top', 'search' ) as $key ) {
954                $options[ $key ] = isset( $_POST[ $key ] ) ? (int) $_POST[ $key ] : $defaults[ $key ];
955            }
956            update_option( 'stats_dashboard_widget', $options );
957        }
958    }
959}
960
961/**
962 * Output HTML for widget controls.
963 *
964 * @param array $intervals Array of intervals.
965 * @param array $periods Array of periods.
966 * @param array $options Array of options.
967 *
968 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
969 *
970 * @access public
971 * @return void
972 */
973function stats_dashboard_widget_controls_html( $intervals, $periods, $options ) {
974    ?>
975    <p>
976    <label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
977    <select id="chart" name="chart">
978    <?php
979    foreach ( $periods as $val => $label ) {
980        ?>
981        <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
982        <?php
983    }
984    ?>
985    </select>.
986    </p>
987
988    <p>
989    <label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
990    <select id="top" name="top">
991    <?php
992    foreach ( $intervals as $val => $label ) {
993        ?>
994        <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
995        <?php
996    }
997    ?>
998    </select>.
999    </p>
1000
1001    <p>
1002    <label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
1003    <select id="search" name="search">
1004    <?php
1005    foreach ( $intervals as $val => $label ) {
1006        ?>
1007        <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
1008        <?php
1009    }
1010    ?>
1011    </select>.
1012    </p>
1013    <?php
1014}
1015
1016/**
1017 * Jetpack Stats Dashboard Widget.
1018 *
1019 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
1020 *
1021 * @access public
1022 * @return void
1023 */
1024function stats_jetpack_dashboard_widget() {
1025    ?>
1026    <form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
1027        <?php stats_dashboard_widget_control(); ?>
1028        <?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
1029        <input type="hidden" name="stats_id" value="dashboard_stats" />
1030        <?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
1031    </form>
1032    <button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
1033        <span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
1034        <span class="toggle-indicator" aria-hidden="true"></span>
1035    </button>
1036    <div id="dashboard_stats" class="is-loading">
1037        <div class="inside">
1038            <div style="height: 250px;"></div>
1039        </div>
1040    </div>
1041    <?php
1042}
1043
1044/**
1045 * Stats Dashboard Widget Content.
1046 *
1047 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
1048 *
1049 * @access public
1050 * @return never
1051 */
1052function stats_dashboard_widget_content() {
1053    $width  = isset( $_GET['width'] ) ? intval( $_GET['width'] ) / 2 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1054    $height = isset( $_GET['height'] ) ? intval( $_GET['height'] ) - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1055    if ( ! $width || $width < 250 ) {
1056        $width = 370;
1057    }
1058    if ( ! $height || $height < 230 ) {
1059        $height = 180;
1060    }
1061
1062    $_width  = $width - 5;
1063    $_height = $height - 5;
1064
1065    $options = stats_dashboard_widget_options();
1066    $blog_id = Jetpack_Options::get_option( 'id' );
1067
1068    $q = array(
1069        'noheader' => 'true',
1070        'proxy'    => '',
1071        'blog'     => $blog_id,
1072        'page'     => 'stats',
1073        'chart'    => '',
1074        'unit'     => $options['chart'],
1075        'color'    => get_user_option( 'admin_color' ),
1076        'width'    => $_width,
1077        'height'   => $_height,
1078        'ssl'      => is_ssl(),
1079        'j'        => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
1080    );
1081
1082    $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
1083
1084    $url     = add_query_arg( $q, $url );
1085    $method  = 'GET';
1086    $timeout = 90;
1087    $user_id = 0; // Means use the blog token.
1088
1089    $get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1090    $get_code = wp_remote_retrieve_response_code( $get );
1091    if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1092        stats_print_wp_remote_error( $get, $url );
1093    } else {
1094        $body = stats_convert_post_titles( $get['body'] );
1095        $body = stats_convert_chart_urls( $body );
1096        $body = stats_convert_image_urls( $body );
1097        echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
1098    }
1099
1100    $post_ids = array();
1101
1102    $csv_end_date = current_time( 'Y-m-d' );
1103    $csv_args     = array(
1104        'top'    => "&limit=6&end=$csv_end_date",
1105        'search' => "&limit=5&end=$csv_end_date",
1106    );
1107
1108    $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
1109    foreach ( $top_posts as $i => $post ) {
1110        if ( 0 === $post['post_id'] ) {
1111            unset( $top_posts[ $i ] );
1112            continue;
1113        }
1114        $post_ids[] = $post['post_id'];
1115    }
1116
1117    // Cache.
1118    get_posts( array( 'include' => implode( ',', array_unique( $post_ids ) ) ) );
1119
1120    $searches     = array();
1121    $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
1122    foreach ( $search_terms as $search_term ) {
1123        if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
1124            continue;
1125        }
1126        $searches[] = esc_html( $search_term['searchterm'] );
1127    }
1128
1129    ?>
1130<div id="stats-info">
1131    <div id="stats-info-container">
1132        <div class="stats-info-header">
1133            <h2><?php esc_html_e( 'Highlights', 'jetpack' ); ?></h2>
1134            <div class="stats-info-header-right">
1135                <a href="<?php echo esc_url( admin_url( 'admin.php?page=stats' ) ); ?>" class="button button-primary">
1136                    <?php esc_html_e( 'View detailed stats', 'jetpack' ); ?>
1137                </a>
1138            </div>
1139        </div>
1140        <div class="stats-info-content">
1141            <div id="top-posts" class="stats-section">
1142                <div class="stats-section-inner">
1143                <h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
1144                <?php
1145                if ( empty( $top_posts ) ) {
1146                    ?>
1147                    <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1148                    <?php
1149                } else {
1150                    foreach ( $top_posts as $post ) {
1151                        if ( ! get_post( $post['post_id'] ) ) {
1152                            continue;
1153                        }
1154                        ?>
1155                        <p>
1156                        <?php
1157                        printf(
1158                            esc_html(
1159                                /* Translators: Stats dashboard widget Post list with view count: "Post Title 1 View (or Views if plural)". */
1160                                _n( '%1$s %2$s View', '%1$s %2$s Views', $post['views'], 'jetpack' )
1161                            ),
1162                            '<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
1163                            esc_html( number_format_i18n( $post['views'] ) )
1164                        );
1165                        ?>
1166                    </p>
1167                        <?php
1168                    }
1169                }
1170                ?>
1171                </div>
1172            </div>
1173            <div id="top-search" class="stats-section">
1174                <div class="stats-section-inner">
1175                <h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
1176                <?php
1177                if ( empty( $searches ) ) {
1178                    ?>
1179                    <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1180                    <?php
1181                } else {
1182                    foreach ( $searches as $search_term_item ) {
1183                        printf(
1184                            '<p>%s</p>',
1185                            esc_html( $search_term_item )
1186                        );
1187                    }
1188                }
1189                ?>
1190                </div>
1191            </div>
1192        </div>
1193    </div>
1194</div>
1195    <?php
1196    exit( 0 );
1197}
1198
1199/**
1200 * Stats Print WP Remote Error.
1201 *
1202 * @access public
1203 * @param mixed $get Get.
1204 * @param mixed $url URL.
1205 * @return void
1206 */
1207function stats_print_wp_remote_error( $get, $url ) {
1208    $state_name     = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
1209    $previous_error = Jetpack::state( $state_name );
1210    $error          = md5( wp_json_encode( compact( 'get', 'url' ), JSON_UNESCAPED_SLASHES ) );
1211    Jetpack::state( $state_name, $error );
1212    if ( $error !== $previous_error ) {
1213        ?>
1214            <div class="wrap">
1215                <p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
1216            </div>
1217        <?php
1218        return;
1219    }
1220    ?>
1221    <div class="wrap">
1222    <p>
1223        <?php
1224            printf(
1225                /* translators: placeholder is an a href for a support site. */
1226                esc_html__( 'We were unable to get your stats just now. Please reload this page to try again. If this error persists, please contact %1$s. In your report, please include the information below.', 'jetpack' ),
1227                sprintf(
1228                    '<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
1229                    esc_html__( 'Jetpack Support', 'jetpack' )
1230                )
1231            );
1232        ?>
1233    </p>
1234    <pre class="stats-widget-error">
1235        User Agent: "<?php echo isset( $_SERVER['HTTP_USER_AGENT'] ) ? esc_html( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>"
1236        Page URL: "http<?php echo ( is_ssl() ? 's' : '' ) . '://' . esc_html( ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>"
1237        API URL: "<?php echo esc_url( $url ); ?>"
1238        <?php
1239        if ( is_wp_error( $get ) ) {
1240            foreach ( $get->get_error_codes() as $code ) {
1241                foreach ( $get->get_error_messages( $code ) as $message ) {
1242                    print esc_html( $code ) . ': "' . esc_html( $message ) . '"';
1243                }
1244            }
1245        } else {
1246            $get_code       = wp_remote_retrieve_response_code( $get );
1247            $content_length = strlen( wp_remote_retrieve_body( $get ) );
1248            ?>
1249                Response code: "<?php print esc_html( $get_code ); ?>"
1250                Content length: "<?php print esc_html( $content_length ); ?>"
1251            <?php
1252        }
1253        ?>
1254    </pre>
1255    </div>
1256    <?php
1257}
1258
1259/**
1260 * Get stats from WordPress.com
1261 *
1262 * @param string $table The stats which you want to retrieve: postviews, or searchterms.
1263 * @param array  $args {
1264 *      An associative array of arguments.
1265 *
1266 *      @type bool    $end        The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
1267 *                                and default timezone is UTC date. Default value is Now.
1268 *      @type string  $days       The length of the desired time frame. Default is 30. Maximum 90 days.
1269 *      @type int     $limit      The maximum number of records to return. Default is 10. Maximum 100.
1270 *      @type int     $post_id    The ID of the post to retrieve stats data for
1271 *      @type string  $summarize  If present, summarizes all matching records. Default Null.
1272 *      @type int     $blog_id    The WordPress.com blog ID to retrieve stats data for. Default is the current blog.
1273 *
1274 * }
1275 *
1276 * @return array {
1277 *      An array of post view data, each post as an array
1278 *
1279 *      array {
1280 *          The post view data for a single post
1281 *
1282 *          @type string  $post_id         The ID of the post
1283 *          @type string  $post_title      The title of the post
1284 *          @type string  $post_permalink  The permalink for the post
1285 *          @type string  $views           The number of views for the post within the $num_days specified
1286 *      }
1287 * }
1288 */
1289function stats_get_csv( $table, $args = null ) {
1290    $defaults = array(
1291        'end'       => false,
1292        'days'      => false,
1293        'limit'     => 3,
1294        'post_id'   => false,
1295        'summarize' => '',
1296        'blog_id'   => Jetpack_Options::get_option( 'id' ),
1297    );
1298
1299    $args          = wp_parse_args( $args, $defaults );
1300    $args['table'] = $table;
1301
1302    $stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
1303
1304    $key = md5( $stats_csv_url );
1305
1306    // Get cache.
1307    $stats_cache = get_option( 'stats_cache' );
1308    if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
1309        $stats_cache = array();
1310    }
1311
1312    // Return or expire this key.
1313    if ( isset( $stats_cache[ $key ] ) ) {
1314        $time = key( $stats_cache[ $key ] );
1315        if ( time() - $time < 300 ) {
1316            return $stats_cache[ $key ][ $time ];
1317        }
1318        unset( $stats_cache[ $key ] );
1319    }
1320
1321    $stats_rows = array();
1322    do {
1323        $stats = stats_get_remote_csv( $stats_csv_url );
1324        if ( ! $stats ) {
1325            break;
1326        }
1327
1328        $labels = array_shift( $stats );
1329
1330        if ( 0 === stripos( $labels[0], 'error' ) ) {
1331            break;
1332        }
1333
1334        $stats_rows = array();
1335        for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
1336            $row = array();
1337            foreach ( $labels as $col => $label ) {
1338                $row[ $label ] = $stats[ $s ][ $col ];
1339            }
1340            $stats_rows[] = $row;
1341        }
1342    } while ( 0 );
1343
1344    // Expire old keys.
1345    foreach ( $stats_cache as $k => $cache ) {
1346        if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
1347            unset( $stats_cache[ $k ] );
1348        }
1349    }
1350
1351        // Set cache.
1352        $stats_cache[ $key ] = array( time() => $stats_rows );
1353    update_option( 'stats_cache', $stats_cache );
1354
1355    return $stats_rows;
1356}
1357
1358/**
1359 * Stats get remote CSV.
1360 *
1361 * @access public
1362 * @param mixed $url URL.
1363 * @return array
1364 */
1365function stats_get_remote_csv( $url ) {
1366    $method  = 'GET';
1367    $timeout = 90;
1368    $user_id = 0; // Blog token.
1369
1370    $get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1371    $get_code = wp_remote_retrieve_response_code( $get );
1372    if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1373        return array(); // @todo: return an error?
1374    } else {
1375        return stats_str_getcsv( $get['body'] );
1376    }
1377}
1378
1379/**
1380 * Recursively run str_getcsv on the stats csv.
1381 *
1382 * @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
1383 *
1384 * @param mixed $csv CSV.
1385 * @return array
1386 */
1387function stats_str_getcsv( $csv ) {
1388    // @todo Correctly handle embedded newlines. Note, despite claims online, `str_getcsv( $csv, "\n" )` does not actually work.
1389    $lines = explode( "\n", rtrim( $csv, "\n" ) );
1390    return array_map(
1391        function ( $line ) {
1392            // @todo When we drop support for PHP <7.4, consider passing empty-string for `$escape` here for better spec compatibility.
1393            return str_getcsv( $line, ',', '"', '\\' );
1394        },
1395        $lines
1396    );
1397}
1398
1399/**
1400 * Abstract out building the rest api stats path.
1401 *
1402 * @param  string $resource Resource.
1403 * @return string
1404 */
1405function jetpack_stats_api_path( $resource = '' ) {
1406    $resource = ltrim( $resource, '/' );
1407    return sprintf( '/sites/%d/stats/%s', Stats_Options::get_option( 'blog_id' ), $resource );
1408}
1409
1410/**
1411 * Fetches stats data from the REST API.  Caches locally for 5 minutes.
1412 *
1413 * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
1414 * @access public
1415 * @deprecated 11.5 Use WPCOM_Stats available methodsinstead.
1416 * @param array  $args (default: array())  The args that are passed to the endpoint.
1417 * @param string $resource (default: '') Optional sub-endpoint following /stats/.
1418 * @return array|WP_Error
1419 */
1420function stats_get_from_restapi( $args = array(), $resource = '' ) {
1421    _deprecated_function( __METHOD__, 'jetpack-11.5', 'Please checkout the methods available in Automattic\Jetpack\Stats\WPCOM_Stats' );
1422    $endpoint    = jetpack_stats_api_path( $resource );
1423    $api_version = '1.1';
1424    $args        = wp_parse_args( $args, array() );
1425    $cache_key   = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args, JSON_UNESCAPED_SLASHES ) ) ) );
1426
1427    $transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
1428
1429    $stats_cache = get_transient( $transient_name );
1430
1431    // Return or expire this key.
1432    if ( $stats_cache ) {
1433        $time = key( $stats_cache );
1434        $data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
1435
1436        if ( is_wp_error( $data ) ) {
1437            return $data;
1438        }
1439
1440        return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
1441    }
1442
1443    // Do the dirty work.
1444    $response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
1445    if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1446        // WP_Error.
1447        $data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
1448        // WP_Error.
1449        $return = $data;
1450    } else {
1451        // string (JSON encoded object).
1452        $data = wp_remote_retrieve_body( $response );
1453        // object (rare: null on JSON failure).
1454        $return = json_decode( $data );
1455    }
1456
1457    // To reduce size in storage: store with time as key, store JSON encoded data (unless error).
1458    set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
1459
1460    return $return;
1461}
1462
1463/**
1464 * Add the Jetpack plugin version to the stats tracking data.
1465 *
1466 * @param  array $kvs The stats array in key values.
1467 * @return array
1468 */
1469function filter_stats_array_add_jp_version( $kvs ) {
1470    $kvs['j'] = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
1471
1472    return $kvs;
1473}
1474
1475/**
1476 * Convert stats array to object after sanity checking the array is valid.
1477 *
1478 * @param  array $stats_array The stats array.
1479 * @return WP_Error|Object|null
1480 */
1481function convert_stats_array_to_object( $stats_array ) {
1482    _deprecated_function( __FUNCTION__, 'jetpack-13.2', 'Automattic\Jetpack\Stats\WPCOM_Stats->convert_stats_array_to_object' );
1483
1484    return ( new WPCOM_Stats() )->convert_stats_array_to_object( $stats_array );
1485}