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