Completed
Push — phpcs/stats ( 6e0c32 )
by
unknown
460:02 queued 449:18
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
			</h2>
604
		</div>
605
		<div id="stats-loading-wrap" class="wrap">
606
		<p class="hide-if-no-js"><img width="32" height="32" alt="<?php esc_attr_e( 'Loading&hellip;', 'jetpack' ); ?>" src="
607
																					<?php
608
																					echo esc_url(
609
																					/**
610
																					 * Sets external resource URL.
611
																					 *
612
																					 * @module stats
613
																					 *
614
																					 * @since 1.4.0
615
																					 *
616
																					 * @param string $args URL of external resource.
617
																					 */
618
																						apply_filters( 'jetpack_static_url', "{$http}://en.wordpress.com/i/loading/loading-64.gif" )
619
																					);
620
																					?>
621
				" /></p>
622
		<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>
623
		<p class="hide-if-js"><?php esc_html_e( 'Your Site Stats work better with JavaScript enabled.', 'jetpack' ); ?><br />
624
		<a href="<?php echo esc_url( $nojs_url ); ?>"><?php esc_html_e( 'View Site Stats without JavaScript', 'jetpack' ); ?></a>.</p>
625
		</div>
626
	</div>
627
		<?php
628
		return;
629
	}
630
631
	$day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
632
	$q   = array(
633
		'noheader' => 'true',
634
		'proxy'    => '',
635
		'page'     => 'stats',
636
		'day'      => $day,
637
		'blog'     => $blog_id,
638
		'charset'  => get_option( 'blog_charset' ),
639
		'color'    => get_user_option( 'admin_color' ),
640
		'ssl'      => is_ssl(),
641
		'j'        => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
642
	);
643
	if ( get_locale() !== 'en_US' ) {
644
		$q['jp_lang'] = get_locale();
645
	}
646
	// Only show the main chart, without extra header data, or metaboxes.
647
	$q['main_chart_only'] = $main_chart_only;
648
	$args                 = array(
649
		'view'                => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
650
		'numdays'             => 'int',
651
		'day'                 => 'date',
652
		'unit'                => array( 1, 7, 31, 'human' ),
653
		'humanize'            => array( 'true' ),
654
		'num'                 => 'int',
655
		'summarize'           => null,
656
		'post'                => 'int',
657
		'width'               => 'int',
658
		'height'              => 'int',
659
		'data'                => 'data',
660
		'blog_subscribers'    => 'int',
661
		'comment_subscribers' => null,
662
		'type'                => array( 'wpcom', 'email', 'pending' ),
663
		'pagenum'             => 'int',
664
	);
665
	foreach ( $args as $var => $vals ) {
666
		if ( ! isset( $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
667
			continue;
668
		}
669
		if ( is_array( $vals ) ) {
670
			if ( in_array( $_REQUEST[ $var ], $vals, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
671
				$q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
672
			}
673
		} elseif ( 'int' === $vals ) {
674
			$q[ $var ] = (int) $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
675
		} elseif ( 'date' === $vals ) {
676
			if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
677
				$q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
678
			}
679
		} elseif ( null === $vals ) {
680
			$q[ $var ] = '';
681
		} elseif ( 'data' === $vals ) {
682
			if ( 'index.php' === substr( $_REQUEST[ $var ], 0, 9 ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
683
				$q[ $var ] = $_REQUEST[ $var ];// phpcs:ignore WordPress.Security.NonceVerification.Recommended
684
			}
685
		}
686
	}
687
688
	if ( isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
689
		if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
690
			$chart = sanitize_title( $_GET['chart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
691
			$url   = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
692
		}
693
	} else {
694
		$url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
695
	}
696
697
	$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...
698
	$method  = 'GET';
699
	$timeout = 90;
700
	$user_id = 0; // Means use the blog token.
701
702
	$get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
703
	$get_code = wp_remote_retrieve_response_code( $get );
704
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
705
		stats_print_wp_remote_error( $get, $url );
706
	} else {
707
		if ( ! empty( $get['headers']['content-type'] ) ) {
708
			$type = $get['headers']['content-type'];
709
			if ( substr( $type, 0, 5 ) === 'image' ) {
710
				$img = $get['body'];
711
				header( 'Content-Type: ' . $type );
712
				header( 'Content-Length: ' . strlen( $img ) );
713
				echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
714
				die();
715
			}
716
		}
717
		$body = stats_convert_post_titles( $get['body'] );
718
		$body = stats_convert_chart_urls( $body );
719
		$body = stats_convert_image_urls( $body );
720
		$body = stats_convert_admin_urls( $body );
721
		echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
722
	}
723
724
	if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
725
		$tracking = new Tracking();
726
		$tracking->record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
727
	}
728
729
	if ( isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
730
		die;
731
	}
732
}
733
734
/**
735
 * Stats Convert Admin Urls.
736
 *
737
 * @access public
738
 * @param mixed $html HTML.
739
 * @return string
740
 */
