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