0e830062219de15f657be73b644b5b9736e8cfc3
[lhc/web/wiklou.git] / tests / phpunit / includes / OutputPageTest.php
1 <?php
2
3 /**
4 *
5 * @author Matthew Flaschen
6 *
7 * @group Output
8 *
9 * @todo factor tests in this class into providers and test methods
10 */
11 class OutputPageTest extends MediaWikiTestCase {
12 const SCREEN_MEDIA_QUERY = 'screen and (min-width: 982px)';
13 const SCREEN_ONLY_MEDIA_QUERY = 'only screen and (min-width: 982px)';
14
15 /**
16 * Tests a particular case of transformCssMedia, using the given input, globals,
17 * expected return, and message
18 *
19 * Asserts that $expectedReturn is returned.
20 *
21 * options['printableQuery'] - value of query string for printable, or omitted for none
22 * options['handheldQuery'] - value of query string for handheld, or omitted for none
23 * options['media'] - passed into the method under the same name
24 * options['expectedReturn'] - expected return value
25 * options['message'] - PHPUnit message for assertion
26 *
27 * @param array $args Key-value array of arguments as shown above
28 */
29 protected function assertTransformCssMediaCase( $args ) {
30 $queryData = [];
31 if ( isset( $args['printableQuery'] ) ) {
32 $queryData['printable'] = $args['printableQuery'];
33 }
34
35 if ( isset( $args['handheldQuery'] ) ) {
36 $queryData['handheld'] = $args['handheldQuery'];
37 }
38
39 $fauxRequest = new FauxRequest( $queryData, false );
40 $this->setMwGlobals( [
41 'wgRequest' => $fauxRequest,
42 ] );
43
44 $actualReturn = OutputPage::transformCssMedia( $args['media'] );
45 $this->assertSame( $args['expectedReturn'], $actualReturn, $args['message'] );
46 }
47
48 /**
49 * Tests print requests
50 * @covers OutputPage::transformCssMedia
51 */
52 public function testPrintRequests() {
53 $this->assertTransformCssMediaCase( [
54 'printableQuery' => '1',
55 'media' => 'screen',
56 'expectedReturn' => null,
57 'message' => 'On printable request, screen returns null'
58 ] );
59
60 $this->assertTransformCssMediaCase( [
61 'printableQuery' => '1',
62 'media' => self::SCREEN_MEDIA_QUERY,
63 'expectedReturn' => null,
64 'message' => 'On printable request, screen media query returns null'
65 ] );
66
67 $this->assertTransformCssMediaCase( [
68 'printableQuery' => '1',
69 'media' => self::SCREEN_ONLY_MEDIA_QUERY,
70 'expectedReturn' => null,
71 'message' => 'On printable request, screen media query with only returns null'
72 ] );
73
74 $this->assertTransformCssMediaCase( [
75 'printableQuery' => '1',
76 'media' => 'print',
77 'expectedReturn' => '',
78 'message' => 'On printable request, media print returns empty string'
79 ] );
80 }
81
82 /**
83 * Tests screen requests, without either query parameter set
84 * @covers OutputPage::transformCssMedia
85 */
86 public function testScreenRequests() {
87 $this->assertTransformCssMediaCase( [
88 'media' => 'screen',
89 'expectedReturn' => 'screen',
90 'message' => 'On screen request, screen media type is preserved'
91 ] );
92
93 $this->assertTransformCssMediaCase( [
94 'media' => 'handheld',
95 'expectedReturn' => 'handheld',
96 'message' => 'On screen request, handheld media type is preserved'
97 ] );
98
99 $this->assertTransformCssMediaCase( [
100 'media' => self::SCREEN_MEDIA_QUERY,
101 'expectedReturn' => self::SCREEN_MEDIA_QUERY,
102 'message' => 'On screen request, screen media query is preserved.'
103 ] );
104
105 $this->assertTransformCssMediaCase( [
106 'media' => self::SCREEN_ONLY_MEDIA_QUERY,
107 'expectedReturn' => self::SCREEN_ONLY_MEDIA_QUERY,
108 'message' => 'On screen request, screen media query with only is preserved.'
109 ] );
110
111 $this->assertTransformCssMediaCase( [
112 'media' => 'print',
113 'expectedReturn' => 'print',
114 'message' => 'On screen request, print media type is preserved'
115 ] );
116 }
117
118 /**
119 * Tests handheld behavior
120 * @covers OutputPage::transformCssMedia
121 */
122 public function testHandheld() {
123 $this->assertTransformCssMediaCase( [
124 'handheldQuery' => '1',
125 'media' => 'handheld',
126 'expectedReturn' => '',
127 'message' => 'On request with handheld querystring and media is handheld, returns empty string'
128 ] );
129
130 $this->assertTransformCssMediaCase( [
131 'handheldQuery' => '1',
132 'media' => 'screen',
133 'expectedReturn' => null,
134 'message' => 'On request with handheld querystring and media is screen, returns null'
135 ] );
136 }
137
138 public static function provideTransformFilePath() {
139 $baseDir = dirname( __DIR__ ) . '/data/media';
140 return [
141 // File that matches basePath, and exists. Hash found and appended.
142 [ 'baseDir' => $baseDir, 'basePath' => '/w', '/w/test.jpg', '/w/test.jpg?edcf2' ],
143 // File that matches basePath, but not found on disk. Empty query.
144 [ 'baseDir' => $baseDir, 'basePath' => '/w', '/w/unknown.png', '/w/unknown.png?' ],
145 // File not matching basePath. Ignored.
146 [ 'baseDir' => $baseDir, 'basePath' => '/w', '/files/test.jpg' ],
147 // Empty string. Ignored.
148 [ 'baseDir' => $baseDir, 'basePath' => '/w', '', '' ],
149 // Similar path, but with domain component. Ignored.
150 [ 'baseDir' => $baseDir, 'basePath' => '/w', '//example.org/w/test.jpg' ],
151 [ 'baseDir' => $baseDir, 'basePath' => '/w', 'https://example.org/w/test.jpg' ],
152 // Unrelated path with domain component. Ignored.
153 [ 'baseDir' => $baseDir, 'basePath' => '/w', 'https://example.org/files/test.jpg' ],
154 [ 'baseDir' => $baseDir, 'basePath' => '/w', '//example.org/files/test.jpg' ],
155 // Unrelated path with domain, and empty base path (root mw install). Ignored.
156 [ 'baseDir' => $baseDir, 'basePath' => '', 'https://example.org/files/test.jpg' ],
157 [ 'baseDir' => $baseDir, 'basePath' => '', '//example.org/files/test.jpg' ], // T155310
158 ];
159 }
160
161 /**
162 * @dataProvider provideTransformFilePath
163 * @covers OutputPage::transformFilePath
164 * @covers OutputPage::transformResourcePath
165 */
166 public function testTransformResourcePath( $baseDir, $basePath, $path, $expected = null ) {
167 $this->setMwGlobals( 'IP', $baseDir );
168 $conf = new HashConfig( [ 'ResourceBasePath' => $basePath ] );
169
170 MediaWiki\suppressWarnings();
171 $actual = OutputPage::transformResourcePath( $conf, $path );
172 MediaWiki\restoreWarnings();
173
174 $this->assertEquals( $expected ?: $path, $actual );
175 }
176
177 public static function provideMakeResourceLoaderLink() {
178 // @codingStandardsIgnoreStart Generic.Files.LineLength
179 return [
180 // Single only=scripts load
181 [
182 [ 'test.foo', ResourceLoaderModule::TYPE_SCRIPTS ],
183 "<script>(window.RLQ=window.RLQ||[]).push(function(){"
184 . 'mw.loader.load("http://127.0.0.1:8080/w/load.php?debug=false\u0026lang=en\u0026modules=test.foo\u0026only=scripts\u0026skin=fallback");'
185 . "});</script>"
186 ],
187 // Multiple only=styles load
188 [
189 [ [ 'test.baz', 'test.foo', 'test.bar' ], ResourceLoaderModule::TYPE_STYLES ],
190
191 '<link rel="stylesheet" href="http://127.0.0.1:8080/w/load.php?debug=false&amp;lang=en&amp;modules=test.bar%2Cbaz%2Cfoo&amp;only=styles&amp;skin=fallback"/>'
192 ],
193 // Private embed (only=scripts)
194 [
195 [ 'test.quux', ResourceLoaderModule::TYPE_SCRIPTS ],
196 "<script>(window.RLQ=window.RLQ||[]).push(function(){"
197 . "mw.test.baz({token:123});mw.loader.state({\"test.quux\":\"ready\"});"
198 . "});</script>"
199 ],
200 ];
201 // @codingStandardsIgnoreEnd
202 }
203
204 /**
205 * See ResourceLoaderClientHtmlTest for full coverage.
206 *
207 * @dataProvider provideMakeResourceLoaderLink
208 * @covers OutputPage::makeResourceLoaderLink
209 */
210 public function testMakeResourceLoaderLink( $args, $expectedHtml ) {
211 $this->setMwGlobals( [
212 'wgResourceLoaderDebug' => false,
213 'wgLoadScript' => 'http://127.0.0.1:8080/w/load.php',
214 ] );
215 $class = new ReflectionClass( 'OutputPage' );
216 $method = $class->getMethod( 'makeResourceLoaderLink' );
217 $method->setAccessible( true );
218 $ctx = new RequestContext();
219 $ctx->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'fallback' ) );
220 $ctx->setLanguage( 'en' );
221 $out = new OutputPage( $ctx );
222 $rl = $out->getResourceLoader();
223 $rl->setMessageBlobStore( new NullMessageBlobStore() );
224 $rl->register( [
225 'test.foo' => new ResourceLoaderTestModule( [
226 'script' => 'mw.test.foo( { a: true } );',
227 'styles' => '.mw-test-foo { content: "style"; }',
228 ] ),
229 'test.bar' => new ResourceLoaderTestModule( [
230 'script' => 'mw.test.bar( { a: true } );',
231 'styles' => '.mw-test-bar { content: "style"; }',
232 ] ),
233 'test.baz' => new ResourceLoaderTestModule( [
234 'script' => 'mw.test.baz( { a: true } );',
235 'styles' => '.mw-test-baz { content: "style"; }',
236 ] ),
237 'test.quux' => new ResourceLoaderTestModule( [
238 'script' => 'mw.test.baz( { token: 123 } );',
239 'styles' => '/* pref-animate=off */ .mw-icon { transition: none; }',
240 'group' => 'private',
241 ] ),
242 ] );
243 $links = $method->invokeArgs( $out, $args );
244 $actualHtml = strval( $links );
245 $this->assertEquals( $expectedHtml, $actualHtml );
246 }
247
248 /**
249 * @dataProvider provideVaryHeaders
250 * @covers OutputPage::addVaryHeader
251 * @covers OutputPage::getVaryHeader
252 * @covers OutputPage::getKeyHeader
253 */
254 public function testVaryHeaders( $calls, $vary, $key ) {
255 // get rid of default Vary fields
256 $outputPage = $this->getMockBuilder( 'OutputPage' )
257 ->setConstructorArgs( [ new RequestContext() ] )
258 ->setMethods( [ 'getCacheVaryCookies' ] )
259 ->getMock();
260 $outputPage->expects( $this->any() )
261 ->method( 'getCacheVaryCookies' )
262 ->will( $this->returnValue( [] ) );
263 TestingAccessWrapper::newFromObject( $outputPage )->mVaryHeader = [];
264
265 foreach ( $calls as $call ) {
266 call_user_func_array( [ $outputPage, 'addVaryHeader' ], $call );
267 }
268 $this->assertEquals( $vary, $outputPage->getVaryHeader(), 'Vary:' );
269 $this->assertEquals( $key, $outputPage->getKeyHeader(), 'Key:' );
270 }
271
272 public function provideVaryHeaders() {
273 // note: getKeyHeader() automatically adds Vary: Cookie
274 return [
275 [ // single header
276 [
277 [ 'Cookie' ],
278 ],
279 'Vary: Cookie',
280 'Key: Cookie',
281 ],
282 [ // non-unique headers
283 [
284 [ 'Cookie' ],
285 [ 'Accept-Language' ],
286 [ 'Cookie' ],
287 ],
288 'Vary: Cookie, Accept-Language',
289 'Key: Cookie,Accept-Language',
290 ],
291 [ // two headers with single options
292 [
293 [ 'Cookie', [ 'param=phpsessid' ] ],
294 [ 'Accept-Language', [ 'substr=en' ] ],
295 ],
296 'Vary: Cookie, Accept-Language',
297 'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
298 ],
299 [ // one header with multiple options
300 [
301 [ 'Cookie', [ 'param=phpsessid', 'param=userId' ] ],
302 ],
303 'Vary: Cookie',
304 'Key: Cookie;param=phpsessid;param=userId',
305 ],
306 [ // Duplicate option
307 [
308 [ 'Cookie', [ 'param=phpsessid' ] ],
309 [ 'Cookie', [ 'param=phpsessid' ] ],
310 [ 'Accept-Language', [ 'substr=en', 'substr=en' ] ],
311 ],
312 'Vary: Cookie, Accept-Language',
313 'Key: Cookie;param=phpsessid,Accept-Language;substr=en',
314 ],
315 [ // Same header, different options
316 [
317 [ 'Cookie', [ 'param=phpsessid' ] ],
318 [ 'Cookie', [ 'param=userId' ] ],
319 ],
320 'Vary: Cookie',
321 'Key: Cookie;param=phpsessid;param=userId',
322 ],
323 ];
324 }
325
326 /**
327 * @covers OutputPage::haveCacheVaryCookies
328 */
329 public function testHaveCacheVaryCookies() {
330 $request = new FauxRequest();
331 $context = new RequestContext();
332 $context->setRequest( $request );
333 $outputPage = new OutputPage( $context );
334
335 // No cookies are set.
336 $this->assertFalse( $outputPage->haveCacheVaryCookies() );
337
338 // 'Token' is present but empty, so it shouldn't count.
339 $request->setCookie( 'Token', '' );
340 $this->assertFalse( $outputPage->haveCacheVaryCookies() );
341
342 // 'Token' present and nonempty.
343 $request->setCookie( 'Token', '123' );
344 $this->assertTrue( $outputPage->haveCacheVaryCookies() );
345 }
346
347 /*
348 * @covers OutputPage::addCategoryLinks
349 * @covers OutputPage::getCategories
350 */
351 public function testGetCategories() {
352 $fakeResultWrapper = new FakeResultWrapper( [
353 (object) [
354 'pp_value' => 1,
355 'page_title' => 'Test'
356 ],
357 (object) [
358 'page_title' => 'Test2'
359 ]
360 ] );
361 $outputPage = $this->getMockBuilder( 'OutputPage' )
362 ->setConstructorArgs( [ new RequestContext() ] )
363 ->setMethods( [ 'addCategoryLinksToLBAndGetResult' ] )
364 ->getMock();
365 $outputPage->expects( $this->any() )
366 ->method( 'addCategoryLinksToLBAndGetResult' )
367 ->will( $this->returnValue( $fakeResultWrapper ) );
368
369 $outputPage->addCategoryLinks( [
370 'Test' => 'Test',
371 'Test2' => 'Test2',
372 ] );
373 $this->assertEquals( [ 0 => 'Test', '1' => 'Test2' ], $outputPage->getCategories() );
374 $this->assertEquals( [ 0 => 'Test2' ], $outputPage->getCategories( 'normal' ) );
375 $this->assertEquals( [ 0 => 'Test' ], $outputPage->getCategories( 'hidden' ) );
376 }
377 }
378
379 /**
380 * MessageBlobStore that doesn't do anything
381 */
382 class NullMessageBlobStore extends MessageBlobStore {
383 public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
384 return [];
385 }
386
387 public function insertMessageBlob( $name, ResourceLoaderModule $module, $lang ) {
388 return false;
389 }
390
391 public function updateModule( $name, ResourceLoaderModule $module, $lang ) {
392 }
393
394 public function updateMessage( $key ) {
395 }
396
397 public function clear() {
398 }
399 }