Completed
Push — fix/revert-phpcs-stats ( 500755 )
by
unknown
152:26 queued 142:04
created

stats.php ➔ stats_dashboard_widget_options()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
231
		} finally {
232
			$wp_the_query->queried_object    = $queried_object;
233
			$wp_the_query->queried_object_id = $queried_object_id;
234
		}
235
	} else {
236
		$post = '0';
237
	}
238
239
	return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
240
}
241
242
243
/**
244
 * Stats Footer.
245
 *
246
 * @access public
247
 * @return void
248
 */
249
function stats_footer() {
250
	$data = stats_build_view_data();
251
	if ( Jetpack_AMP_Support::is_amp_request() ) {
252
		stats_render_amp_footer( $data );
253
	} else {
254
		stats_render_footer( $data );
255
	}
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 src='{$script}' defer></script>
265
<script>
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
	$stats_url = Redirect::get_url( 'calypso-stats' );
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="<?php echo esc_url( $stats_url ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'View stats on WordPress.com right now', 'jetpack' ); ?></a></p>
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] = (int) $_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 = 0; // Means use the blog token.
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 !== (int) ( $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 data-ampdevmode 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
		'title' => "<div><img src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
900
	);
901
902
	$wp_admin_bar->add_menu( $menu );
903
}
904
905
/**
906
 * Stats Update Blog.
907
 *
908
 * @access public
909
 * @return void
910
 */
911
function stats_update_blog() {
912
	XMLRPC_Async_Call::add_call( 'jetpack.updateBlog', 0, stats_get_blog() );
913
}
914
915
/**
916
 * Stats Get Blog.
917
 *
918
 * @access public
919
 * @return string
920
 */
921
function stats_get_blog() {
922
	$home = wp_parse_url( trailingslashit( get_option( 'home' ) ) );
923
	$blog = array(
924
		'host'                => $home['host'],
925
		'path'                => $home['path'],
926
		'blogname'            => get_option( 'blogname' ),
927
		'blogdescription'     => get_option( 'blogdescription' ),
928
		'siteurl'             => get_option( 'siteurl' ),
929
		'gmt_offset'          => get_option( 'gmt_offset' ),
930
		'timezone_string'     => get_option( 'timezone_string' ),
931
		'stats_version'       => STATS_VERSION,
932
		'stats_api'           => 'jetpack',
933
		'page_on_front'       => get_option( 'page_on_front' ),
934
		'permalink_structure' => get_option( 'permalink_structure' ),
935
		'category_base'       => get_option( 'category_base' ),
936
		'tag_base'            => get_option( 'tag_base' ),
937
	);
938
	$blog = array_merge( stats_get_options(), $blog );
939
	unset( $blog['roles'], $blog['blog_id'] );
940
	return stats_esc_html_deep( $blog );
941
}
942
943
/**
944
 * Modified from stripslashes_deep()
945
 *
946
 * @access public
947
 * @param mixed $value Value.
948
 * @return string
949
 */
950
function stats_esc_html_deep( $value ) {
951
	if ( is_array( $value ) ) {
952
		$value = array_map( 'stats_esc_html_deep', $value );
953
	} elseif ( is_object( $value ) ) {
954
		$vars = get_object_vars( $value );
955
		foreach ( $vars as $key => $data ) {
956
			$value->{$key} = stats_esc_html_deep( $data );
957
		}
958
	} elseif ( is_string( $value ) ) {
959
		$value = esc_html( $value );
960
	}
961
962
	return $value;
963
}
964
965
/**
966
 * Stats xmlrpc_methods function.
967
 *
968
 * @access public
969
 * @param mixed $methods Methods.
970
 * @return array
971
 */
972
function stats_xmlrpc_methods( $methods ) {
973
	$my_methods = array(
974
		'jetpack.getBlog' => 'stats_get_blog',
975
	);
976
977
	return array_merge( $methods, $my_methods );
978
}
979
980
/**
981
 * Register Stats Dashboard Widget.
982
 *
983
 * @access public
984
 * @return void
985
 */