741
function stats_convert_admin_urls( $html ) {
742
	return str_replace( 'index.php?page=stats', 'admin.php?page=stats', $html );
743
}
744
745
/**
746
 * Stats Convert Image URLs.
747
 *
748
 * @access public
749
 * @param mixed $html HTML.
750
 * @return string
751
 */
752
function stats_convert_image_urls( $html ) {
753
	$url  = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
754
	$html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
755
	return $html;
756
}
757
758
/**
759
 * Callback for preg_replace_callback used in stats_convert_chart_urls()
760
 *
761
 * @since 5.6.0
762
 *
763
 * @param  array $matches The matches resulting from the preg_replace_callback call.
764
 * @return string          The admin url for the chart.
765
 */
766
function jetpack_stats_convert_chart_urls_callback( $matches ) {
767
	// If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
768
	return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
769
}
770
771
/**
772
 * Stats Convert Chart URLs.
773
 *
774
 * @access public
775
 * @param mixed $html HTML.
776
 * @return string
777
 */
778
function stats_convert_chart_urls( $html ) {
779
	$html = preg_replace_callback(
780
		'|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
781
		'jetpack_stats_convert_chart_urls_callback',
782
		$html
783
	);
784
	return $html;
785
}
786
787
/**
788
 * Stats Convert Post Title HTML
789
 *
790
 * @access public
791
 * @param mixed $html HTML.
792
 * @return string
793
 */
794
function stats_convert_post_titles( $html ) {
795
	global $stats_posts;
796
	$pattern = "<span class='post-(\d+)-link'>.*?</span>";
797
	if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
798
		return $html;
799
	}
800
	$posts = get_posts(
801
		array(
802
			'include'          => implode( ',', $matches[1] ),
803
			'post_type'        => 'any',
804
			'post_status'      => 'any',
805
			'numberposts'      => -1,
806
			'suppress_filters' => false,
807
		)
808
	);
809
	foreach ( $posts as $post ) {
810
		$stats_posts[ $post->ID ] = $post;
811
	}
812
	$html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
813
	return $html;
814
}
815
816
/**
817
 * Stats Convert Post Title Matches.
818
 *
819
 * @access public
820
 * @param mixed $matches Matches.
821
 * @return string
822
 */
823
function stats_convert_post_title( $matches ) {
824
	global $stats_posts;
825
	$post_id = $matches[1];
826
	if ( isset( $stats_posts[ $post_id ] ) ) {
827
		return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
828
	}
829
	return $matches[0];
830
}
831
832
/**
833
 * Stats Hide Smile.
834
 *
835
 * @access public
836
 * @return void
837
 */
838
function stats_hide_smile_css() {
839
	$options = stats_get_options();
840
	if ( isset( $options['hide_smile'] ) && $options['hide_smile'] ) {
841
		?>
842
<style type='text/css'>img#wpstats{display:none}</style>
843
		<?php
844
	}
845
}
846
847
/**
848
 * Stats Admin Bar Head.
849
 *
850
 * @access public
851
 * @return void
852
 */
853
function stats_admin_bar_head() {
854
	if ( ! stats_get_option( 'admin_bar' ) ) {
855
		return;
856
	}
857
858
	if ( ! current_user_can( 'view_stats' ) ) {
859
		return;
860
	}
861
862
	if ( ! is_admin_bar_showing() ) {
863
		return;
864
	}
865
866
	add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
867
	?>
868
869
<style data-ampdevmode type='text/css'>
870
#wpadminbar .quicklinks li#wp-admin-bar-stats {
871
	height: 32px;
872
}
873
#wpadminbar .quicklinks li#wp-admin-bar-stats a {
874
	height: 32px;
875
	padding: 0;
