wiki.techinc.nl/tests/qunit/data/load.mock.php
Timo Tijhof c0eaa457ef resourceloader: Fix load.mock.php query parameter corruption in tests
=== Observe the bug

1. Run Special:JavaScriptTest
   (add ?module=mediawiki.loader to run only the relevant tests)
2. In the Network panel, check the JS requests to load.mock.php?…
3. Without this patch, they are like:
   "load.mock.php?1234?lang=en&modules=…&…"
   With this patch, they are like:
   "load.mock.php?lang=en&modules=…&…"

The question mark is only valid as the start of the query string,
not as divider between them. This means without this patch, the
"lang" parameter is simply ignored because it becomes part
of the key "1234?lang" with value "en".

=== What

The mock server doesn't do anything with "lang". And given that
RL sorts its query parameters for optimum cache-hit rate, the
corrupted parameter is always "lang", as its sorts before
"module" or "version", which our mock server does utilize.

As part of server-side compression of the startup module (d13e5b75),
we filter redundant base parameters that match the default. For
RLContext, this is `{ debug: false, lang: qqx, skin: fallback }`.

As such, if one were to mock the localisation backend with
uselang=qqx internally, the "lang" parameter will not need to be
sent, and thus the above bug will start corrupting the "modules"
paramater instead, which our test suite correctly detects as being
very badly broken.

=== Why

mediawiki.loader.test.js used QUnit.fixurl() as paranoid way to
avoid accidental caching. This blindly adds "?<random>" to the
url. Upstream QUnit assumes the URL will be a simple file on disk,
not expecting existing query parameters.

=== Fix

* Removing the call to QUnit.fixurl(). It was set by me years ago.
  But, there is no reason to believe a browser would cache this
  anyway. Plus, the file hardly ever changes. Just in case,
  set a no-cache header on the server side instead.

* Relatedly, the export of $VARS.reqBase is an associative array in
  PHP and becomes an object in JSON. Make sure this works even if
  the PHP array is empty, by casting to an object. Otherwise, it
  becomes `[]` instead of `{}` given an PHP php array is ambiguous
  in terms of whether it is meant as hashtable or list.

Bug: T250045
Change-Id: I3b8ff427577af9df3f1c26500ecf3646973ad34c
2020-04-28 16:04:50 +00:00

110 lines
3.6 KiB
PHP

<?php
/**
* Mock load.php with pre-defined test modules.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @author Lupo
*/
// This file doesn't run as part of MediaWiki
// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
header( 'Cache-Control: private, no-cache, must-revalidate' );
header( 'Content-Type: text/javascript; charset=utf-8' );
$moduleImplementations = [
'testUsesMissing' => "
mw.loader.implement( 'testUsesMissing', function () {
mw.loader.testFail( 'Module usesMissing script should not run.' );
}, {}, {});
",
'testUsesNestedMissing' => "
mw.loader.implement( 'testUsesNestedMissing', function () {
mw.loader.testFail('Module testUsesNestedMissing script should not run.' );
}, {}, {});
",
'testSkipped' => "
mw.loader.implement( 'testSkipped', function () {
mw.loader.testFail( false, 'Module testSkipped was supposed to be skipped.' );
}, {}, {});
",
'testNotSkipped' => "
mw.loader.implement( 'testNotSkipped', function () {}, {}, {});
",
'testUsesSkippable' => "
mw.loader.implement( 'testUsesSkippable', function () {}, {}, {});
",
'testUrlInc' => "
mw.loader.implement( 'testUrlInc', function () {} );
",
'testUrlInc.a' => "
mw.loader.implement( 'testUrlInc.a', function () {} );
",
'testUrlInc.b' => "
mw.loader.implement( 'testUrlInc.b', function () {} );
",
'testUrlOrder' => "
mw.loader.implement( 'testUrlOrder', function () {} );
",
'testUrlOrder.a' => "
mw.loader.implement( 'testUrlOrder.a', function () {} );
",
'testUrlOrder.b' => "
mw.loader.implement( 'testUrlOrder.b', function () {} );
",
];
$response = '';
// Does not support the full behaviour of the real load.php.
// This only supports dotless module names joined by comma,
// with the exception of the hardcoded cases for testUrl*.
if ( isset( $_GET['modules'] ) ) {
if ( $_GET['modules'] === 'testUrlInc,testUrlIncDump|testUrlInc.a,b' ) {
$modules = [ 'testUrlInc', 'testUrlIncDump', 'testUrlInc.a', 'testUrlInc.b' ];
} elseif ( $_GET['modules'] === 'testUrlOrder,testUrlOrderDump|testUrlOrder.a,b' ) {
$modules = [ 'testUrlOrder', 'testUrlOrderDump', 'testUrlOrder.a', 'testUrlOrder.b' ];
} else {
$modules = explode( ',', $_GET['modules'] );
}
foreach ( $modules as $module ) {
if ( isset( $moduleImplementations[$module] ) ) {
$response .= $moduleImplementations[$module];
} elseif ( preg_match( '/^test.*Dump$/', $module ) === 1 ) {
$queryModules = $_GET['modules'];
$queryVersion = isset( $_GET['version'] ) ? strval( $_GET['version'] ) : null;
$response .= 'mw.loader.implement( ' . json_encode( $module )
. ', function ( $, jQuery, require, module ) {'
. 'module.exports.query = { '
. 'modules: ' . json_encode( $queryModules ) . ','
. 'version: ' . json_encode( $queryVersion )
. ' };'
. '} );';
} else {
// Default
$response .= 'mw.loader.state(' . json_encode( [ $module => 'missing' ] ) . ');' . "\n";
}
}
}
echo $response;