Skip to content

Commit 836062c

Browse files
committed
Ajax: Improve sanitization and screen state in dashboard widget updates.
1 parent 4d3b0b9 commit 836062c

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

src/wp-admin/includes/ajax-actions.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,14 @@ function wp_ajax_get_community_events() {
420420
function wp_ajax_dashboard_widgets() {
421421
require_once ABSPATH . 'wp-admin/includes/dashboard.php';
422422

423-
$pagenow = $_GET['pagenow'];
423+
$pagenow = isset( $_GET['pagenow'] ) ? sanitize_key( $_GET['pagenow'] ) : '';
424+
424425
if ( 'dashboard-user' === $pagenow || 'dashboard-network' === $pagenow || 'dashboard' === $pagenow ) {
425426
set_current_screen( $pagenow );
426427
}
427428

428-
switch ( $_GET['widget'] ) {
429+
$widget = isset( $_GET['widget'] ) ? sanitize_key( $_GET['widget'] ) : '';
430+
switch ( $widget ) {
429431
case 'dashboard_primary':
430432
wp_dashboard_primary();
431433
break;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
/**
4+
* Admin Ajax functions to be tested.
5+
*/
6+
require_once ABSPATH . 'wp-admin/includes/ajax-actions.php';
7+
8+
/**
9+
* Testing Dashboard Widgets AJAX functionality.
10+
*
11+
* @group ajax
12+
*
13+
* @covers ::wp_ajax_dashboard_widgets
14+
*/
15+
class Tests_Ajax_wpAjaxDashboardWidgets extends WP_Ajax_UnitTestCase {
16+
17+
/**
18+
* @var int
19+
*/
20+
protected static int $admin_id;
21+
22+
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void {
23+
self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) );
24+
}
25+
26+
public function set_up(): void {
27+
parent::set_up();
28+
29+
wp_set_current_user( self::$admin_id );
30+
set_current_screen( 'dashboard' );
31+
32+
//Prevent time waste due to all external HTTP requests from RSS feeds
33+
add_filter( 'pre_http_request', '__return_true' );
34+
$GLOBALS['post'] = null;
35+
}
36+
37+
public function tear_down(): void {
38+
set_current_screen( 'front' );
39+
unset( $_GET['pagenow'], $_GET['widget'], $_POST['post_ID'], $_POST['action'] );
40+
unset( $GLOBALS['post'] );
41+
parent::tear_down();
42+
}
43+
44+
/**
45+
* wp_ajax_dashboard_widgets Happy Path。
46+
*
47+
* All valid inputs should correctly set the current screen and output the expected widget content.
48+
*/
49+
public function test_wp_ajax_dashboard_widgets_happy_path(): void {
50+
$this->_setRole( 'administrator' );
51+
52+
$_GET['pagenow'] = 'dashboard';
53+
$_GET['widget'] = 'dashboard_primary';
54+
$_POST['action'] = 'dashboard-widgets';
55+
56+
update_option( 'dashboard_primary_feeds', array( 'test' => array( 'link' => 'https://wordpress.org' ) ) );
57+
58+
try {
59+
$this->_handleAjax( 'dashboard-widgets' );
60+
} catch ( \WPAjaxDieContinueException $e ) { // 捕捉所有 AJAX 死亡例外 (包括 Stop 和 Continue)
61+
unset( $e );
62+
}
63+
64+
$output = $this->_last_response;
65+
66+
$this->assertStringContainsString( 'rss-widget', $output );
67+
$this->assertSame( 'dashboard', $GLOBALS['current_screen']->id );
68+
}
69+
70+
/**
71+
* Test empty parameters should not trigger PHP Warning
72+
*
73+
* @ticket 65054
74+
*/
75+
public function test_wp_ajax_dashboard_widgets_empty_params(): void {
76+
$this->_setRole( 'administrator' );
77+
$_GET = array();
78+
$_POST['action'] = 'dashboard-widgets';
79+
80+
try {
81+
$this->_handleAjax( 'dashboard-widgets' );
82+
} catch ( \Exception $e ) {
83+
$this->assertTrue( true );
84+
}
85+
86+
$this->assertEmpty( $this->_last_response );
87+
}
88+
89+
/**
90+
* Test that the current screen is not affected by global post
91+
*
92+
* @ticket 65054
93+
*/
94+
public function test_wp_ajax_dashboard_widgets_should_not_be_affected_by_global_post(): void {
95+
//Should Not See This
96+
$pollution_post_id = self::factory()->post->create( array( 'post_title' => 'Should Not See This' ) );
97+
$GLOBALS['post'] = get_post( $pollution_post_id );
98+
99+
$_GET['pagenow'] = 'dashboard';
100+
$_GET['widget'] = 'dashboard_primary';
101+
$_GET['action'] = 'dashboard-widgets';
102+
103+
wp_dashboard_setup();
104+
try {
105+
$this->_handleAjax( 'dashboard-widgets' );
106+
} catch ( \WPAjaxDieContinueException $e ) {
107+
unset( $e );
108+
}
109+
110+
$output = $this->_last_response;
111+
112+
$this->assertStringContainsString( 'rss-widget', $output );
113+
$this->assertSame( 'dashboard', $GLOBALS['current_screen']->id );
114+
}
115+
116+
/**
117+
* Invalid request should not trigger global fallback
118+
*
119+
* @ticket 65054
120+
*/
121+
public function test_wp_ajax_dashboard_widgets_invalid_request_no_fallback(): void {
122+
$_GET['widget'] = 'non_existent_widget';
123+
$_POST['action'] = 'dashboard-widgets';
124+
125+
try {
126+
$this->_handleAjax( 'dashboard-widgets' );
127+
} catch ( \Exception $e ) {
128+
$is_ajax_die = $e instanceof \WPAjaxDieStopException || $e instanceof \WPAjaxDieContinueException;
129+
$this->assertTrue( $is_ajax_die, 'Captured exception should be an AJAX die exception' );
130+
}
131+
132+
$this->assertEmpty( $this->_last_response );
133+
}
134+
135+
/**
136+
* Test that when an invalid pagenow is passed, the screen should not be changed.
137+
*
138+
* @ticket 65054
139+
*/
140+
public function test_wp_ajax_dashboard_widgets_invalid_pagenow(): void {
141+
$this->_setRole( 'administrator' );
142+
143+
$_GET['pagenow'] = 'malicious-script-tag'; //malicious-script-tag
144+
$_GET['widget'] = 'dashboard_primary';
145+
146+
try {
147+
$this->_handleAjax( 'dashboard-widgets' );
148+
} catch ( \WPAjaxDieContinueException $e ) {
149+
unset( $e );
150+
}
151+
152+
$this->assertNotSame( 'malicious-script-tag', $GLOBALS['current_screen']->id );
153+
}
154+
}

0 commit comments

Comments
 (0)