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