Completed
Push — renovate/jest-monorepo ( bd2eaf...d289c3 )
by
unknown
44:07 queued 37:28
created

stats.php ➔ stats_add_shutdown_action()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
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 Jetpack
15
 */
16
17
use Automattic\Jetpack\Tracking;
18
use Automattic\Jetpack\Connection\Client;
19
20
if ( defined( 'STATS_VERSION' ) ) {
21
	return;
22
}
23
24
define( 'STATS_VERSION', '9' );
25
defined( 'STATS_DASHBOARD_SERVER' ) or define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
26
27
add_action( 'jetpack_modules_loaded', 'stats_load' );
28
29
/**
30
 * Load Stats.
31
 *
32
 * @access public
33
 * @return void
34
 */
35
function stats_load() {
36
	Jetpack::enable_module_configurable( __FILE__ );
37
38
	// Generate the tracking code after wp() has queried for posts.
39
	add_action( 'template_redirect', 'stats_template_redirect', 1 );
40
41
	add_action( 'wp_head', 'stats_admin_bar_head', 100 );
42
43
	add_action( 'wp_head', 'stats_hide_smile_css' );
44
45
	add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
46
47
	// Map stats caps.
48
	add_filter( 'map_meta_cap', 'stats_map_meta_caps', 10, 3 );
49
50
	if ( isset( $_GET['oldwidget'] ) ) {
51
		// Old one.
52
		add_action( 'wp_dashboard_setup', 'stats_register_dashboard_widget' );
53
	} else {
54
		add_action( 'admin_init', 'stats_merged_widget_admin_init' );
55
	}
56
57
	add_filter( 'jetpack_xmlrpc_methods', 'stats_xmlrpc_methods' );
58
59
	add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
60
61
	// Add an icon to see stats in WordPress.com for a particular post
62
	add_action( 'admin_print_styles-edit.php', 'jetpack_stats_load_admin_css' );
63
	add_filter( 'manage_posts_columns', 'jetpack_stats_post_table' );
64
	add_filter( 'manage_pages_columns', 'jetpack_stats_post_table' );
65
	add_action( 'manage_posts_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
66
	add_action( 'manage_pages_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
67
}
68
69
/**
70
 * Delay conditional for current_user_can to after init.
71
 *
72
 * @access public
73
 * @return void
74
 */
75
function stats_merged_widget_admin_init() {
76
	if ( current_user_can( 'view_stats' ) ) {
77
		add_action( 'load-index.php', 'stats_enqueue_dashboard_head' );
78
		add_action( 'wp_dashboard_setup', 'stats_register_widget_control_callback' ); // Hacky but works.
79
		add_action( 'jetpack_dashboard_widget', 'stats_jetpack_dashboard_widget' );
80
	}
81
}
82
83
/**
84
 * Enqueue Stats Dashboard
85
 *
86
 * @access public
87
 * @return void
88
 */
89
function stats_enqueue_dashboard_head() {
90
	add_action( 'admin_head', 'stats_dashboard_head' );
91
}
92
93
/**
94
 * Checks if filter is set and dnt is enabled.
95
 *
96
 * @return bool
97
 */
98
function jetpack_is_dnt_enabled() {
99
	/**
100
	 * Filter the option which decides honor DNT or not.
101
	 *
102
	 * @module stats
103
	 * @since 6.1.0
104
	 *
105
	 * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
106
	 */
107
	if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
108
		return false;
109
	}
110
111
	foreach ( $_SERVER as $name => $value ) {
112
		if ( 'http_dnt' == strtolower( $name ) && 1 == $value ) {
113
			return true;
114
		}
115
	}
116
117
	return false;
118
}
119
120
/**
121
 * Prevent sparkline img requests being redirected to upgrade.php.
122
 * See wp-admin/admin.php where it checks $wp_db_version.
123
 *
124
 * @access public
125
 * @param mixed $version Version.
126
 * @return string $version.
127
 */
128
function stats_ignore_db_version( $version ) {
129
	if (
130
		is_admin() &&
131
		isset( $_GET['page'] ) && 'stats' === $_GET['page'] &&
132
		isset( $_GET['chart'] ) && strpos($_GET['chart'], 'admin-bar-hours') === 0
133
	) {
134
		global $wp_db_version;
135
		return $wp_db_version;
136
	}
137
	return $version;
138
}
139
140
/**
141
 * Maps view_stats cap to read cap as needed.
142
 *
143
 * @access public
144
 * @param mixed $caps Caps.
145
 * @param mixed $cap Cap.
146
 * @param mixed $user_id User ID.
147
 * @return array Possibly mapped capabilities for meta capability.
148
 */
149
function stats_map_meta_caps( $caps, $cap, $user_id ) {
150
	// Map view_stats to exists.
151
	if ( 'view_stats' === $cap ) {
152
		$user        = new WP_User( $user_id );
153
		$user_role   = array_shift( $user->roles );
154
		$stats_roles = stats_get_option( 'roles' );
155
156
		// Is the users role in the available stats roles?
157
		if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles ) ) {
158
			$caps = array( 'read' );
159
		}
160
	}