986
function stats_register_dashboard_widget() {
987
	if ( ! current_user_can( 'view_stats' ) )
988
		return;
989
990
	// With wp_dashboard_empty: we load in the content after the page load via JS.
991
	wp_add_dashboard_widget( 'dashboard_stats', __( 'Site Stats', 'jetpack' ), 'wp_dashboard_empty', 'stats_dashboard_widget_control' );
992
993
	add_action( 'admin_head', 'stats_dashboard_head' );
994
}
995
996
/**
997
 * Stats Dashboard Widget Options.
998
 *
999
 * @access public
1000
 * @return array
1001
 */
1002
function stats_dashboard_widget_options() {
1003
	$defaults = array( 'chart' => 1, 'top' => 1, 'search' => 7 );
1004
	if ( ( ! $options = get_option( 'stats_dashboard_widget' ) ) || ! is_array( $options ) ) {
1005
		$options = array();
1006
	}
1007
1008
	// Ignore obsolete option values.
1009
	$intervals = array( 1, 7, 31, 90, 365 );
1010
	foreach ( array( 'top', 'search' ) as $key ) {
1011
		if ( isset( $options[ $key ] ) && ! in_array( $options[ $key ], $intervals ) ) {
1012
			unset( $options[ $key ] );
1013
		}
1014
	}
1015
1016
		return array_merge( $defaults, $options );
1017
}
1018
1019
/**
1020
 * Stats Dashboard Widget Control.
1021
 *
1022
 * @access public
1023
 * @return void
1024
 */
1025
function stats_dashboard_widget_control() {
1026
	$periods   = array(
1027
		'1' => __( 'day', 'jetpack' ),
1028
		'7' => __( 'week', 'jetpack' ),
1029
		'31' => __( 'month', 'jetpack' ),
1030
	);
1031
	$intervals = array(
1032
		'1' => __( 'the past day', 'jetpack' ),
1033
		'7' => __( 'the past week', 'jetpack' ),
1034
		'31' => __( 'the past month', 'jetpack' ),
1035
		'90' => __( 'the past quarter', 'jetpack' ),
1036
		'365' => __( 'the past year', 'jetpack' ),
1037
	);
1038
	$defaults = array(
1039
		'top' => 1,
1040
		'search' => 7,
1041
	);
1042
1043
	$options = stats_dashboard_widget_options();
1044
1045
	if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) {
1046
		if ( isset( $periods[ $_POST['chart'] ] ) ) {
1047
			$options['chart'] = $_POST['chart'];
1048
		}
1049
		foreach ( array( 'top', 'search' ) as $key ) {
1050
			if ( isset( $intervals[ $_POST[ $key ] ] ) ) {
1051
				$options[ $key ] = $_POST[ $key ];
1052
			} else { $options[ $key ] = $defaults[ $key ];
1053
			}
1054
		}
1055
		update_option( 'stats_dashboard_widget', $options );
1056
	}
1057
?>
1058
	<p>
1059
	<label for="chart"><?php esc_html_e( 'Chart stats by' , 'jetpack' ); ?></label>
1060
	<select id="chart" name="chart">
1061
	<?php
1062
	foreach ( $periods as $val => $label ) {
1063
?>
1064
		<option value="<?php echo $val; ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
1065
		<?php
1066
	}
1067
?>
1068
	</select>.
1069
	</p>
1070
1071
	<p>
1072
	<label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
1073
	<select id="top" name="top">
1074
	<?php
1075 View Code Duplication
	foreach ( $intervals as $val => $label ) {
1076
?>
1077
		<option value="<?php echo $val; ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
1078
		<?php
1079
	}
1080
?>
1081
	</select>.
1082
	</p>
1083
1084
	<p>
1085
	<label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
1086
	<select id="search" name="search">
1087
	<?php
