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