876
}
877
#wpadminbar .quicklinks li#wp-admin-bar-stats a div {
878
	height: 32px;
879
	width: 95px;
880
	overflow: hidden;
881
	margin: 0 10px;
882
}
883
#wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
884
	width: auto;
885
	margin: 0 8px 0 10px;
886
}
887
#wpadminbar .quicklinks li#wp-admin-bar-stats a img {
888
	height: 24px;
889
	margin: 4px 0;
890
	max-width: none;
891
	border: none;
892
}
893
</style>
894
	<?php
895
}
896
897
/**
898
 * Stats AdminBar.
899
 *
900
 * @access public
901
 * @param mixed $wp_admin_bar WPAdminBar.
902
 * @return void
903
 */
904
function stats_admin_bar_menu( &$wp_admin_bar ) {
905
	$url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ); // no menu_page_url() blog-side.
906
907
	$img_src    = esc_attr(
908
		add_query_arg(
909
			array(
910
				'noheader' => '',
911
				'proxy'    => '',
912
				'chart'    => 'admin-bar-hours-scale',
913
			),
914
			$url
915
		)
916
	);
917
	$img_src_2x = esc_attr(
918
		add_query_arg(
919
			array(
920
				'noheader' => '',
921
				'proxy'    => '',
922
				'chart'    => 'admin-bar-hours-scale-2x',
923
			),
924
			$url
925
		)
926
	);
927
928
	$alt = esc_attr( __( 'Stats', 'jetpack' ) );
929
930
	$title = esc_attr( __( 'Views over 48 hours. Click for more Site Stats.', 'jetpack' ) );
931
932
	$menu = array(
933
		'id'    => 'stats',
934
		'href'  => $url,
935
		'title' => "<div><img src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
936
	);
937
938
	$wp_admin_bar->add_menu( $menu );
939
}
940
941
/**
942
 * Stats Update Blog.
943
 *
944
 * @access public
945
 * @return void
946
 */
947
function stats_update_blog() {
948
	XMLRPC_Async_Call::add_call( 'jetpack.updateBlog', 0, stats_get_blog() );
949
}
950
951
/**
952
 * Stats Get Blog.
953
 *
954
 * @access public
955
 * @return string
956
 */
957
function stats_get_blog() {
958
	$home = wp_parse_url( trailingslashit( get_option( 'home' ) ) );
959
	$blog = array(
960
		'host'                => $home['host'],
961
		'path'                => $home['path'],
962
		'blogname'            => get_option( 'blogname' ),
963
		'blogdescription'     => get_option( 'blogdescription' ),
964
		'siteurl'             => get_option( 'siteurl' ),
965
		'gmt_offset'          => get_option( 'gmt_offset' ),
966
		'timezone_string'     => get_option( 'timezone_string' ),
967
		'stats_version'       => STATS_VERSION,
968
		'stats_api'           => 'jetpack',
969
		'page_on_front'       => get_option( 'page_on_front' ),
970
		'permalink_structure' => get_option( 'permalink_structure' ),
971
		'category_base'       => get_option( 'category_base' ),
972
		'tag_base'            => get_option( 'tag_base' ),
973
	);
974
	$blog = array_merge( stats_get_options(), $blog );
975
	unset( $blog['roles'], $blog['blog_id'] );
976
	return stats_esc_html_deep( $blog );
977
}
978
979
/**
980
 * Modified from stripslashes_deep()
981
 *
982
 * @access public
983
 * @param mixed $value Value.
984
 * @return string
985
 */
986
function stats_esc_html_deep( $value ) {
987
	if ( is_array( $value ) ) {
988
		$value = array_map( 'stats_esc_html_deep', $value );
989
	} elseif ( is_object( $value ) ) {
990
		$vars = get_object_vars( $value );
991
		foreach ( $vars as $key => $data ) {
992
			$value->{$key} = stats_esc_html_deep( $data );
993
		}
994
	} elseif ( is_string( $value ) ) {
995
		$value = esc_html( $value );
996
	}
997
998
	return $value;
999
}
1000
1001
/**
1002
 * Stats xmlrpc_methods function.
1003
 *
1004
 * @access public
1005
 * @param mixed $methods Methods.
1006
 * @return array
1007
 */