1088 View Code Duplication
	foreach ( $intervals as $val => $label ) {
1089
?>
1090
		<option value="<?php echo $val; ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
1091
		<?php
1092
	}
1093
?>
1094
	</select>.
1095
	</p>
1096
	<?php
1097
}
1098
1099
/**
1100
 * Jetpack Stats Dashboard Widget.
1101
 *
1102
 * @access public
1103
 * @return void
1104
 */
1105
function stats_jetpack_dashboard_widget() {
1106
	?>
1107
	<form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
1108
		<?php stats_dashboard_widget_control(); ?>
1109
		<?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
1110
		<input type="hidden" name="widget_id" value="dashboard_stats" />
1111
		<?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
1112
	</form>
1113
	<button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
1114
		<span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
1115
		<span class="toggle-indicator" aria-hidden="true"></span>
1116
	</button>
1117
	<div id="dashboard_stats">
1118
		<div class="inside">
1119
			<div style="height: 250px;"></div>
1120
		</div>
1121
	</div>
1122
	<?php
1123
}
1124
1125
/**
1126
 * Register Stats Widget Control Callback.
1127
 *
1128
 * @access public
1129
 * @return void
1130
 */
1131
function stats_register_widget_control_callback() {
1132
	$GLOBALS['wp_dashboard_control_callbacks']['dashboard_stats'] = 'stats_dashboard_widget_control';
1133
}
1134
1135
/**
1136
 * JavaScript and CSS for dashboard widget.
1137
 *
1138
 * @access public
1139
 * @return void
1140
 */
1141
function stats_dashboard_head() {
1142
	?>
1143
<script type="text/javascript">
1144
/* <![CDATA[ */
1145
jQuery( function($) {
1146
	var dashStats = jQuery( '#dashboard_stats div.inside' );
1147
1148
	if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
1149
		return;
1150
	}
1151
1152
	if ( ! dashStats.length ) {
1153
		dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
1154
		var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
1155
		var args = 'width=' + dashStats.width() + '&height=' + h.toString();
1156
	} else {
1157
		if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
1158
			var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
1159
		} else {
1160
			var args = 'width=' + ( dashStats.width() * 2 ).toString();
1161
		}
1162
	}
1163
1164
	dashStats
1165
		.not( '.dashboard-widget-control' )
1166
		.load( 'admin.php?page=stats&noheader&dashboard&' + args );
1167
1168
	jQuery( window ).one( 'resize', function() {
1169
		jQuery( '#stat-chart' ).css( 'width', 'auto' );
1170
	} );
1171
1172
1173
	// Widget settings toggle container.
1174
	var toggle = $( '.js-toggle-stats_dashboard_widget_control' );
1175
1176
	// Move the toggle in the widget header.
1177
	toggle.appendTo( '#jetpack_summary_widget .handle-actions' );
1178
1179
	// Toggle settings when clicking on it.
1180
	toggle.show().click( function( e ) {
1181
		e.preventDefault();
1182
		e.stopImmediatePropagation();
1183
		$( this ).parent().toggleClass( 'controlVisible' );
1184
		$( '#stats_dashboard_widget_control' ).slideToggle();
1185
	} );
1186
} );
1187
/* ]]> */
1188
</script>
1189
	<?php
1190
}
1191
1192
/**
1193
 * Stats Dashboard Widget Content.
1194
 *
1195
 * @access public
1196
 * @return void
1197
 */
