Completed
Push — update/admin-menu-sync-wpcom ( b0ac91...43248e )
by
unknown
124:17 queued 113:08
created

stats.php ➔ stats_dashboard_widget_options()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 6
nop 0
dl 0
loc 21
rs 8.9617
c 0
b 0
f 0
1
<?php
2
/**
3
 * Module Name: Site Stats
4
 * Module Description: Collect valuable traffic stats and insights.
5
 * Sort Order: 1
6
 * Recommendation Order: 2
7
 * First Introduced: 1.1
8
 * Requires Connection: Yes
9
 * Auto Activate: Yes
10
 * Module Tags: Site Stats, Recommended
11
 * Feature: Engagement
12
 * Additional Search Queries: statistics, tracking, analytics, views, traffic, stats
13
 *
14
 * @package automattic/jetpack
15
 */
16
17
use Automattic\Jetpack\Connection\Client;
18
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
19
use Automattic\Jetpack\Connection\XMLRPC_Async_Call;
20
use Automattic\Jetpack\Redirect;
21
use Automattic\Jetpack\Status;
22
use Automattic\Jetpack\Tracking;
23
24
if ( defined( 'STATS_VERSION' ) ) {
25
	return;
26
}
27
28
define( 'STATS_VERSION', '9' );
29
defined( 'STATS_DASHBOARD_SERVER' ) || define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
30
31
add_action( 'jetpack_modules_loaded', 'stats_load' );
32
33
/**
34
 * Load Stats.
35
 *
36
 * @access public
37
 * @return void
38
 */
39
function stats_load() {
40
	Jetpack::enable_module_configurable( __FILE__ );
41
42
	// Generate the tracking code after wp() has queried for posts.
43
	add_action( 'template_redirect', 'stats_template_redirect', 1 );
44
45
	add_action( 'wp_head', 'stats_admin_bar_head', 100 );
46
47
	add_action( 'wp_head', 'stats_hide_smile_css' );
48
	add_action( 'embed_head', 'stats_hide_smile_css' );
49
50
	add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
51
52
	// Map stats caps.
53
	add_filter( 'map_meta_cap', 'stats_map_meta_caps', 10, 3 );
54
55
	add_action( 'admin_init', 'stats_merged_widget_admin_init' );
56
57
	add_filter( 'jetpack_xmlrpc_unauthenticated_methods', 'stats_xmlrpc_methods' );
58
59
	add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
60
61
	// Add an icon to see stats in WordPress.com for a particular post.
62
	add_action( 'admin_print_styles-edit.php', 'jetpack_stats_load_admin_css' );
63
	add_filter( 'manage_posts_columns', 'jetpack_stats_post_table' );
64
	add_filter( 'manage_pages_columns', 'jetpack_stats_post_table' );
65
	add_action( 'manage_posts_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
66
	add_action( 'manage_pages_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
67
}
68
69
/**
70
 * Delay conditional for current_user_can to after init.
71
 *
72
 * @access public
73
 * @return void
74
 */
75
function stats_merged_widget_admin_init() {
76
	if ( current_user_can( 'view_stats' ) ) {
77
		add_action( 'load-index.php', 'stats_enqueue_dashboard_head' );
78
		add_action( 'jetpack_dashboard_widget', 'stats_jetpack_dashboard_widget' );
79
	}
80
}
81
82
/**
83
 * Enqueue Stats Dashboard
84
 *
85
 * @access public
86
 * @return void
87
 */
88
function stats_enqueue_dashboard_head() {
89
	add_action( 'admin_head', 'stats_dashboard_head' );
90
}
91
92
/**
93
 * Checks if filter is set and dnt is enabled.
94
 *
95
 * @return bool
96
 */
97
function jetpack_is_dnt_enabled() {
98
	/**
99
	 * Filter the option which decides honor DNT or not.
100
	 *
101
	 * @module stats
102
	 * @since 6.1.0
103
	 *
104
	 * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
105
	 */
106
	if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
107
		return false;
108
	}
109
110
	foreach ( $_SERVER as $name => $value ) {
111
		if ( 'http_dnt' === strtolower( $name ) && 1 === (int) $value ) {
112
			return true;
113
		}
114
	}
115
116
	return false;
117
}
118
119
/**
120
 * Prevent sparkline img requests being redirected to upgrade.php.
121
 * See wp-admin/admin.php where it checks $wp_db_version.
122
 *
123
 * @access public
124
 * @param mixed $version Version.
125
 * @return string $version.
126
 */
127
function stats_ignore_db_version( $version ) {
128
	if (
129
		is_admin() &&
130
		isset( $_GET['page'] ) && 'stats' === $_GET['page'] && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
131
		isset( $_GET['chart'] ) && strpos( $_GET['chart'], 'admin-bar-hours' ) === 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
132
	) {
133
		global $wp_db_version;
134
		return $wp_db_version;
135
	}
136
	return $version;
137
}
138
139
/**
140
 * Maps view_stats cap to read cap as needed.
141
 *
142
 * @access public
143
 * @param mixed $caps Caps.
144
 * @param mixed $cap Cap.
145
 * @param mixed $user_id User ID.
146
 * @return array Possibly mapped capabilities for meta capability.
147
 */
148
function stats_map_meta_caps( $caps, $cap, $user_id ) {
149
	// Map view_stats to exists.
150
	if ( 'view_stats' === $cap ) {
151
		$user        = new WP_User( $user_id );
152
		$user_role   = array_shift( $user->roles );
153
		$stats_roles = stats_get_option( 'roles' );
154
155
		// Is the users role in the available stats roles?
156
		if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles, true ) ) {
157
			$caps = array( 'read' );
158
		}
159
	}
160
161
	return $caps;
162
}
163
164
/**
165
 * Stats Template Redirect.
166
 *
167
 * @access public
168
 * @return void
169
 */
170
function stats_template_redirect() {
171
	global $current_user;
172
173
	if ( is_feed() || is_robots() || is_trackback() || is_preview() || jetpack_is_dnt_enabled() ) {
174
		return;
175
	}
176
177
	// Staging Sites should not generate tracking stats.
178
	$status = new Status();
179
	if ( $status->is_staging_site() ) {
180
		return;
181
	}
182
183
	// Should we be counting this user's views?
184
	if ( ! empty( $current_user->ID ) ) {
185
		$count_roles = stats_get_option( 'count_roles' );
186
		if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
187
			return;
188
		}
189
	}
190
191
	add_action( 'wp_footer', 'stats_footer', 101 );
192
	add_action( 'web_stories_print_analytics', 'stats_footer' );
193
194
}
195
196
/**
197
 * Stats Build View Data.
198
 *
199
 * @access public
200
 * @return array.
0 ignored issues
show
Documentation introduced by
The doc-type array. could not be parsed: Unknown type name "array." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
201
 */