1008
function stats_xmlrpc_methods( $methods ) {
1009
	$my_methods = array(
1010
		'jetpack.getBlog' => 'stats_get_blog',
1011
	);
1012
1013
	return array_merge( $methods, $my_methods );
1014
}
1015
1016
/**
1017
 * Stats Dashboard Widget Options.
1018
 *
1019
 * @access public
1020
 * @return array
1021
 */
1022
function stats_dashboard_widget_options() {
1023
	$defaults = array(
1024
		'chart'  => 1,
1025
		'top'    => 1,
1026
		'search' => 7,
1027
	);
1028
	$options  = get_option( 'stats_dashboard_widget' );
1029
	if ( ( ! $options ) || ! is_array( $options ) ) {
1030
		$options = array();
1031
	}
1032
1033
	// Ignore obsolete option values.
1034
	$intervals = array( 1, 7, 31, 90, 365 );
1035
	foreach ( array( 'top', 'search' ) as $key ) {
1036
		if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
1037
			unset( $options[ $key ] );
1038
		}
1039
	}
1040
1041
		return array_merge( $defaults, $options );
1042
}
1043
1044
/**
1045
 * Stats Dashboard Widget Control.
1046
 *
1047
 * @access public
1048
 * @return void
1049
 */
1050
function stats_dashboard_widget_control() {
1051
	$periods   = array(
1052
		'1'  => __( 'day', 'jetpack' ),
1053
		'7'  => __( 'week', 'jetpack' ),
1054
		'31' => __( 'month', 'jetpack' ),
1055
	);
1056
	$intervals = array(
1057
		'1'   => __( 'the past day', 'jetpack' ),
1058
		'7'   => __( 'the past week', 'jetpack' ),
1059
		'31'  => __( 'the past month', 'jetpack' ),
1060
		'90'  => __( 'the past quarter', 'jetpack' ),
1061
		'365' => __( 'the past year', 'jetpack' ),
1062
	);
1063
	$defaults  = array(
1064
		'top'    => 1,
1065
		'search' => 7,
1066
	);
1067
1068
	$options = stats_dashboard_widget_options();
1069
1070
	if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) { // phpcs:ignore WordPress.Security.NonceVerification
1071
		if ( isset( $periods[ $_POST['chart'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1072
			$options['chart'] = $_POST['chart']; // phpcs:ignore WordPress.Security.NonceVerification
1073
		}
1074
		foreach ( array( 'top', 'search' ) as $key ) {
1075
			if ( isset( $intervals[ $_POST[ $key ] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1076
				$options[ $key ] = $_POST[ $key ]; // phpcs:ignore WordPress.Security.NonceVerification
1077
			} else {
1078
				$options[ $key ] = $defaults[ $key ];
1079
			}
1080
		}
1081
		update_option( 'stats_dashboard_widget', $options );
1082
	}
1083
	?>
1084
	<p>
1085
	<label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
1086
	<select id="chart" name="chart">
1087
	<?php
1088 View Code Duplication
	foreach ( $periods as $val => $label ) {
1089
		?>
1090
		<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
1091
		<?php
1092
	}
1093
	?>
1094
	</select>.
1095
	</p>
1096
1097
	<p>
1098
	<label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
1099
	<select id="top" name="top">
1100
	<?php
1101 View Code Duplication
	foreach ( $intervals as $val => $label ) {
1102
		?>
1103
		<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
1104
		<?php
1105
	}
1106
	?>
1107
	</select>.
1108
	</p>
1109
1110
	<p>
1111
	<label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
1112
	<select id="search" name="search">
1113
	<?php
1114 View Code Duplication
	foreach ( $intervals as $val => $label ) {
1115
		?>
1116
		<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
1117
		<?php
1118
	}
1119
	?>
1120
	</select>.
1121
	</p>
1122
	<?php
1123
}
1124
1125
/**
1126
 * Jetpack Stats Dashboard Widget.
1127
 *
1128
 * @access public
1129
 * @return void
1130
 */
1131
function stats_jetpack_dashboard_widget() {
1132
	?>
1133
	<form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
1134
		<?php stats_dashboard_widget_control(); ?>
1135
		<?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
1136
		<input type="hidden" name="widget_id" value="dashboard_stats" />
1137
		<?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
1138
	</form>
1139
	<button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
1140
		<span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
1141
		<span class="toggle-indicator" aria-hidden="true"></span>
1142
	</button>
1143
	<div id="dashboard_stats">
1144
		<div class="inside">
1145
			<div style="height: 250px;"></div>
1146
		</div>
1147
	</div>
1148
	<?php
1149
}
1150
1151
/**
1152
 * JavaScript and CSS for dashboard widget.
1153
 *
1154
 * @access public
1155
 * @return void
1156
 */
1157
function stats_dashboard_head() {
1158
	?>
1159
<script type="text/javascript">
1160
/* <![CDATA[ */
1161
jQuery( function($) {
1162
	var dashStats = jQuery( '#dashboard_stats div.inside' );
1163
1164
	if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
1165
		return;
1166
	}
1167
1168
	if ( ! dashStats.length ) {
1169
		dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
1170
		var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
1171
		var args = 'width=' + dashStats.width() + '&height=' + h.toString();
1172
	} else {
1173
		if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
1174
			var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
1175
		} else {
1176
			var args = 'width=' + ( dashStats.width() * 2 ).toString();
1177
		}
1178
	}
1179
1180
	dashStats
1181
		.not( '.dashboard-widget-control' )
1182
		.load( 'admin.php?page=stats&noheader&dashboard&' + args );
1183
1184
	jQuery( window ).one( 'resize', function() {
1185
		jQuery( '#stat-chart' ).css( 'width', 'auto' );
1186
	} );
1187
1188
1189
	// Widget settings toggle container.
1190
	var toggle = $( '.js-toggle-stats_dashboard_widget_control' );
1191
1192
	// Move the toggle in the widget header.
1193
	toggle.appendTo( '#jetpack_summary_widget .handle-actions' );
1194
1195
	// Toggle settings when clicking on it.
1196
	toggle.show().click( function( e ) {
1197
		e.preventDefault();
1198
		e.stopImmediatePropagation();
1199
		$( this ).parent().toggleClass( 'controlVisible' );
1200
		$( '#stats_dashboard_widget_control' ).slideToggle();
1201
	} );
1202
} );
1203
/* ]]> */
1204
</script>
1205
	<?php
1206
}
1207
1208
/**
1209
 * Stats Dashboard Widget Content.
1210
 *
1211
 * @access public
1212
 * @return void
1213
 */
1214
function stats_dashboard_widget_content() {
1215
	$width  = isset( $_GET['width'] ) ? (int) ( $_GET['width'] / 2 ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1216
	$height = isset( $_GET['height'] ) ? (int) $_GET['height'] - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1217
	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...
1218
		$width = 370;
1219
	}
1220
	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...
1221
		$height = 180;
1222
	}
1223
1224
	$_width  = $width - 5;
1225
	$_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack! @todo Remove WordPress 5.8 is minimum. IE should be fully deprecated.
1226
1227
	$options = stats_dashboard_widget_options();
1228
	$blog_id = Jetpack_Options::get_option( 'id' );
1229
1230
	$q = array(
1231
		'noheader' => 'true',
1232
		'proxy'    => '',
1233
		'blog'     => $blog_id,
1234
		'page'     => 'stats',
1235
		'chart'    => '',
1236
		'unit'     => $options['chart'],
1237
		'color'    => get_user_option( 'admin_color' ),
1238
		'width'    => $_width,
1239
		'height'   => $_height,
1240
		'ssl'      => is_ssl(),
1241
		'j'        => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
1242
	);
1243
1244
	$url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
1245
1246
	$url     = add_query_arg( $q, $url );
1247
	$method  = 'GET';
1248
	$timeout = 90;
1249
	$user_id = 0; // Means use the blog token.
1250
1251
	$get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1252
	$get_code = wp_remote_retrieve_response_code( $get );
1253
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1254
		stats_print_wp_remote_error( $get, $url );
1255
	} else {
1256
		$body = stats_convert_post_titles( $get['body'] );
1257
		$body = stats_convert_chart_urls( $body );
1258
		$body = stats_convert_image_urls( $body );
1259
		echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
1260
	}
1261
1262
	$post_ids = array();
1263
1264
	$csv_end_date = gmdate( 'Y-m-d' );
1265
	$csv_args     = array(
1266
		'top'    => "&limit=8&end=$csv_end_date",
1267
		'search' => "&limit=5&end=$csv_end_date",
1268
	);
1269
1270
	$top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
1271
	foreach ( $top_posts as $i => $post ) {
1272
		if ( 0 === $post['post_id'] ) {
1273
			unset( $top_posts[ $i ] );
1274
			continue;
1275
		}
1276
		$post_ids[] = $post['post_id'];
1277
	}
1278
1279
	// Cache.
1280
	get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
1281
1282
	$searches     = array();
1283
	$search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
1284
	foreach ( $search_terms as $search_term ) {
1285
		if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
1286
			continue;
1287
		}
1288
		$searches[] = esc_html( $search_term['searchterm'] );
1289
	}
