/**
* If set, any SquidPurge call on a URL or URLs will send a second purge no less than
* this many seconds later via the job queue. This requires delayed job support.
- * This should be safely higher than the 'max lag' value in $wgLBFactoryConf.
+ * This should be safely higher than the 'max lag' value in $wgLBFactoryConf, so that
+ * slave lag does not cause page to be stuck in stales states in CDN.
+ *
+ * This also fixes race conditions in two-tiered CDN setups (e.g. cdn2 => cdn1 => MediaWiki).
+ * If a purge for a URL reaches cdn2 before cdn1 and a request reaches cdn2 for that URL,
+ * it will populate the response from the stale cdn1 value. When cdn1 gets the purge, cdn2
+ * will still be stale. If the rebound purge delay is safely higher than the time to relay
+ * a purge to all nodes, then the rebound puge will clear cdn2 after cdn1 was cleared.
*
* @since 1.27
*/
* @return bool|mixed
*/
private function processRevision( $pageInfo, $revisionInfo ) {
+ global $wgMaxArticleSize;
+
+ // Make sure revisions won't violate $wgMaxArticleSize, which could lead to
+ // database errors and instability. Testing for revisions with only listed
+ // content models, as other content models might use serialization formats
+ // which aren't checked against $wgMaxArticleSize.
+ if ( ( !isset( $revisionInfo['model'] ) ||
+ in_array( $revisionInfo['model'], array(
+ 'wikitext',
+ 'css',
+ 'json',
+ 'javascript',
+ 'text',
+ ''
+ ) ) ) &&
+ (int)( strlen( $revisionInfo['text'] ) / 1024 ) > $wgMaxArticleSize
+ ) {
+ throw new MWException( 'The text of ' .
+ ( isset( $revisionInfo['id'] ) ?
+ "the revision with ID $revisionInfo[id]" :
+ 'a revision'
+ ) . " exceeds the maximum allowable size ($wgMaxArticleSize KB)" );
+ }
+
$revision = new WikiRevision( $this->config );
if ( isset( $revisionInfo['id'] ) ) {
} );
} );
// Commit all changes
- $factory->commitMasterChanges();
+ $factory->commitMasterChanges( __METHOD__ );
// Record ChronologyProtector positions
$factory->shutdown();
wfDebug( __METHOD__ . ': all transactions committed' );
*/
public function restInPeace( $mode = 'fast' ) {
// Assure deferred updates are not in the main transaction
- wfGetLBFactory()->commitMasterChanges();
+ wfGetLBFactory()->commitMasterChanges( __METHOD__ );
// Ignore things like master queries/connections on GET requests
// as long as they are in deferred updates (which catch errors).
// Commit and close up!
$factory = wfGetLBFactory();
- $factory->commitMasterChanges();
+ $factory->commitMasterChanges( __METHOD__ );
$factory->shutdown( LBFactory::SHUTDOWN_NO_CHRONPROT );
wfDebug( "Request ended normally\n" );
# * data-mw-<name here> is reserved for extensions (or core) if
# they need to communicate some data to the client and want to be
# sure that it isn't coming from an untrusted user.
- if ( !preg_match( '/^data-(?!ooui|mw|parsoid)/i', $attribute )
+ # * Ensure that the attribute is not namespaced by banning
+ # colons.
+ if ( !preg_match( '/^data-(?!ooui|mw|parsoid)[^:]*$/i', $attribute )
&& !isset( $whitelist[$attribute] )
) {
continue;
);
}
+ /**
+ * Throw a UsageException, which will (if uncaught) call the main module's
+ * error handler and die with an error message including block info.
+ *
+ * @since 1.27
+ * @param Block $block The block used to generate the UsageException
+ * @throws UsageException always
+ */
+ public function dieBlocked( Block $block ) {
+ // Die using the appropriate message depending on block type
+ if ( $block->getType() == Block::TYPE_AUTO ) {
+ $this->dieUsage(
+ 'Your IP address has been blocked automatically, because it was used by a blocked user',
+ 'autoblocked',
+ 0,
+ array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) )
+ );
+ } else {
+ $this->dieUsage(
+ 'You have been blocked from editing',
+ 'blocked',
+ 0,
+ array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) )
+ );
+ }
+ }
+
/**
* Get error (as code, string) from a Status object.
*
$params = $this->extractRequestParams();
$user = $this->getUser();
-
if ( !$user->isAllowed( RevisionDeleter::getRestriction( $params['type'] ) ) ) {
$this->dieUsageMsg( 'badaccess-group0' );
}
if ( $user->isBlocked() ) {
- $block = $user->getBlock();
-
- // Die using the appropriate message depending on block type
- if ( $block->getType() == TYPE_AUTO ) {
- $this->dieUsage(
- 'Your IP address has been blocked automatically, because it was used by a blocked user',
- 'autoblocked',
- 0,
- array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) )
- );
- } else {
- $this->dieUsage(
- 'You have been blocked from editing',
- 'blocked',
- 0,
- array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) )
- );
- }
+ $this->dieBlocked( $user->getBlock() );
}
if ( !$params['ids'] ) {
}
if ( $user->isBlocked() ) {
- $block = $user->getBlock();
-
- // Die using the appropriate message depending on block type
- if ( $block->getType() == TYPE_AUTO ) {
- $this->dieUsage(
- 'Your IP address has been blocked automatically, because it was used by a blocked user',
- 'autoblocked',
- 0,
- array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) )
- );
- } else {
- $this->dieUsage(
- 'You have been blocked from editing',
- 'blocked',
- 0,
- array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) )
- );
- }
+ $this->dieBlocked( $user->getBlock() );
}
// validate and process each revid, rcid and logid
$this->useTransactionalTimeLimit();
$params = $this->extractRequestParams();
-
- if ( !$this->getUser()->isAllowed( 'undelete' ) ) {
+ $user = $this->getUser();
+ if ( !$user->isAllowed( 'undelete' ) ) {
$this->dieUsageMsg( 'permdenied-undelete' );
}
- if ( $this->getUser()->isBlocked() ) {
- $this->dieUsage(
- 'You have been blocked from editing',
- 'blocked',
- 0,
- array( 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $this->getUser()->getBlock() ) )
- );
+ if ( $user->isBlocked() ) {
+ $this->dieBlocked( $user->getBlock() );
}
$titleObj = Title::newFromText( $params['title'] );
}
public function execute() {
+ $pUser = $this->getUser();
+
+ // Deny if the user is blocked and doesn't have the full 'userrights' permission.
+ // This matches what Special:UserRights does for the web UI.
+ if ( $pUser->isBlocked() && !$pUser->isAllowed( 'userrights' ) ) {
+ $this->dieBlocked( $pUser->getBlock() );
+ }
+
$params = $this->extractRequestParams();
$user = $this->getUrUser( $params );
if ( !( $flags & self::SHUTDOWN_NO_CHRONPROT ) ) {
$this->shutdownChronologyProtector( $this->chronProt );
}
- $this->commitMasterChanges(); // sanity
+ $this->commitMasterChanges( __METHOD__ ); // sanity
}
}
if ( !( $flags & self::SHUTDOWN_NO_CHRONPROT ) ) {
$this->shutdownChronologyProtector( $this->chronProt );
}
- $this->commitMasterChanges(); // sanity
+ $this->commitMasterChanges( __METHOD__ ); // sanity
}
}
foreach ( $otherUpdates as $update ) {
try {
$update->doUpdate();
- wfGetLBFactory()->commitMasterChanges();
+ wfGetLBFactory()->commitMasterChanges( __METHOD__ );
} catch ( Exception $e ) {
// We don't want exceptions thrown during deferred updates to
// be reported to the user since the output is already sent
}
// Make sure incomplete transactions are not committed and end any
// open atomic sections so that other DB updates have a chance to run
- wfGetLBFactory()->rollbackMasterChanges();
+ wfGetLBFactory()->rollbackMasterChanges( __METHOD__ );
}
}
}
/**
- * Get a link to mark the change as patrolled, or '' if there's either no
- * revision to patrol or the user is not allowed to to it.
+ * Build a link to mark a change as patrolled.
+ *
+ * Returns empty string if there's either no revision to patrol or the user is not allowed to.
* Side effect: When the patrol link is build, this method will call
* OutputPage::preventClickjacking() and load mediawiki.page.patrol.ajax.
*
- * @return string
+ * @return string HTML or empty string
*/
protected function markPatrolledLink() {
+ if ( $this->mMarkPatrolledLink === null ) {
+ $linkInfo = $this->getMarkPatrolledLinkInfo();
+ // If false, there is no patrol link needed/allowed
+ if ( !$linkInfo ) {
+ $this->mMarkPatrolledLink = '';
+ } else {
+ $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown(
+ $this->mNewPage,
+ $this->msg( 'markaspatrolleddiff' )->escaped(),
+ array(),
+ array(
+ 'action' => 'markpatrolled',
+ 'rcid' => $linkInfo['rcid'],
+ 'token' => $linkInfo['token'],
+ )
+ ) . ']</span>';
+ }
+ }
+ return $this->mMarkPatrolledLink;
+ }
+
+ /**
+ * Returns an array of meta data needed to build a "mark as patrolled" link and
+ * adds the mediawiki.page.patrol.ajax to the output.
+ *
+ * @return array|false An array of meta data for a patrol link (rcid & token)
+ * or false if no link is needed
+ */
+ protected function getMarkPatrolledLinkInfo() {
global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
+
$user = $this->getUser();
- if ( $this->mMarkPatrolledLink === null ) {
- // Prepare a change patrol link, if applicable
- if (
- // Is patrolling enabled and the user allowed to?
- $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
- // Only do this if the revision isn't more than 6 hours older
- // than the Max RC age (6h because the RC might not be cleaned out regularly)
- RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
- ) {
- // Look for an unpatrolled change corresponding to this diff
-
- $db = wfGetDB( DB_SLAVE );
- $change = RecentChange::newFromConds(
- array(
- 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
- 'rc_this_oldid' => $this->mNewid,
- 'rc_patrolled' => 0
- ),
- __METHOD__
- );
+ // Prepare a change patrol link, if applicable
+ if (
+ // Is patrolling enabled and the user allowed to?
+ $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
+ // Only do this if the revision isn't more than 6 hours older
+ // than the Max RC age (6h because the RC might not be cleaned out regularly)
+ RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
+ ) {
+ // Look for an unpatrolled change corresponding to this diff
+ $db = wfGetDB( DB_SLAVE );
+ $change = RecentChange::newFromConds(
+ array(
+ 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
+ 'rc_this_oldid' => $this->mNewid,
+ 'rc_patrolled' => 0
+ ),
+ __METHOD__
+ );
- if ( $change && !$change->getPerformer()->equals( $user ) ) {
- $rcid = $change->getAttribute( 'rc_id' );
- } else {
- // None found or the page has been created by the current user.
- // If the user could patrol this it already would be patrolled
- $rcid = 0;
+ if ( $change && !$change->getPerformer()->equals( $user ) ) {
+ $rcid = $change->getAttribute( 'rc_id' );
+ } else {
+ // None found or the page has been created by the current user.
+ // If the user could patrol this it already would be patrolled
+ $rcid = 0;
+ }
+ // Build the link
+ if ( $rcid ) {
+ $this->getOutput()->preventClickjacking();
+ if ( $wgEnableAPI && $wgEnableWriteAPI
+ && $user->isAllowed( 'writeapi' )
+ ) {
+ $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
}
- // Build the link
- if ( $rcid ) {
- $this->getOutput()->preventClickjacking();
- if ( $wgEnableAPI && $wgEnableWriteAPI
- && $user->isAllowed( 'writeapi' )
- ) {
- $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
- }
- $token = $user->getEditToken( $rcid );
- $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown(
- $this->mNewPage,
- $this->msg( 'markaspatrolleddiff' )->escaped(),
- array(),
- array(
- 'action' => 'markpatrolled',
- 'rcid' => $rcid,
- 'token' => $token,
- )
- ) . ']</span>';
- } else {
- $this->mMarkPatrolledLink = '';
- }
- } else {
- $this->mMarkPatrolledLink = '';
+ $token = $user->getEditToken( $rcid );
+ return array(
+ 'rcid' => $rcid,
+ 'token' => $token,
+ );
}
}
- return $this->mMarkPatrolledLink;
+ // No mark as patrolled link applicable
+ return false;
}
/**
self::getLogMessage( $e ),
self::getLogContext( $e )
);
- $factory->rollbackMasterChanges();
+ $factory->rollbackMasterChanges( __METHOD__ );
}
}
$group = JobQueueGroup::singleton();
// Flush any pending DB writes for sanity
- wfGetLBFactory()->commitAll();
+ wfGetLBFactory()->commitAll( __METHOD__ );
// Some jobs types should not run until a certain timestamp
$backoffs = array(); // map of (type => UNIX expiry)
// Commit all outstanding connections that are in a transaction
// to get a fresh repeatable read snapshot on every connection.
// Note that jobs are still responsible for handling slave lag.
- wfGetLBFactory()->commitAll();
+ wfGetLBFactory()->commitAll( __METHOD__ );
// Clear out title cache data from prior snapshots
LinkCache::singleton()->clear();
$timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
) {
// Writes are all to foreign DBs, named locks don't form queues,
// or $wgJobSerialCommitThreshold is not reached; commit changes now
- wfGetLBFactory()->commitMasterChanges();
+ wfGetLBFactory()->commitMasterChanges( __METHOD__ );
return;
}
} );
// Actually commit the DB master changes
- wfGetLBFactory()->commitMasterChanges();
+ wfGetLBFactory()->commitMasterChanges( __METHOD__ );
// Release the lock
$dbwSerial->unlock( 'jobrunner-serial-commit', __METHOD__ );
/**
* Increase stored value of $key by $value while preserving its TTL
*
- * This will create the key with value $init and TTL $ttl if not present
+ * This will create the key with value $init and TTL $ttl instead if not present
*
* @param string $key
* @param int $ttl
* @param int $value
* @param int $init
- * @return bool
+ * @return int|bool New value or false on failure
* @since 1.24
*/
public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
- return $this->incr( $key, $value ) ||
- $this->add( $key, (int)$init, $ttl ) || $this->incr( $key, $value );
+ $newValue = $this->incr( $key, $value );
+ if ( $newValue === false ) {
+ // No key set; initialize
+ $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
+ }
+ if ( $newValue === false ) {
+ // Raced out initializing; increment
+ $newValue = $this->incr( $key, $value );
+ }
+
+ return $newValue;
}
/**
// Build CSS rules
$rules = array();
- // Underline: 2 = browser default, 1 = always, 0 = never
+ // Underline: 2 = skin default, 1 = always, 0 = never
if ( $options['underline'] < 2 ) {
$rules[] = "a { text-decoration: " .
( $options['underline'] ? 'underline' : 'none' ) . "; }";
- } else {
- # The scripts of these languages are very hard to read with underlines
- $rules[] = 'a:lang(ar), a:lang(kk-arab), a:lang(mzn), ' .
- 'a:lang(ps), a:lang(ur) { text-decoration: none; }';
}
if ( $options['editfont'] !== 'default' ) {
// Double-check that $options['editfont'] consists of safe characters only
return array( 'all' => $style );
}
+ /**
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function isKnownEmpty( ResourceLoaderContext $context ) {
+ $styles = $this->getStyles( $context );
+ return isset( $styles['all'] ) && $styles['all'] === '';
+ }
+
/**
* @return string
*/
public function alterForm( HTMLForm $form ) {
Hooks::run( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) );
+ $form->setSubmitTextMsg( 'pagelang-submit' );
}
/**
$cache = ObjectCache::getLocalServerInstance();
}
if ( $cache ) {
- $counter = $cache->incr( $bucket, $count );
+ $counter = $cache->incrWithInit( $bucket, $cache::TTL_INDEFINITE, $count, $count );
if ( $counter === false ) {
- if ( !$cache->add( $bucket, (int)$count ) ) {
- throw new RuntimeException( 'Unable to set value to ' . get_class( $cache ) );
- }
- $counter = $count;
+ throw new RuntimeException( 'Unable to set value to ' . get_class( $cache ) );
}
}
"unprotectthispage": "Change protection of this page",
"newpage": "New page",
"talkpage": "Discuss this page",
- "talkpagelinktext": "Talk",
+ "talkpagelinktext": "talk",
"specialpage": "Special page",
"personaltools": "Personal tools",
"addsection": "+",
"pagelang-language": "Language",
"pagelang-use-default": "Use default language",
"pagelang-select-lang": "Select language",
+ "pagelang-submit": "Submit",
"right-pagelang": "Change page language",
"action-pagelang": "change the page language",
"log-name-pagelang": "Change language log",
"pagelang-language": "Language selector label for Special:PageLanguage\n{{Identical|Language}}",
"pagelang-use-default": "Radio label for selector on Special:PageLanguage for default language",
"pagelang-select-lang": "Radio label for selector on Special:PageLanguage for language selection\n{{Identical|Select language}}",
+ "pagelang-submit": "Submit button label for Special:PageLanguage form\n{{Identical|Submit}}",
"right-pagelang": "{{Doc-right|pagelang}}\nRight to change page language on Special:PageLanguage",
"action-pagelang": "{{Doc-action|pagelang}}",
"log-name-pagelang": "Display entry for log name for changes in page language in Special:Log.",
// Commit and close up!
$factory = wfGetLBFactory();
-$factory->commitMasterChanges();
+$factory->commitMasterChanges( 'doMaintenance' );
$factory->shutdown();
border: 1px solid #aaaaaa;
background-color: #f9f9f9;
padding: 5px;
- display: inline-block;
display: table;
- /* IE7 and earlier */
- zoom: 1;
- *display: inline;
}
/* Separate columns for tocnumber and toctext */
-/* Ignored by IE7 and lower */
.tocnumber,
.toctext {
display: table-cell;
}
/* Space between the columns for tocnumber and toctext */
-/* Ignored by IE7 and lower */
.tocnumber:after {
content: "";
padding-right: 0.5em;
list-style: none;
list-style-type: none;
list-style-image: none;
- vertical-align: middle !ie;
}
.catlinks li {
padding: 0 .4em;
border-left: 1px solid #AAA;
margin: 0.1em 0;
- zoom: 1;
- display: inline !ie;
}
.catlinks li:first-child {
text-decoration: underline;
}
+a:lang(ar),
+a:lang(kk-arab),
+a:lang(mzn),
+a:lang(ps),
+a:lang(ur) {
+ text-decoration: none;
+}
+
a.stub {
color: #772233;
}
} );
};
+ /**
+ * @inheritdoc
+ */
+ CSP.getItemFromData = function ( data ) {
+ // This is a bit of a hack... We have to canonicalize the data in the same way that
+ // #createItemWidget and CategoryCapsuleItemWidget will do, otherwise we won't find duplicates.
+ data = mw.Title.newFromText( data, NS_CATEGORY ).getMainText();
+ return OO.ui.mixin.GroupElement.prototype.getItemFromData.call( this, data );
+ };
+
/**
* Validates the values in `this.searchType`.
*
!! test
Strip reserved data attributes
!! wikitext
-<div data-mw="foo" data-parsoid="bar" data-mw-someext="baz" data-ok="fred" data-ooui="xyzzy">d</div>
+<div data-mw="foo" data-parsoid="bar" data-mw-someext="baz" data-ok="fred" data-ooui="xyzzy" data-bad:ns="ns">d</div>
!! html
<div data-ok="fred">d</div>
),
'child' => array(
'tag' => 'a',
- 'content' => 'Talk',
+ 'content' => 'talk',
)
),
$cacheEntry->usertalklink,
$this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
}
+ /**
+ * @covers BagOStuff::incrWithInit
+ */
+ public function testIncrWithInit() {
+ $key = wfMemcKey( 'test' );
+ $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
+ $this->assertEquals( 3, $val, "Correct init value" );
+
+ $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
+ $this->assertEquals( 4, $val, "Correct init value" );
+ }
+
/**
* @covers BagOStuff::getMulti
*/