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