1290
1291
	?>
1292
<div id="stats-info">
1293
	<div id="top-posts" class='stats-section'>
1294
		<div class="stats-section-inner">
1295
		<h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
1296
		<?php
1297
		if ( empty( $top_posts ) ) {
1298
			?>
1299
			<p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1300
			<?php
1301
		} else {
1302
			foreach ( $top_posts as $post ) {
1303
				if ( ! get_post( $post['post_id'] ) ) {
1304
					continue;
1305
				}
1306
				?>
1307
				<p>
1308
				<?php
1309
				printf(
1310
					/* Translators: Stats dashboard widget postviews list: "$post_title $views Views". */
1311
					esc_html__( '%1$s %2$s Views', 'jetpack' ),
1312
					'<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
1313
					esc_html( number_format_i18n( $post['views'] ) )
1314
				);
1315
				?>
1316
			</p>
1317
				<?php
1318
			}
1319
		}
1320
		?>
1321
		</div>
1322
	</div>
1323
	<div id="top-search" class='stats-section'>
1324
		<div class="stats-section-inner">
1325
		<h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
1326
		<?php
1327
		if ( empty( $searches ) ) {
1328
			?>
1329
			<p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1330
			<?php
1331
		} else {
1332
			foreach ( $searches as $search_term_item ) {
1333
				printf(
1334
					'<p>%s</p>',
1335
					esc_html( $search_term_item )
1336
				);
1337
			}
1338
		}
