PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.0
Jetpack – WP Security, Backup, Speed, & Growth v16.0
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 14.2.2 14.3.1 All 501 releases
jetpack / modules / stats.php

stats.php in Jetpack – WP Security, Backup, Speed, & Growth 16.0, at modules/stats.php

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