Completed
Push — renovate/slack-web-api-5.x ( 42edfe...6ad45d )
by
unknown
184:59 queued 173:57
created

stats.php ➔ stats_jetpack_dashboard_widget()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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