1339
		?>
1340
		</div>
1341
	</div>
1342
</div>
1343
<div class="clear"></div>
1344
<div class="stats-view-all">
1345
	<?php
1346
	$stats_day_url = Redirect::get_url( 'calypso-stats-day' );
1347
	printf(
1348
		'<a class="button" target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a>',
1349
		esc_url( $stats_day_url ),
1350
		esc_html__( 'View all stats', 'jetpack' )
1351
	);
1352
	?>
1353
</div>
1354
<div class="clear"></div>
1355
	<?php
1356
	exit;
1357
}
1358
1359
/**
1360
 * Stats Print WP Remote Error.
1361
 *
1362
 * @access public
1363
 * @param mixed $get Get.
1364
 * @param mixed $url URL.
1365
 * @return void
1366
 */
1367
function stats_print_wp_remote_error( $get, $url ) {
1368
	$state_name     = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
1369
	$previous_error = Jetpack::state( $state_name );
1370
	$error          = md5( wp_json_encode( compact( 'get', 'url' ) ) );
1371
	Jetpack::state( $state_name, $error );
1372
	if ( $error !== $previous_error ) {
1373
		?>
1374
	<div class="wrap">
1375
	<p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
1376
	</div>
1377
		<?php
1378
		return;
1379
	}
1380
	?>
1381
	<div class="wrap">
1382
	<p>
1383
	<?php
1384
		printf(
1385
			/* translators: placeholder is an a href for a support site. */
1386
			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' ),
1387
			sprintf(
1388
				'<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
1389
				esc_html__( 'Jetpack Support', 'jetpack' )
1390
			)
1391
		);
1392
	?>
1393
		</p>
1394
	<pre>
1395
	User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
1396
	Page URL: "http<?php echo ( is_ssl() ? 's' : '' ) . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
1397
	API URL: "<?php echo esc_url( $url ); ?>"
1398
	<?php
1399
	if ( is_wp_error( $get ) ) {
1400
		foreach ( $get->get_error_codes() as $code ) {
1401
			foreach ( $get->get_error_messages( $code ) as $message ) {
1402
				?>
1403
				<?php print esc_html( $code ) . ': "' . esc_html( $message ) . '"'; ?>
1404
1405
				<?php
1406
			}
1407
		}
1408
	} else {
1409
		$get_code       = wp_remote_retrieve_response_code( $get );
1410
		$content_length = strlen( wp_remote_retrieve_body( $get ) );
1411
		?>
1412
Response code: "<?php print esc_html( $get_code ); ?>"
1413
Content length: "<?php print esc_html( $content_length ); ?>"
1414
1415
		<?php
1416
	}
1417
	?>
1418
	</pre>
1419
	</div>
1420
	<?php
1421
}
1422
1423
/**
1424
 * Get stats from WordPress.com
1425
 *
1426
 * @param string $table The stats which you want to retrieve: postviews, or searchterms.
1427
 * @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...
1428
 *      An associative array of arguments.
1429
 *
1430
 *      @type bool    $end        The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
1431
 *                                and default timezone is UTC date. Default value is Now.
1432
 *      @type string  $days       The length of the desired time frame. Default is 30. Maximum 90 days.
1433
 *      @type int     $limit      The maximum number of records to return. Default is 10. Maximum 100.
1434
 *      @type int     $post_id    The ID of the post to retrieve stats data for
1435
 *      @type string  $summarize  If present, summarizes all matching records. Default Null.
1436
 *
1437
 * }
1438
 *
1439
 * @return array {
1440
 *      An array of post view data, each post as an array
1441
 *
1442
 *      array {
1443
 *          The post view data for a single post
1444
 *
1445
 *          @type string  $post_id         The ID of the post
1446
 *          @type string  $post_title      The title of the post
1447
 *          @type string  $post_permalink  The permalink for the post
1448
 *          @type string  $views           The number of views for the post within the $num_days specified
1449
 *      }
1450
 * }
1451
 */