202
function stats_build_view_data() {
203
	global $wp_the_query;
204
205
	$blog     = Jetpack_Options::get_option( 'id' );
206
	$tz       = get_option( 'gmt_offset' );
207
	$v        = 'ext';
208
	$blog_url = wp_parse_url( site_url() );
209
	$srv      = $blog_url['host'];
210
	$j        = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
211
	if ( $wp_the_query->is_single || $wp_the_query->is_page || $wp_the_query->is_posts_page ) {
212
		// Store and reset the queried_object and queried_object_id
213
		// Otherwise, redirect_canonical() will redirect to home_url( '/' ) for show_on_front = page sites where home_url() is not all lowercase.
214
		// Repro:
215
		// 1. Set home_url = https://ExamPle.com/
216
		// 2. Set show_on_front = page
217
		// 3. Set page_on_front = something
218
		// 4. Visit https://example.com/ !
219
		$queried_object    = isset( $wp_the_query->queried_object ) ? $wp_the_query->queried_object : null;
220
		$queried_object_id = isset( $wp_the_query->queried_object_id ) ? $wp_the_query->queried_object_id : null;
221
		try {
222
			$post_obj = $wp_the_query->get_queried_object();
223
			$post     = $post_obj instanceof WP_Post ? $post_obj->ID : '0';
0 ignored issues
show
Bug introduced by
The class WP_Post does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
224
		} finally {
225
			$wp_the_query->queried_object    = $queried_object;
226
			$wp_the_query->queried_object_id = $queried_object_id;
227
		}
228
	} else {
229
		$post = '0';
230
	}
231
232
	return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
233
}
234
235
/**
236
 * Stats Footer.
237
 *
238
 * @access public
239
 * @return void
240
 */
241
function stats_footer() {
242
	$data = stats_build_view_data();
243
	if ( Jetpack_AMP_Support::is_amp_request() ) {
244
		stats_render_amp_footer( $data );
245
	} else {
246
		stats_render_footer( $data );
247
	}
248
249
}
250
251
/**
252
 * Render the stats footer
253
 *
254
 * @param array $data Array of data for the JS stats tracker.
255
 */
256
function stats_render_footer( $data ) {
257
	// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
258
	// When there is a way to use defer with enqueue, we can move to it and inline the custom data.
259
	$script           = 'https://stats.wp.com/e-' . gmdate( 'YW' ) . '.js';
260
	$data_stats_array = stats_array( $data );
261
262
	$stats_footer = <<<END
263
<script src='{$script}' defer></script>
264
<script>
265
	_stq = window._stq || [];
266
	_stq.push([ 'view', {{$data_stats_array}} ]);
267
	_stq.push([ 'clickTrackerInit', '{$data['blog']}', '{$data['post']}' ]);
268
</script>
269
270
END;
271
	// phpcs:enable
272
	print $stats_footer; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
273
}
274
275
/**
276
 * Render the stats footer for AMP output.
277
 *
278
 * @param array $data Array of data for the JS stats tracker.
279
 */
280
function stats_render_amp_footer( $data ) {
281
	$data['host'] = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var ok.
282
	$data['rand'] = 'RANDOM'; // AMP placeholder.
283
	$data['ref']  = 'DOCUMENT_REFERRER'; // AMP placeholder.
284
	$data         = array_map( 'rawurlencode', $data );
285
	$pixel_url    = add_query_arg( $data, 'https://pixel.wp.com/g.gif' );
286
287
	?>
288
	<amp-pixel src="<?php echo esc_url( $pixel_url ); ?>"></amp-pixel>
289
	<?php
290
}
291
292
/**
293
 * Stats Get Options.
294
 *
295
 * @access public
296
 * @return array.
0 ignored issues
show
Documentation introduced by
The doc-type array. could not be parsed: Unknown type name "array." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
297
 */
298
function stats_get_options() {
299
	$options = get_option( 'stats_options' );
300
301
	if ( ! isset( $options['version'] ) || $options['version'] < STATS_VERSION ) {
302
		$options = stats_upgrade_options( $options );
303
	}
304
305
	return $options;
306
}
307
308
/**
309
 * Get Stats Options.
310
 *
311
 * @access public
312
 * @param mixed $option Option.
313
 * @return mixed|null.
0 ignored issues
show
Documentation introduced by
The doc-type mixed|null. could not be parsed: Unknown type name "null." at position 6. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
314
 */
315
function stats_get_option( $option ) {
316
	$options = stats_get_options();
317
318
	if ( 'blog_id' === $option ) {
319
		return Jetpack_Options::get_option( 'id' );
320
	}
321
322
	if ( isset( $options[ $option ] ) ) {
323
		return $options[ $option ];
324
	}
325
326
	return null;
327
}
328
329
/**
330
 * Stats Set Options.
331
 *
332
 * @access public
333
 * @param mixed $option Option.
334
 * @param mixed $value Value.
335
 * @return bool.
0 ignored issues
show
Documentation introduced by
The doc-type bool. could not be parsed: Unknown type name "bool." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
336
 */
337
function stats_set_option( $option, $value ) {
338
	$options = stats_get_options();
339
340
	$options[ $option ] = $value;
341
342
	return stats_set_options( $options );
343
}
344
345
/**
346
 * Stats Set Options.
347
 *
348
 * @access public
349
 * @param mixed $options Options.
350
 * @return bool
351
 */
352
function stats_set_options( $options ) {
353
	return update_option( 'stats_options', $options );
354
}
355
356
/**
357
 * Stats Upgrade Options.
358
 *
359
 * @access public
360
 * @param mixed $options Options.
361
 * @return array|bool
362
 */
363
function stats_upgrade_options( $options ) {
364
	$defaults = array(
365
		'admin_bar'    => true,
366
		'roles'        => array( 'administrator' ),
367
		'count_roles'  => array(),
368
		'blog_id'      => Jetpack_Options::get_option( 'id' ),
369
		'do_not_track' => true, // @todo
370
		'hide_smile'   => true,
371
	);
372
373
	if ( isset( $options['reg_users'] ) ) {
374
		if ( ! function_exists( 'get_editable_roles' ) ) {
375
			require_once ABSPATH . 'wp-admin/includes/user.php';
376
		}
377
		if ( $options['reg_users'] ) {
378
			$options['count_roles'] = array_keys( get_editable_roles() );
379
		}
380
		unset( $options['reg_users'] );
381
	}
382
383
	if ( is_array( $options ) && ! empty( $options ) ) {
384
		$new_options = array_merge( $defaults, $options );
385
	} else {
386
		$new_options = $defaults;
387
	}
388
389
	$new_options['version'] = STATS_VERSION;
390
391
	if ( ! stats_set_options( $new_options ) ) {
392
		return false;
393
	}
394
395
	stats_update_blog();
396
397
	return $new_options;
398
}
399
400
/**
401
 * Creates the "array" string used as part of the JS tracker.
402
 *
403
 * @access public
404
 * @param array $kvs KVS.
405
 * @return string
406
 */
