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