161
162
	return $caps;
163
}
164
165
/**
166
 * Stats Template Redirect.
167
 *
168
 * @access public
169
 * @return void
170
 */
171
function stats_template_redirect() {
172
	global $current_user;
173
174
	if ( is_feed() || is_robots() || is_trackback() || is_preview() || jetpack_is_dnt_enabled() ) {
175
		return;
176
	}
177
178
	// Should we be counting this user's views?
179
	if ( ! empty( $current_user->ID ) ) {
180
		$count_roles = stats_get_option( 'count_roles' );
181
		if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
182
			return;
183
		}
184
	}
185
186
	add_action( 'wp_footer', 'stats_footer', 101 );
187
188
}
189
190
191
/**
192
 * Stats Build View Data.
193
 *
194
 * @access public
195
 * @return array.
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...
196
 */
197
function stats_build_view_data() {
198
	global $wp_the_query;
199
200
	$blog = Jetpack_Options::get_option( 'id' );
201
	$tz = get_option( 'gmt_offset' );
202
	$v = 'ext';
203
	$blog_url = wp_parse_url( site_url() );
204
	$srv = $blog_url['host'];
205
	$j = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
206
	if ( $wp_the_query->is_single || $wp_the_query->is_page || $wp_the_query->is_posts_page ) {
207
		// Store and reset the queried_object and queried_object_id
208
		// Otherwise, redirect_canonical() will redirect to home_url( '/' ) for show_on_front = page sites where home_url() is not all lowercase.
209
		// Repro:
210
		// 1. Set home_url = https://ExamPle.com/
211
		// 2. Set show_on_front = page
212
		// 3. Set page_on_front = something
213
		// 4. Visit https://example.com/ !
214
		$queried_object = ( isset( $wp_the_query->queried_object ) ) ? $wp_the_query->queried_object : null;
215
		$queried_object_id = ( isset( $wp_the_query->queried_object_id ) ) ? $wp_the_query->queried_object_id : null;
216
		$post = $wp_the_query->get_queried_object_id();
217
		$wp_the_query->queried_object = $queried_object;
218
		$wp_the_query->queried_object_id = $queried_object_id;
219
	} else {
220
		$post = '0';
221
	}
222
223
	return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
224
}
225
226
227
/**
228
 * Stats Footer.
229
 *
230
 * @access public
231
 * @return void
232
 */
233
function stats_footer() {
234
	$data = stats_build_view_data();
235
	if ( Jetpack_AMP_Support::is_amp_request() ) {
236
		stats_render_amp_footer( $data );
237
	} else {
238
		stats_render_footer( $data );
239
	}
240
	
241
}
242
243
function stats_render_footer( $data ) {
244
	$script = 'https://stats.wp.com/e-' . gmdate( 'YW' ) . '.js';
245
	$data_stats_array = stats_array( $data );
246
247
	$stats_footer = <<<END
248
<script type='text/javascript' src='{$script}' async='async' defer='defer'></script>
249
<script type='text/javascript'>
250
	_stq = window._stq || [];
251
	_stq.push([ 'view', {{$data_stats_array}} ]);
252
	_stq.push([ 'clickTrackerInit', '{$data['blog']}', '{$data['post']}' ]);
253
</script>
254
255
END;
256
	print $stats_footer;
257
}
258
259
function stats_render_amp_footer( $data ) {
260
	$data['host'] = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var ok.
261
	$data['rand'] = 'RANDOM'; // AMP placeholder.
262
	$data['ref']  = 'DOCUMENT_REFERRER'; // AMP placeholder.
263
	$data         = array_map( 'rawurlencode', $data );
264
	$pixel_url    = add_query_arg( $data, 'https://pixel.wp.com/g.gif' );
265
266
	?>
267
	<amp-pixel src="<?php echo esc_url( $pixel_url ); ?>"></amp-pixel>
268
	<?php
269
}
270
271
/**
272
 * Stats Get Options.
273
 *
274
 * @access public
275
 * @return array.
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...
276
 */