407
function stats_array( $kvs ) {
408
	/**
409
	 * Filter the options added to the JavaScript Stats tracking code.
410
	 *
411
	 * @module stats
412
	 *
413
	 * @since 1.1.0
414
	 *
415
	 * @param array $kvs Array of options about the site and page you're on.
416
	 */
417
	$kvs   = (array) apply_filters( 'stats_array', $kvs );
418
	$kvs   = array_map( 'addslashes', $kvs );
419
	$jskvs = array();
420
	foreach ( $kvs as $k => $v ) {
421
		$jskvs[] = "$k:'$v'";
422
	}
423
	return join( ',', $jskvs );
424
}
425
426
/**
427
 * Admin Pages.
428
 *
429
 * @access public
430
 * @return void
431
 */
432
function stats_admin_menu() {
433
	global $pagenow;
434
435
	// If we're at an old Stats URL, redirect to the new one.
436
	// Don't even bother with caps, menu_page_url(), etc.  Just do it.
437
	if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
438
		$redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', $_SERVER['REQUEST_URI'] );
439
		$relative_pos = strpos( $redirect_url, '/wp-admin/' );
440
		if ( false !== $relative_pos ) {
441
			wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
442
			exit;
443
		}
444
	}
445
446
	$hook = add_submenu_page( 'jetpack', __( 'Site Stats', 'jetpack' ), __( 'Site Stats', 'jetpack' ), 'view_stats', 'stats', 'jetpack_admin_ui_stats_report_page_wrapper' );
447
	add_action( "load-$hook", 'stats_reports_load' );
448
}
449
450
/**
451
 * Stats Admin Path.
452
 *
453
 * @access public
454
 * @return string
455
 */
456
function stats_admin_path() {
457
	return Jetpack::module_configuration_url( __FILE__ );
458
}
459
460
/**
461
 * Stats Reports Load.
462
 *
463
 * @access public
464
 * @return void
465
 */
466
function stats_reports_load() {
467
	wp_enqueue_script( 'jquery' );
468
	wp_enqueue_script( 'postbox' );
469
	wp_enqueue_script( 'underscore' );
470
471
	Jetpack_Admin_Page::load_wrapper_styles();
472
	add_action( 'admin_print_styles', 'stats_reports_css' );
473
474
	if ( isset( $_GET['nojs'] ) && $_GET['nojs'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
475
		$parsed = wp_parse_url( admin_url() );
476
		// Remember user doesn't want JS.
477
		setcookie( 'stnojs', '1', time() + 172800, $parsed['path'] ); // 2 days.
478
	}
479
480
	if ( isset( $_COOKIE['stnojs'] ) && $_COOKIE['stnojs'] ) {
481
		// Detect if JS is on.  If so, remove cookie so next page load is via JS.
482
		add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
483
	} elseif ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
484
		// Normal page load.  Load page content via JS.
485
		add_action( 'admin_print_footer_scripts', 'stats_js_load_page_via_ajax' );
486
	}
487
}
488
489
/**
490
 * Stats Reports CSS.
491
 *
492
 * @access public
493
 * @return void
494
 */
495
function stats_reports_css() {
496
	?>
497
<style type="text/css">
498
#jp-stats-wrap {
499
	max-width: 1040px;
500
	margin: 0 auto;
501
	overflow: hidden;
502
}
503
504
#stats-loading-wrap p {
505
	text-align: center;
506
	font-size: 2em;
507
	margin: 7.5em 15px 0 0;
508
	height: 64px;
509
	line-height: 64px;
510
}
511
</style>
512
	<?php
513
}
514
515
/**
516
 * Detect if JS is on.  If so, remove cookie so next page load is via JS.
517
 *
518
 * @access public
519
 * @return void
520
 */
521
function stats_js_remove_stnojs_cookie() {
522
	$parsed = wp_parse_url( admin_url() );
523
	?>
524
<script type="text/javascript">
525
/* <![CDATA[ */
526
document.cookie = 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=<?php echo esc_js( $parsed['path'] ); ?>';
527
/* ]]> */
528
</script>
529
	<?php
530
}
531
532
/**
533
 * Normal page load.  Load page content via JS.
534
 *
535
 * @access public
536
 * @return void
537
 */
538
function stats_js_load_page_via_ajax() {
539
	?>
540
<script type="text/javascript">
541
/* <![CDATA[ */
542
if ( -1 == document.location.href.indexOf( 'noheader' ) ) {
543
	jQuery( function( $ ) {
544
		$.get( document.location.href + '&noheader', function( responseText ) {
545
			$( '#stats-loading-wrap' ).replaceWith( responseText );
546
		} );
547
	} );
548
}
549
/* ]]> */
550
</script>
551
	<?php
552
}
553
554
/**
555
 * Jetpack Admin Page Wrapper.
556
 */
557
function jetpack_admin_ui_stats_report_page_wrapper() {
558
	if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
559
		Jetpack_Admin_Page::wrap_ui( 'stats_reports_page', array( 'is-wide' => true ) );
560
	} else {
561
		stats_reports_page();
562
	}
563
564
}
565
566
/**
567
 * Stats Report Page.
568
 *
569
 * @access public
570
 * @param bool $main_chart_only (default: false) Main Chart Only.
571
 */
