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