277
function stats_get_options() {
278
	$options = get_option( 'stats_options' );
279
280
	if ( ! isset( $options['version'] ) || $options['version'] < STATS_VERSION ) {
281
		$options = stats_upgrade_options( $options );
282
	}
283
284
	return $options;
285
}
286
287
/**
288
 * Get Stats Options.
289
 *
290
 * @access public
291
 * @param mixed $option Option.
292
 * @return mixed|null.
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...
293
 */
294
function stats_get_option( $option ) {
295
	$options = stats_get_options();
296
297
	if ( 'blog_id' === $option ) {
298
		return Jetpack_Options::get_option( 'id' );
299
	}
300
301
	if ( isset( $options[ $option ] ) ) {
302
		return $options[ $option ];
303
	}
304
305
	return null;
306
}
307
308
/**
309
 * Stats Set Options.
310
 *
311
 * @access public
312
 * @param mixed $option Option.
313
 * @param mixed $value Value.
314
 * @return bool.
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...
315
 */
316
function stats_set_option( $option, $value ) {
317
	$options = stats_get_options();
318
319
	$options[ $option ] = $value;
320
321
	return stats_set_options( $options );
322
}
323
324
/**
325
 * Stats Set Options.
326
 *
327
 * @access public
328
 * @param mixed $options Options.
329
 * @return bool
330
 */
331
function stats_set_options( $options ) {
332
	return update_option( 'stats_options', $options );
333
}
334
335
/**
336
 * Stats Upgrade Options.
337
 *
338
 * @access public
339
 * @param mixed $options Options.
340
 * @return array|bool
341
 */
342
function stats_upgrade_options( $options ) {
343
	$defaults = array(
344
		'admin_bar'    => true,
345
		'roles'        => array( 'administrator' ),
346
		'count_roles'  => array(),
347
		'blog_id'      => Jetpack_Options::get_option( 'id' ),
348
		'do_not_track' => true, // @todo
349
		'hide_smile'   => true,
350
	);
351
352
	if ( isset( $options['reg_users'] ) ) {
353
		if ( ! function_exists( 'get_editable_roles' ) ) {
354
			require_once ABSPATH . 'wp-admin/includes/user.php';
355
		}
356
		if ( $options['reg_users'] ) {
357
			$options['count_roles'] = array_keys( get_editable_roles() );
358
		}
359
		unset( $options['reg_users'] );
360
	}
361
362
	if ( is_array( $options ) && ! empty( $options ) ) {
363
		$new_options = array_merge( $defaults, $options );
364
	} else { $new_options = $defaults;
365
	}
366
367
	$new_options['version'] = STATS_VERSION;
368
369
	if ( ! stats_set_options( $new_options ) ) {
370
		return false;
371
	}
372
373
	stats_update_blog();
374
375
	return $new_options;
376
}
377
378
/**
379
 * Stats Array.
380
 *
381
 * @access public
382
 * @param mixed $kvs KVS.
383
 * @return array
384
 */
385
function stats_array( $kvs ) {
386
	/**
387
	 * Filter the options added to the JavaScript Stats tracking code.
388
	 *
389
	 * @module stats
390
	 *
391
	 * @since 1.1.0
392
	 *
393
	 * @param array $kvs Array of options about the site and page you're on.
394
	 */
395
	$kvs = apply_filters( 'stats_array', $kvs );
396
	$kvs = array_map( 'addslashes', $kvs );
397
	foreach ( $kvs as $k => $v ) {
398
		$jskvs[] = "$k:'$v'";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$jskvs was never initialized. Although not strictly required by PHP, it is generally a good practice to add $jskvs = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
399
	}
400
	return join( ',', $jskvs );
0 ignored issues
show
Bug introduced by
The variable $jskvs 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...
401
}
402
403
/**
404
 * Admin Pages.
405
 *
406
 * @access public
407
 * @return void
408
 */
409
function stats_admin_menu() {
410
	global $pagenow;
411
412
	// If we're at an old Stats URL, redirect to the new one.
413
	// Don't even bother with caps, menu_page_url(), etc.  Just do it.
414
	if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) {
415
		$redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', $_SERVER['REQUEST_URI'] );
416
		$relative_pos = strpos( $redirect_url, '/wp-admin/' );
417
		if ( false !== $relative_pos ) {
418
			wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
419
			exit;
420
		}
421
	}
