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