1198
function stats_dashboard_widget_content() {
1199
	if ( ! isset( $_GET['width'] ) || ( ! $width = (int) ( $_GET['width'] / 2 ) ) || $width < 250 ) {
1200
		$width = 370;
1201
	}
1202
	if ( ! isset( $_GET['height'] ) || ( ! $height = (int) $_GET['height'] - 36 ) || $height < 230 ) {
1203
		$height = 180;
1204
	}
1205
1206
	$_width  = $width  - 5;
1207
	$_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack!
1208
1209
	$options = stats_dashboard_widget_options();
1210
	$blog_id = Jetpack_Options::get_option( 'id' );
1211
1212
	$q = array(
1213
		'noheader' => 'true',
1214
		'proxy' => '',
1215
		'blog' => $blog_id,
1216
		'page' => 'stats',
1217
		'chart' => '',
1218
		'unit' => $options['chart'],
1219
		'color' => get_user_option( 'admin_color' ),
1220
		'width' => $_width,
1221
		'height' => $_height,
1222
		'ssl' => is_ssl(),
1223
		'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
1224
	);
1225
1226
	$url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-admin/index.php";
1227
1228
	$url = add_query_arg( $q, $url );
1229
	$method = 'GET';
1230
	$timeout = 90;
1231
	$user_id = 0; // Means use the blog token.
1232
1233
	$get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1234
	$get_code = wp_remote_retrieve_response_code( $get );
1235
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1236
		stats_print_wp_remote_error( $get, $url );
1237
	} else {
1238
		$body = stats_convert_post_titles( $get['body'] );
1239
		$body = stats_convert_chart_urls( $body );
1240
		$body = stats_convert_image_urls( $body );
1241
		echo $body;
1242
	}
1243
1244
	$post_ids = array();
1245
1246
	$csv_end_date = date( 'Y-m-d', current_time( 'timestamp' ) );
1247
	$csv_args = array( 'top' => "&limit=8&end=$csv_end_date", 'search' => "&limit=5&end=$csv_end_date" );
1248
	/* Translators: Stats dashboard widget postviews list: "$post_title $views Views". */
1249
	$printf = __( '%1$s %2$s Views' , 'jetpack' );
1250
1251
	foreach ( $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" ) as $i => $post ) {
1252
		if ( 0 === $post['post_id'] ) {
1253
			unset( $top_posts[$i] );
1254
			continue;
1255
		}
1256
		$post_ids[] = $post['post_id'];
1257
	}
1258
1259
	// Cache.
1260
	get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
1261
1262
	$searches = array();
1263
	foreach ( $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" ) as $search_term ) {
1264
		if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
1265
			continue;
1266
		}
1267
		$searches[] = esc_html( $search_term['searchterm'] );
1268
	}
1269
1270
?>
1271
<div id="stats-info">
1272
	<div id="top-posts" class='stats-section'>
1273
		<div class="stats-section-inner">
1274
		<h3 class="heading"><?php  esc_html_e( 'Top Posts' , 'jetpack' ); ?></h3>
1275
		<?php
1276
	if ( empty( $top_posts ) ) {
1277
?>
1278
			<p class="nothing"><?php  esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1279
			<?php
1280
	} else {
1281
		foreach ( $top_posts as $post ) {
1282
			if ( ! get_post( $post['post_id'] ) ) {
1283
				continue;
1284
			}
1285
?>
1286
				<p><?php printf(
1287
				$printf,
1288
				'<a href="' . get_permalink( $post['post_id'] ) . '">' . get_the_title( $post['post_id'] ) . '</a>',
1289
				number_format_i18n( $post['views'] )
1290
			); ?></p>
1291
				<?php
1292
		}
1293
	}
1294
?>
1295
		</div>
1296
	</div>
1297
	<div id="top-search" class='stats-section'>
1298
		<div class="stats-section-inner">
1299
		<h3 class="heading"><?php  esc_html_e( 'Top Searches' , 'jetpack' ); ?></h3>
1300
		<?php
1301
	if ( empty( $searches ) ) {
1302
?>
1303
			<p class="nothing"><?php  esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
1304
			<?php
1305
	} else {
1306
		foreach ( $searches as $search_term_item ) {
1307
			printf(
1308
				'<p>%s</p>',
1309
				$search_term_item
1310
			);
1311
		}
1312
	}
1313
?>
1314
		</div>
1315
	</div>