422
423
	$hook = add_submenu_page( 'jetpack', __( 'Site Stats', 'jetpack' ), __( 'Site Stats', 'jetpack' ), 'view_stats', 'stats', 'jetpack_admin_ui_stats_report_page_wrapper' );
424
	add_action( "load-$hook", 'stats_reports_load' );
425
}
426
427
/**
428
 * Stats Admin Path.
429
 *
430
 * @access public
431
 * @return string
432
 */
433
function stats_admin_path() {
434
	return Jetpack::module_configuration_url( __FILE__ );
435
}
436
437
/**
438
 * Stats Reports Load.
439
 *
440
 * @access public
441
 * @return void
442
 */
443
function stats_reports_load() {
444
	wp_enqueue_script( 'jquery' );
445
	wp_enqueue_script( 'postbox' );
446
	wp_enqueue_script( 'underscore' );
447
448
	Jetpack_Admin_Page::load_wrapper_styles();
449
	add_action( 'admin_print_styles', 'stats_reports_css' );
450
451
	if ( isset( $_GET['nojs'] ) && $_GET['nojs'] ) {
452
		$parsed = wp_parse_url( admin_url() );
453
		// Remember user doesn't want JS.
454
		setcookie( 'stnojs', '1', time() + 172800, $parsed['path'] ); // 2 days.
455
	}
456
457
	if ( isset( $_COOKIE['stnojs'] ) && $_COOKIE['stnojs'] ) {
458
		// Detect if JS is on.  If so, remove cookie so next page load is via JS.
459
		add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
460
	} else if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) {
461
		// Normal page load.  Load page content via JS.
462
		add_action( 'admin_print_footer_scripts', 'stats_js_load_page_via_ajax' );
463
	}
464
}
465
466
/**
467
 * Stats Reports CSS.
468
 *
469
 * @access public
470
 * @return void
471
 */
472
function stats_reports_css() {
473
?>
474
<style type="text/css">
475
#jp-stats-wrap {
476
	max-width: 1040px;
477
	margin: 0 auto;
478
	overflow: hidden;
479
}
480
481
#stats-loading-wrap p {
482
	text-align: center;
483
	font-size: 2em;
484
	margin: 7.5em 15px 0 0;
485
	height: 64px;
486
	line-height: 64px;
487
}
488
</style>
489
<?php
490
}
491
492
493
/**
494
 * Detect if JS is on.  If so, remove cookie so next page load is via JS.
495
 *
496
 * @access public
497
 * @return void
498
 */
499
function stats_js_remove_stnojs_cookie() {
500
	$parsed = wp_parse_url( admin_url() );
501
?>
502
<script type="text/javascript">
503
/* <![CDATA[ */
504
document.cookie = 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=<?php echo esc_js( $parsed['path'] ); ?>';
505
/* ]]> */
506
</script>
507
<?php
508
}
509
510
/**
511
 * Normal page load.  Load page content via JS.
512
 *
513
 * @access public
514
 * @return void
515
 */
516
function stats_js_load_page_via_ajax() {
517
?>
518
<script type="text/javascript">
519
/* <![CDATA[ */
520
if ( -1 == document.location.href.indexOf( 'noheader' ) ) {
521
	jQuery( function( $ ) {
522
		$.get( document.location.href + '&noheader', function( responseText ) {
523
			$( '#stats-loading-wrap' ).replaceWith( responseText );
524
		} );
525
	} );
526
}
527
/* ]]> */
528
</script>
529
<?php
530
}
531
532
function jetpack_admin_ui_stats_report_page_wrapper()  {
533
	if( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) {
534
		Jetpack_Admin_Page::wrap_ui( 'stats_reports_page', array( 'is-wide' => true ) );
535
	} else {
536
		stats_reports_page();
537
	}
538
539
}
540
541
/**
542
 * Stats Report Page.
543
 *
544
 * @access public
545
 * @param bool $main_chart_only (default: false) Main Chart Only.
546
 */
547
function stats_reports_page( $main_chart_only = false ) {
548
549
	if ( isset( $_GET['dashboard'] ) ) {
550
		return stats_dashboard_widget_content();
551
	}
552
553
	$blog_id = stats_get_option( 'blog_id' );
554
	$domain = Jetpack::build_raw_urls( get_home_url() );
555
556
	$jetpack_admin_url = admin_url() . 'admin.php?page=jetpack';
0 ignored issues
show
Unused Code introduced by
$jetpack_admin_url is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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