572
function stats_reports_page( $main_chart_only = false ) {
573
574
	if ( isset( $_GET['dashboard'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
575
		return stats_dashboard_widget_content();
576
	}
577
578
	$blog_id   = stats_get_option( 'blog_id' );
579
	$stats_url = Redirect::get_url( 'calypso-stats' );
580
581
	if ( ! $main_chart_only && ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
582
		$nojs_url = add_query_arg( 'nojs', '1' );
583
		$http     = is_ssl() ? 'https' : 'http';
584
		// Loading message. No JS fallback message.
585
		?>
586
587
	<div id="jp-stats-wrap">
588
		<div class="wrap">
589
			<h2><?php esc_html_e( 'Site Stats', 'jetpack' ); ?>
590
			<?php
591
			if ( current_user_can( 'jetpack_manage_modules' ) ) :
592
				$i18n_headers = jetpack_get_module_i18n( 'stats' );
593
				?>
594
				<a
595
					style="font-size:13px;"
596
					href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack#/settings?term=' . rawurlencode( $i18n_headers['name'] ) ) ); ?>"
597
				>
598
				<?php esc_html_e( 'Configure', 'jetpack' ); ?>
599
				</a>
600
				<?php
601
				endif;
602
603
			/**
604
			 * Sets external resource URL.
605
			 *
606
			 * @module stats
607
			 *
608
			 * @since 1.4.0
609
			 * @todo Clean up various uses of this filter. It's seemingly filtering different types of images in different places.
610
			 *
611
			 * @param string $args URL of external resource.
612
			 */
613
			$static_url = apply_filters( 'jetpack_static_url', "{$http}://en.wordpress.com/i/loading/loading-64.gif" );
614
			?>
615
			</h2>
616
		</div>
617
		<div id="stats-loading-wrap" class="wrap">
618
		<p class="hide-if-no-js"><img width="32" height="32" alt="<?php esc_attr_e( 'Loading&hellip;', 'jetpack' ); ?>" src="<?php echo esc_url( $static_url ); ?>" /></p>
619
		<p style="font-size: 11pt; margin: 0;"><a href="<?php echo esc_url( $stats_url ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'View stats on WordPress.com right now', 'jetpack' ); ?></a></p>
620
		<p class="hide-if-js"><?php esc_html_e( 'Your Site Stats work better with JavaScript enabled.', 'jetpack' ); ?><br />
621
		<a href="<?php echo esc_url( $nojs_url ); ?>"><?php esc_html_e( 'View Site Stats without JavaScript', 'jetpack' ); ?></a>.</p>
622
		</div>
623
	</div>
624
		<?php
625
		return;
626
	}
627
628
	$day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
629
	$q   = array(
630
		'noheader' => 'true',
631
		'proxy'    => '',
632
		'page'     => 'stats',
633
		'day'      => $day,
634
		'blog'     => $blog_id,
635
		'charset'  => get_option( 'blog_charset' ),
636
		'color'    => get_user_option( 'admin_color' ),
637
		'ssl'      => is_ssl(),
638
		'j'        => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
639
	);
640
	if ( get_locale() !== 'en_US' ) {
641
		$q['jp_lang'] = get_locale();
642
	}
643
	// Only show the main chart, without extra header data, or metaboxes.
644
	$q['main_chart_only'] = $main_chart_only;
645
	$args                 = array(
646
		'view'                => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
647
		'numdays'             => 'int',
648
		'day'                 => 'date',
649
		'unit'                => array( 1, 7, 31, 'human' ),
650
		'humanize'            => array( 'true' ),
651
		'num'                 => 'int',
652
		'summarize'           => null,
653
		'post'                => 'int',
654
		'width'               => 'int',
655
		'height'              => 'int',
656
		'data'                => 'data',
657
		'blog_subscribers'    => 'int',
658
		'comment_subscribers' => null,
659
		'type'                => array( 'wpcom', 'email', 'pending' ),
660
		'pagenum'             => 'int',
661
	);
662
	foreach ( $args as $var => $vals ) {
663
		if ( ! isset( $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
664
			continue;
665
		}
666
		if ( is_array( $vals ) ) {
667
			if ( in_array( $_REQUEST[ $var ], $vals, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
668
				$q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
669
			}
670
		} elseif ( 'int' === $vals ) {
671
			$q[ $var ] = (int) $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
672
		} elseif ( 'date' === $vals ) {
673
			if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
674
				$q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
675
			}
676
		} elseif ( null === $vals ) {
677
			$q[ $var ] = '';
678
		} elseif ( 'data' === $vals ) {
679
			if ( 'index.php' === substr( $_REQUEST[ $var ], 0, 9 ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
680
				$q[ $var ] = $_REQUEST[ $var ];// phpcs:ignore WordPress.Security.NonceVerification.Recommended
681
			}
682
		}
683
	}
684
685
	if ( isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
686
		if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
687
			$chart = sanitize_title( $_GET['chart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
688
			$url   = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
689
		}
690
	} else {
691
		$url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
692
	}
693
694
	$url     = add_query_arg( $q, $url );
0 ignored issues
show
Bug introduced by
The variable $url does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
695
	$method  = 'GET';
696
	$timeout = 90;
697
	$user_id = 0; // Means use the blog token.
698
699
	$get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
700
	$get_code = wp_remote_retrieve_response_code( $get );
701
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
702
		stats_print_wp_remote_error( $get, $url );
703
	} else {
704
		if ( ! empty( $get['headers']['content-type'] ) ) {
705
			$type = $get['headers']['content-type'];
706
			if ( substr( $type, 0, 5 ) === 'image' ) {
707
				$img = $get['body'];
708
				header( 'Content-Type: ' . $type );
709
				header( 'Content-Length: ' . strlen( $img ) );
710
				echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
711
				die();
712
			}
713
		}
714
		$body = stats_convert_post_titles( $get['body'] );
715
		$body = stats_convert_chart_urls( $body );
716
		$body = stats_convert_image_urls( $body );
717
		$body = stats_convert_admin_urls( $body );
718
		echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
719
	}
720
721
	if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
722
		$tracking = new Tracking();
723
		$tracking->record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
724
	}
725
726
	if ( isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
727
		die;
728
	}
729
}
730
731
/**
732
 * Stats Convert Admin Urls.
733
 *
734
 * @access public
735
 * @param mixed $html HTML.
736
 * @return string
737
 */
738
function stats_convert_admin_urls( $html ) {
739
	return str_replace( 'index.php?page=stats', 'admin.php?page=stats', $html );
740
}
741
742
/**
743
 * Stats Convert Image URLs.
744
 *
745
 * @access public
746
 * @param mixed $html HTML.
747
 * @return string
748
 */
749
function stats_convert_image_urls( $html ) {
750
	$url  = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
751
	$html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
752
	return $html;
753
}
754
755
/**
756
 * Callback for preg_replace_callback used in stats_convert_chart_urls()
757
 *
758
 * @since 5.6.0
759
 *
760
 * @param  array $matches The matches resulting from the preg_replace_callback call.
761
 * @return string          The admin url for the chart.
762
 */
763
function jetpack_stats_convert_chart_urls_callback( $matches ) {
764
	// If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
765
	return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
766
}
767
768
/**
769
 * Stats Convert Chart URLs.
770
 *
771
 * @access public
772
 * @param mixed $html HTML.
773
 * @return string
774
 */
775
function stats_convert_chart_urls( $html ) {
776
	$html = preg_replace_callback(
777
		'|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
778
		'jetpack_stats_convert_chart_urls_callback',
779
		$html
780
	);
781
	return $html;
782
}
783
784
/**
785
 * Stats Convert Post Title HTML
786
 *
787
 * @access public
788
 * @param mixed $html HTML.
789
 * @return string
790
 */
791
function stats_convert_post_titles( $html ) {
792
	global $stats_posts;
793
	$pattern = "<span class='post-(\d+)-link'>.*?</span>";
794
	if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
795
		return $html;
796
	}
797
	$posts = get_posts(
798
		array(
799
			'include'          => implode( ',', $matches[1] ),
800
			'post_type'        => 'any',
801
			'post_status'      => 'any',
802
			'numberposts'      => -1,
803
			'suppress_filters' => false,
804
		)
805
	);
806
	foreach ( $posts as $post ) {
807
		$stats_posts[ $post->ID ] = $post;
808
	}
809
	$html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
810
	return $html;
811
}
812
813
/**
814
 * Stats Convert Post Title Matches.
815
 *
816
 * @access public
817
 * @param mixed $matches Matches.
818
 * @return string
819
 */
820
function stats_convert_post_title( $matches ) {
821
	global $stats_posts;
822
	$post_id = $matches[1];
823
	if ( isset( $stats_posts[ $post_id ] ) ) {
824
		return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
825
	}
826
	return $matches[0];
827
}
828
829
/**
830
 * Stats Hide Smile.
831
 *
832
 * @access public
833
 * @return void
834
 */
835
function stats_hide_smile_css() {
836
	$options = stats_get_options();
837
	if ( isset( $options['hide_smile'] ) && $options['hide_smile'] ) {
838
		?>
839
<style type='text/css'>img#wpstats{display:none}</style>
840
		<?php
841
	}
842
}
843
844
/**
845
 * Stats Admin Bar Head.
846
 *
847
 * @access public
848
 * @return void
849
 */
850
function stats_admin_bar_head() {
851
	if ( ! stats_get_option( 'admin_bar' ) ) {
852
		return;
853
	}
854
855
	if ( ! current_user_can( 'view_stats' ) ) {
856
		return;
857
	}
858
859
	if ( ! is_admin_bar_showing() ) {
860
		return;
861
	}
862
863
	add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
864
	?>
865
866
<style data-ampdevmode type='text/css'>
867
#wpadminbar .quicklinks li#wp-admin-bar-stats {
868
	height: 32px;
869
}
870
#wpadminbar .quicklinks li#wp-admin-bar-stats a {
871
	height: 32px;
872
	padding: 0;
873
}
874
#wpadminbar .quicklinks li#wp-admin-bar-stats a div {
875
	height: 32px;
876
	width: 95px;
877
	overflow: hidden;
878
	margin: 0 10px;
879
}
880
#wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
881
	width: auto;
