1<?php
2
3namespace MediaWiki\ParamValidator\TypeDef;
4
5use User;
6use Wikimedia\Message\DataMessageValue;
7use Wikimedia\ParamValidator\ParamValidator;
8use Wikimedia\ParamValidator\TypeDef\TypeDefTestCase;
9use Wikimedia\ParamValidator\ValidationException;
10
11/**
12 * @covers MediaWiki\ParamValidator\TypeDef\UserDef
13 */
14class UserDefTest extends TypeDefTestCase {
15
16	protected static $testClass = UserDef::class;
17
18	private $wgHooks = null;
19
20	protected function setUp(): void {
21		global $wgHooks;
22
23		parent::setUp();
24
25		// We don't have MediaWikiIntegrationTestCase's methods available, so we have to do it ourself.
26		$this->wgHooks = $wgHooks;
27		$wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$iwdata ) {
28			if ( $prefix === 'interwiki' ) {
29				$iwdata = [
30					'iw_url' => 'http://example.com/',
31					'iw_local' => 0,
32					'iw_trans' => 0,
33				];
34				return false;
35			}
36		};
37	}
38
39	protected function tearDown(): void {
40		global $wgHooks;
41
42		$wgHooks = $this->wgHooks;
43
44		parent::tearDown();
45	}
46
47	public function provideValidate() {
48		// General tests of string inputs
49		$data = [
50			'Basic' => [ 'name', 'Some user', 'Some user' ],
51			'Normalized' => [ 'name', 'some_user', 'Some user' ],
52			'External' => [ 'interwiki', 'm>some_user', 'm>some_user' ],
53			'IPv4' => [ 'ip', '192.168.0.1', '192.168.0.1' ],
54			'IPv4, normalized' => [ 'ip', '192.168.000.001', '192.168.0.1' ],
55			'IPv6' => [ 'ip', '2001:DB8:0:0:0:0:0:0', '2001:DB8:0:0:0:0:0:0' ],
56			'IPv6, normalized' => [ 'ip', '2001:0db8::', '2001:DB8:0:0:0:0:0:0' ],
57			'IPv6, with leading ::' => [ 'ip', '::1', '0:0:0:0:0:0:0:1' ],
58			'IPv4 range' => [ 'cidr', '192.168.000.000/16', '192.168.0.0/16' ],
59			'IPv6 range' => [ 'cidr', '2001:0DB8::/64', '2001:DB8:0:0:0:0:0:0/64' ],
60			'Usemod IP' => [ 'ip', '192.168.0.xxx', '192.168.0.xxx' ],
61			'Bogus IP' => [ '', '192.168.0.256', null ],
62			'Bogus Usemod IP' => [ '', '192.268.0.xxx', null ],
63			'Usemod IP as range' => [ '', '192.168.0.xxx/16', null ],
64			'Bad username' => [ '', '[[Foo]]', null ],
65			'No namespaces' => [ '', 'Talk:Foo', null ],
66			'No namespaces (2)' => [ '', 'Help:Foo', null ],
67			'No namespaces (except User is ok)' => [ 'name', 'User:some_user', 'Some user' ],
68			'No namespaces (except User is ok) (IPv6)' => [ 'ip', 'User:::1', '0:0:0:0:0:0:0:1' ],
69			'No interwiki prefixes' => [ '', 'interwiki:Foo', null ],
70			'No fragment in IP' => [ '', '192.168.0.256#', null ],
71		];
72		foreach ( $data as $key => [ $type, $input, $expect ] ) {
73			$ex = new ValidationException(
74				DataMessageValue::new( 'paramvalidator-baduser', [], 'baduser' ),
75				'test', $input, []
76			);
77			if ( $type === '' ) {
78				yield $key => [ $input, $ex ];
79				continue;
80			}
81
82			yield $key => [ $input, $expect ];
83
84			yield "$key, only '$type' allowed" => [
85				$input,
86				$expect,
87				[ UserDef::PARAM_ALLOWED_USER_TYPES => [ $type ] ],
88			];
89
90			$types = array_diff( [ 'name', 'ip', 'cidr', 'interwiki' ], [ $type ] );
91			yield "$key, without '$type' allowed" => [
92				$input,
93				$ex,
94				[ UserDef::PARAM_ALLOWED_USER_TYPES => $types ],
95			];
96
97			$obj = $type === 'name' ? User::newFromName( $expect ) : User::newFromAnyId( 0, $expect, null );
98			yield "$key, returning object" => [ $input, $obj, [ UserDef::PARAM_RETURN_OBJECT => true ] ];
99		}
100
101		// Test input by user ID
102		// We can't test not returning object here, because we don't have a test
103		// database and there's no "UserFactory" (yet) to inject a mock of.
104		$input = '#1234';
105		$ex = new ValidationException(
106			DataMessageValue::new( 'paramvalidator-baduser', [], 'baduser' ),
107			'test', $input, []
108		);
109		yield 'User ID' => [ $input, $ex, [ UserDef::PARAM_RETURN_OBJECT => true ] ];
110		yield 'User ID, with \'id\' allowed, returning object' => [
111			$input,
112			User::newFromId( 1234 ),
113			[ UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ], UserDef::PARAM_RETURN_OBJECT => true ],
114		];
115
116		// Tests for T232672 (consistent treatment of whitespace and BIDI characters)
117		$data = [
118			'name' => [ 'Foo', [ 1 ], 'Foo' ],
119			'interwiki' => [ 'm>some_user', [ 1, 2, 6 ], null ],
120			'ip (v4)' => [ '192.168.0.1', [ 1, 3, 4 ], '192.168.0.1' ],
121			'ip (v6)' => [ '2001:DB8:0:0:0:0:0:0', [ 2, 5, 6 ], '2001:DB8:0:0:0:0:0:0' ],
122			'ip (v6, colons)' => [ '::1', [ 1, 2 ], '0:0:0:0:0:0:0:1' ],
123			'cidr (v4)' => [ '192.168.0.0/16', [ 1, 3, 4, 11, 12, 13 ], '192.168.0.0/16' ],
124			'cidr (v6)' => [ '2001:db8::/64', [ 2, 5, 6, 20, 21, 22 ], '2001:DB8:0:0:0:0:0:0/64' ],
125		];
126		foreach ( $data as $key => [ $name, $positions, $expect ] ) {
127			$input = " $name ";
128			yield "T232672: leading/trailing whitespace for $key" => [ $input, $expect ?? $input ];
129
130			$input = "_{$name}_";
131			yield "T232672: leading/trailing underscores for $key" => [ $input, $expect ?? $input ];
132
133			$positions = array_merge( [ 0, strlen( $name ) ], $positions );
134			foreach ( $positions as $i ) {
135				$input = substr_replace( $name, "\u{200E}", $i, 0 );
136				yield "T232672: U+200E at position $i for $key" => [ $input, $expect ?? $input ];
137			}
138		}
139	}
140
141	public function provideNormalizeSettings() {
142		return [
143			'Basic test' => [
144				[ 'param-foo' => 'bar' ],
145				[
146					'param-foo' => 'bar',
147					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'interwiki' ],
148				],
149			],
150			'Types not overridden' => [
151				[
152					'param-foo' => 'bar',
153					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
154				],
155				[
156					'param-foo' => 'bar',
157					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
158				],
159			],
160		];
161	}
162
163	public function provideCheckSettings() {
164		$keys = [ 'Y', UserDef::PARAM_ALLOWED_USER_TYPES, UserDef::PARAM_RETURN_OBJECT ];
165		$ismultiIssue = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
166			. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
167			. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
168			. 'once we have a real use case to look at.)';
169
170		return [
171			'Basic test' => [
172				[],
173				self::STDRET,
174				[
175					'issues' => [ 'X' ],
176					'allowedKeys' => $keys,
177					'messages' => [],
178				],
179			],
180			'Test with everything' => [
181				[
182					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
183					UserDef::PARAM_RETURN_OBJECT => true,
184				],
185				self::STDRET,
186				[
187					'issues' => [ 'X' ],
188					'allowedKeys' => $keys,
189					'messages' => [],
190				],
191			],
192			'Bad types' => [
193				[
194					UserDef::PARAM_ALLOWED_USER_TYPES => 'name',
195					UserDef::PARAM_RETURN_OBJECT => 1,
196				],
197				self::STDRET,
198				[
199					'issues' => [
200						'X',
201						UserDef::PARAM_RETURN_OBJECT => 'PARAM_RETURN_OBJECT must be boolean, got integer',
202						UserDef::PARAM_ALLOWED_USER_TYPES => 'PARAM_ALLOWED_USER_TYPES must be an array, got string',
203					],
204					'allowedKeys' => $keys,
205					'messages' => [],
206				],
207			],
208			'PARAM_ALLOWED_USER_TYPES cannot be empty' => [
209				[
210					UserDef::PARAM_ALLOWED_USER_TYPES => [],
211				],
212				self::STDRET,
213				[
214					'issues' => [
215						'X',
216						UserDef::PARAM_ALLOWED_USER_TYPES => 'PARAM_ALLOWED_USER_TYPES cannot be empty',
217					],
218					'allowedKeys' => $keys,
219					'messages' => [],
220				],
221			],
222			'PARAM_ALLOWED_USER_TYPES invalid values' => [
223				[
224					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id', 'ssn', 'Q-number' ],
225				],
226				self::STDRET,
227				[
228					'issues' => [
229						'X',
230						UserDef::PARAM_ALLOWED_USER_TYPES
231							=> 'PARAM_ALLOWED_USER_TYPES contains invalid values: ssn, Q-number',
232					],
233					'allowedKeys' => $keys,
234					'messages' => [],
235				],
236			],
237			'ISMULTI generally ok' => [
238				[
239					ParamValidator::PARAM_ISMULTI => true,
240				],
241				self::STDRET,
242				[
243					'issues' => [ 'X' ],
244					'allowedKeys' => $keys,
245					'messages' => [],
246				],
247			],
248			'ISMULTI with ID not ok (1)' => [
249				[
250					ParamValidator::PARAM_ISMULTI => true,
251					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
252				],
253				self::STDRET,
254				[
255					'issues' => [ 'X', $ismultiIssue ],
256					'allowedKeys' => $keys,
257					'messages' => [],
258				],
259			],
260			'ISMULTI with ID not ok (2)' => [
261				[
262					ParamValidator::PARAM_ISMULTI => true,
263					ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
264					ParamValidator::PARAM_ISMULTI_LIMIT2 => 11,
265					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
266				],
267				self::STDRET,
268				[
269					'issues' => [ 'X', $ismultiIssue ],
270					'allowedKeys' => $keys,
271					'messages' => [],
272				],
273			],
274			'ISMULTI with ID ok with low limits' => [
275				[
276					ParamValidator::PARAM_ISMULTI => true,
277					ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
278					ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
279					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
280				],
281				self::STDRET,
282				[
283					'issues' => [ 'X' ],
284					'allowedKeys' => $keys,
285					'messages' => [],
286				],
287			],
288			'ISMULTI with RETURN_OBJECT also not ok' => [
289				[
290					ParamValidator::PARAM_ISMULTI => true,
291					UserDef::PARAM_RETURN_OBJECT => true,
292				],
293				self::STDRET,
294				[
295					'issues' => [ 'X', $ismultiIssue ],
296					'allowedKeys' => $keys,
297					'messages' => [],
298				],
299			],
300			'ISMULTI with RETURN_OBJECT also ok with low limits' => [
301				[
302					ParamValidator::PARAM_ISMULTI => true,
303					ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
304					ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
305					UserDef::PARAM_RETURN_OBJECT => true,
306				],
307				self::STDRET,
308				[
309					'issues' => [ 'X' ],
310					'allowedKeys' => $keys,
311					'messages' => [],
312				],
313			],
314		];
315	}
316
317	public function provideGetInfo() {
318		return [
319			'Basic test' => [
320				[],
321				[
322					'subtypes' => [ 'name', 'ip', 'cidr', 'interwiki' ],
323				],
324				[
325					// phpcs:ignore Generic.Files.LineLength.TooLong
326					ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-user"><text>1</text><list listType="text"><text><message key="paramvalidator-help-type-user-subtype-name"></message></text><text><message key="paramvalidator-help-type-user-subtype-ip"></message></text><text><message key="paramvalidator-help-type-user-subtype-cidr"></message></text><text><message key="paramvalidator-help-type-user-subtype-interwiki"></message></text></list><num>4</num></message>',
327				],
328			],
329			'Specific types' => [
330				[
331					ParamValidator::PARAM_ISMULTI => true,
332					UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
333					UserDef::PARAM_RETURN_OBJECT => true,
334				],
335				[
336					'subtypes' => [ 'name', 'id' ],
337				],
338				[
339					// phpcs:ignore Generic.Files.LineLength.TooLong
340					ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-user"><text>2</text><list listType="text"><text><message key="paramvalidator-help-type-user-subtype-name"></message></text><text><message key="paramvalidator-help-type-user-subtype-id"></message></text></list><num>2</num></message>',
341				],
342			],
343		];
344	}
345
346}
347