1316
</div>
1317
<div class="clear"></div>
1318
<div class="stats-view-all">
1319
<?php
1320
	$stats_day_url = Redirect::get_url( 'calypso-stats-day' );
1321
	printf(
1322
		'<a class="button" target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a>',
1323
		esc_url( $stats_day_url ),
1324
		esc_html__( 'View all stats', 'jetpack' )
1325
	);
1326
?>
1327
</div>
1328
<div class="clear"></div>
1329
<?php
1330
	exit;
1331
}
1332
1333
/**
1334
 * Stats Print WP Remote Error.
1335
 *
1336
 * @access public
1337
 * @param mixed $get Get.
1338
 * @param mixed $url URL.
1339
 * @return void
1340
 */
1341
function stats_print_wp_remote_error( $get, $url ) {
1342
	$state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
1343
	$previous_error = Jetpack::state( $state_name );
1344
	$error = md5( serialize( compact( 'get', 'url' ) ) );
1345
	Jetpack::state( $state_name, $error );
1346
	if ( $error !== $previous_error ) {
1347
?>
1348
	<div class="wrap">
1349
	<p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
1350
	</div>
1351
<?php
1352
		return;
1353
	}
1354
?>
1355
	<div class="wrap">
1356
	<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>
1357
	<pre>
1358
	User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
1359
	Page URL: "http<?php echo (is_ssl()?'s':'') . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
1360
	API URL: "<?php echo esc_url( $url ); ?>"
1361
<?php
1362
if ( is_wp_error( $get ) ) {
1363
	foreach ( $get->get_error_codes() as $code ) {
1364
		foreach ( $get->get_error_messages( $code ) as $message ) {
1365
?>
1366
<?php print $code . ': "' . $message . '"' ?>
1367
1368
<?php
1369
		}
1370
	}
1371
} else {
1372
	$get_code = wp_remote_retrieve_response_code( $get );
1373
	$content_length = strlen( wp_remote_retrieve_body( $get ) );
1374
?>
1375
Response code: "<?php print $get_code ?>"
1376
Content length: "<?php print $content_length ?>"
1377
1378
<?php
1379
}
1380
	?></pre>
1381
	</div>
1382
	<?php
1383
}
1384
1385
/**
1386
 * Get stats from WordPress.com
1387
 *
1388
 * @param string $table The stats which you want to retrieve: postviews, or searchterms.
1389
 * @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...
1390
 *      An associative array of arguments.
1391
 *
1392
 *      @type bool    $end        The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
1393
 *                                and default timezone is UTC date. Default value is Now.
1394
 *      @type string  $days       The length of the desired time frame. Default is 30. Maximum 90 days.
1395
 *      @type int     $limit      The maximum number of records to return. Default is 10. Maximum 100.
1396
 *      @type int     $post_id    The ID of the post to retrieve stats data for
1397
 *      @type string  $summarize  If present, summarizes all matching records. Default Null.
1398
 *
1399
 * }
1400
 *
1401
 * @return array {
1402
 *      An array of post view data, each post as an array
1403
 *
1404
 *      array {
1405
 *          The post view data for a single post
1406
 *
1407
 *          @type string  $post_id         The ID of the post
1408
 *          @type string  $post_title      The title of the post
1409
 *          @type string  $post_permalink  The permalink for the post
1410
 *          @type string  $views           The number of views for the post within the $num_days specified
1411
 *      }
1412
 * }
1413
 */
1414
function stats_get_csv( $table, $args = null ) {
1415
	$defaults = array( 'end' => false, 'days' => false, 'limit' => 3, 'post_id' => false, 'summarize' => '' );
1416
1417
	$args = wp_parse_args( $args, $defaults );
0 ignored issues
show
Documentation introduced by
$defaults is of type array<string,false|integ...,"summarize":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1418
	$args['table'] = $table;
1419
	$args['blog_id'] = Jetpack_Options::get_option( 'id' );
1420
1421
	$stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
1422
1423
	$key = md5( $stats_csv_url );
1424
1425
	// Get cache.
1426
	$stats_cache = get_option( 'stats_cache' );
1427
	if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
1428
		$stats_cache = array();
1429
	}
