* Get a WikiPage object from a title or pageid param, if possible.
* Can die, if no param is set or if the title or page id is not valid.
*
- * @param array $params
+ * @param array $params User provided set of parameters, as from $this->extractRequestParams()
* @param bool|string $load Whether load the object's state from the database:
* - false: don't load (if the pageid is given, it will still be loaded)
* - 'fromdb': load from a replica DB
* Can die, if no param is set or if the title or page id is not valid.
*
* @since 1.29
- * @param array $params
+ * @param array $params User provided set of parameters, as from $this->extractRequestParams()
* @return Title
*/
public function getTitleFromTitleOrPageId( $params ) {
const PRESUME_FRESH_TTL_SEC = 30;
const MAX_CACHE_TTL = 300; // 5 minutes
+ const MAX_SIGNATURE_TTL = 60;
public function execute() {
$user = $this->getUser();
// Put an upper limit on the TTL for sanity to avoid extreme template/file staleness.
$since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() );
$ttl = min( $parserOutput->getCacheExpiry() - $since, self::MAX_CACHE_TTL );
+
+ // Avoid extremely stale user signature timestamps (T84843)
+ if ( $parserOutput->getFlag( 'user-signature' ) ) {
+ $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
+ }
+
if ( $ttl <= 0 ) {
return [ null, 0, 'no_ttl' ];
}
public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
global $wgCommandLineMode;
- // This is a sub-DeferredUpdate, run it right after its parent update
if ( self::$executeContext && self::$executeContext['stage'] >= $stage ) {
+ // This is a sub-DeferredUpdate; run it right after its parent update.
+ // Also, while post-send updates are running, push any "pre-send" jobs to the
+ // active post-send queue to make sure they get run this round (or at all).
self::$executeContext['subqueue'][] = $update;
+
return;
}
while ( $updates ) {
$queue = []; // clear the queue
- if ( $mode === 'enqueue' ) {
- try {
- // Push enqueuable updates to the job queue and get the rest
- $updates = self::enqueueUpdates( $updates );
- } catch ( Exception $e ) {
- // Let other updates have a chance to run if this failed
- MWExceptionHandler::rollbackMasterChangesAndLog( $e );
- }
- }
-
// Order will be DataUpdate followed by generic DeferrableUpdate tasks
$updatesByType = [ 'data' => [], 'generic' => [] ];
foreach ( $updates as $du ) {
// Execute all remaining tasks...
foreach ( $updatesByType as $updatesForType ) {
foreach ( $updatesForType as $update ) {
- self::$executeContext = [
- 'update' => $update,
- 'stage' => $stage,
- 'subqueue' => []
- ];
+ self::$executeContext = [ 'stage' => $stage, 'subqueue' => [] ];
/** @var DeferrableUpdate $update */
- $guiError = self::runUpdate( $update, $lbFactory, $stage );
+ $guiError = self::runUpdate( $update, $lbFactory, $mode, $stage );
$reportableError = $reportableError ?: $guiError;
// Do the subqueue updates for $update until there are none
while ( self::$executeContext['subqueue'] ) {
$subUpdate->setTransactionTicket( $ticket );
}
- $guiError = self::runUpdate( $subUpdate, $lbFactory, $stage );
+ $guiError = self::runUpdate( $subUpdate, $lbFactory, $mode, $stage );
$reportableError = $reportableError ?: $guiError;
}
self::$executeContext = null;
/**
* @param DeferrableUpdate $update
* @param LBFactory $lbFactory
+ * @param string $mode
* @param integer $stage
* @return ErrorPageError|null
*/
- private static function runUpdate( DeferrableUpdate $update, LBFactory $lbFactory, $stage ) {
+ private static function runUpdate(
+ DeferrableUpdate $update, LBFactory $lbFactory, $mode, $stage
+ ) {
$guiError = null;
try {
- $fnameTrxOwner = get_class( $update ) . '::doUpdate';
- $lbFactory->beginMasterChanges( $fnameTrxOwner );
- $update->doUpdate();
- $lbFactory->commitMasterChanges( $fnameTrxOwner );
+ if ( $mode === 'enqueue' && $update instanceof EnqueueableDataUpdate ) {
+ // Run only the job enqueue logic to complete the update later
+ $spec = $update->getAsJobSpecification();
+ JobQueueGroup::singleton( $spec['wiki'] )->push( $spec['job'] );
+ } else {
+ // Run the bulk of the update now
+ $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+ $lbFactory->beginMasterChanges( $fnameTrxOwner );
+ $update->doUpdate();
+ $lbFactory->commitMasterChanges( $fnameTrxOwner );
+ }
} catch ( Exception $e ) {
// Reporting GUI exceptions does not work post-send
if ( $e instanceof ErrorPageError && $stage === self::PRESEND ) {
* @param array $selectJoinConds Join conditions for the SELECT part of the query, see
* IDatabase::select() for details.
*
- * @return IResultWrapper
+ * @return bool
*/
public function insertSelect( $destTable, $srcTable, $varMap, $conds,
$fname = __METHOD__,
protected function getSeparateMainLB() {
global $wgDBtype;
- if ( $wgDBtype === 'mysql' && $this->usesMainDB() ) {
+ if ( $this->usesMainDB() && $wgDBtype !== 'sqlite' ) {
if ( !$this->separateMainLB ) {
// We must keep a separate connection to MySQL in order to avoid deadlocks
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
}
return $this->separateMainLB;
} else {
- // However, SQLite has an opposite behavior. And PostgreSQL needs to know
- // if we are in transaction or not (@TODO: find some PostgreSQL work-around).
+ // However, SQLite has an opposite behavior due to DB-level locking
return null;
}
}
# which may corrupt this parser instance via its wfMessage()->text() call-
# Signatures
- $sigText = $this->getUserSig( $user );
- $text = strtr( $text, [
- '~~~~~' => $d,
- '~~~~' => "$sigText $d",
- '~~~' => $sigText
- ] );
+ if ( strpos( $text, '~~~' ) !== false ) {
+ $sigText = $this->getUserSig( $user );
+ $text = strtr( $text, [
+ '~~~~~' => $d,
+ '~~~~' => "$sigText $d",
+ '~~~' => $sigText
+ ] );
+ # The main two signature forms used above are time-sensitive
+ $this->mOutput->setFlag( 'user-signature' );
+ }
# Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
$tc = '[' . Title::legalChars() . ']';
}
public function execute( $par ) {
+ $context = new DerivativeContext( $this->getContext() );
+
$this->setHeaders();
$this->outputHeader();
$mimeAnalyzer = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
$opts->setValue( 'start', $start, true );
$opts->setValue( 'end', $end, true );
+
+ // also swap values in request object, which is used by HTMLForm
+ // to pre-populate the fields with the previous input
+ $request = $context->getRequest();
+ $context->setRequest( new DerivativeRequest(
+ $request,
+ [ 'start' => $start, 'end' => $end ] + $request->getValues(),
+ $request->wasPosted()
+ ) );
}
// if all media types have been selected, wipe out the array to prevent
if ( !$this->including() ) {
$this->setTopText();
- $this->buildForm();
+ $this->buildForm( $context );
}
- $pager = new NewFilesPager( $this->getContext(), $opts );
+ $pager = new NewFilesPager( $context, $opts );
$out->addHTML( $pager->getBody() );
if ( !$this->including() ) {
}
}
- protected function buildForm() {
+ protected function buildForm( IContextSource $context ) {
$mediaTypesText = array_map( function ( $type ) {
// mediastatistics-header-unknown, mediastatistics-header-bitmap,
// mediastatistics-header-drawing, mediastatistics-header-audio,
unset( $formDescriptor['hidepatrolled'] );
}
- HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
+ HTMLForm::factory( 'ooui', $formDescriptor, $context )
// For the 'multiselect' field values to be preserved on submit
->setFormIdentifier( 'specialnewimages' )
->setWrapperLegendMsg( 'newimages-legend' )
->setMethod( 'get' )
->prepareForm()
->displayForm( false );
-
- $this->getOutput()->addModules( 'mediawiki.special.newFiles' );
}
protected function getGroupName() {
"karma-firefox-launcher": "1.0.1",
"karma-mocha-reporter": "2.2.3",
"karma-qunit": "1.0.0",
+ "nodemw": "0.10.1",
"qunitjs": "1.23.1",
"stylelint-config-wikimedia": "0.4.1",
"wdio-junit-reporter": "0.2.0",
'mediawiki.special.movePage.styles' => [
'styles' => 'resources/src/mediawiki.special/mediawiki.special.movePage.css',
],
- 'mediawiki.special.newFiles' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.newFiles.js',
- 'dependencies' => [
- 'mediawiki.widgets.datetime',
- ],
- ],
'mediawiki.special.pageLanguage' => [
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.pageLanguage.js',
'dependencies' => [
+++ /dev/null
-/*!
- * JavaScript for Special:NewFiles
- */
-( function ( mw, $ ) {
- $( function () {
- var start = mw.widgets.datetime.DateTimeInputWidget.static.infuse( 'mw-input-start' ),
- end = mw.widgets.datetime.DateTimeInputWidget.static.infuse( 'mw-input-end' ),
- temp;
-
- // If the start date comes after the end date, swap the two values.
- // This swap is already done internally when the form is submitted with a start date that
- // comes after the end date, but this swap makes the change visible in the HTMLForm.
- if ( start.getValue() !== '' &&
- end.getValue() !== '' &&
- start.getValue() > end.getValue() ) {
- temp = start.getValue();
- start.setValue( end.getValue() );
- end.setValue( temp );
- }
- } );
-}( mediaWiki, jQuery ) );
$setup['wgSVGConverters'] = [ 'null' => 'echo "1">$output' ];
// Fake constant timestamp
- Hooks::register( 'ParserGetVariableValueTs', 'ParserTestRunner::getFakeTimestamp' );
+ Hooks::register( 'ParserGetVariableValueTs', function ( &$parser, &$ts ) {
+ $ts = $this->getFakeTimestamp();
+ return true;
+ } );
$teardown[] = function () {
Hooks::clear( 'ParserGetVariableValueTs' );
};
$context = RequestContext::getMain();
$user = $context->getUser();
$options = ParserOptions::newFromContext( $context );
+ $options->setTimestamp( $this->getFakeTimestamp() );
if ( !isset( $opts['wrap'] ) ) {
$options->setWrapOutputClass( false );
if ( isset( $opts['pst'] ) ) {
$out = $parser->preSaveTransform( $test['input'], $title, $user, $options );
+ $output = $parser->getOutput();
} elseif ( isset( $opts['msg'] ) ) {
$out = $parser->transformMsg( $test['input'], $options, $title );
} elseif ( isset( $opts['section'] ) ) {
}
}
+ if ( isset( $output ) && isset( $opts['showflags'] ) ) {
+ $actualFlags = array_keys( TestingAccessWrapper::newFromObject( $output )->mFlags );
+ sort( $actualFlags );
+ $out .= "\nflags=" . join( ', ', $actualFlags );
+ }
+
ScopedCallback::consume( $teardownGuard );
$expected = $test['result'];
}
/**
- * The ParserGetVariableValueTs hook, used to make sure time-related parser
+ * Fake constant timestamp to make sure time-related parser
* functions give a persistent value.
+ *
+ * - Parser::getVariableValue (via ParserGetVariableValueTs hook)
+ * - Parser::preSaveTransform (via ParserOptions)
*/
- static function getFakeTimestamp( &$parser, &$ts ) {
- $ts = 123; // parsed as '1970-01-01T00:02:03Z'
- return true;
+ private function getFakeTimestamp() {
+ // parsed as '1970-01-01T00:02:03Z'
+ return 123;
}
}
Magic Word: {{REVISIONID}}
!! options
parsoid={ "modes": ["wt2html","wt2wt"], "normalizePhp": true }
+showflags
!! wikitext
{{REVISIONID}}
!! html/*
<p>1337
</p>
+flags=vary-revision-id
!! end
!! test
pst
!! wikitext
* ~~~
+* ~~~~
+* ~~~~~
* <noinclude>~~~</noinclude>
* <includeonly>~~~</includeonly>
* <onlyinclude>~~~</onlyinclude>
!! html/php
* [[Special:Contributions/127.0.0.1|127.0.0.1]]
+* [[Special:Contributions/127.0.0.1|127.0.0.1]] 00:02, 1 January 1970 (UTC)
+* 00:02, 1 January 1970 (UTC)
* <noinclude>[[Special:Contributions/127.0.0.1|127.0.0.1]]</noinclude>
* <includeonly>[[Special:Contributions/127.0.0.1|127.0.0.1]]</includeonly>
* <onlyinclude>[[Special:Contributions/127.0.0.1|127.0.0.1]]</onlyinclude>
!! end
+!! test
+ParserOutput flags from signature expansion (T84843)
+!! options
+pst
+showflags
+!! wikitext
+~~~~
+!! html/php
+[[Special:Contributions/127.0.0.1|127.0.0.1]] 00:02, 1 January 1970 (UTC)
+flags=user-signature
+!! end
+
+
!! test
pre-save transform: Signature expansion in nowiki tags (T2093)
!! options
DeferredUpdates::doUpdates();
}
+
+ public function testPresendAddOnPostsendRun() {
+ $this->setMwGlobals( 'wgCommandLineMode', true );
+
+ $x = false;
+ $y = false;
+ wfGetLBFactory()->commitMasterChanges( __METHOD__ ); // clear anything
+
+ DeferredUpdates::addCallableUpdate(
+ function () use ( &$x, &$y ) {
+ $x = true;
+ DeferredUpdates::addCallableUpdate(
+ function () use ( &$y ) {
+ $y = true;
+ },
+ DeferredUpdates::PRESEND
+ );
+ },
+ DeferredUpdates::POSTSEND
+ );
+
+ DeferredUpdates::doUpdates();
+
+ $this->assertTrue( $x, "Outer POSTSEND update ran" );
+ $this->assertTrue( $y, "Nested PRESEND update ran" );
+ }
}
},
"globals": {
"browser": false
+ },
+ "rules":{
+ "no-console":0
}
}
Then in another terminal:
- cd mediawiki/tests/selenium
- ../../node_modules/.bin/wdio --spec page.js
+ cd tests/selenium
+ ../../node_modules/.bin/wdio --spec specs/page.js
The runner reads the config file `wdio.conf.js` and runs the spec listed in
`page.js`.
this.create.click();
}
+ apiCreateAccount( username, password ) {
+ const url = require( 'url' ), // https://nodejs.org/docs/latest/api/url.html
+ baseUrl = url.parse( browser.options.baseUrl ), // http://webdriver.io/guide/testrunner/browserobject.html
+ Bot = require( 'nodemw' ), // https://github.com/macbre/nodemw
+ client = new Bot( {
+ protocol: baseUrl.protocol,
+ server: baseUrl.hostname,
+ port: baseUrl.port,
+ path: baseUrl.path,
+ debug: false
+ } );
+
+ return new Promise( ( resolve, reject ) => {
+ client.api.call(
+ {
+ action: 'query',
+ meta: 'tokens',
+ type: 'createaccount'
+ },
+ /**
+ * @param {Error|null} err
+ * @param {Object} info Processed query result
+ * @param {Object} next More results?
+ * @param {Object} data Raw data
+ */
+ function ( err, info, next, data ) {
+ if ( err ) {
+ reject( err );
+ return;
+ }
+ client.api.call( {
+ action: 'createaccount',
+ createreturnurl: browser.options.baseUrl,
+ createtoken: data.query.tokens.createaccounttoken,
+ username: username,
+ password: password,
+ retype: password
+ }, function ( err ) {
+ if ( err ) {
+ reject( err );
+ return;
+ }
+ resolve();
+ }, 'POST' );
+ },
+ 'POST'
+ );
+
+ } );
+
+ }
+
}
module.exports = new CreateAccountPage();
get heading() { return browser.element( '#firstHeading' ); }
get save() { return browser.element( '#wpSave' ); }
- open( name ) {
+ openForEditing( name ) {
super.open( name + '&action=edit' );
}
edit( name, content ) {
- this.open( name );
+ this.openForEditing( name );
this.content.setValue( content );
this.save.click();
}
+ apiEdit( name, content ) {
+ const url = require( 'url' ), // https://nodejs.org/docs/latest/api/url.html
+ baseUrl = url.parse( browser.options.baseUrl ), // http://webdriver.io/guide/testrunner/browserobject.html
+ Bot = require( 'nodemw' ), // https://github.com/macbre/nodemw
+ client = new Bot( {
+ protocol: baseUrl.protocol,
+ server: baseUrl.hostname,
+ port: baseUrl.port,
+ path: baseUrl.path,
+ debug: false
+ } );
+
+ return new Promise( ( resolve, reject ) => {
+ client.edit( name, content, `Created page with "${content}"`, function ( err ) {
+ if ( err ) {
+ return reject( err );
+ }
+ resolve();
+ } );
+ } );
+ }
+
}
module.exports = new EditPage();
+++ /dev/null
-'use strict';
-const Page = require( './page' );
-
-class UserLogoutPage extends Page {
-
- open() {
- super.open( 'Special:UserLogout' );
- }
-
-}
-module.exports = new UserLogoutPage();
'use strict';
const assert = require( 'assert' ),
+ EditPage = require( '../pageobjects/edit.page' ),
HistoryPage = require( '../pageobjects/history.page' ),
- EditPage = require( '../pageobjects/edit.page' );
+ UserLoginPage = require( '../pageobjects/userlogin.page' );
describe( 'Page', function () {
var content,
name;
+ before( function () {
+ // disable VisualEditor welcome dialog
+ UserLoginPage.open();
+ browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } );
+ } );
+
beforeEach( function () {
+ browser.deleteCookie();
content = Math.random().toString();
name = Math.random().toString();
} );
var content2 = Math.random().toString();
// create
- EditPage.edit( name, content );
+ browser.call( function () {
+ return EditPage.apiEdit( name, content );
+ } );
// edit
EditPage.edit( name, content2 );
- // check content
+ // check
assert.equal( EditPage.heading.getText(), name );
assert.equal( EditPage.displayedContent.getText(), content2 );
it( 'should have history', function () {
// create
- EditPage.edit( name, content );
+ browser.call( function () {
+ return EditPage.apiEdit( name, content );
+ } );
// check
HistoryPage.open( name );
'use strict';
const assert = require( 'assert' ),
CreateAccountPage = require( '../pageobjects/createaccount.page' ),
- UserLoginPage = require( '../pageobjects/userlogin.page' ),
- UserLogoutPage = require( '../pageobjects/userlogout.page' ),
- PreferencesPage = require( '../pageobjects/preferences.page' );
+ PreferencesPage = require( '../pageobjects/preferences.page' ),
+ UserLoginPage = require( '../pageobjects/userlogin.page' );
describe( 'User', function () {
var password,
username;
+ before( function () {
+ // disable VisualEditor welcome dialog
+ UserLoginPage.open();
+ browser.localStorage( 'POST', { key: 've-beta-welcome-dialog', value: '1' } );
+ } );
+
beforeEach( function () {
+ browser.deleteCookie();
username = `User-${Math.random().toString()}`;
password = Math.random().toString();
} );
it( 'should be able to log in', function () {
// create
- CreateAccountPage.createAccount( username, password );
-
- // logout
- UserLogoutPage.open();
+ browser.call( function () {
+ return CreateAccountPage.apiCreateAccount( username, password );
+ } );
// log in
UserLoginPage.login( username, password );
var realName = Math.random().toString();
// create
- CreateAccountPage.createAccount( username, password );
+ browser.call( function () {
+ return CreateAccountPage.apiCreateAccount( username, password );
+ } );
+
+ // log in
+ UserLoginPage.login( username, password );
- // change real name
+ // change
PreferencesPage.changeRealName( realName );
// check