PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
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 13.9.2 14.0.1 14.1.1 All 503 releases
jetpack / modules / stats.php

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

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