1430
1431
	// Return or expire this key.
1432
	if ( isset( $stats_cache[ $key ] ) ) {
1433
		$time = key( $stats_cache[ $key ] );
1434
		if ( time() - $time < 300 ) {
1435
			return $stats_cache[ $key ][ $time ];
1436
		}
1437
		unset( $stats_cache[ $key ] );
1438
	}
1439
1440
	$stats_rows = array();
1441
	do {
1442
		if ( ! $stats = stats_get_remote_csv( $stats_csv_url ) ) {
1443
			break;
1444
		}
1445
1446
		$labels = array_shift( $stats );
1447
1448
		if ( 0 === stripos( $labels[0], 'error' ) ) {
1449
			break;
1450
		}
1451
1452
		$stats_rows = array();
1453
		for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
1454
			$row = array();
1455
			foreach ( $labels as $col => $label ) {
1456
				$row[ $label ] = $stats[ $s ][ $col ];
1457
			}
1458
			$stats_rows[] = $row;
1459
		}
1460
	} while ( 0 );
1461
1462
	// Expire old keys.
1463
	foreach ( $stats_cache as $k => $cache ) {
1464
		if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
1465
			unset( $stats_cache[ $k ] );
1466
		}
1467
	}
1468
1469
		// Set cache.
1470
		$stats_cache[ $key ] = array( time() => $stats_rows );
1471
	update_option( 'stats_cache', $stats_cache );
1472
1473
	return $stats_rows;
1474
}
1475
1476
/**
1477
 * Stats get remote CSV.
1478
 *
1479
 * @access public
1480
 * @param mixed $url URL.
1481
 * @return array
1482
 */
1483
function stats_get_remote_csv( $url ) {
1484
	$method = 'GET';
1485
	$timeout = 90;
1486
	$user_id = 0; // Blog token.
1487
1488
	$get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
1489
	$get_code = wp_remote_retrieve_response_code( $get );
1490
	if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
1491
		return array(); // @todo: return an error?
1492
	} else {
1493
		return stats_str_getcsv( $get['body'] );
1494
	}
1495
}
1496
1497
/**
1498
 * Rather than parsing the csv and its special cases, we create a new file and do fgetcsv on it.
1499
 *
1500
 * @access public
1501
 * @param mixed $csv CSV.
1502
 * @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...
1503
 */
