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