PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 9.7.2
Jetpack – WP Security, Backup, Speed, & Growth v9.7.2
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 14.4.2 All 500 releases
jetpack / modules / stats.php
stats.php
1,686 lines 46.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Module Name: Site Stats
4 * Module Description: Collect valuable traffic stats and insights.
5 * Sort Order: 1
6 * Recommendation Order: 2
7 * First Introduced: 1.1
8 * Requires Connection: Yes
9 * Auto Activate: Yes
10 * Module Tags: 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\Connection\Client;
18 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
19 use Automattic\Jetpack\Connection\XMLRPC_Async_Call;
20 use Automattic\Jetpack\Redirect;
21 use Automattic\Jetpack\Status;
22 use Automattic\Jetpack\Tracking;
23
24 if ( defined( 'STATS_VERSION' ) ) {
25 return;
26 }
27
28 define( 'STATS_VERSION', '9' );
29 defined( 'STATS_DASHBOARD_SERVER' ) || define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
30
31 add_action( 'jetpack_modules_loaded', 'stats_load' );
32
33 /**
34 * Load Stats.
35 *
36 * @access public
37 * @return void
38 */
39 function stats_load() {
40 Jetpack::enable_module_configurable( __FILE__ );
41
42 // Generate the tracking code after wp() has queried for posts.
43 add_action( 'template_redirect', 'stats_template_redirect', 1 );
44
45 add_action( 'wp_head', 'stats_admin_bar_head', 100 );
46
47 add_action( 'wp_head', 'stats_hide_smile_css' );
48 add_action( 'embed_head', 'stats_hide_smile_css' );
49
50 add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
51
52 // Map stats caps.
53 add_filter( 'map_meta_cap', 'stats_map_meta_caps', 10, 3 );
54
55 add_action( 'admin_init', 'stats_merged_widget_admin_init' );
56
57 add_filter( 'jetpack_xmlrpc_unauthenticated_methods', 'stats_xmlrpc_methods' );
58
59 add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
60
61 // Add an icon to see stats in WordPress.com for a particular post.
62 add_action( 'admin_print_styles-edit.php', 'jetpack_stats_load_admin_css' );
63 add_filter( 'manage_posts_columns', 'jetpack_stats_post_table' );
64 add_filter( 'manage_pages_columns', 'jetpack_stats_post_table' );
65 add_action( 'manage_posts_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
66 add_action( 'manage_pages_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
67 }
68
69 /**
70 * Delay conditional for current_user_can to after init.
71 *
72 * @access public
73 * @return void
74 */
75 function stats_merged_widget_admin_init() {
76 if ( current_user_can( 'view_stats' ) ) {
77 add_action( 'load-index.php', 'stats_enqueue_dashboard_head' );
78 add_action( 'jetpack_dashboard_widget', 'stats_jetpack_dashboard_widget' );
79 }
80 }
81
82 /**
83 * Enqueue Stats Dashboard
84 *
85 * @access public
86 * @return void
87 */
88 function stats_enqueue_dashboard_head() {
89 add_action( 'admin_head', 'stats_dashboard_head' );
90 }
91
92 /**
93 * Checks if filter is set and dnt is enabled.
94 *
95 * @return bool
96 */
97 function jetpack_is_dnt_enabled() {
98 /**
99 * Filter the option which decides honor DNT or not.
100 *
101 * @module stats
102 * @since 6.1.0
103 *
104 * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
105 */
106 if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
107 return false;
108 }
109
110 foreach ( $_SERVER as $name => $value ) {
111 if ( 'http_dnt' === strtolower( $name ) && 1 === (int) $value ) {
112 return true;
113 }
114 }
115
116 return false;
117 }
118
119 /**
120 * Prevent sparkline img requests being redirected to upgrade.php.
121 * See wp-admin/admin.php where it checks $wp_db_version.
122 *
123 * @access public
124 * @param mixed $version Version.
125 * @return string $version.
126 */
127 function stats_ignore_db_version( $version ) {
128 if (
129 is_admin() &&
130 isset( $_GET['page'] ) && 'stats' === $_GET['page'] && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
131 isset( $_GET['chart'] ) && strpos( $_GET['chart'], 'admin-bar-hours' ) === 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
132 ) {
133 global $wp_db_version;
134 return $wp_db_version;
135 }
136 return $version;
137 }
138
139 /**
140 * Maps view_stats cap to read cap as needed.
141 *
142 * @access public
143 * @param mixed $caps Caps.
144 * @param mixed $cap Cap.
145 * @param mixed $user_id User ID.
146 * @return array Possibly mapped capabilities for meta capability.
147 */
148 function stats_map_meta_caps( $caps, $cap, $user_id ) {
149 // Map view_stats to exists.
150 if ( 'view_stats' === $cap ) {
151 $user = new WP_User( $user_id );
152 $user_role = array_shift( $user->roles );
153 $stats_roles = stats_get_option( 'roles' );
154
155 // Is the users role in the available stats roles?
156 if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles, true ) ) {
157 $caps = array( 'read' );
158 }
159 }
160
161 return $caps;
162 }
163
164 /**
165 * Stats Template Redirect.
166 *
167 * @access public
168 * @return void
169 */
170 function stats_template_redirect() {
171 global $current_user;
172
173 if ( is_feed() || is_robots() || is_trackback() || is_preview() || jetpack_is_dnt_enabled() ) {
174 return;
175 }
176
177 // Staging Sites should not generate tracking stats.
178 $status = new Status();
179 if ( $status->is_staging_site() ) {
180 return;
181 }
182
183 // Should we be counting this user's views?
184 if ( ! empty( $current_user->ID ) ) {
185 $count_roles = stats_get_option( 'count_roles' );
186 if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
187 return;
188 }
189 }
190
191 add_action( 'wp_footer', 'stats_footer', 101 );
192 add_action( 'web_stories_print_analytics', 'stats_footer' );
193
194 }
195
196 /**
197 * Stats Build View Data.
198 *
199 * @access public
200 * @return array.
201 */
202 function stats_build_view_data() {
203 global $wp_the_query;
204
205 $blog = Jetpack_Options::get_option( 'id' );
206 $tz = get_option( 'gmt_offset' );
207 $v = 'ext';
208 $blog_url = wp_parse_url( site_url() );
209 $srv = $blog_url['host'];
210 $j = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
211 if ( $wp_the_query->is_single || $wp_the_query->is_page || $wp_the_query->is_posts_page ) {
212 // Store and reset the queried_object and queried_object_id
213 // Otherwise, redirect_canonical() will redirect to home_url( '/' ) for show_on_front = page sites where home_url() is not all lowercase.
214 // Repro:
215 // 1. Set home_url = https://ExamPle.com/
216 // 2. Set show_on_front = page
217 // 3. Set page_on_front = something
218 // 4. Visit https://example.com/ !
219 $queried_object = isset( $wp_the_query->queried_object ) ? $wp_the_query->queried_object : null;
220 $queried_object_id = isset( $wp_the_query->queried_object_id ) ? $wp_the_query->queried_object_id : null;
221 try {
222 $post_obj = $wp_the_query->get_queried_object();
223 $post = $post_obj instanceof WP_Post ? $post_obj->ID : '0';
224 } finally {
225 $wp_the_query->queried_object = $queried_object;
226 $wp_the_query->queried_object_id = $queried_object_id;
227 }
228 } else {
229 $post = '0';
230 }
231
232 return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
233 }
234
235 /**
236 * Stats Footer.
237 *
238 * @access public
239 * @return void
240 */
241 function stats_footer() {
242 $data = stats_build_view_data();
243 if ( Jetpack_AMP_Support::is_amp_request() ) {
244 stats_render_amp_footer( $data );
245 } else {
246 stats_render_footer( $data );
247 }
248
249 }
250
251 /**
252 * Render the stats footer
253 *
254 * @param array $data Array of data for the JS stats tracker.
255 */
256 function stats_render_footer( $data ) {
257 // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
258 // When there is a way to use defer with enqueue, we can move to it and inline the custom data.
259 $script = 'https://stats.wp.com/e-' . gmdate( 'YW' ) . '.js';
260 $data_stats_array = stats_array( $data );
261
262 $stats_footer = <<<END
263 <script src='{$script}' defer></script>
264 <script>
265 _stq = window._stq || [];
266 _stq.push([ 'view', {{$data_stats_array}} ]);
267 _stq.push([ 'clickTrackerInit', '{$data['blog']}', '{$data['post']}' ]);
268 </script>
269
270 END;
271 // phpcs:enable
272 print $stats_footer; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
273 }
274
275 /**
276 * Render the stats footer for AMP output.
277 *
278 * @param array $data Array of data for the JS stats tracker.
279 */
280 function stats_render_amp_footer( $data ) {
281 $data['host'] = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var ok.
282 $data['rand'] = 'RANDOM'; // AMP placeholder.
283 $data['ref'] = 'DOCUMENT_REFERRER'; // AMP placeholder.
284 $data = array_map( 'rawurlencode', $data );
285 $pixel_url = add_query_arg( $data, 'https://pixel.wp.com/g.gif' );
286
287 ?>
288 <amp-pixel src="<?php echo esc_url( $pixel_url ); ?>"></amp-pixel>
289 <?php
290 }
291
292 /**
293 * Stats Get Options.
294 *
295 * @access public
296 * @return array.
297 */
298 function stats_get_options() {
299 $options = get_option( 'stats_options' );
300
301 if ( ! isset( $options['version'] ) || $options['version'] < STATS_VERSION ) {
302 $options = stats_upgrade_options( $options );
303 }
304
305 return $options;
306 }
307
308 /**
309 * Get Stats Options.
310 *
311 * @access public
312 * @param mixed $option Option.
313 * @return mixed|null.
314 */
315 function stats_get_option( $option ) {
316 $options = stats_get_options();
317
318 if ( 'blog_id' === $option ) {
319 return Jetpack_Options::get_option( 'id' );
320 }
321
322 if ( isset( $options[ $option ] ) ) {
323 return $options[ $option ];
324 }
325
326 return null;
327 }
328
329 /**
330 * Stats Set Options.
331 *
332 * @access public
333 * @param mixed $option Option.
334 * @param mixed $value Value.
335 * @return bool.
336 */
337 function stats_set_option( $option, $value ) {
338 $options = stats_get_options();
339
340 $options[ $option ] = $value;
341
342 return stats_set_options( $options );
343 }
344
345 /**
346 * Stats Set Options.
347 *
348 * @access public
349 * @param mixed $options Options.
350 * @return bool
351 */
352 function stats_set_options( $options ) {
353 return update_option( 'stats_options', $options );
354 }
355
356 /**
357 * Stats Upgrade Options.
358 *
359 * @access public
360 * @param mixed $options Options.
361 * @return array|bool
362 */
363 function stats_upgrade_options( $options ) {
364 $defaults = array(
365 'admin_bar' => true,
366 'roles' => array( 'administrator' ),
367 'count_roles' => array(),
368 'blog_id' => Jetpack_Options::get_option( 'id' ),
369 'do_not_track' => true, // @todo
370 'hide_smile' => true,
371 );
372
373 if ( isset( $options['reg_users'] ) ) {
374 if ( ! function_exists( 'get_editable_roles' ) ) {
375 require_once ABSPATH . 'wp-admin/includes/user.php';
376 }
377 if ( $options['reg_users'] ) {
378 $options['count_roles'] = array_keys( get_editable_roles() );
379 }
380 unset( $options['reg_users'] );
381 }
382
383 if ( is_array( $options ) && ! empty( $options ) ) {
384 $new_options = array_merge( $defaults, $options );
385 } else {
386 $new_options = $defaults;
387 }
388
389 $new_options['version'] = STATS_VERSION;
390
391 if ( ! stats_set_options( $new_options ) ) {
392 return false;
393 }
394
395 stats_update_blog();
396
397 return $new_options;
398 }
399
400 /**
401 * Creates the "array" string used as part of the JS tracker.
402 *
403 * @access public
404 * @param array $kvs KVS.
405 * @return string
406 */
407 function stats_array( $kvs ) {
408 /**
409 * Filter the options added to the JavaScript Stats tracking code.
410 *
411 * @module stats
412 *
413 * @since 1.1.0
414 *
415 * @param array $kvs Array of options about the site and page you're on.
416 */
417 $kvs = (array) apply_filters( 'stats_array', $kvs );
418 $kvs = array_map( 'addslashes', $kvs );
419 $jskvs = array();
420 foreach ( $kvs as $k => $v ) {
421 $jskvs[] = "$k:'$v'";
422 }
423 return join( ',', $jskvs );
424 }
425
426 /**
427 * Admin Pages.
428 *
429 * @access public
430 * @return void
431 */
432 function stats_admin_menu() {
433 global $pagenow;
434
435 // If we're at an old Stats URL, redirect to the new one.
436 // Don't even bother with caps, menu_page_url(), etc. Just do it.
437 if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
438 $redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', $_SERVER['REQUEST_URI'] );
439 $relative_pos = strpos( $redirect_url, '/wp-admin/' );
440 if ( false !== $relative_pos ) {
441 wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
442 exit;
443 }
444 }
445
446 $hook = add_submenu_page( 'jetpack', __( 'Site Stats', 'jetpack' ), __( 'Site Stats', 'jetpack' ), 'view_stats', 'stats', 'jetpack_admin_ui_stats_report_page_wrapper' );
447 add_action( "load-$hook", 'stats_reports_load' );
448 }
449
450 /**
451 * Stats Admin Path.
452 *
453 * @access public
454 * @return string
455 */
456 function stats_admin_path() {
457 return Jetpack::module_configuration_url( __FILE__ );
458 }
459
460 /**
461 * Stats Reports Load.
462 *
463 * @access public
464 * @return void
465 */
466 function stats_reports_load() {
467 wp_enqueue_script( 'jquery' );
468 wp_enqueue_script( 'postbox' );
469 wp_enqueue_script( 'underscore' );
470
471 Jetpack_Admin_Page::load_wrapper_styles();
472 add_action( 'admin_print_styles', 'stats_reports_css' );
473
474 if ( isset( $_GET['nojs'] ) && $_GET['nojs'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
475 $parsed = wp_parse_url( admin_url() );
476 // Remember user doesn't want JS.
477 setcookie( 'stnojs', '1', time() + 172800, $parsed['path'] ); // 2 days.
478 }
479
480 if ( isset( $_COOKIE['stnojs'] ) && $_COOKIE['stnojs'] ) {
481 // Detect if JS is on. If so, remove cookie so next page load is via JS.
482 add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
483 } elseif ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
484 // Normal page load. Load page content via JS.
485 add_action( 'admin_print_footer_scripts', 'stats_js_load_page_via_ajax' );
486 }
487 }
488
489 /**
490 * Stats Reports CSS.
491 *
492 * @access public
493 * @return void
494 */
495 function stats_reports_css() {
496 ?>
497 <style type="text/css">
498 #jp-stats-wrap {
499 max-width: 1040px;
500 margin: 0 auto;
501 overflow: hidden;
502 }
503
504 #stats-loading-wrap p {
505 text-align: center;
506 font-size: 2em;
507 margin: 7.5em 15px 0 0;
508 height: 64px;
509 line-height: 64px;
510 }
511 </style>
512 <?php
513 }
514
515 /**
516 * Detect if JS is on. If so, remove cookie so next page load is via JS.
517 *
518 * @access public
519 * @return void
520 */
521 function stats_js_remove_stnojs_cookie() {
522 $parsed = wp_parse_url( admin_url() );
523 ?>
524 <script type="text/javascript">
525 /* <![CDATA[ */
526 document.cookie = 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=<?php echo esc_js( $parsed['path'] ); ?>';
527 /* ]]> */
528 </script>
529 <?php
530 }
531
532 /**
533 * Normal page load. Load page content via JS.
534 *
535 * @access public
536 * @return void
537 */
538 function stats_js_load_page_via_ajax() {
539 ?>
540 <script type="text/javascript">
541 /* <![CDATA[ */
542 if ( -1 == document.location.href.indexOf( 'noheader' ) ) {
543 jQuery( function( $ ) {
544 $.get( document.location.href + '&noheader', function( responseText ) {
545 $( '#stats-loading-wrap' ).replaceWith( responseText );
546 } );
547 } );
548 }
549 /* ]]> */
550 </script>
551 <?php
552 }
553
554 /**
555 * Jetpack Admin Page Wrapper.
556 */
557 function jetpack_admin_ui_stats_report_page_wrapper() {
558 if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
559 Jetpack_Admin_Page::wrap_ui( 'stats_reports_page', array( 'is-wide' => true ) );
560 } else {
561 stats_reports_page();
562 }
563
564 }
565
566 /**
567 * Stats Report Page.
568 *
569 * @access public
570 * @param bool $main_chart_only (default: false) Main Chart Only.
571 */
572 function stats_reports_page( $main_chart_only = false ) {
573
574 if ( isset( $_GET['dashboard'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
575 return stats_dashboard_widget_content();
576 }
577
578 $blog_id = stats_get_option( 'blog_id' );
579 $stats_url = Redirect::get_url( 'calypso-stats' );
580
581 if ( ! $main_chart_only && ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
582 $nojs_url = add_query_arg( 'nojs', '1' );
583 $http = is_ssl() ? 'https' : 'http';
584 // Loading message. No JS fallback message.
585 ?>
586
587 <div id="jp-stats-wrap">
588 <div class="wrap">
589 <h2><?php esc_html_e( 'Site Stats', 'jetpack' ); ?>
590 <?php
591 if ( current_user_can( 'jetpack_manage_modules' ) ) :
592 $i18n_headers = jetpack_get_module_i18n( 'stats' );
593 ?>
594 <a
595 style="font-size:13px;"
596 href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack#/settings?term=' . rawurlencode( $i18n_headers['name'] ) ) ); ?>"
597 >
598 <?php esc_html_e( 'Configure', 'jetpack' ); ?>
599 </a>
600 <?php
601 endif;
602
603 /**
604 * Sets external resource URL.
605 *
606 * @module stats
607 *
608 * @since 1.4.0
609 * @todo Clean up various uses of this filter. It's seemingly filtering different types of images in different places.
610 *
611 * @param string $args URL of external resource.
612 */
613 $static_url = apply_filters( 'jetpack_static_url', "{$http}://en.wordpress.com/i/loading/loading-64.gif" );
614 ?>
615 </h2>
616 </div>
617 <div id="stats-loading-wrap" class="wrap">
618 <p class="hide-if-no-js"><img width="32" height="32" alt="<?php esc_attr_e( 'Loading&hellip;', 'jetpack' ); ?>" src="<?php echo esc_url( $static_url ); ?>" /></p>
619 <p style="font-size: 11pt; margin: 0;"><a href="<?php echo esc_url( $stats_url ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'View stats on WordPress.com right now', 'jetpack' ); ?></a></p>
620 <p class="hide-if-js"><?php esc_html_e( 'Your Site Stats work better with JavaScript enabled.', 'jetpack' ); ?><br />
621 <a href="<?php echo esc_url( $nojs_url ); ?>"><?php esc_html_e( 'View Site Stats without JavaScript', 'jetpack' ); ?></a>.</p>
622 </div>
623 </div>
624 <?php
625 return;
626 }
627
628 $day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
629 $q = array(
630 'noheader' => 'true',
631 'proxy' => '',
632 'page' => 'stats',
633 'day' => $day,
634 'blog' => $blog_id,
635 'charset' => get_option( 'blog_charset' ),
636 'color' => get_user_option( 'admin_color' ),
637 'ssl' => is_ssl(),
638 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
639 );
640 if ( get_locale() !== 'en_US' ) {
641 $q['jp_lang'] = get_locale();
642 }
643 // Only show the main chart, without extra header data, or metaboxes.
644 $q['main_chart_only'] = $main_chart_only;
645 $args = array(
646 'view' => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
647 'numdays' => 'int',
648 'day' => 'date',
649 'unit' => array( '1', '7', '31', 'human' ),
650 'humanize' => array( 'true' ),
651 'num' => 'int',
652 'summarize' => null,
653 'post' => 'int',
654 'width' => 'int',
655 'height' => 'int',
656 'data' => 'data',
657 'blog_subscribers' => 'int',
658 'comment_subscribers' => null,
659 'type' => array( 'wpcom', 'email', 'pending' ),
660 'pagenum' => 'int',
661 );
662 foreach ( $args as $var => $vals ) {
663 if ( ! isset( $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
664 continue;
665 }
666 if ( is_array( $vals ) ) {
667 if ( in_array( $_REQUEST[ $var ], $vals, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
668 $q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
669 }
670 } elseif ( 'int' === $vals ) {
671 $q[ $var ] = (int) $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
672 } elseif ( 'date' === $vals ) {
673 if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
674 $q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
675 }
676 } elseif ( null === $vals ) {
677 $q[ $var ] = '';
678 } elseif ( 'data' === $vals ) {
679 if ( 'index.php' === substr( $_REQUEST[ $var ], 0, 9 ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
680 $q[ $var ] = $_REQUEST[ $var ];// phpcs:ignore WordPress.Security.NonceVerification.Recommended
681 }
682 }
683 }
684
685 if ( isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
686 if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
687 $chart = sanitize_title( $_GET['chart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
688 $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
689 }
690 } else {
691 $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
692 }
693
694 $url = add_query_arg( $q, $url );
695 $method = 'GET';
696 $timeout = 90;
697 $user_id = 0; // Means use the blog token.
698
699 $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
700 $get_code = wp_remote_retrieve_response_code( $get );
701 if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
702 stats_print_wp_remote_error( $get, $url );
703 } else {
704 if ( ! empty( $get['headers']['content-type'] ) ) {
705 $type = $get['headers']['content-type'];
706 if ( substr( $type, 0, 5 ) === 'image' ) {
707 $img = $get['body'];
708 header( 'Content-Type: ' . $type );
709 header( 'Content-Length: ' . strlen( $img ) );
710 echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
711 die();
712 }
713 }
714 $body = stats_convert_post_titles( $get['body'] );
715 $body = stats_convert_chart_urls( $body );
716 $body = stats_convert_image_urls( $body );
717 $body = stats_convert_admin_urls( $body );
718 echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
719 }
720
721 if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
722 $tracking = new Tracking();
723 $tracking->record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
724 }
725
726 if ( isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
727 die;
728 }
729 }
730
731 /**
732 * Stats Convert Admin Urls.
733 *
734 * @access public
735 * @param mixed $html HTML.
736 * @return string
737 */
738 function stats_convert_admin_urls( $html ) {
739 return str_replace( 'index.php?page=stats', 'admin.php?page=stats', $html );
740 }
741
742 /**
743 * Stats Convert Image URLs.
744 *
745 * @access public
746 * @param mixed $html HTML.
747 * @return string
748 */
749 function stats_convert_image_urls( $html ) {
750 $url = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
751 $html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
752 return $html;
753 }
754
755 /**
756 * Callback for preg_replace_callback used in stats_convert_chart_urls()
757 *
758 * @since 5.6.0
759 *
760 * @param array $matches The matches resulting from the preg_replace_callback call.
761 * @return string The admin url for the chart.
762 */
763 function jetpack_stats_convert_chart_urls_callback( $matches ) {
764 // If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
765 return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
766 }
767
768 /**
769 * Stats Convert Chart URLs.
770 *
771 * @access public
772 * @param mixed $html HTML.
773 * @return string
774 */
775 function stats_convert_chart_urls( $html ) {
776 $html = preg_replace_callback(
777 '|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
778 'jetpack_stats_convert_chart_urls_callback',
779 $html
780 );
781 return $html;
782 }
783
784 /**
785 * Stats Convert Post Title HTML
786 *
787 * @access public
788 * @param mixed $html HTML.
789 * @return string
790 */
791 function stats_convert_post_titles( $html ) {
792 global $stats_posts;
793 $pattern = "<span class='post-(\d+)-link'>.*?</span>";
794 if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
795 return $html;
796 }
797 $posts = get_posts(
798 array(
799 'include' => implode( ',', $matches[1] ),
800 'post_type' => 'any',
801 'post_status' => 'any',
802 'numberposts' => -1,
803 'suppress_filters' => false,
804 )
805 );
806 foreach ( $posts as $post ) {
807 $stats_posts[ $post->ID ] = $post;
808 }
809 $html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
810 return $html;
811 }
812
813 /**
814 * Stats Convert Post Title Matches.
815 *
816 * @access public
817 * @param mixed $matches Matches.
818 * @return string
819 */
820 function stats_convert_post_title( $matches ) {
821 global $stats_posts;
822 $post_id = $matches[1];
823 if ( isset( $stats_posts[ $post_id ] ) ) {
824 return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
825 }
826 return $matches[0];
827 }
828
829 /**
830 * Stats Hide Smile.
831 *
832 * @access public
833 * @return void
834 */
835 function stats_hide_smile_css() {
836 $options = stats_get_options();
837 if ( isset( $options['hide_smile'] ) && $options['hide_smile'] ) {
838 ?>
839 <style type='text/css'>img#wpstats{display:none}</style>
840 <?php
841 }
842 }
843
844 /**
845 * Stats Admin Bar Head.
846 *
847 * @access public
848 * @return void
849 */
850 function stats_admin_bar_head() {
851 if ( ! stats_get_option( 'admin_bar' ) ) {
852 return;
853 }
854
855 if ( ! current_user_can( 'view_stats' ) ) {
856 return;
857 }
858
859 if ( ! is_admin_bar_showing() ) {
860 return;
861 }
862
863 add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
864 ?>
865
866 <style data-ampdevmode type='text/css'>
867 #wpadminbar .quicklinks li#wp-admin-bar-stats {
868 height: 32px;
869 }
870 #wpadminbar .quicklinks li#wp-admin-bar-stats a {
871 height: 32px;
872 padding: 0;
873 }
874 #wpadminbar .quicklinks li#wp-admin-bar-stats a div {
875 height: 32px;
876 width: 95px;
877 overflow: hidden;
878 margin: 0 10px;
879 }
880 #wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
881 width: auto;
882 margin: 0 8px 0 10px;
883 }
884 #wpadminbar .quicklinks li#wp-admin-bar-stats a img {
885 height: 24px;
886 margin: 4px 0;
887 max-width: none;
888 border: none;
889 }
890 </style>
891 <?php
892 }
893
894 /**
895 * Stats AdminBar.
896 *
897 * @access public
898 * @param mixed $wp_admin_bar WPAdminBar.
899 * @return void
900 */
901 function stats_admin_bar_menu( &$wp_admin_bar ) {
902 $url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ); // no menu_page_url() blog-side.
903
904 $img_src = esc_attr(
905 add_query_arg(
906 array(
907 'noheader' => '',
908 'proxy' => '',
909 'chart' => 'admin-bar-hours-scale',
910 ),
911 $url
912 )
913 );
914 $img_src_2x = esc_attr(
915 add_query_arg(
916 array(
917 'noheader' => '',
918 'proxy' => '',
919 'chart' => 'admin-bar-hours-scale-2x',
920 ),
921 $url
922 )
923 );
924
925 $alt = esc_attr( __( 'Stats', 'jetpack' ) );
926
927 $title = esc_attr( __( 'Views over 48 hours. Click for more Site Stats.', 'jetpack' ) );
928
929 $menu = array(
930 'id' => 'stats',
931 'href' => $url,
932 'title' => "<div><img src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
933 );
934
935 $wp_admin_bar->add_menu( $menu );
936 }
937
938 /**
939 * Stats Update Blog.
940 *
941 * @access public
942 * @return void
943 */
944 function stats_update_blog() {
945 XMLRPC_Async_Call::add_call( 'jetpack.updateBlog', 0, stats_get_blog() );
946 }
947
948 /**
949 * Stats Get Blog.
950 *
951 * @access public
952 * @return string
953 */
954 function stats_get_blog() {
955 $home = wp_parse_url( trailingslashit( get_option( 'home' ) ) );
956 $blog = array(
957 'host' => $home['host'],
958 'path' => $home['path'],
959 'blogname' => get_option( 'blogname' ),
960 'blogdescription' => get_option( 'blogdescription' ),
961 'siteurl' => get_option( 'siteurl' ),
962 'gmt_offset' => get_option( 'gmt_offset' ),
963 'timezone_string' => get_option( 'timezone_string' ),
964 'stats_version' => STATS_VERSION,
965 'stats_api' => 'jetpack',
966 'page_on_front' => get_option( 'page_on_front' ),
967 'permalink_structure' => get_option( 'permalink_structure' ),
968 'category_base' => get_option( 'category_base' ),
969 'tag_base' => get_option( 'tag_base' ),
970 );
971 $blog = array_merge( stats_get_options(), $blog );
972 unset( $blog['roles'], $blog['blog_id'] );
973 return stats_esc_html_deep( $blog );
974 }
975
976 /**
977 * Modified from stripslashes_deep()
978 *
979 * @access public
980 * @param mixed $value Value.
981 * @return string
982 */
983 function stats_esc_html_deep( $value ) {
984 if ( is_array( $value ) ) {
985 $value = array_map( 'stats_esc_html_deep', $value );
986 } elseif ( is_object( $value ) ) {
987 $vars = get_object_vars( $value );
988 foreach ( $vars as $key => $data ) {
989 $value->{$key} = stats_esc_html_deep( $data );
990 }
991 } elseif ( is_string( $value ) ) {
992 $value = esc_html( $value );
993 }
994
995 return $value;
996 }
997
998 /**
999 * Stats xmlrpc_methods function.
1000 *
1001 * @access public
1002 * @param mixed $methods Methods.
1003 * @return array
1004 */
1005 function stats_xmlrpc_methods( $methods ) {
1006 $my_methods = array(
1007 'jetpack.getBlog' => 'stats_get_blog',
1008 );
1009
1010 return array_merge( $methods, $my_methods );
1011 }
1012
1013 /**
1014 * Stats Dashboard Widget Options.
1015 *
1016 * @access public
1017 * @return array
1018 */
1019 function stats_dashboard_widget_options() {
1020 $defaults = array(
1021 'chart' => 1,
1022 'top' => 1,
1023 'search' => 7,
1024 );
1025 $options = get_option( 'stats_dashboard_widget' );
1026 if ( ( ! $options ) || ! is_array( $options ) ) {
1027 $options = array();
1028 }
1029
1030 // Ignore obsolete option values.
1031 $intervals = array( 1, 7, 31, 90, 365 );
1032 foreach ( array( 'top', 'search' ) as $key ) {
1033 if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
1034 unset( $options[ $key ] );
1035 }
1036 }
1037
1038 return array_merge( $defaults, $options );
1039 }
1040
1041 /**
1042 * Stats Dashboard Widget Control.
1043 *
1044 * @access public
1045 * @return void
1046 */
1047 function stats_dashboard_widget_control() {
1048 $periods = array(
1049 '1' => __( 'day', 'jetpack' ),
1050 '7' => __( 'week', 'jetpack' ),
1051 '31' => __( 'month', 'jetpack' ),
1052 );
1053 $intervals = array(
1054 '1' => __( 'the past day', 'jetpack' ),
1055 '7' => __( 'the past week', 'jetpack' ),
1056 '31' => __( 'the past month', 'jetpack' ),
1057 '90' => __( 'the past quarter', 'jetpack' ),
1058 '365' => __( 'the past year', 'jetpack' ),
1059 );
1060 $defaults = array(
1061 'top' => 1,
1062 'search' => 7,
1063 );
1064
1065 $options = stats_dashboard_widget_options();
1066
1067 if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) { // phpcs:ignore WordPress.Security.NonceVerification
1068 if ( isset( $periods[ $_POST['chart'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1069 $options['chart'] = $_POST['chart']; // phpcs:ignore WordPress.Security.NonceVerification
1070 }
1071 foreach ( array( 'top', 'search' ) as $key ) {
1072 if ( isset( $intervals[ $_POST[ $key ] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1073 $options[ $key ] = $_POST[ $key ]; // phpcs:ignore WordPress.Security.NonceVerification
1074 } else {
1075 $options[ $key ] = $defaults[ $key ];
1076 }
1077 }
1078 update_option( 'stats_dashboard_widget', $options );
1079 }
1080 ?>
1081 <p>
1082 <label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
1083 <select id="chart" name="chart">
1084 <?php
1085 foreach ( $periods as $val => $label ) {
1086 ?>
1087 <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
1088 <?php
1089 }
1090 ?>
1091 </select>.
1092 </p>
1093
1094 <p>
1095 <label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
1096 <select id="top" name="top">
1097 <?php
1098 foreach ( $intervals as $val => $label ) {
1099 ?>
1100 <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
1101 <?php
1102 }
1103 ?>
1104 </select>.
1105 </p>
1106
1107 <p>
1108 <label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
1109 <select id="search" name="search">
1110 <?php
1111 foreach ( $intervals as $val => $label ) {
1112 ?>
1113 <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
1114 <?php
1115 }
1116 ?>
1117 </select>.
1118 </p>
1119 <?php
1120 }
1121
1122 /**
1123 * Jetpack Stats Dashboard Widget.
1124 *
1125 * @access public
1126 * @return void
1127 */
1128 function stats_jetpack_dashboard_widget() {
1129 ?>
1130 <form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
1131 <?php stats_dashboard_widget_control(); ?>
1132 <?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
1133 <input type="hidden" name="widget_id" value="dashboard_stats" />
1134 <?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
1135 </form>
1136 <button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
1137 <span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
1138 <span class="toggle-indicator" aria-hidden="true"></span>
1139 </button>
1140 <div id="dashboard_stats">
1141 <div class="inside">
1142 <div style="height: 250px;"></div>
1143 </div>
1144 </div>
1145 <?php
1146 }
1147
1148 /**
1149 * JavaScript and CSS for dashboard widget.
1150 *
1151 * @access public
1152 * @return void
1153 */
1154 function stats_dashboard_head() {
1155 ?>
1156 <script type="text/javascript">
1157 /* <![CDATA[ */
1158 jQuery( function($) {
1159 var dashStats = jQuery( '#dashboard_stats div.inside' );
1160
1161 if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
1162 return;
1163 }
1164
1165 if ( ! dashStats.length ) {
1166 dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
1167 var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
1168 var args = 'width=' + dashStats.width() + '&height=' + h.toString();
1169 } else {
1170 if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
1171 var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
1172 } else {
1173 var args = 'width=' + ( dashStats.width() * 2 ).toString();
1174 }
1175 }
1176
1177 dashStats
1178 .not( '.dashboard-widget-control' )
1179 .load( 'admin.php?page=stats&noheader&dashboard&' + args );
1180
1181 jQuery( window ).one( 'resize', function() {
1182 jQuery( '#stat-chart' ).css( 'width', 'auto' );
1183 } );
1184
1185
1186 // Widget settings toggle container.
1187 var toggle = $( '.js-toggle-stats_dashboard_widget_control' );
1188
1189 // Move the toggle in the widget header.
1190 toggle.appendTo( '#jetpack_summary_widget .handle-actions' );
1191
1192 // Toggle settings when clicking on it.
1193 toggle.show().click( function( e ) {
1194 e.preventDefault();
1195 e.stopImmediatePropagation();
1196 $( this ).parent().toggleClass( 'controlVisible' );
1197 $( '#stats_dashboard_widget_control' ).slideToggle();
1198 } );
1199 } );
1200 /* ]]> */
1201 </script>
1202 <?php
1203 }
1204
1205 /**
1206 * Stats Dashboard Widget Content.
1207 *
1208 * @access public
1209 * @return void
1210 */
1211 function stats_dashboard_widget_content() {
1212 $width = isset( $_GET['width'] ) ? (int) ( $_GET['width'] / 2 ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1213 $height = isset( $_GET['height'] ) ? (int) $_GET['height'] - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1214 if ( ! $width || $width < 250 ) {
1215 $width = 370;
1216 }
1217 if ( ! $height || $height < 230 ) {
1218 $height = 180;
1219 }
1220
1221 $_width = $width - 5;
1222 $_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack! @todo Remove WordPress 5.8 is minimum. IE should be fully deprecated.
1223
1224 $options = stats_dashboard_widget_options();
1225 $blog_id = Jetpack_Options::get_option( 'id' );
1226
1227 $q = array(
1228 'noheader' => 'true',
1229 'proxy' => '',
1230 'blog' => $blog_id,
1231 'page' => 'stats',
1232 'chart' => '',
1233 'unit' => $options['chart'],
1234 'color' => get_user_option( 'admin_color' ),
1235 'width' => $_width,
1236 'height' => $_height,
1237 'ssl' => is_ssl(),
1238 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
1239 );
1240
1241 $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
1242
1243 $url = add_query_arg( $q, $url );
1244 $method = 'GET';
1245 $timeout = 90;
1246 $user_id = 0; // Means use the blog token.
1247
1248 $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1249 $get_code = wp_remote_retrieve_response_code( $get );
1250 if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1251 stats_print_wp_remote_error( $get, $url );
1252 } else {
1253 $body = stats_convert_post_titles( $get['body'] );
1254 $body = stats_convert_chart_urls( $body );
1255 $body = stats_convert_image_urls( $body );
1256 echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
1257 }
1258
1259 $post_ids = array();
1260
1261 $csv_end_date = gmdate( 'Y-m-d' );
1262 $csv_args = array(
1263 'top' => "&limit=8&end=$csv_end_date",
1264 'search' => "&limit=5&end=$csv_end_date",
1265 );
1266
1267 $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
1268 foreach ( $top_posts as $i => $post ) {
1269 if ( 0 === $post['post_id'] ) {
1270 unset( $top_posts[ $i ] );
1271 continue;
1272 }
1273 $post_ids[] = $post['post_id'];
1274 }
1275
1276 // Cache.
1277 get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
1278
1279 $searches = array();
1280 $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
1281 foreach ( $search_terms as $search_term ) {
1282 if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
1283 continue;
1284 }
1285 $searches[] = esc_html( $search_term['searchterm'] );
1286 }
1287
1288 ?>
1289 <div id="stats-info">
1290 <div id="top-posts" class='stats-section'>
1291 <div class="stats-section-inner">
1292 <h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
1293 <?php
1294 if ( empty( $top_posts ) ) {
1295 ?>
1296 <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1297 <?php
1298 } else {
1299 foreach ( $top_posts as $post ) {
1300 if ( ! get_post( $post['post_id'] ) ) {
1301 continue;
1302 }
1303 ?>
1304 <p>
1305 <?php
1306 printf(
1307 /* Translators: Stats dashboard widget postviews list: "$post_title $views Views". */
1308 esc_html__( '%1$s %2$s Views', 'jetpack' ),
1309 '<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
1310 esc_html( number_format_i18n( $post['views'] ) )
1311 );
1312 ?>
1313 </p>
1314 <?php
1315 }
1316 }
1317 ?>
1318 </div>
1319 </div>
1320 <div id="top-search" class='stats-section'>
1321 <div class="stats-section-inner">
1322 <h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
1323 <?php
1324 if ( empty( $searches ) ) {
1325 ?>
1326 <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1327 <?php
1328 } else {
1329 foreach ( $searches as $search_term_item ) {
1330 printf(
1331 '<p>%s</p>',
1332 esc_html( $search_term_item )
1333 );
1334 }
1335 }
1336 ?>
1337 </div>
1338 </div>
1339 </div>
1340 <div class="clear"></div>
1341 <div class="stats-view-all">
1342 <?php
1343 $stats_day_url = Redirect::get_url( 'calypso-stats-day' );
1344 printf(
1345 '<a class="button" target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a>',
1346 esc_url( $stats_day_url ),
1347 esc_html__( 'View all stats', 'jetpack' )
1348 );
1349 ?>
1350 </div>
1351 <div class="clear"></div>
1352 <?php
1353 exit;
1354 }
1355
1356 /**
1357 * Stats Print WP Remote Error.
1358 *
1359 * @access public
1360 * @param mixed $get Get.
1361 * @param mixed $url URL.
1362 * @return void
1363 */
1364 function stats_print_wp_remote_error( $get, $url ) {
1365 $state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
1366 $previous_error = Jetpack::state( $state_name );
1367 $error = md5( wp_json_encode( compact( 'get', 'url' ) ) );
1368 Jetpack::state( $state_name, $error );
1369 if ( $error !== $previous_error ) {
1370 ?>
1371 <div class="wrap">
1372 <p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
1373 </div>
1374 <?php
1375 return;
1376 }
1377 ?>
1378 <div class="wrap">
1379 <p>
1380 <?php
1381 printf(
1382 /* translators: placeholder is an a href for a support site. */
1383 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' ),
1384 sprintf(
1385 '<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
1386 esc_html__( 'Jetpack Support', 'jetpack' )
1387 )
1388 );
1389 ?>
1390 </p>
1391 <pre>
1392 User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
1393 Page URL: "http<?php echo ( is_ssl() ? 's' : '' ) . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
1394 API URL: "<?php echo esc_url( $url ); ?>"
1395 <?php
1396 if ( is_wp_error( $get ) ) {
1397 foreach ( $get->get_error_codes() as $code ) {
1398 foreach ( $get->get_error_messages( $code ) as $message ) {
1399 ?>
1400 <?php print esc_html( $code ) . ': "' . esc_html( $message ) . '"'; ?>
1401
1402 <?php
1403 }
1404 }
1405 } else {
1406 $get_code = wp_remote_retrieve_response_code( $get );
1407 $content_length = strlen( wp_remote_retrieve_body( $get ) );
1408 ?>
1409 Response code: "<?php print esc_html( $get_code ); ?>"
1410 Content length: "<?php print esc_html( $content_length ); ?>"
1411
1412 <?php
1413 }
1414 ?>
1415 </pre>
1416 </div>
1417 <?php
1418 }
1419
1420 /**
1421 * Get stats from WordPress.com
1422 *
1423 * @param string $table The stats which you want to retrieve: postviews, or searchterms.
1424 * @param array $args {
1425 * An associative array of arguments.
1426 *
1427 * @type bool $end The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
1428 * and default timezone is UTC date. Default value is Now.
1429 * @type string $days The length of the desired time frame. Default is 30. Maximum 90 days.
1430 * @type int $limit The maximum number of records to return. Default is 10. Maximum 100.
1431 * @type int $post_id The ID of the post to retrieve stats data for
1432 * @type string $summarize If present, summarizes all matching records. Default Null.
1433 *
1434 * }
1435 *
1436 * @return array {
1437 * An array of post view data, each post as an array
1438 *
1439 * array {
1440 * The post view data for a single post
1441 *
1442 * @type string $post_id The ID of the post
1443 * @type string $post_title The title of the post
1444 * @type string $post_permalink The permalink for the post
1445 * @type string $views The number of views for the post within the $num_days specified
1446 * }
1447 * }
1448 */
1449 function stats_get_csv( $table, $args = null ) {
1450 $defaults = array(
1451 'end' => false,
1452 'days' => false,
1453 'limit' => 3,
1454 'post_id' => false,
1455 'summarize' => '',
1456 );
1457
1458 $args = wp_parse_args( $args, $defaults );
1459 $args['table'] = $table;
1460 $args['blog_id'] = Jetpack_Options::get_option( 'id' );
1461
1462 $stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
1463
1464 $key = md5( $stats_csv_url );
1465
1466 // Get cache.
1467 $stats_cache = get_option( 'stats_cache' );
1468 if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
1469 $stats_cache = array();
1470 }
1471
1472 // Return or expire this key.
1473 if ( isset( $stats_cache[ $key ] ) ) {
1474 $time = key( $stats_cache[ $key ] );
1475 if ( time() - $time < 300 ) {
1476 return $stats_cache[ $key ][ $time ];
1477 }
1478 unset( $stats_cache[ $key ] );
1479 }
1480
1481 $stats_rows = array();
1482 do {
1483 $stats = stats_get_remote_csv( $stats_csv_url );
1484 if ( ! $stats ) {
1485 break;
1486 }
1487
1488 $labels = array_shift( $stats );
1489
1490 if ( 0 === stripos( $labels[0], 'error' ) ) {
1491 break;
1492 }
1493
1494 $stats_rows = array();
1495 for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
1496 $row = array();
1497 foreach ( $labels as $col => $label ) {
1498 $row[ $label ] = $stats[ $s ][ $col ];
1499 }
1500 $stats_rows[] = $row;
1501 }
1502 } while ( 0 );
1503
1504 // Expire old keys.
1505 foreach ( $stats_cache as $k => $cache ) {
1506 if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
1507 unset( $stats_cache[ $k ] );
1508 }
1509 }
1510
1511 // Set cache.
1512 $stats_cache[ $key ] = array( time() => $stats_rows );
1513 update_option( 'stats_cache', $stats_cache );
1514
1515 return $stats_rows;
1516 }
1517
1518 /**
1519 * Stats get remote CSV.
1520 *
1521 * @access public
1522 * @param mixed $url URL.
1523 * @return array
1524 */
1525 function stats_get_remote_csv( $url ) {
1526 $method = 'GET';
1527 $timeout = 90;
1528 $user_id = 0; // Blog token.
1529
1530 $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1531 $get_code = wp_remote_retrieve_response_code( $get );
1532 if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1533 return array(); // @todo: return an error?
1534 } else {
1535 return stats_str_getcsv( $get['body'] );
1536 }
1537 }
1538
1539 /**
1540 * Recursively run str_getcsv on the stats csv.
1541 *
1542 * @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
1543 *
1544 * @param mixed $csv CSV.
1545 * @return array.
1546 */
1547 function stats_str_getcsv( $csv ) {
1548 $lines = str_getcsv( $csv, "\n" );
1549 return array_map( 'str_getcsv', $lines );
1550 }
1551
1552 /**
1553 * Abstract out building the rest api stats path.
1554 *
1555 * @param string $resource Resource.
1556 * @return string
1557 */
1558 function jetpack_stats_api_path( $resource = '' ) {
1559 $resource = ltrim( $resource, '/' );
1560 return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
1561 }
1562
1563 /**
1564 * Fetches stats data from the REST API. Caches locally for 5 minutes.
1565 *
1566 * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
1567 * @access public
1568 * @param array $args (default: array()) The args that are passed to the endpoint.
1569 * @param string $resource (default: '') Optional sub-endpoint following /stats/.
1570 * @return array|WP_Error.
1571 */
1572 function stats_get_from_restapi( $args = array(), $resource = '' ) {
1573 $endpoint = jetpack_stats_api_path( $resource );
1574 $api_version = '1.1';
1575 $args = wp_parse_args( $args, array() );
1576 $cache_key = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args ) ) ) );
1577
1578 $transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
1579
1580 $stats_cache = get_transient( $transient_name );
1581
1582 // Return or expire this key.
1583 if ( $stats_cache ) {
1584 $time = key( $stats_cache );
1585 $data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
1586
1587 if ( is_wp_error( $data ) ) {
1588 return $data;
1589 }
1590
1591 return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
1592 }
1593
1594 // Do the dirty work.
1595 $response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
1596 if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1597 // WP_Error.
1598 $data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
1599 // WP_Error.
1600 $return = $data;
1601 } else {
1602 // string (JSON encoded object).
1603 $data = wp_remote_retrieve_body( $response );
1604 // object (rare: null on JSON failure).
1605 $return = json_decode( $data );
1606 }
1607
1608 // To reduce size in storage: store with time as key, store JSON encoded data (unless error).
1609 set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
1610
1611 return $return;
1612 }
1613
1614 /**
1615 * Load CSS needed for Stats column width in WP-Admin area.
1616 *
1617 * @since 4.7.0
1618 */
1619 function jetpack_stats_load_admin_css() {
1620 ?>
1621 <style type="text/css">
1622 .fixed .column-stats {
1623 width: 5em;
1624 }
1625 </style>
1626 <?php
1627 }
1628
1629 /**
1630 * Set header for column that allows to go to WordPress.com to see an entry's stats.
1631 *
1632 * @param array $columns An array of column names.
1633 *
1634 * @since 4.7.0
1635 *
1636 * @return mixed
1637 */
1638 function jetpack_stats_post_table( $columns ) {
1639 // Adds a stats link on the edit posts page.
1640 if ( ! current_user_can( 'view_stats' ) || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1641 return $columns;
1642 }
1643 // Array-Fu to add before comments.
1644 $pos = array_search( 'comments', array_keys( $columns ), true );
1645 if ( ! is_int( $pos ) ) {
1646 return $columns;
1647 }
1648 $chunks = array_chunk( $columns, $pos, true );
1649 $chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
1650
1651 return call_user_func_array( 'array_merge', $chunks );
1652 }
1653
1654 /**
1655 * Set content for cell with link to an entry's stats in WordPress.com.
1656 *
1657 * @param string $column The name of the column to display.
1658 * @param int $post_id The current post ID.
1659 *
1660 * @since 4.7.0
1661 *
1662 * @return mixed
1663 */
1664 function jetpack_stats_post_table_cell( $column, $post_id ) {
1665 if ( 'stats' === $column ) {
1666 if ( 'publish' !== get_post_status( $post_id ) ) {
1667 printf(
1668 '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
1669 esc_html__( 'No stats', 'jetpack' )
1670 );
1671 } else {
1672 $stats_post_url = Redirect::get_url(
1673 'calypso-stats-post',
1674 array(
1675 'path' => $post_id,
1676 )
1677 );
1678 printf(
1679 '<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
1680 esc_url( $stats_post_url ),
1681 esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
1682 );
1683 }
1684 }
1685 }
1686