1504
function stats_str_getcsv( $csv ) {
1505
	if ( function_exists( 'str_getcsv' ) ) {
1506
		$lines = str_getcsv( $csv, "\n" ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.str_getcsvFound
1507
		return array_map( 'str_getcsv', $lines );
1508
	}
1509
	if ( ! $temp = tmpfile() ) { // The tmpfile() automatically unlinks.
1510
		return false;
1511
	}
1512
1513
	$data = array();
1514
1515
	fwrite( $temp, $csv, strlen( $csv ) );
1516
	fseek( $temp, 0 );
1517
	while ( false !== $row = fgetcsv( $temp, 2000 ) ) {
1518
		$data[] = $row;
1519
	}
1520
	fclose( $temp );
1521
1522
	return $data;
1523
}
1524
1525
/**
1526
 * Abstract out building the rest api stats path.
1527
 *
1528
 * @param  string $resource Resource.
1529
 * @return string
1530
 */
1531
function jetpack_stats_api_path( $resource = '' ) {
1532
	$resource = ltrim( $resource, '/' );
1533
	return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
1534
}
1535
1536
/**
1537
 * Fetches stats data from the REST API.  Caches locally for 5 minutes.
1538
 *
1539
 * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
1540
 * @access public
1541
 * @param array  $args (default: array())  The args that are passed to the endpoint.
1542
 * @param string $resource (default: '') Optional sub-endpoint following /stats/.
1543
 * @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...
1544
 */
1545
function stats_get_from_restapi( $args = array(), $resource = '' ) {
1546
	$endpoint    = jetpack_stats_api_path( $resource );
1547
	$api_version = '1.1';
1548
	$args        = wp_parse_args( $args, array() );
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1549
	$cache_key   = md5( implode( '|', array( $endpoint, $api_version, serialize( $args ) ) ) );
1550
1551
	$transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
1552
1553
	$stats_cache = get_transient( $transient_name );
1554
1555
	// Return or expire this key.
1556
	if ( $stats_cache ) {
1557
		$time = key( $stats_cache );
1558
		$data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object)
1559
1560
		if ( is_wp_error( $data ) ) {
1561
			return $data;
1562
		}
1563
1564
		return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
1565
	}
1566
1567
	// Do the dirty work.
1568
	$response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
0 ignored issues
show
Bug introduced by
It seems like $args defined by wp_parse_args($args, array()) on line 1548 can also be of type null; however, Automattic\Jetpack\Conne...n_api_request_as_blog() does only seem to accept array, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1569
	if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1570
		// WP_Error
1571
		$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...
1572
		// WP_Error
1573
		$return = $data;
1574
	} else {
1575
		// string (JSON encoded object)
1576
		$data = wp_remote_retrieve_body( $response );
1577
		// object (rare: null on JSON failure)
1578
		$return = json_decode( $data );
1579
	}
1580
1581
	// To reduce size in storage: store with time as key, store JSON encoded data (unless error).
1582
	set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
1583
1584
	return $return;
1585
}
1586
1587
/**
1588
 * Load CSS needed for Stats column width in WP-Admin area.
1589
 *
1590
 * @since 4.7.0
1591
 */
1592
function jetpack_stats_load_admin_css() {
1593
	?>
1594
	<style type="text/css">
1595
		.fixed .column-stats {
1596
			width: 5em;
1597
		}
1598
	</style>
1599
	<?php
1600
}
1601
1602
/**
1603
 * Set header for column that allows to go to WordPress.com to see an entry's stats.
1604
 *
1605
 * @param array $columns An array of column names.
1606
 *
1607
 * @since 4.7.0
1608
 *
1609
 * @return mixed
1610
 */
1611
function jetpack_stats_post_table( $columns ) { // Adds a stats link on the edit posts page
1612
	if ( ! current_user_can( 'view_stats' ) || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
1613
		return $columns;
1614
	}
1615
	// Array-Fu to add before comments
1616
	$pos = array_search( 'comments', array_keys( $columns ) );
1617
	if ( ! is_int( $pos ) ) {
1618
		return $columns;
1619
	}
1620
	$chunks             = array_chunk( $columns, $pos, true );
1621
	$chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
1622
1623
	return call_user_func_array( 'array_merge', $chunks );
1624
}
1625
1626
/**
1627
 * Set content for cell with link to an entry's stats in WordPress.com.
1628
 *
1629
 * @param string $column  The name of the column to display.
1630
 * @param int    $post_id The current post ID.
1631
 *
1632
 * @since 4.7.0
1633
 *
1634
 * @return mixed
1635
 */
1636
function jetpack_stats_post_table_cell( $column, $post_id ) {
1637
	if ( 'stats' == $column ) {
1638
		if ( 'publish' != get_post_status( $post_id ) ) {
1639
			printf(
1640
				'<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
1641
				esc_html__( 'No stats', 'jetpack' )
1642
			);
1643
		} else {
1644
			$stats_post_url = Redirect::get_url(
1645
				'calypso-stats-post',
1646
				array(
1647
					'path' => $post_id,
1648
				)
1649
			);
1650
			printf(
1651
				'<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
1652
				esc_url( $stats_post_url ),
1653
				esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
1654
			);
1655
		}
1656
	}
1657
}
1658