882
	margin: 0 8px 0 10px;
883
}
884
#wpadminbar .quicklinks li#wp-admin-bar-stats a img {
885
	height: 24px;
886
	margin: 4px 0;
887
	max-width: none;
888
	border: none;
889
}
890
</style>
891
	<?php
892
}
893
894
/**
895
 * Stats AdminBar.
896
 *
897
 * @access public
898
 * @param mixed $wp_admin_bar WPAdminBar.
899
 * @return void
900
 */
901
function stats_admin_bar_menu( &$wp_admin_bar ) {
902
	$url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ); // no menu_page_url() blog-side.
903
904
	$img_src    = esc_attr(
905
		add_query_arg(
906
			array(
907
				'noheader' => '',
908
				'proxy'    => '',
909
				'chart'    => 'admin-bar-hours-scale',
910
			),
911
			$url
912
		)
913
	);
914
	$img_src_2x = esc_attr(
915
		add_query_arg(
916
			array(
917
				'noheader' => '',
918
				'proxy'    => '',
919
				'chart'    => 'admin-bar-hours-scale-2x',
920
			),
921
			$url
922
		)
923
	);
924
925
	$alt = esc_attr( __( 'Stats', 'jetpack' ) );
926
927
	$title = esc_attr( __( 'Views over 48 hours. Click for more Site Stats.', 'jetpack' ) );
928
929
	$menu = array(
930
		'id'    => 'stats',
931
		'href'  => $url,
932
		'title' => "<div><img src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
933
	);
934
935
	$wp_admin_bar->add_menu( $menu );
936
}
937
938
/**
939
 * Stats Update Blog.
940
 *
941
 * @access public
942
 * @return void
943
 */
944
function stats_update_blog() {
945
	XMLRPC_Async_Call::add_call( 'jetpack.updateBlog', 0, stats_get_blog() );
946
}
947
948
/**
949
 * Stats Get Blog.
950
 *
951
 * @access public
952
 * @return string
953
 */
954
function stats_get_blog() {
955
	$home = wp_parse_url( trailingslashit( get_option( 'home' ) ) );
956
	$blog = array(
957
		'host'                => $home['host'],
958
		'path'                => $home['path'],
959
		'blogname'            => get_option( 'blogname' ),
960
		'blogdescription'     => get_option( 'blogdescription' ),
961
		'siteurl'             => get_option( 'siteurl' ),
962
		'gmt_offset'          => get_option( 'gmt_offset' ),
963
		'timezone_string'     => get_option( 'timezone_string' ),
964
		'stats_version'       => STATS_VERSION,
965
		'stats_api'           => 'jetpack',
966
		'page_on_front'       => get_option( 'page_on_front' ),
967
		'permalink_structure' => get_option( 'permalink_structure' ),
968
		'category_base'       => get_option( 'category_base' ),
969
		'tag_base'            => get_option( 'tag_base' ),
970
	);
971
	$blog = array_merge( stats_get_options(), $blog );
972
	unset( $blog['roles'], $blog['blog_id'] );
973
	return stats_esc_html_deep( $blog );
974
}
975
976
/**
977
 * Modified from stripslashes_deep()
978
 *
979
 * @access public
980
 * @param mixed $value Value.
981
 * @return string
982
 */
983
function stats_esc_html_deep( $value ) {
984
	if ( is_array( $value ) ) {
985
		$value = array_map( 'stats_esc_html_deep', $value );
986
	} elseif ( is_object( $value ) ) {
987
		$vars = get_object_vars( $value );
988
		foreach ( $vars as $key => $data ) {
989
			$value->{$key} = stats_esc_html_deep( $data );
990
		}
991
	} elseif ( is_string( $value ) ) {
992
		$value = esc_html( $value );
993
	}
994
995
	return $value;
996
}
997
998
/**
999
 * Stats xmlrpc_methods function.
1000
 *
1001
 * @access public
1002
 * @param mixed $methods Methods.
1003
 * @return array
1004
 */
1005
function stats_xmlrpc_methods( $methods ) {
1006
	$my_methods = array(
1007
		'jetpack.getBlog' => 'stats_get_blog',
1008
	);
1009
1010
	return array_merge( $methods, $my_methods );
1011
}
1012
1013
/**
1014
 * Stats Dashboard Widget Options.
1015
 *
1016
 * @access public
1017
 * @return array
1018
 */
1019
function stats_dashboard_widget_options() {
1020
	$defaults = array(
1021
		'chart'  => 1,
1022
		'top'    => 1,
1023
		'search' => 7,
1024
	);
1025
	$options  = get_option( 'stats_dashboard_widget' );
1026
	if ( ( ! $options ) || ! is_array( $options ) ) {
1027
		$options = array();
1028
	}
1029
1030
	// Ignore obsolete option values.
1031
	$intervals = array( 1, 7, 31, 90, 365 );
1032
	foreach ( array( 'top', 'search' ) as $key ) {
1033
		if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
1034
			unset( $options[ $key ] );
1035
		}
1036
	}
1037
1038
		return array_merge( $defaults, $options );
1039
}
1040
1041
/**
1042
 * Stats Dashboard Widget Control.
1043
 *
1044
 * @access public
1045
 * @return void
1046
 */