1452
function stats_get_csv( $table, $args = null ) {
1453
	$defaults = array(
1454
		'end'       => false,
1455
		'days'      => false,
1456
		'limit'     => 3,
1457
		'post_id'   => false,
1458
		'summarize' => '',
1459
	);
1460
1461
	$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...
1462
	$args['table']   = $table;
1463
	$args['blog_id'] = Jetpack_Options::get_option( 'id' );
1464
1465
	$stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
1466
1467
	$key = md5( $stats_csv_url );
1468
1469
	// Get cache.
1470
	$stats_cache = get_option( 'stats_cache' );
1471
	if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
1472
		$stats_cache = array();
1473
	}
1474
1475
	// Return or expire this key.
1476
	if ( isset( $stats_cache[ $key ] ) ) {
1477
		$time = key( $stats_cache[ $key ] );
1478
		if ( time() - $time < 300 ) {
1479
			return $stats_cache[ $key ][ $time ];
1480
		}
1481
		unset( $stats_cache[ $key ] );
1482
	}
1483
1484
	$stats_rows = array();
1485
	do {
1486
		$stats = stats_get_remote_csv( $stats_csv_url );
1487
		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...
1488
			break;
1489
		}
1490
1491
		$labels = array_shift( $stats );
1492
1493
		if ( 0 === stripos( $labels[0], 'error' ) ) {
1494
			break;
1495
		}
1496
1497
		$stats_rows = array();
1498
		for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
1499
			$row = array();
1500
			foreach ( $labels as $col => $label ) {
1501
				$row[ $label ] = $stats[ $s ][ $col ];
1502
			}
1503
			$stats_rows[] = $row;
1504
		}
1505
	} while ( 0 );
1506
1507
	// Expire old keys.
1508
	foreach ( $stats_cache as $k => $cache ) {
1509
		if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
1510
			unset( $stats_cache[ $k ] );
1511
		}
1512
	}
1513
1514
		// Set cache.
1515
		$stats_cache[ $key ] = array( time() => $stats_rows );
1516
	update_option( 'stats_cache', $stats_cache );
1517
1518
	return $stats_rows;
1519
}
1520
1521
/**
1522
 * Stats get remote CSV.
1523
 *
1524
 * @access public
1525
 * @param mixed $url URL.
1526
 * @return array
1527
 */
1528
function stats_get_remote_csv( $url ) {
1529
	$method  = 'GET';
1530
	$timeout = 90;
1531
	$user_id = 0; // Blog token.
1532
1533
	$get      = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1534
	$get_code = wp_remote_retrieve_response_code( $get );
1535
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1536
		return array(); // @todo: return an error?
1537
	} else {
1538
		return stats_str_getcsv( $get['body'] );
1539
	}
1540
}
1541
1542
/**
1543
 * Recursively run str_getcsv on the stats csv.
1544
 *
1545
 * @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
1546
 *
1547
 * @param mixed $csv CSV.
1548
 * @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...
1549
 */
