PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / modules / stats.php

stats.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at modules/stats.php

1,417 lines 42.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
17 use Automattic\Jetpack\Admin_UI\Admin_Menu;
18 use Automattic\Jetpack\Connection\Client;
19 use Automattic\Jetpack\Redirect;
20 use Automattic\Jetpack\Stats\Main as Stats;
21 use Automattic\Jetpack\Stats\Options as Stats_Options;
22 use Automattic\Jetpack\Stats\Tracking_Pixel as Stats_Tracking_Pixel;
23 use Automattic\Jetpack\Stats\WPCOM_Stats;
24 use Automattic\Jetpack\Stats\XMLRPC_Provider as Stats_XMLRPC;
25 use Automattic\Jetpack\Stats_Admin\Admin_Bar as Stats_Admin_Bar;
26 use Automattic\Jetpack\Stats_Admin\Admin_Post_List_Column;
27 use Automattic\Jetpack\Stats_Admin\Dashboard as Stats_Dashboard;
28 use Automattic\Jetpack\Stats_Admin\Main as Stats_Main;
29 use Automattic\Jetpack\Status\Host;
30 use Automattic\Jetpack\Tracking;
31
32 if ( ! defined( 'ABSPATH' ) ) {
33 exit( 0 );
34 }
35
36 if ( defined( 'STATS_DASHBOARD_SERVER' ) ) {
37 return;
38 }
39
40 define( '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 */
46 const STATS_BODY_MARKER = '<div id="statchart"';
47 const STATS_CONTENT_MARKER = '<div class="gotonewdash">';
48
49 add_action( 'jetpack_modules_loaded', 'stats_load' );
50
51 /**
52 * Load Stats.
53 *
54 * @access public
55 * @return void
56 */
57 function 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 */
78 function 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 */
92 function 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 */
108 function 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 */
120 function 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 */
132 function 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 */
145 function 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 */
159 function 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 */
174 function 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 */
188 function 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 */
202 function 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 */
213 function 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 */
264 function 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 */
274 function 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 */
306 function 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 */
339 function 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 */
357 function stats_js_remove_stnojs_cookie() {
358 $parsed = wp_parse_url( admin_url() );
359 ?>
360 <script type="text/javascript">
361 /* <![CDATA[ */
362 document.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 */
371 function 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 */
385 function 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 */
633 function 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 */
647 function 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 */
659 function 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 */
675 function 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 */
704 function 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 */
720 function 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 */
734 function 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 */
748 function 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 $url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) );
752
753 return add_query_arg(
754 array_merge(
755 array(
756 'noheader' => '',
757 'proxy' => '',
758 'chart' => $chart,
759 ),
760 $args
761 ),
762 $url
763 );
764 }
765
766 /**
767 * Stats AdminBar.
768 *
769 * @deprecated 16.3
770 *
771 * @access public
772 * @param mixed $wp_admin_bar WPAdminBar.
773 * @return void
774 */
775 function stats_admin_bar_menu( &$wp_admin_bar ) {
776 _deprecated_function( __FUNCTION__, 'jetpack-16.3', 'Automattic\Jetpack\Stats_Admin\Admin_Bar::add_chart_node' );
777 Stats_Admin_Bar::add_chart_node( $wp_admin_bar );
778 }
779
780 /**
781 * Adds a Stats link to the site-name admin bar submenu, alongside Dashboard.
782 *
783 * @deprecated 16.3
784 *
785 * @access public
786 * @return void
787 */
788 function stats_add_link_to_admin_bar_site_menu() {
789 _deprecated_function( __FUNCTION__, 'jetpack-16.3', 'Automattic\Jetpack\Stats_Admin\Admin_Bar::add_site_menu_link' );
790 Stats_Admin_Bar::add_site_menu_link();
791 }
792
793 /**
794 * Stats Get Blog.
795 *
796 * @deprecated 11.5
797 *
798 * @access public
799 * @return string
800 */
801 function stats_get_blog() {
802 _deprecated_function( __METHOD__, 'jetpack-11.5' );
803 return Stats_XMLRPC::init()->get_blog();
804 }
805
806 /**
807 * Stats Dashboard Widget Options.
808 *
809 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
810 *
811 * @access public
812 * @return array
813 */
814 function stats_dashboard_widget_options() {
815 $defaults = array(
816 'chart' => 1,
817 'top' => 1,
818 'search' => 7,
819 );
820 $options = get_option( 'stats_dashboard_widget' );
821 if ( ( ! $options ) || ! is_array( $options ) ) {
822 $options = array();
823 }
824
825 // Ignore obsolete option values.
826 $intervals = array( 1, 7, 31, 90, 365 );
827 foreach ( array( 'top', 'search' ) as $key ) {
828 if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
829 unset( $options[ $key ] );
830 }
831 }
832
833 return array_merge( $defaults, $options );
834 }
835
836 /**
837 * Stats Dashboard Widget Control.
838 *
839 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
840 *
841 * @access public
842 * @return void
843 */
844 function stats_dashboard_widget_control() {
845 stats_dashboard_widget_controls_handle_submission();
846 $periods = array(
847 '1' => __( 'day', 'jetpack' ),
848 '7' => __( 'week', 'jetpack' ),
849 '31' => __( 'month', 'jetpack' ),
850 );
851 $intervals = array(
852 '1' => __( 'the past day', 'jetpack' ),
853 '7' => __( 'the past week', 'jetpack' ),
854 '31' => __( 'the past month', 'jetpack' ),
855 '90' => __( 'the past quarter', 'jetpack' ),
856 '365' => __( 'the past year', 'jetpack' ),
857 );
858 stats_dashboard_widget_controls_html( $intervals, $periods, stats_dashboard_widget_options() );
859 }
860
861 /**
862 * Handle widget controls form submission.
863 *
864 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
865 *
866 * @access public
867 * @return void
868 */
869 function stats_dashboard_widget_controls_handle_submission() {
870 $options = stats_dashboard_widget_options();
871 $defaults = array(
872 'top' => 1,
873 'search' => 7,
874 );
875
876 // Check if the correct form was submitted.
877 if ( isset( $_POST['stats_id'] ) && 'dashboard_stats' === $_POST['stats_id'] ) {
878 // Perform nonce verification.
879 if (
880 isset( $_POST['dashboard-widget-nonce'] ) &&
881 wp_verify_nonce( filter_var( wp_unslash( $_POST['dashboard-widget-nonce'] ) ), 'edit-dashboard-widget_dashboard_stats' )
882 ) {
883 // Update options.
884 $options['chart'] = isset( $_POST['chart'] ) ? (int) $_POST['chart'] : 1;
885 foreach ( array( 'top', 'search' ) as $key ) {
886 $options[ $key ] = isset( $_POST[ $key ] ) ? (int) $_POST[ $key ] : $defaults[ $key ];
887 }
888 update_option( 'stats_dashboard_widget', $options );
889 }
890 }
891 }
892
893 /**
894 * Output HTML for widget controls.
895 *
896 * @param array $intervals Array of intervals.
897 * @param array $periods Array of periods.
898 * @param array $options Array of options.
899 *
900 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
901 *
902 * @access public
903 * @return void
904 */
905 function stats_dashboard_widget_controls_html( $intervals, $periods, $options ) {
906 ?>
907 <p>
908 <label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
909 <select id="chart" name="chart">
910 <?php
911 foreach ( $periods as $val => $label ) {
912 ?>
913 <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
914 <?php
915 }
916 ?>
917 </select>.
918 </p>
919
920 <p>
921 <label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
922 <select id="top" name="top">
923 <?php
924 foreach ( $intervals as $val => $label ) {
925 ?>
926 <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
927 <?php
928 }
929 ?>
930 </select>.
931 </p>
932
933 <p>
934 <label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
935 <select id="search" name="search">
936 <?php
937 foreach ( $intervals as $val => $label ) {
938 ?>
939 <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
940 <?php
941 }
942 ?>
943 </select>.
944 </p>
945 <?php
946 }
947
948 /**
949 * Jetpack Stats Dashboard Widget.
950 *
951 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
952 *
953 * @access public
954 * @return void
955 */
956 function stats_jetpack_dashboard_widget() {
957 ?>
958 <form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
959 <?php stats_dashboard_widget_control(); ?>
960 <?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
961 <input type="hidden" name="stats_id" value="dashboard_stats" />
962 <?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
963 </form>
964 <button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
965 <span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
966 <span class="toggle-indicator" aria-hidden="true"></span>
967 </button>
968 <div id="dashboard_stats" class="is-loading">
969 <div class="inside">
970 <div style="height: 250px;"></div>
971 </div>
972 </div>
973 <?php
974 }
975
976 /**
977 * Stats Dashboard Widget Content.
978 *
979 * TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
980 *
981 * @access public
982 * @return never
983 */
984 function stats_dashboard_widget_content() {
985 $width = isset( $_GET['width'] ) ? intval( $_GET['width'] ) / 2 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
986 $height = isset( $_GET['height'] ) ? intval( $_GET['height'] ) - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
987 if ( ! $width || $width < 250 ) {
988 $width = 370;
989 }
990 if ( ! $height || $height < 230 ) {
991 $height = 180;
992 }
993
994 $_width = $width - 5;
995 $_height = $height - 5;
996
997 $options = stats_dashboard_widget_options();
998 $blog_id = Jetpack_Options::get_option( 'id' );
999
1000 $q = array(
1001 'noheader' => 'true',
1002 'proxy' => '',
1003 'blog' => $blog_id,
1004 'page' => 'stats',
1005 'chart' => '',
1006 'unit' => $options['chart'],
1007 'color' => get_user_option( 'admin_color' ),
1008 'width' => $_width,
1009 'height' => $_height,
1010 'ssl' => is_ssl(),
1011 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
1012 );
1013
1014 $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
1015
1016 $url = add_query_arg( $q, $url );
1017 $method = 'GET';
1018 $timeout = 90;
1019 $user_id = 0; // Means use the blog token.
1020
1021 $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1022 $get_code = wp_remote_retrieve_response_code( $get );
1023 if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1024 stats_print_wp_remote_error( $get, $url );
1025 } else {
1026 $body = stats_convert_post_titles( $get['body'] );
1027 $body = stats_convert_chart_urls( $body );
1028 $body = stats_convert_image_urls( $body );
1029 echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
1030 }
1031
1032 $post_ids = array();
1033
1034 $csv_end_date = current_time( 'Y-m-d' );
1035 $csv_args = array(
1036 'top' => "&limit=6&end=$csv_end_date",
1037 'search' => "&limit=5&end=$csv_end_date",
1038 );
1039
1040 $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
1041 foreach ( $top_posts as $i => $post ) {
1042 if ( 0 === $post['post_id'] ) {
1043 unset( $top_posts[ $i ] );
1044 continue;
1045 }
1046 $post_ids[] = $post['post_id'];
1047 }
1048
1049 // Cache.
1050 get_posts( array( 'include' => implode( ',', array_unique( $post_ids ) ) ) );
1051
1052 $searches = array();
1053 $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
1054 foreach ( $search_terms as $search_term ) {
1055 if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
1056 continue;
1057 }
1058 $searches[] = esc_html( $search_term['searchterm'] );
1059 }
1060
1061 ?>
1062 <div id="stats-info">
1063 <div id="stats-info-container">
1064 <div class="stats-info-header">
1065 <h2><?php esc_html_e( 'Highlights', 'jetpack' ); ?></h2>
1066 <div class="stats-info-header-right">
1067 <a href="<?php echo esc_url( admin_url( 'admin.php?page=stats' ) ); ?>" class="button button-primary">
1068 <?php esc_html_e( 'View detailed stats', 'jetpack' ); ?>
1069 </a>
1070 </div>
1071 </div>
1072 <div class="stats-info-content">
1073 <div id="top-posts" class="stats-section">
1074 <div class="stats-section-inner">
1075 <h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
1076 <?php
1077 if ( empty( $top_posts ) ) {
1078 ?>
1079 <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1080 <?php
1081 } else {
1082 foreach ( $top_posts as $post ) {
1083 if ( ! get_post( $post['post_id'] ) ) {
1084 continue;
1085 }
1086 ?>
1087 <p>
1088 <?php
1089 printf(
1090 esc_html(
1091 /* Translators: Stats dashboard widget Post list with view count: "Post Title 1 View (or Views if plural)". */
1092 _n( '%1$s %2$s View', '%1$s %2$s Views', $post['views'], 'jetpack' )
1093 ),
1094 '<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
1095 esc_html( number_format_i18n( $post['views'] ) )
1096 );
1097 ?>
1098 </p>
1099 <?php
1100 }
1101 }
1102 ?>
1103 </div>
1104 </div>
1105 <div id="top-search" class="stats-section">
1106 <div class="stats-section-inner">
1107 <h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
1108 <?php
1109 if ( empty( $searches ) ) {
1110 ?>
1111 <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1112 <?php
1113 } else {
1114 foreach ( $searches as $search_term_item ) {
1115 printf(
1116 '<p>%s</p>',
1117 esc_html( $search_term_item )
1118 );
1119 }
1120 }
1121 ?>
1122 </div>
1123 </div>
1124 </div>
1125 </div>
1126 </div>
1127 <?php
1128 exit( 0 );
1129 }
1130
1131 /**
1132 * Stats Print WP Remote Error.
1133 *
1134 * @access public
1135 * @param mixed $get Get.
1136 * @param mixed $url URL.
1137 * @return void
1138 */
1139 function stats_print_wp_remote_error( $get, $url ) {
1140 $state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
1141 $previous_error = Jetpack::state( $state_name );
1142 $error = md5( wp_json_encode( compact( 'get', 'url' ), JSON_UNESCAPED_SLASHES ) );
1143 Jetpack::state( $state_name, $error );
1144 if ( $error !== $previous_error ) {
1145 ?>
1146 <div class="wrap">
1147 <p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
1148 </div>
1149 <?php
1150 return;
1151 }
1152 ?>
1153 <div class="wrap">
1154 <p>
1155 <?php
1156 printf(
1157 /* translators: placeholder is an a href for a support site. */
1158 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' ),
1159 sprintf(
1160 '<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
1161 esc_html__( 'Jetpack Support', 'jetpack' )
1162 )
1163 );
1164 ?>
1165 </p>
1166 <pre class="stats-widget-error">
1167 User Agent: "<?php echo isset( $_SERVER['HTTP_USER_AGENT'] ) ? esc_html( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>"
1168 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 ?>"
1169 API URL: "<?php echo esc_url( $url ); ?>"
1170 <?php
1171 if ( is_wp_error( $get ) ) {
1172 foreach ( $get->get_error_codes() as $code ) {
1173 foreach ( $get->get_error_messages( $code ) as $message ) {
1174 print esc_html( $code ) . ': "' . esc_html( $message ) . '"';
1175 }
1176 }
1177 } else {
1178 $get_code = wp_remote_retrieve_response_code( $get );
1179 $content_length = strlen( wp_remote_retrieve_body( $get ) );
1180 ?>
1181 Response code: "<?php print esc_html( $get_code ); ?>"
1182 Content length: "<?php print esc_html( $content_length ); ?>"
1183 <?php
1184 }
1185 ?>
1186 </pre>
1187 </div>
1188 <?php
1189 }
1190
1191 /**
1192 * Get stats from WordPress.com
1193 *
1194 * @param string $table The stats which you want to retrieve: postviews, or searchterms.
1195 * @param array $args {
1196 * An associative array of arguments.
1197 *
1198 * @type bool $end The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
1199 * and default timezone is UTC date. Default value is Now.
1200 * @type string $days The length of the desired time frame. Default is 30. Maximum 90 days.
1201 * @type int $limit The maximum number of records to return. Default is 10. Maximum 100.
1202 * @type int $post_id The ID of the post to retrieve stats data for
1203 * @type string $summarize If present, summarizes all matching records. Default Null.
1204 * @type int $blog_id The WordPress.com blog ID to retrieve stats data for. Default is the current blog.
1205 *
1206 * }
1207 *
1208 * @return array {
1209 * An array of post view data, each post as an array
1210 *
1211 * array {
1212 * The post view data for a single post
1213 *
1214 * @type string $post_id The ID of the post
1215 * @type string $post_title The title of the post
1216 * @type string $post_permalink The permalink for the post
1217 * @type string $views The number of views for the post within the $num_days specified
1218 * }
1219 * }
1220 */
1221 function stats_get_csv( $table, $args = null ) {
1222 $defaults = array(
1223 'end' => false,
1224 'days' => false,
1225 'limit' => 3,
1226 'post_id' => false,
1227 'summarize' => '',
1228 'blog_id' => Jetpack_Options::get_option( 'id' ),
1229 );
1230
1231 $args = wp_parse_args( $args, $defaults );
1232 $args['table'] = $table;
1233
1234 $stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
1235
1236 $key = md5( $stats_csv_url );
1237
1238 // Get cache.
1239 $stats_cache = get_option( 'stats_cache' );
1240 if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
1241 $stats_cache = array();
1242 }
1243
1244 // Return or expire this key.
1245 if ( isset( $stats_cache[ $key ] ) ) {
1246 $time = key( $stats_cache[ $key ] );
1247 if ( time() - $time < 300 ) {
1248 return $stats_cache[ $key ][ $time ];
1249 }
1250 unset( $stats_cache[ $key ] );
1251 }
1252
1253 $stats_rows = array();
1254 do {
1255 $stats = stats_get_remote_csv( $stats_csv_url );
1256 if ( ! $stats ) {
1257 break;
1258 }
1259
1260 $labels = array_shift( $stats );
1261
1262 if ( 0 === stripos( $labels[0], 'error' ) ) {
1263 break;
1264 }
1265
1266 $stats_rows = array();
1267 for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
1268 $row = array();
1269 foreach ( $labels as $col => $label ) {
1270 $row[ $label ] = $stats[ $s ][ $col ];
1271 }
1272 $stats_rows[] = $row;
1273 }
1274 } while ( 0 );
1275
1276 // Expire old keys.
1277 foreach ( $stats_cache as $k => $cache ) {
1278 if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
1279 unset( $stats_cache[ $k ] );
1280 }
1281 }
1282
1283 // Set cache.
1284 $stats_cache[ $key ] = array( time() => $stats_rows );
1285 update_option( 'stats_cache', $stats_cache );
1286
1287 return $stats_rows;
1288 }
1289
1290 /**
1291 * Stats get remote CSV.
1292 *
1293 * @access public
1294 * @param mixed $url URL.
1295 * @return array
1296 */
1297 function stats_get_remote_csv( $url ) {
1298 $method = 'GET';
1299 $timeout = 90;
1300 $user_id = 0; // Blog token.
1301
1302 $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1303 $get_code = wp_remote_retrieve_response_code( $get );
1304 if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1305 return array(); // @todo: return an error?
1306 } else {
1307 return stats_str_getcsv( $get['body'] );
1308 }
1309 }
1310
1311 /**
1312 * Recursively run str_getcsv on the stats csv.
1313 *
1314 * @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
1315 *
1316 * @param mixed $csv CSV.
1317 * @return array
1318 */
1319 function stats_str_getcsv( $csv ) {
1320 // @todo Correctly handle embedded newlines. Note, despite claims online, `str_getcsv( $csv, "\n" )` does not actually work.
1321 $lines = explode( "\n", rtrim( $csv, "\n" ) );
1322 return array_map(
1323 function ( $line ) {
1324 return str_getcsv( $line, ',', '"', '' );
1325 },
1326 $lines
1327 );
1328 }
1329
1330 /**
1331 * Abstract out building the rest api stats path.
1332 *
1333 * @param string $resource Resource.
1334 * @return string
1335 */
1336 function jetpack_stats_api_path( $resource = '' ) {
1337 $resource = ltrim( $resource, '/' );
1338 return sprintf( '/sites/%d/stats/%s', Stats_Options::get_option( 'blog_id' ), $resource );
1339 }
1340
1341 /**
1342 * Fetches stats data from the REST API. Caches locally for 5 minutes.
1343 *
1344 * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
1345 * @access public
1346 * @deprecated 11.5 Use WPCOM_Stats available methodsinstead.
1347 * @param array $args (default: array()) The args that are passed to the endpoint.
1348 * @param string $resource (default: '') Optional sub-endpoint following /stats/.
1349 * @return array|WP_Error
1350 */
1351 function stats_get_from_restapi( $args = array(), $resource = '' ) {
1352 _deprecated_function( __METHOD__, 'jetpack-11.5', 'Please checkout the methods available in Automattic\Jetpack\Stats\WPCOM_Stats' );
1353 $endpoint = jetpack_stats_api_path( $resource );
1354 $api_version = '1.1';
1355 $args = wp_parse_args( $args, array() );
1356 $cache_key = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args, JSON_UNESCAPED_SLASHES ) ) ) );
1357
1358 $transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
1359
1360 $stats_cache = get_transient( $transient_name );
1361
1362 // Return or expire this key.
1363 if ( $stats_cache ) {
1364 $time = key( $stats_cache );
1365 $data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
1366
1367 if ( is_wp_error( $data ) ) {
1368 return $data;
1369 }
1370
1371 return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
1372 }
1373
1374 // Do the dirty work.
1375 $response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
1376 if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1377 // WP_Error.
1378 $data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
1379 // WP_Error.
1380 $return = $data;
1381 } else {
1382 // string (JSON encoded object).
1383 $data = wp_remote_retrieve_body( $response );
1384 // object (rare: null on JSON failure).
1385 $return = json_decode( $data );
1386 }
1387
1388 // To reduce size in storage: store with time as key, store JSON encoded data (unless error).
1389 set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
1390
1391 return $return;
1392 }
1393
1394 /**
1395 * Add the Jetpack plugin version to the stats tracking data.
1396 *
1397 * @param array $kvs The stats array in key values.
1398 * @return array
1399 */
1400 function filter_stats_array_add_jp_version( $kvs ) {
1401 $kvs['j'] = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
1402
1403 return $kvs;
1404 }
1405
1406 /**
1407 * Convert stats array to object after sanity checking the array is valid.
1408 *
1409 * @param array $stats_array The stats array.
1410 * @return WP_Error|Object|null
1411 */
1412 function convert_stats_array_to_object( $stats_array ) {
1413 _deprecated_function( __FUNCTION__, 'jetpack-13.2', 'Automattic\Jetpack\Stats\WPCOM_Stats->convert_stats_array_to_object' );
1414
1415 return ( new WPCOM_Stats() )->convert_stats_array_to_object( $stats_array );
1416 }
1417