1047
function stats_dashboard_widget_control() {
1048
	$periods   = array(
1049
		'1'  => __( 'day', 'jetpack' ),
1050
		'7'  => __( 'week', 'jetpack' ),
1051
		'31' => __( 'month', 'jetpack' ),
1052
	);
1053
	$intervals = array(
1054
		'1'   => __( 'the past day', 'jetpack' ),
1055
		'7'   => __( 'the past week', 'jetpack' ),
1056
		'31'  => __( 'the past month', 'jetpack' ),
1057
		'90'  => __( 'the past quarter', 'jetpack' ),
1058
		'365' => __( 'the past year', 'jetpack' ),
1059
	);
1060
	$defaults  = array(
1061
		'top'    => 1,
1062
		'search' => 7,
1063
	);
1064
1065
	$options = stats_dashboard_widget_options();
1066
1067
	if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) { // phpcs:ignore WordPress.Security.NonceVerification
1068
		if ( isset( $periods[ $_POST['chart'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1069
			$options['chart'] = $_POST['chart']; // phpcs:ignore WordPress.Security.NonceVerification
1070
		}
1071
		foreach ( array( 'top', 'search' ) as $key ) {
1072
			if ( isset( $intervals[ $_POST[ $key ] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1073
				$options[ $key ] = $_POST[ $key ]; // phpcs:ignore WordPress.Security.NonceVerification
1074
			} else {
1075
				$options[ $key ] = $defaults[ $key ];
1076
			}
1077
		}
1078
		update_option( 'stats_dashboard_widget', $options );
1079
	}
1080
	?>
1081
	<p>
1082
	<label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
1083
	<select id="chart" name="chart">
1084
	<?php
1085 View Code Duplication
	foreach ( $periods as $val => $label ) {
1086
		?>
1087
		<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
1088
		<?php
1089
	}
1090
	?>
1091
	</select>.
1092
	</p>
1093
1094
	<p>
1095
	<label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
1096
	<select id="top" name="top">
1097
	<?php
1098 View Code Duplication
	foreach ( $intervals as $val => $label ) {
1099
		?>
1100
		<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
1101
		<?php
1102
	}
1103
	?>
1104
	</select>.
1105
	</p>
1106
1107
	<p>
1108
	<label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
1109
	<select id="search" name="search">
1110
	<?php
1111 View Code Duplication
	foreach ( $intervals as $val => $label ) {
1112
		?>
1113
		<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
1114
		<?php
1115
	}
1116
	?>
1117
	</select>.
1118
	</p>
1119
	<?php
1120
}
1121
1122
/**
1123
 * Jetpack Stats Dashboard Widget.
1124
 *
1125
 * @access public
1126
 * @return void
1127
 */
1128
function stats_jetpack_dashboard_widget() {
1129
	?>
1130
	<form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
1131
		<?php stats_dashboard_widget_control(); ?>
1132
		<?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
1133
		<input type="hidden" name="widget_id" value="dashboard_stats" />
1134
		<?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
1135
	</form>
1136
	<button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
1137
		<span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
1138
		<span class="toggle-indicator" aria-hidden="true"></span>
1139
	</button>
1140
	<div id="dashboard_stats">
1141
		<div class="inside">
1142
			<div style="height: 250px;"></div>
1143
		</div>
1144
	</div>
1145
	<?php
1146
}
1147
1148
/**
1149
 * JavaScript and CSS for dashboard widget.
1150
 *
1151
 * @access public
1152
 * @return void
1153
 */
1154
function stats_dashboard_head() {
1155
	?>
1156
<script type="text/javascript">
1157
/* <![CDATA[ */
1158
jQuery( function($) {
1159
	var dashStats = jQuery( '#dashboard_stats div.inside' );
1160
1161
	if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
1162
		return;
1163
	}
1164
1165
	if ( ! dashStats.length ) {
1166
		dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
1167
		var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
1168
		var args = 'width=' + dashStats.width() + '&height=' + h.toString();
1169
	} else {
1170
		if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
1171
			var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
1172
		} else {
1173
			var args = 'width=' + ( dashStats.width() * 2 ).toString();
1174
		}
1175
	}
1176
1177
	dashStats
1178
		.not( '.dashboard-widget-control' )
1179
		.load( 'admin.php?page=stats&noheader&dashboard&' + args );
1180
1181
	jQuery( window ).one( 'resize', function() {
1182
		jQuery( '#stat-chart' ).css( 'width', 'auto' );
1183
	} );
1184
1185
1186
	// Widget settings toggle container.
1187
	var toggle = $( '.js-toggle-stats_dashboard_widget_control' );
1188
1189
	// Move the toggle in the widget header.
1190
	toggle.appendTo( '#jetpack_summary_widget .handle-actions' );
1191
1192
	// Toggle settings when clicking on it.
1193
	toggle.show().click( function( e ) {
1194
		e.preventDefault();
1195
		e.stopImmediatePropagation();
1196
		$( this ).parent().toggleClass( 'controlVisible' );
1197
		$( '#stats_dashboard_widget_control' ).slideToggle();
1198
	} );
1199
} );
1200
/* ]]> */
1201
</script>
1202
	<?php
1203
}
1204
1205
/**
1206
 * Stats Dashboard Widget Content.
1207
 *
1208
 * @access public
1209
 * @return void
1210
 */
1211
function stats_dashboard_widget_content() {
1212
	$width  = isset( $_GET['width'] ) ? (int) ( $_GET['width'] / 2 ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1213
	$height = isset( $_GET['height'] ) ? (int) $_GET['height'] - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1214
	if ( ! $width || $width < 250 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $width of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1215
		$width = 370;
1216
	}
1217
	if ( ! $height || $height < 230 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $height of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1218
		$height = 180;
1219
	}
1220
1221
	$_width  = $width - 5;
1222
	$_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack! @todo Remove WordPress 5.8 is minimum. IE should be fully deprecated.
1223
1224
	$options = stats_dashboard_widget_options();
1225
	$blog_id = Jetpack_Options::get_option( 'id' );
1226
1227
	$q = array(
1228
		'noheader' => 'true',
1229
		'proxy'    => '',
1230
		'blog'     => $blog_id,
1231
		'page'     => 'stats',
1232
		'chart'    => '',
1233
		'unit'     => $options['chart'],
1234
		'color'    => get_user_option( 'admin_color' ),
1235
		'width'    => $_width,
1236
		'height'   => $_height,
1237
		'ssl'      => is_ssl(),
1238
		'j'        => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
1239
	);
1240
1241
	$url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
1242
1243
	$url     = add_query_arg( $q, $url );
1244
	$method  = 'GET';
1245
	$timeout = 90;
1246
	$user_id = 0; // Means use the blog token.
1247
1248
	$get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1249
	$get_code = wp_remote_retrieve_response_code( $get );
1250
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1251
		stats_print_wp_remote_error( $get, $url );
1252
	} else {
1253
		$body = stats_convert_post_titles( $get['body'] );
1254
		$body = stats_convert_chart_urls( $body );
1255
		$body = stats_convert_image_urls( $body );
1256
		echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
1257
	}
1258
1259
	$post_ids = array();
1260
1261
	$csv_end_date = gmdate( 'Y-m-d' );
1262
	$csv_args     = array(
1263
		'top'    => "&limit=8&end=$csv_end_date",
1264
		'search' => "&limit=5&end=$csv_end_date",
1265
	);
1266
1267
	$top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
1268
	foreach ( $top_posts as $i => $post ) {
1269
		if ( 0 === $post['post_id'] ) {
1270
			unset( $top_posts[ $i ] );
1271
			continue;
1272
		}
1273
		$post_ids[] = $post['post_id'];
1274
	}
1275
1276
	// Cache.
1277
	get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
1278
1279
	$searches     = array();
1280
	$search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
1281
	foreach ( $search_terms as $search_term ) {
1282
		if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
1283
			continue;
1284
		}
1285
		$searches[] = esc_html( $search_term['searchterm'] );
1286
	}
1287
1288
	?>
1289
<div id="stats-info">
1290
	<div id="top-posts" class='stats-section'>
1291
		<div class="stats-section-inner">
1292
		<h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
1293
		<?php
1294
		if ( empty( $top_posts ) ) {
1295
			?>
1296
			<p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1297
			<?php
1298
		} else {
1299
			foreach ( $top_posts as $post ) {
1300
				if ( ! get_post( $post['post_id'] ) ) {
1301
					continue;
1302
				}
1303
				?>
1304
				<p>
1305
				<?php
1306
				printf(
1307
					/* Translators: Stats dashboard widget postviews list: "$post_title $views Views". */
1308
					esc_html__( '%1$s %2$s Views', 'jetpack' ),
1309
					'<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
1310
					esc_html( number_format_i18n( $post['views'] ) )
1311
				);
1312
				?>
1313
			</p>
1314
				<?php
1315
			}
1316
		}
1317
		?>
1318
		</div>
1319
	</div>
1320
	<div id="top-search" class='stats-section'>
1321
		<div class="stats-section-inner">
1322
		<h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
1323
		<?php
1324
		if ( empty( $searches ) ) {
1325
			?>
1326
			<p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1327
			<?php
1328
		} else {
1329
			foreach ( $searches as $search_term_item ) {
1330
				printf(
1331
					'<p>%s</p>',
1332
					esc_html( $search_term_item )
1333
				);
1334
			}
1335
		}
1336
		?>
1337
		</div>
1338
	</div>
1339
</div>
1340
<div class="clear"></div>
1341
<div class="stats-view-all">
1342
	<?php
1343
	$stats_day_url = Redirect::get_url( 'calypso-stats-day' );
1344
	printf(
1345
		'<a class="button" target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a>',
1346
		esc_url( $stats_day_url ),
1347
		esc_html__( 'View all stats', 'jetpack' )
1348
	);
1349
	?>
1350
</div>
1351
<div class="clear"></div>
1352
	<?php
1353
	exit;
1354
}
1355
1356
/**
1357
 * Stats Print WP Remote Error.
1358
 *
1359
 * @access public
1360
 * @param mixed $get Get.
1361
 * @param mixed $url URL.
1362
 * @return void
1363
 */
1364
function stats_print_wp_remote_error( $get, $url ) {
1365
	$state_name     = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
1366
	$previous_error = Jetpack::state( $state_name );
1367
	$error          = md5( wp_json_encode( compact( 'get', 'url' ) ) );
1368
	Jetpack::state( $state_name, $error );
1369
	if ( $error !== $previous_error ) {
1370
		?>
1371
	<div class="wrap">
1372
	<p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
1373
	</div>
1374
		<?php
1375
		return;
1376
	}
1377
	?>
1378
	<div class="wrap">
1379
	<p>
1380
	<?php
1381
		printf(
1382
			/* translators: placeholder is an a href for a support site. */
1383
			esc_html__( 'We were unable to get your stats just now. Please reload this page to try again. If this error persists, please contact %1$s. In your report, please include the information below.', 'jetpack' ),
1384
			sprintf(
1385
				'<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
1386
				esc_html__( 'Jetpack Support', 'jetpack' )
1387
			)
1388
		);
1389
	?>
1390
		</p>
1391
	<pre>
1392
	User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
1393
	Page URL: "http<?php echo ( is_ssl() ? 's' : '' ) . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
1394
	API URL: "<?php echo esc_url( $url ); ?>"
1395
	<?php
1396
	if ( is_wp_error( $get ) ) {
1397
		foreach ( $get->get_error_codes() as $code ) {
1398
			foreach ( $get->get_error_messages( $code ) as $message ) {
1399
				?>
1400
				<?php print esc_html( $code ) . ': "' . esc_html( $message ) . '"'; ?>
1401
1402
				<?php
1403
			}
1404
		}
1405
	} else {
1406
		$get_code       = wp_remote_retrieve_response_code( $get );
1407
		$content_length = strlen( wp_remote_retrieve_body( $get ) );
1408
		?>
1409
Response code: "<?php print esc_html( $get_code ); ?>"
1410
Content length: "<?php print esc_html( $content_length ); ?>"
1411
1412
		<?php
1413
	}
1414
	?>
1415
	</pre>
1416
	</div>
1417
	<?php
1418
}
1419
1420
/**
1421
 * Get stats from WordPress.com
1422
 *
1423
 * @param string $table The stats which you want to retrieve: postviews, or searchterms.
1424
 * @param array  $args {
0 ignored issues
show
Documentation introduced by
Should the type for parameter $args not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1425
 *      An associative array of arguments.
1426
 *
1427
 *      @type bool    $end        The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
1428
 *                                and default timezone is UTC date. Default value is Now.
1429
 *      @type string  $days       The length of the desired time frame. Default is 30. Maximum 90 days.
1430
 *      @type int     $limit      The maximum number of records to return. Default is 10. Maximum 100.
1431
 *      @type int     $post_id    The ID of the post to retrieve stats data for
1432
 *      @type string  $summarize  If present, summarizes all matching records. Default Null.
1433
 *
1434
 * }
1435
 *
1436
 * @return array {
1437
 *      An array of post view data, each post as an array
1438
 *
1439
 *      array {
1440
 *          The post view data for a single post
1441
 *
1442
 *          @type string  $post_id         The ID of the post
1443
 *          @type string  $post_title      The title of the post
1444
 *          @type string  $post_permalink  The permalink for the post
1445
 *          @type string  $views           The number of views for the post within the $num_days specified
1446
 *      }
1447
 * }
1448
 */
1449
function stats_get_csv( $table, $args = null ) {
1450
	$defaults = array(
1451
		'end'       => false,
1452
		'days'      => false,
1453
		'limit'     => 3,
1454
		'post_id'   => false,
1455
		'summarize' => '',
1456
	);
1457
1458
	$args            = wp_parse_args( $args, $defaults );
0 ignored issues
show
Documentation introduced by
$defaults is of type array<string,false|integ...,"summarize":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1459
	$args['table']   = $table;
1460
	$args['blog_id'] = Jetpack_Options::get_option( 'id' );
1461
1462
	$stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
1463
1464
	$key = md5( $stats_csv_url );
1465
1466
	// Get cache.
1467
	$stats_cache = get_option( 'stats_cache' );
1468
	if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
1469
		$stats_cache = array();
1470
	}
1471
1472
	// Return or expire this key.
1473
	if ( isset( $stats_cache[ $key ] ) ) {
1474
		$time = key( $stats_cache[ $key ] );
1475
		if ( time() - $time < 300 ) {
1476
			return $stats_cache[ $key ][ $time ];
1477
		}
1478
		unset( $stats_cache[ $key ] );
1479
	}
1480
1481
	$stats_rows = array();
1482
	do {
1483
		$stats = stats_get_remote_csv( $stats_csv_url );
1484
		if ( ! $stats ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stats of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1485
			break;
1486
		}
1487
1488
		$labels = array_shift( $stats );
1489
1490
		if ( 0 === stripos( $labels[0], 'error' ) ) {
1491
			break;
1492
		}
1493
1494
		$stats_rows = array();
1495
		for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
1496
			$row = array();
1497
			foreach ( $labels as $col => $label ) {
1498
				$row[ $label ] = $stats[ $s ][ $col ];
1499
			}
1500
			$stats_rows[] = $row;
1501
		}
1502
	} while ( 0 );