1550
function stats_str_getcsv( $csv ) {
1551
	$lines = str_getcsv( $csv, "\n" );
1552
	return array_map( 'str_getcsv', $lines );
1553
}
1554
1555
/**
1556
 * Abstract out building the rest api stats path.
1557
 *
1558
 * @param  string $resource Resource.
1559
 * @return string
1560
 */
1561
function jetpack_stats_api_path( $resource = '' ) {
1562
	$resource = ltrim( $resource, '/' );
1563
	return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
1564
}
1565
1566
/**
1567
 * Fetches stats data from the REST API.  Caches locally for 5 minutes.
1568
 *
1569
 * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
1570
 * @access public
1571
 * @param array  $args (default: array())  The args that are passed to the endpoint.
1572
 * @param string $resource (default: '') Optional sub-endpoint following /stats/.
1573
 * @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...
1574
 */
1575
function stats_get_from_restapi( $args = array(), $resource = '' ) {
1576
	$endpoint    = jetpack_stats_api_path( $resource );
1577
	$api_version = '1.1';
1578
	$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...
1579
	$cache_key   = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args ) ) ) );
1580
1581
	$transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
1582
1583
	$stats_cache = get_transient( $transient_name );
1584
1585
	// Return or expire this key.
1586
	if ( $stats_cache ) {
1587
		$time = key( $stats_cache );
1588
		$data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
1589
1590
		if ( is_wp_error( $data ) ) {
1591
			return $data;
1592
		}
1593
1594
		return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
1595
	}
1596
1597
	// Do the dirty work.
1598
	$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 1578 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...
1599
	if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1600
		// WP_Error.
1601
		$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...
1602
		// WP_Error.
1603
		$return = $data;
1604
	} else {
1605
		// string (JSON encoded object).
1606
		$data = wp_remote_retrieve_body( $response );
1607
		// object (rare: null on JSON failure).
1608
		$return = json_decode( $data );
1609
	}
1610
1611
	// To reduce size in storage: store with time as key, store JSON encoded data (unless error).
1612
	set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
1613
1614
	return $return;
1615
}
1616
1617
/**
1618
 * Load CSS needed for Stats column width in WP-Admin area.
1619
 *
1620
 * @since 4.7.0
1621
 */
1622
function jetpack_stats_load_admin_css() {
1623
	?>
1624
	<style type="text/css">
1625
		.fixed .column-stats {
1626
			width: 5em;
1627
		}
1628
	</style>
1629
	<?php
1630
}
1631
1632
/**
1633
 * Set header for column that allows to go to WordPress.com to see an entry's stats.
1634
 *
1635
 * @param array $columns An array of column names.
1636
 *
1637
 * @since 4.7.0
1638
 *
1639
 * @return mixed
1640
 */
1641
function jetpack_stats_post_table( $columns ) {
1642
	// Adds a stats link on the edit posts page.
1643
	if ( ! current_user_can( 'view_stats' ) || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1644
		return $columns;
1645
	}
1646
	// Array-Fu to add before comments.
1647
	$pos = array_search( 'comments', array_keys( $columns ), true );
1648
	if ( ! is_int( $pos ) ) {
1649
		return $columns;
1650
	}
1651
	$chunks             = array_chunk( $columns, $pos, true );
1652
	$chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
1653
1654
	return call_user_func_array( 'array_merge', $chunks );
1655
}
1656
1657
/**
1658
 * Set content for cell with link to an entry's stats in WordPress.com.
1659
 *
1660
 * @param string $column  The name of the column to display.
1661
 * @param int    $post_id The current post ID.
1662
 *
1663
 * @since 4.7.0
1664
 *
1665
 * @return mixed
1666
 */
1667
function jetpack_stats_post_table_cell( $column, $post_id ) {
1668
	if ( 'stats' === $column ) {
1669
		if ( 'publish' !== get_post_status( $post_id ) ) {
1670
			printf(
1671
				'<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
1672
				esc_html__( 'No stats', 'jetpack' )
1673
			);
1674
		} else {
1675
			$stats_post_url = Redirect::get_url(
1676
				'calypso-stats-post',
1677
				array(
1678
					'path' => $post_id,
1679
				)
1680
			);
1681
			printf(
1682
				'<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
1683
				esc_url( $stats_post_url ),
1684
				esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
1685
			);
1686
		}
1687
	}
1688
}
1689