1503
1504
	// Expire old keys.
1505
	foreach ( $stats_cache as $k => $cache ) {
1506
		if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
1507
			unset( $stats_cache[ $k ] );
1508
		}
1509
	}
1510
1511
		// Set cache.
1512
		$stats_cache[ $key ] = array( time() => $stats_rows );
1513
	update_option( 'stats_cache', $stats_cache );
1514
1515
	return $stats_rows;
1516
}
1517
1518
/**
1519
 * Stats get remote CSV.
1520
 *
1521
 * @access public
1522
 * @param mixed $url URL.
1523
 * @return array
1524
 */
1525
function stats_get_remote_csv( $url ) {
1526
	$method  = 'GET';
1527
	$timeout = 90;
1528
	$user_id = 0; // Blog token.
1529
1530
	$get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1531
	$get_code = wp_remote_retrieve_response_code( $get );
1532
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1533
		return array(); // @todo: return an error?
1534
	} else {
1535
		return stats_str_getcsv( $get['body'] );
1536
	}
1537
}
1538
1539
/**
1540
 * Recursively run str_getcsv on the stats csv.
1541
 *
1542
 * @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
1543
 *
1544
 * @param mixed $csv CSV.
1545
 * @return array.
0 ignored issues
show
Documentation introduced by
The doc-type array. could not be parsed: Unknown type name "array." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1546
 */
1547
function stats_str_getcsv( $csv ) {
1548
	$lines = str_getcsv( $csv, "\n" );
1549
	return array_map( 'str_getcsv', $lines );
1550
}
1551
1552
/**
1553
 * Abstract out building the rest api stats path.
1554
 *
1555
 * @param  string $resource Resource.
1556
 * @return string
1557
 */
1558
function jetpack_stats_api_path( $resource = '' ) {
1559
	$resource = ltrim( $resource, '/' );
1560
	return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
1561
}
1562
1563
/**
1564
 * Fetches stats data from the REST API.  Caches locally for 5 minutes.
1565
 *
1566
 * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
1567
 * @access public
1568
 * @param array  $args (default: array())  The args that are passed to the endpoint.
1569
 * @param string $resource (default: '') Optional sub-endpoint following /stats/.
1570
 * @return array|WP_Error.
0 ignored issues
show
Documentation introduced by
The doc-type array|WP_Error. could not be parsed: Unknown type name "WP_Error." at position 6. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1571
 */
1572
function stats_get_from_restapi( $args = array(), $resource = '' ) {
1573
	$endpoint    = jetpack_stats_api_path( $resource );
1574
	$api_version = '1.1';
1575
	$args        = wp_parse_args( $args, array() );
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1576
	$cache_key   = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args ) ) ) );
1577
1578
	$transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
1579
1580
	$stats_cache = get_transient( $transient_name );
1581
1582
	// Return or expire this key.
1583
	if ( $stats_cache ) {
1584
		$time = key( $stats_cache );
1585
		$data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
1586
1587
		if ( is_wp_error( $data ) ) {
1588
			return $data;
1589
		}
1590
1591
		return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
1592
	}
1593
1594
	// Do the dirty work.
1595
	$response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
0 ignored issues
show
Bug introduced by
It seems like $args defined by wp_parse_args($args, array()) on line 1575 can also be of type null; however, Automattic\Jetpack\Conne...n_api_request_as_blog() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1596
	if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1597
		// WP_Error.
1598
		$data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'stats_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1599
		// WP_Error.
1600
		$return = $data;
1601
	} else {
1602
		// string (JSON encoded object).
1603
		$data = wp_remote_retrieve_body( $response );
1604
		// object (rare: null on JSON failure).
1605
		$return = json_decode( $data );
1606
	}
1607
1608
	// To reduce size in storage: store with time as key, store JSON encoded data (unless error).
1609
	set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
1610
1611
	return $return;
1612
}
1613
1614
/**
1615
 * Load CSS needed for Stats column width in WP-Admin area.
1616
 *
1617
 * @since 4.7.0
1618
 */
1619
function jetpack_stats_load_admin_css() {
1620
	?>
1621
	<style type="text/css">
1622
		.fixed .column-stats {
1623
			width: 5em;
1624
		}
1625
	</style>
1626
	<?php
1627
}
1628
1629
/**
1630
 * Set header for column that allows to go to WordPress.com to see an entry's stats.
1631
 *
1632
 * @param array $columns An array of column names.
1633
 *
1634
 * @since 4.7.0
1635
 *
1636
 * @return mixed
1637
 */
1638
function jetpack_stats_post_table( $columns ) {
1639
	// Adds a stats link on the edit posts page.
1640
	if ( ! current_user_can( 'view_stats' ) || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1641
		return $columns;
1642
	}
1643
	// Array-Fu to add before comments.
1644
	$pos = array_search( 'comments', array_keys( $columns ), true );
1645
	if ( ! is_int( $pos ) ) {
1646
		return $columns;
1647
	}
1648
	$chunks             = array_chunk( $columns, $pos, true );
1649
	$chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
1650
1651
	return call_user_func_array( 'array_merge', $chunks );
1652
}
1653
1654
/**
1655
 * Set content for cell with link to an entry's stats in WordPress.com.
1656
 *
1657
 * @param string $column  The name of the column to display.
1658
 * @param int    $post_id The current post ID.
1659
 *
1660
 * @since 4.7.0
1661
 *
1662
 * @return mixed
1663
 */
1664
function jetpack_stats_post_table_cell( $column, $post_id ) {
1665
	if ( 'stats' === $column ) {
1666
		if ( 'publish' !== get_post_status( $post_id ) ) {
1667
			printf(
1668
				'<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
1669
				esc_html__( 'No stats', 'jetpack' )
1670
			);
1671
		} else {
1672
			$stats_post_url = Redirect::get_url(
1673
				'calypso-stats-post',
1674
				array(
1675
					'path' => $post_id,
1676
				)
1677
			);
1678
			printf(
1679
				'<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
1680
				esc_url( $stats_post_url ),
1681
				esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
1682
			);
1683
		}
1684
	}
1685
}
1686