into the redirect destination.
* prop=imageinfo&iiprop=uploadwarning will no longer include the possibility of
"was-deleted" warning.
+* Added difftotextpst to query=revisions which preforms a pre-save transform on
+ the text before diffing it.
=== Action API internal changes in 1.27 ===
* ApiQueryORM removed.
$wgSend404Code = true;
/**
- * The $wgShowRollbackEditCount variable is used to show how many edits will be
- * rollback. The numeric value of the variable are the limit up to are counted.
- * If the value is false or 0, the edits are not counted. Disabling this will
- * furthermore prevent MediaWiki from hiding some useless rollback links.
+ * The $wgShowRollbackEditCount variable is used to show how many edits can be rolled back.
+ * The numeric value of the variable controls how many edits MediaWiki will look back to
+ * determine whether a rollback is allowed (by checking that they are all from the same author).
+ * If the value is false or 0, the edits are not counted. Disabling this will prevent MediaWiki
+ * from hiding some useless rollback links.
*
* @since 1.20
*/
return;
}
+ $revision = $this->mArticle->getRevisionFetched();
+ // Disallow editing revisions with content models different from the current one
+ if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
+ $this->displayViewSourcePage(
+ $this->getContentObject(),
+ wfMessage(
+ 'contentmodelediterror',
+ $revision->getContentModel(),
+ $this->contentModel
+ )->plain()
+ );
+ return;
+ }
+
$this->isConflict = false;
// css / js subpages of user pages get a special treatment
$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
throw new PermissionsError( $action, $permErrors );
}
+ $this->displayViewSourcePage(
+ $content,
+ $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
+ );
+ }
+
+ /**
+ * Display a read-only View Source page
+ * @param Content $content content object
+ * @param string $errorMessage additional wikitext error message to display
+ */
+ protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
+ global $wgOut;
+
Hooks::run( 'EditPage::showReadOnlyForm:initial', array( $this, &$wgOut ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addHTML( $this->editFormPageTop );
$wgOut->addHTML( $this->editFormTextTop );
- $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) );
- $wgOut->addHTML( "<hr />\n" );
+ if ( $errorMessage !== '' ) {
+ $wgOut->addWikiText( $errorMessage );
+ $wgOut->addHTML( "<hr />\n" );
+ }
# If the user made changes, preserve them when showing the markup
# (This happens when a user is blocked during edit, for instance)
$text = $this->textbox1;
$wgOut->addWikiMsg( 'viewyourtext' );
} else {
- $text = $this->toEditText( $content );
+ try {
+ $text = $this->toEditText( $content );
+ } catch ( MWException $e ) {
+ # Serialize using the default format if the content model is not supported
+ # (e.g. for an old revision with a different model)
+ $text = $content->serialize();
+ }
$wgOut->addWikiMsg( 'viewsourcetext' );
}
/**
* Handle PHP errors issued inside a hook. Catch errors that have to do
- * with a function expecting a reference, and pass all others through to
- * MWExceptionHandler::handleError() for default processing.
+ * with a function expecting a reference, missing arguments, or wrong argument
+ * types. Pass all others through to to the default error handler.
+ *
+ * This is useful for throwing errors for major callback invocation errors
+ * (with regard to parameter signature) which PHP just gives warnings for.
*
* @since 1.18
*
* @return bool
*/
public static function hookErrorHandler( $errno, $errstr ) {
- if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) {
+ if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false
+ || strpos( $errstr, 'Missing argument ' ) !== false
+ || strpos( $errstr, ' expects parameter ' ) !== false
+ ) {
throw new MWHookException( $errstr, $errno );
}
- // Delegate unhandled errors to the default MW handler
- return call_user_func_array(
- 'MWExceptionHandler::handleError', func_get_args()
- );
+ // Delegate unhandled errors to the default handlers
+ return false;
}
}
if ( $factory->laggedSlaveUsed() ) {
$maxAge = $this->config->get( 'CdnMaxageLagged' );
$this->context->getOutput()->lowerCdnMaxage( $maxAge );
+ $request->response()->header( "X-Database-Lagged: true" );
wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
}
}
Profiler::instance()->getTransactionProfiler()->resetExpectations();
// Do any deferred jobs
- DeferredUpdates::doUpdates( 'commit', 'enqueue' );
+ DeferredUpdates::doUpdates( 'enqueue' );
// Make sure any lazy jobs are pushed
JobQueueGroup::pushLazyJobs();
* @param boolean $forceRecompile
*/
public function __construct( $templateDir = null, $forceRecompile = false ) {
- $this->templateDir = $templateDir ? $templateDir : __DIR__ . '/templates';
+ $this->templateDir = $templateDir ?: __DIR__ . '/templates';
$this->forceRecompile = $forceRecompile;
}
* Constructs the location of the the source Mustache template
* @param string $templateName The name of the template
* @return string
- * @throws UnexpectedValueException Disallows upwards directory traversal via $templateName
+ * @throws UnexpectedValueException If $templateName attempts upwards directory traversal
*/
protected function getTemplateFilename( $templateName ) {
// Prevent upwards directory traversal using same methods as Title::secureAndSplit
if ( $secretKey ) {
// See if the compiled PHP code is stored in cache.
- // CACHE_ACCEL throws an exception if no suitable object cache is present, so fall
- // back to CACHE_ANYTHING.
$cache = ObjectCache::newAccelerator( CACHE_ANYTHING );
- $key = wfMemcKey( 'template', $templateName, $fastHash );
+ $key = $cache->makeKey( 'template', $templateName, $fastHash );
$code = $this->forceRecompile ? null : $cache->get( $key );
if ( !$code ) {
*/
abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
- protected $limit, $diffto, $difftotext, $expandTemplates, $generateXML, $section,
- $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true;
+ protected $limit, $diffto, $difftotext, $difftotextpst, $expandTemplates, $generateXML,
+ $section, $parseContent, $fetchContent, $contentFormat, $setParsedLimit = true;
protected $fld_ids = false, $fld_flags = false, $fld_timestamp = false,
$fld_size = false, $fld_sha1 = false, $fld_comment = false,
protected function parseParameters( $params ) {
if ( !is_null( $params['difftotext'] ) ) {
$this->difftotext = $params['difftotext'];
+ $this->difftotextpst = $params['difftotextpst'];
} elseif ( !is_null( $params['diffto'] ) ) {
if ( $params['diffto'] == 'cur' ) {
$params['diffto'] = 0;
$this->contentFormat
);
+ if ( $this->difftotextpst ) {
+ $popts = ParserOptions::newFromContext( $this->getContext() );
+ $difftocontent = $difftocontent->preSaveTransform( $title, $user, $popts );
+ }
+
$engine = $handler->createDifferenceEngine( $context );
$engine->setContent( $content, $difftocontent );
}
ApiBase::PARAM_DFLT => null,
ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotext',
),
+ 'difftotextpst' => array(
+ ApiBase::PARAM_DFLT => false,
+ ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst',
+ ),
'contentformat' => array(
ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
ApiBase::PARAM_DFLT => null,
"apihelp-query+revisions+base-param-section": "Only retrieve the content of this section number.",
"apihelp-query+revisions+base-param-diffto": "Revision ID to diff each revision to. Use <kbd>prev</kbd>, <kbd>next</kbd> and <kbd>cur</kbd> for the previous, next and current revision respectively.",
"apihelp-query+revisions+base-param-difftotext": "Text to diff each revision to. Only diffs a limited number of revisions. Overrides <var>$1diffto</var>. If <var>$1section</var> is set, only that section will be diffed against this text.",
+ "apihelp-query+revisions+base-param-difftotextpst": "Perform a pre-save transform on the text before diffing it. Only valid when used with <var>$1difftotext</var>.",
"apihelp-query+revisions+base-param-contentformat": "Serialization format used for <var>$1difftotext</var> and expected for output of content.",
"apihelp-query+search-description": "Perform a full text search.",
"apihelp-query+revisions+base-param-section": "{{doc-apihelp-param|query+revisions+base|section|description=the \"section\" parameter to revision querying modules|noseealso=1}}",
"apihelp-query+revisions+base-param-diffto": "{{doc-apihelp-param|query+revisions+base|diffto|description=the \"diffto\" parameter to revision querying modules|noseealso=1}}",
"apihelp-query+revisions+base-param-difftotext": "{{doc-apihelp-param|query+revisions+base|difftotext|description=the \"difftotext\" parameter to revision querying modules|noseealso=1}}",
+ "apihelp-query+revisions+base-param-difftotextpst": "{{doc-apihelp-param|query+revisions+base|difftotextpst|description=the \"difftotextpst\" parameter to revision querying modules|noseealso=1}}",
"apihelp-query+revisions+base-param-contentformat": "{{doc-apihelp-param|query+revisions+base|contentformat|description=the \"contentformat\" parameter to revision querying modules|noseealso=1}}",
"apihelp-query+search-description": "{{doc-apihelp-description|query+search}}",
"apihelp-query+search-param-search": "{{doc-apihelp-param|query+search|search}}",
return array();
}
- $config = $resourceLoader->getConfig();
$retval = array();
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'msg_resource',
throw new MWException( __METHOD__ . ' passed an invalid module name' );
}
- // Update the module's blobs if the set of messages changed or if the blob is
- // older than the CacheEpoch setting
- $keys = array_keys( FormatJson::decode( $row->mr_blob, true ) );
- $values = array_values( array_unique( $module->getMessages() ) );
- if ( $keys !== $values
- || wfTimestamp( TS_MW, $row->mr_timestamp ) <= $config->get( 'CacheEpoch' )
- ) {
+ // Update the module's blob if the list of messages changed
+ $blobKeys = array_keys( FormatJson::decode( $row->mr_blob, true ) );
+ $moduleMsgs = array_values( array_unique( $module->getMessages() ) );
+ if ( $blobKeys !== $moduleMsgs ) {
$retval[$row->mr_resource] = $this->updateModule( $row->mr_resource, $module, $lang );
} else {
$retval[$row->mr_resource] = $row->mr_blob;
$this->parent = $parent;
$this->srvCache = ObjectCache::newAccelerator( 'hash' );
- $this->mainCache = wfGetMainCache();
+ $this->mainCache = ObjectCache::getLocalClusterInstance();
}
public function scaleLoads( &$loads, $group = false, $wiki = false ) {
/**
* Do any deferred updates and clear the list
*
- * @param string $commit Set to 'commit' to commit after every update to
* @param string $mode Use "enqueue" to use the job queue when possible [Default: run]
* prevent lock contention
+ * @param string $oldMode Unused
*/
- public static function doUpdates( $commit = '', $mode = 'run' ) {
+ public static function doUpdates( $mode = 'run', $oldMode = '' ) {
+ // B/C for ( $commit, $mode ) args
+ $mode = $oldMode ?: $mode;
+ if ( $mode === 'commit' ) {
+ $mode = 'run';
+ }
+
$updates = self::$updates;
while ( count( $updates ) ) {
foreach ( $otherUpdates as $update ) {
try {
$update->doUpdate();
- if ( $commit === 'commit' ) {
- wfGetLBFactory()->commitMasterChanges();
- }
+ wfGetLBFactory()->commitMasterChanges();
} catch ( Exception $e ) {
// We don't want exceptions thrown during deferred updates to
// be reported to the user since the output is already sent.
// Cache auth token information to avoid RTTs
if ( !empty( $config['cacheAuthInfo'] ) ) {
if ( PHP_SAPI === 'cli' ) {
- $this->srvCache = wfGetMainCache(); // preferrably memcached
+ // Preferrably memcached
+ $this->srvCache = ObjectCache::getLocalClusterInstance();
} else {
- // look for APC, XCache, WinCache, ect...
+ // Look for APC, XCache, WinCache, ect...
$this->srvCache = ObjectCache::newAccelerator( CACHE_NONE );
}
} else {
return $val;
}
- public function set( $key, $value, $exptime = 0 ) {
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
apc_store( $key . self::KEY_SUFFIX, $value, $exptime );
return true;
/** Bitfield constants for get()/getMulti() */
const READ_LATEST = 1; // use latest data for replicated stores
const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
+ /** Bitfield constants for set()/merge() */
+ const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
public function __construct( array $params = array() ) {
if ( isset( $params['logger'] ) ) {
* @param mixed $casToken
* @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
* @return mixed Returns false on failure and if the item does not exist
+ * @throws Exception
*/
protected function getWithToken( $key, &$casToken, $flags = 0 ) {
throw new Exception( __METHOD__ . ' not implemented.' );
* @param string $key
* @param mixed $value
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
- abstract public function set( $key, $value, $exptime = 0 );
+ abstract public function set( $key, $value, $exptime = 0, $flags = 0 );
/**
* Delete an item
* @param callable $callback Callback method to be executed
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $attempts The amount of times to attempt a merge in case of failure
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
* @throws InvalidArgumentException
*/
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
if ( !is_callable( $callback ) ) {
throw new InvalidArgumentException( "Got invalid callback." );
}
- return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
+ return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
}
/**
* @param callable $callback Callback method to be executed
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $attempts The amount of times to attempt a merge in case of failure
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
- protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
if ( !$this->lock( $key, 6 ) ) {
return false;
}
if ( $value === false ) {
$success = true; // do nothing
} else {
- $success = $this->set( $key, $value, $exptime ); // set the new value
+ $success = $this->set( $key, $value, $exptime, $flags ); // set the new value
}
}
return false;
}
- public function set( $key, $value, $exp = 0 ) {
+ public function set( $key, $value, $exp = 0, $flags = 0 ) {
return true;
}
return true;
}
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
return true; // faster
}
}
return $this->bag[$key][0];
}
- public function set( $key, $value, $exptime = 0 ) {
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) );
return true;
}
: $this->readStore->getMulti( $keys, $flags );
}
- public function set( $key, $value, $exptime = 0 ) {
- return $this->writeStore->set( $key, $value, $exptime );
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+ return $this->writeStore->set( $key, $value, $exptime, $flags );
}
public function delete( $key ) {
return $this->writeStore->unlock( $key );
}
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
- return $this->writeStore->merge( $key, $callback, $exptime, $attempts );
+ public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+ return $this->writeStore->merge( $key, $callback, $exptime, $attempts, $flags );
}
public function getLastError() {
return $val;
}
- public function set( $key, $value, $expire = 0 ) {
+ public function set( $key, $value, $expire = 0, $flags = 0 ) {
$result = wincache_ucache_set( $key, serialize( $value ), $expire );
/* wincache_ucache_set returns an empty array on success if $value
return true;
}
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
if ( !is_callable( $callback ) ) {
throw new Exception( "Got invalid callback." );
}
return $val;
}
- public function set( $key, $value, $expire = 0 ) {
+ public function set( $key, $value, $expire = 0, $flags = 0 ) {
if ( !$this->isInteger( $value ) ) {
$value = serialize( $value );
}
* @ingroup Cache
*/
class MemcachedBagOStuff extends BagOStuff {
+ /** @var MWMemcached|Memcached */
protected $client;
/**
return $this->client->get( $this->encodeKey( $key ), $casToken );
}
- /**
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
- public function set( $key, $value, $exptime = 0 ) {
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
return $this->client->set( $this->encodeKey( $key ), $value,
$this->fixExpiry( $exptime ) );
}
- /**
- * @param mixed $casToken
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
protected function cas( $casToken, $key, $value, $exptime = 0 ) {
return $this->client->cas( $casToken, $this->encodeKey( $key ),
$value, $this->fixExpiry( $exptime ) );
}
- /**
- * @param string $key
- * @return bool
- */
public function delete( $key ) {
return $this->client->delete( $this->encodeKey( $key ) );
}
- /**
- * @param string $key
- * @param int $value
- * @param int $exptime (default 0)
- * @return mixed
- */
public function add( $key, $value, $exptime = 0 ) {
return $this->client->add( $this->encodeKey( $key ), $value,
$this->fixExpiry( $exptime ) );
}
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
if ( !is_callable( $callback ) ) {
throw new Exception( "Got invalid callback." );
}
return $result;
}
- /**
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
- public function set( $key, $value, $exptime = 0 ) {
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debugLog( "set($key)" );
return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
}
- /**
- * @param float $casToken
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
protected function cas( $casToken, $key, $value, $exptime = 0 ) {
$this->debugLog( "cas($key)" );
return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
}
- /**
- * @param string $key
- * @return bool
- */
public function delete( $key ) {
$this->debugLog( "delete($key)" );
$result = parent::delete( $key );
}
}
- /**
- * @param string $key
- * @param int $value
- * @param int $exptime
- * @return mixed
- */
public function add( $key, $value, $exptime = 0 ) {
$this->debugLog( "add($key)" );
return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
}
- /**
- * @param string $key
- * @param int $value
- * @return mixed
- */
public function incr( $key, $value = 1 ) {
$this->debugLog( "incr($key)" );
$result = $this->client->increment( $key, $value );
return $this->checkResult( $key, $result );
}
- /**
- * @param string $key
- * @param int $value
- * @return mixed
- */
public function decr( $key, $value = 1 ) {
$this->debugLog( "decr($key)" );
$result = $this->client->decrement( $key, $value );
);
}
- /**
- * @param bool $debug
- */
public function setDebug( $debug ) {
- $this->doWrite( self::ALL, 'setDebug', $debug );
+ foreach ( $this->caches as $cache ) {
+ $cache->setDebug( $debug );
+ }
}
protected function doGet( $key, $flags = 0 ) {
+ if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) {
+ // If the latest write was a delete(), we do NOT want to fallback
+ // to the other tiers and possibly see the old value. Also, this
+ // is used by mergeViaLock(), which only needs to hit the primary.
+ return $this->caches[0]->get( $key, $flags );
+ }
+
$misses = 0; // number backends checked
$value = false;
foreach ( $this->caches as $cache ) {
&& $misses > 0
&& ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED
) {
- $this->doWrite( $misses, 'set', $key, $value, self::UPGRADE_TTL );
+ $this->doWrite( $misses, $this->asyncWrites, 'set', $key, $value, self::UPGRADE_TTL );
}
return $value;
}
- /**
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
- public function set( $key, $value, $exptime = 0 ) {
- return $this->doWrite( self::ALL, 'set', $key, $value, $exptime );
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+ $asyncWrites = ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC )
+ ? false
+ : $this->asyncWrites;
+
+ return $this->doWrite( self::ALL, $asyncWrites, 'set', $key, $value, $exptime );
}
- /**
- * @param string $key
- * @return bool
- */
public function delete( $key ) {
- return $this->doWrite( self::ALL, 'delete', $key );
+ return $this->doWrite( self::ALL, $this->asyncWrites, 'delete', $key );
}
- /**
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
public function add( $key, $value, $exptime = 0 ) {
- return $this->doWrite( self::ALL, 'add', $key, $value, $exptime );
+ return $this->doWrite( self::ALL, $this->asyncWrites, 'add', $key, $value, $exptime );
}
- /**
- * @param string $key
- * @param int $value
- * @return bool|null
- */
public function incr( $key, $value = 1 ) {
- return $this->doWrite( self::ALL, 'incr', $key, $value );
+ return $this->doWrite( self::ALL, $this->asyncWrites, 'incr', $key, $value );
}
- /**
- * @param string $key
- * @param int $value
- * @return bool
- */
public function decr( $key, $value = 1 ) {
- return $this->doWrite( self::ALL, 'decr', $key, $value );
+ return $this->doWrite( self::ALL, $this->asyncWrites, 'decr', $key, $value );
}
- /**
- * @param string $key
- * @param int $timeout
- * @param int $expiry
- * @param string $rclass
- * @return bool
- */
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
- // Lock only the first cache, to avoid deadlocks
+ // Only need to lock the first cache; also avoids deadlocks
return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
}
- /**
- * @param string $key
- * @return bool
- */
public function unlock( $key ) {
+ // Only the first cache is locked
return $this->caches[0]->unlock( $key );
}
- /**
- * @param string $key
- * @param callable $callback Callback method to be executed
- * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
- * @param int $attempts The amount of times to attempt a merge in case of failure
- * @return bool Success
- */
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
- return $this->doWrite( self::ALL, 'merge', $key, $callback, $exptime );
- }
-
public function getLastError() {
return $this->caches[0]->getLastError();
}
* Apply a write method to the first $count backing caches
*
* @param integer $count
+ * @param bool $asyncWrites
* @param string $method
* @param mixed ...
* @return bool
*/
- protected function doWrite( $count, $method /*, ... */ ) {
+ protected function doWrite( $count, $asyncWrites, $method /*, ... */ ) {
$ret = true;
- $args = array_slice( func_get_args(), 2 );
+ $args = array_slice( func_get_args(), 3 );
foreach ( $this->caches as $i => $cache ) {
if ( $i >= $count ) {
break; // ignore the lower tiers
}
- if ( $i == 0 || !$this->asyncWrites ) {
+ if ( $i == 0 || !$asyncWrites ) {
// First store or in sync mode: write now and get result
if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
$ret = false;
return $result;
}
- public function set( $key, $value, $expiry = 0 ) {
+ public function set( $key, $value, $expiry = 0, $flags = 0 ) {
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
return false;
return $values;
}
- /**
- * @param array $data
- * @param int $expiry
- * @return bool
- */
public function setMulti( array $data, $expiry = 0 ) {
$keysByTable = array();
foreach ( $data as $key => $value ) {
return $result;
}
- /**
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
- public function set( $key, $value, $exptime = 0 ) {
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
return $this->setMulti( array( $key => $value ), $exptime );
}
- /**
- * @param mixed $casToken
- * @param string $key
- * @param mixed $value
- * @param int $exptime
- * @return bool
- */
protected function cas( $casToken, $key, $value, $exptime = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
try {
return (bool)$db->affectedRows();
}
- /**
- * @param string $key
- * @return bool
- */
public function delete( $key ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
try {
return true;
}
- /**
- * @param string $key
- * @param int $step
- * @return int|null
- */
public function incr( $key, $step = 1 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
try {
return $newValue;
}
- public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
if ( !is_callable( $callback ) ) {
throw new Exception( "Got invalid callback." );
}
$outputPage = $this->getContext()->getOutput();
$user = $this->getContext()->getUser();
- $cache = wfGetMainCache();
$rc = false;
if ( !$this->getTitle()->quickUserCan( 'patrol', $user )
}
// Check for cached results
- $key = wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() );
+ $key = wfMemcKey( 'unpatrollable-page', $this->getTitle()->getArticleID() );
+ $cache = ObjectCache::getMainWANInstance();
if ( $cache->get( $key ) ) {
return false;
}
}
$config = $this->getConfig();
- if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) {
+ if ( $config->get( 'UseTidy' ) && $options->getTidy() ) {
$tmp = MWTidy::tidy( $tmp );
}
"permissionserrors": "Permission error",
"permissionserrorstext": "You do not have permission to do that, for the following {{PLURAL:$1|reason|reasons}}:",
"permissionserrorstext-withaction": "You do not have permission to $2, for the following {{PLURAL:$1|reason|reasons}}:",
+ "contentmodelediterror": "You cannot edit this revision because its content model is <code>$1</code>, and the current content model of the page is <code>$2</code>.",
"recreate-moveddeleted-warn": "<strong>Warning: You are recreating a page that was previously deleted.</strong>\n\nYou should consider whether it is appropriate to continue editing this page.\nThe deletion and move log for this page are provided here for convenience:",
"moveddeleted-notice": "This page has been deleted.\nThe deletion and move log for the page are provided below for reference.",
"moveddeleted-notice-recent": "Sorry, this page was recently deleted (within the last 24 hours).\nThe deletion and move log for the page are provided below for reference.",
"permissionserrors": "Used as title of error message.\n\nSee also:\n* {{msg-mw|loginreqtitle}}\n{{Identical|Permission error}}",
"permissionserrorstext": "This message is \"without action\" version of {{msg-mw|Permissionserrorstext-withaction}}.\n\nParameters:\n* $1 - the number of reasons that were found why ''the action'' cannot be performed",
"permissionserrorstext-withaction": "This message is \"with action\" version of {{msg-mw|Permissionserrorstext}}.\n\nParameters:\n* $1 - the number of reasons that were found why the action cannot be performed\n* $2 - one of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.",
+ "contentmodelediterror": "Error message shown when trying to edit an old revision with a content model different from that of the current revision\n* $1 - content model of the old revision\n* $2 - content model of the current revision",
"recreate-moveddeleted-warn": "Warning shown when creating a page which has already been deleted. See for example [[Test]].",
"moveddeleted-notice": "Shown on top of a deleted page in normal view modus ([{{canonicalurl:Test}} example]).",
"moveddeleted-notice-recent": "Shown on top of a recently deleted page in normal view modus ([{{canonicalurl:Test}} example]).",
$maintenance->globals();
// Perform deferred updates.
-DeferredUpdates::doUpdates( 'commit' );
+DeferredUpdates::doUpdates();
// log profiling info
wfLogProfilingData();
),
'mediawiki.ForeignStructuredUpload.BookletLayout' => array(
'scripts' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js',
+ 'styles' => 'resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.css',
'dependencies' => array(
'mediawiki.ForeignStructuredUpload',
'mediawiki.Upload.BookletLayout',
'dependencies' => array(
'oojs-ui',
'mediawiki.api',
+ 'mediawiki.ForeignApi',
+ 'mediawiki.Title',
),
'targets' => array( 'desktop', 'mobile' ),
),
( function ( $, mw ) {
/**
- * @class mw.widgets.CategoryCapsuleItemWidget
+ * @class mw.widgets.PageExistenceCache
+ * @private
+ * @param {mw.Api} [api]
*/
-
- var processExistenceCheckQueueDebounced,
- api = new mw.Api(),
- currentRequest = null,
- existenceCache = {},
- existenceCheckQueue = {};
-
- // The existence checking code really could be refactored into a separate class.
+ function PageExistenceCache( api ) {
+ this.api = api || new mw.Api();
+ this.processExistenceCheckQueueDebounced = OO.ui.debounce( this.processExistenceCheckQueue );
+ this.currentRequest = null;
+ this.existenceCache = {};
+ this.existenceCheckQueue = {};
+ }
/**
+ * Check for existence of pages in the queue.
+ *
* @private
*/
- function processExistenceCheckQueue() {
+ PageExistenceCache.prototype.processExistenceCheckQueue = function () {
var queue, titles;
- if ( currentRequest ) {
+ if ( this.currentRequest ) {
// Don't fire off a million requests at the same time
- currentRequest.always( function () {
- currentRequest = null;
- processExistenceCheckQueueDebounced();
- } );
+ this.currentRequest.always( function () {
+ this.currentRequest = null;
+ this.processExistenceCheckQueueDebounced();
+ }.bind( this ) );
return;
}
- queue = existenceCheckQueue;
- existenceCheckQueue = {};
+ queue = this.existenceCheckQueue;
+ this.existenceCheckQueue = {};
titles = Object.keys( queue ).filter( function ( title ) {
- if ( existenceCache.hasOwnProperty( title ) ) {
- queue[ title ].resolve( existenceCache[ title ] );
+ if ( this.existenceCache.hasOwnProperty( title ) ) {
+ queue[ title ].resolve( this.existenceCache[ title ] );
}
- return !existenceCache.hasOwnProperty( title );
- } );
+ return !this.existenceCache.hasOwnProperty( title );
+ }.bind( this ) );
if ( !titles.length ) {
return;
}
- currentRequest = api.get( {
+ this.currentRequest = this.api.get( {
action: 'query',
prop: [ 'info' ],
titles: titles
var index, curr, title;
for ( index in response.query.pages ) {
curr = response.query.pages[ index ];
- title = mw.Title.newFromText( curr.title ).getPrefixedText();
- existenceCache[ title ] = curr.missing === undefined;
- queue[ title ].resolve( existenceCache[ title ] );
+ title = new ForeignTitle( curr.title ).getPrefixedText();
+ this.existenceCache[ title ] = curr.missing === undefined;
+ queue[ title ].resolve( this.existenceCache[ title ] );
}
- } );
- }
-
- processExistenceCheckQueueDebounced = OO.ui.debounce( processExistenceCheckQueue );
+ }.bind( this ) );
+ };
/**
* Register a request to check whether a page exists.
* @param {mw.Title} title
* @return {jQuery.Promise} Promise resolved with true if the page exists or false otherwise
*/
- function checkPageExistence( title ) {
+ PageExistenceCache.prototype.checkPageExistence = function ( title ) {
var key = title.getPrefixedText();
- if ( !existenceCheckQueue[ key ] ) {
- existenceCheckQueue[ key ] = $.Deferred();
+ if ( !this.existenceCheckQueue[ key ] ) {
+ this.existenceCheckQueue[ key ] = $.Deferred();
}
- processExistenceCheckQueueDebounced();
- return existenceCheckQueue[ key ].promise();
+ this.processExistenceCheckQueueDebounced();
+ return this.existenceCheckQueue[ key ].promise();
+ };
+
+ /**
+ * @class mw.widgets.ForeignTitle
+ * @private
+ * @extends mw.Title
+ *
+ * @constructor
+ * @inheritdoc
+ */
+ function ForeignTitle() {
+ ForeignTitle.parent.apply( this, arguments );
}
+ OO.inheritClass( ForeignTitle, mw.Title );
+ ForeignTitle.prototype.getNamespacePrefix = function () {
+ // We only need to handle categories here...
+ return 'Category:'; // HACK
+ };
/**
+ * @class mw.widgets.CategoryCapsuleItemWidget
+ *
* Category selector capsule item widget. Extends OO.ui.CapsuleItemWidget with the ability to link
* to the given page, and to show its existence status (i.e., whether it is a redlink).
*
* @constructor
* @param {Object} config Configuration options
* @cfg {mw.Title} title Page title to use (required)
+ * @cfg {string} [apiUrl] API URL, if not the current wiki's API
*/
mw.widgets.CategoryCapsuleItemWidget = function MWWCategoryCapsuleItemWidget( config ) {
// Parent constructor
// Properties
this.title = config.title;
+ this.apiUrl = config.apiUrl || '';
this.$link = $( '<a>' )
.text( this.label )
.attr( 'target', '_blank' )
this.setMissing( false );
this.$label.replaceWith( this.$link );
this.setLabelElement( this.$link );
- checkPageExistence( this.title ).done( function ( exists ) {
- this.setMissing( !exists );
- }.bind( this ) );
+
+ /*jshint -W024*/
+ if ( !this.constructor.static.pageExistenceCaches[ this.apiUrl ] ) {
+ this.constructor.static.pageExistenceCaches[ this.apiUrl ] =
+ new PageExistenceCache( new mw.ForeignApi( this.apiUrl ) );
+ }
+ this.constructor.static.pageExistenceCaches[ this.apiUrl ]
+ .checkPageExistence( new ForeignTitle( this.title.getPrefixedText() ) )
+ .done( function ( exists ) {
+ this.setMissing( !exists );
+ }.bind( this ) );
+ /*jshint +W024*/
};
/* Setup */
OO.inheritClass( mw.widgets.CategoryCapsuleItemWidget, OO.ui.CapsuleItemWidget );
+ /* Static Properties */
+
+ /*jshint -W024*/
+ /**
+ * Map of API URLs to PageExistenceCache objects.
+ *
+ * @static
+ * @inheritable
+ * @property {Object}
+ */
+ mw.widgets.CategoryCapsuleItemWidget.static.pageExistenceCaches = {
+ '': new PageExistenceCache()
+ };
+ /*jshint +W024*/
+
/* Methods */
/**
* @param {boolean} missing Whether the page is missing (does not exist)
*/
mw.widgets.CategoryCapsuleItemWidget.prototype.setMissing = function ( missing ) {
+ var
+ title = new ForeignTitle( this.title.getPrefixedText() ), // HACK
+ prefix = this.apiUrl.replace( '/w/api.php', '' ); // HACK
+
if ( !missing ) {
this.$link
- .attr( 'href', this.title.getUrl() )
+ .attr( 'href', prefix + title.getUrl() )
.removeClass( 'new' );
} else {
this.$link
- .attr( 'href', this.title.getUrl( { action: 'edit', redlink: 1 } ) )
+ .attr( 'href', prefix + title.getUrl( { action: 'edit', redlink: 1 } ) )
.addClass( 'new' );
}
};
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {mw.Api} [api] Instance of mw.Api (or subclass thereof) to use for queries
* @cfg {number} [limit=10] Maximum number of results to load
* @cfg {mw.widgets.CategorySelector.SearchType[]} [searchTypes=[mw.widgets.CategorySelector.SearchType.OpenSearch]]
* Default search API to use when searching.
this.$input.on( 'change input cut paste', OO.ui.debounce( this.updateMenuItems.bind( this ), 100 ) );
// Initialize
- this.api = new mw.Api();
+ this.api = config.api || new mw.Api();
}
/* Setup */
*/
CSP.createItemWidget = function ( data ) {
return new mw.widgets.CategoryCapsuleItemWidget( {
+ apiUrl: this.api.apiUrl || undefined,
title: mw.Title.newFromText( data, NS_CATEGORY )
} );
};
* each individual request by passing them to #get or #post (or directly #ajax) later on.
*/
mw.Api = function ( options ) {
- // TODO: Share API objects with exact same config.
options = options || {};
// Force a string if we got a mw.Uri object
*/
abort: function () {
$.each( this.requests, function ( index, request ) {
- request.abort();
+ if ( request ) {
+ request.abort();
+ }
} );
},
/**
* Perform API post request
*
- * TODO: Post actions for non-local hostnames will need proxy.
- *
* @param {Object} parameters
* @param {Object} [ajaxOptions]
* @return {jQuery.Promise}
* Fail: Error code
*/
ajax: function ( parameters, ajaxOptions ) {
- var token,
+ var token, requestIndex,
+ api = this,
apiDeferred = $.Deferred(),
xhr, key, formData;
}
} );
+ requestIndex = this.requests.length;
this.requests.push( xhr );
+ xhr.always( function () {
+ api.requests[ requestIndex ] = null;
+ } );
// Return the Promise
return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
--- /dev/null
+.mw-foreignStructuredUpload-bookletLayout-license {
+ font-size: 90%;
+ line-height: 1.4em;
+ color: #555;
+}
/* Uploading */
+ /**
+ * @inheritdoc
+ */
+ mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () {
+ mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this );
+ // Point the CategorySelector to the right wiki as soon as we know what the right wiki is
+ this.upload.apiPromise.done( function ( api ) {
+ // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
+ if ( api.apiUrl ) {
+ // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
+ this.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
+ }
+ }.bind( this ) );
+ };
+
/**
* Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
* with the {@link #cfg-target target} specified in config.
notOwnWorkLocal = mw.message( 'foreign-structured-upload-form-label-not-own-work-local-default' );
}
- $ownWorkMessage = $( '<p>' ).html( ownWorkMessage.parse() );
+ $ownWorkMessage = $( '<p>' ).html( ownWorkMessage.parse() )
+ .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
$notOwnWorkMessage = $( '<div>' ).append(
$( '<p>' ).html( notOwnWorkMessage.parse() ),
$( '<p>' ).html( notOwnWorkLocal.parse() )
label: $notOwnWorkMessage
} );
this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
- if ( on ) {
- layout.messageLabel.setLabel( $ownWorkMessage );
- } else {
- layout.messageLabel.setLabel( $notOwnWorkMessage );
- }
+ layout.messageLabel.toggle( !on );
} );
fieldset = new OO.ui.FieldsetLayout();
} ),
new OO.ui.FieldLayout( this.ownWorkCheckbox, {
align: 'inline',
- label: mw.msg( 'foreign-structured-upload-form-label-own-work' )
+ label: $( '<div>' ).append(
+ $( '<p>' ).text( mw.msg( 'foreign-structured-upload-form-label-own-work' ) ),
+ $ownWorkMessage
+ )
} ),
- this.messageLabel
+ new OO.ui.FieldLayout( this.messageLabel, {
+ align: 'top'
+ } )
] );
this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
} );
this.categoriesWidget = new mw.widgets.CategorySelector( {
+ // Can't be done here because we don't know the target wiki yet... done in #initialize.
+ // api: new mw.ForeignApi( ... ),
$overlay: this.$overlay
} );
# Plus any combination of these:
#
# cat add category links
+# (ignored by Parsoid, since it emits <link>s)
# ill add inter-language links
+# (ignored by Parsoid, since it emits <links>s)
# subpage enable subpages (disabled by default)
# noxml don't check for XML well-formedness
# title=[[XXX]] run test using article title XXX
<span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":" [[Category:foo]]"}},"i":0}}]}'> </span><link rel="mw:PageProp/Category" href="./Category:Foo" about="#mwt1"> <!-- No pre-wrapping -->
!! end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
!! test
7b. Indent-pre and category links
!! options
-parsoid=wt2html,wt2wt
+parsoid=wt2html
!! wikitext
[[Category:foo]] a
[[Category:foo]] {{echo|b}}
-!! html
+!! html/parsoid
<pre><link rel="mw:PageProp/Category" href="./Category:Foo"> a
<link rel="mw:PageProp/Category" href="./Category:Foo"> <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"b"}},"i":0}}]}'>b</span></pre>
!! end
<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>)
<a rel="mw:ExtLink" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a>
(<a rel="mw:ExtLink" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>)
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity ">http://example.com/url_with_entity </a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity ">http://example.com/url_with_entity </a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity ">http://example.com/url_with_entity </a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity"><</span>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity<">http://example.com/url_with_entity<</a>
-<a rel="mw:ExtLink" href="http://example.com/url_with_entity<">http://example.com/url_with_entity<</a></p>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":" "}'> </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&lt;","srcContent":"<"}'><</span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x3C;","srcContent":"<"}'><</span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#60;","srcContent":"<"}'><</span></p>
+!! end
+
+!! test
+External links: tricky Parsoid html2html case
+!! options
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+http://example.com/url_with_entity&amp;
+!! html/php
+<p><a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>;
+</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>;</p>
+!! end
+
+!! test
+External links: Free with trailing quotes (T113666)
+!! wikitext
+'''News:''' Stuff here
+
+news:'a'b''c''d e
+!! html/php
+<p><b>News:</b> Stuff here
+</p><p><a rel="nofollow" class="external free" href="news:'a'b">news:'a'b</a><i>c</i>d e
+</p>
+!! html/parsoid
+<p><b>News:</b> Stuff here</p>
+<p><a rel="mw:ExtLink" href="news:'a'b">news:'a'b</a><i>c</i>d e</p>
!! end
!! test
!! test
External links: wiki links within external link (Bug 3695)
+!! options
+parsoid=wt2html,html2html
!! wikitext
[http://example.com [[wikilink]] embedded in ext link]
!! html/php
!! test
Serialize <a> tags with invalid link targets as plain text
!! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
!! html/parsoid
<a rel="mw:WikiLink" href="[[foo]]">text</a>
<a rel="mw:WikiLink" href="[[foo]]">*text</a>
1234</p>
!! end
+# <nowiki> nodes shouldn't be inserted during html2wt by Parsoid,
+# since these are ExtLinkText, not MagicLinkText
+!! test
+Magic links: use appropriate serialization for "almost" magic links.
+!! wikitext
+X[[Special:BookSources/0978739256|foo]]
+
+X[//tools.ietf.org/html/rfc1234 foo]
+!! html/php
+<p>X<a href="/wiki/Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a>
+</p><p>X<a rel="nofollow" class="external text" href="//tools.ietf.org/html/rfc1234">foo</a>
+</p>
+!! html/parsoid
+<p>X<a rel="mw:WikiLink" href="./Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a></p>
+<p>X<a rel="mw:ExtLink" href="//tools.ietf.org/html/rfc1234">foo</a></p>
+!! end
+
###
### Templates
####
!! html
!! end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize the include directives to serialize on their own line.
+## Selser will take care of preserving formatting in scenarios where they
+## intermingled with other wikitext.
!! test
Includes and comments at SOL
+!! options
+parsoid=wt2html,html2html
!! wikitext
<!-- comment --><noinclude><!-- comment --></noinclude><!-- comment -->== hu ==
</tbody></table>
!!end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize the include directives to serialize on their own line.
+## Selser will take care of preserving formatting in scenarios where they
+## intermingled with other wikitext.
!!test
2. Table tag in SOL posn. should get reparsed correctly with valid TSR
!!options
-parsoid=wt2html,wt2wt
+parsoid=wt2html
!!wikitext
<includeonly>a</includeonly>{| {{{b}}}
|c
[[Image:Foobar.jpg|<nowiki>|</nowiki>]]
!! end
+# wgExternalLinkTarget not supported by Parsoid
!! test
Image with link parameter, wgExternalLinkTarget
!! wikitext
[[Image:foobar.jpg|link=http://example.com/]]
!! config
wgExternalLinkTarget='foobar'
-!! html
+!! html/php
<p><a href="http://example.com/" target="foobar" rel="nofollow"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
</p>
!! end
</p>
!! end
+# wgExternalLinkTarget not supported by Parsoid
!! test
Image with link parameter, wgExternalLinkTarget, unnamed parameter
!! wikitext
[[Image:foobar.jpg|link=http://example.com/|Title]]
!! config
wgExternalLinkTarget='foobar'
-!! html
+!! html/php
<p><a href="http://example.com/" title="Title" target="foobar" rel="nofollow"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
</p>
!! end
[[Category:Foo (bar)|Foo]]
!! end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
!! test
Category with link tail
!! options
cat
pst
+parsoid=wt2html
!! wikitext
123[[Category:Foo]]456
-!! html
+!! html/php
123[[Category:Foo]]456
+!! html/parsoid
+<p>123<link rel="mw:PageProp/Category" href="Category:Foo"/>456</p>
!! end
!! test
[[Category:{{echo|Foo}}|{{echo|Bar}}]]
!! end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
!! test
Category / paragraph interactions
+!! options
+parsoid=wt2html
!! wikitext
Foo [[Category:Baz]] Bar
[[Category:Baz]]
{{echo|[[Category:Baz]]}}
[[Category:Baz]]
-!! html
+!! html/php
<p>Foo Bar
</p><p>Foo
Bar
</p><p>Foo
Bar
</p>
+!! html/parsoid
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar</p>
+<p>Foo <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> Bar <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz"/> <link rel="mw:PageProp/Category" href="Category:Baz" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[Category:Baz]]"}},"i":0}}]}'/></p>
+<link rel="mw:PageProp/Category" href="Category:Baz"/>
!! end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
+##
## The whitespace on the empty line is part of the test. Please do not delete
!! test
1. Categories and newlines: All preceding newlines should be suppressed (courtesy bug 87)
!! options
-parsoid=wt2html,wt2wt
+parsoid=wt2html
!! wikitext
This
[[Category:Foo]] and this should be part of same paragraph (not an indent-pre)
{{echo|[[Category:Foo]] and so should this!}}
-!! html
+!! html/php
<p>This and this should be part of same paragraph (not an indent-pre) and so should this!
</p>
!! html/parsoid
<link rel="mw:PageProp/Category" href="./Category:Foo" data-parsoid='{"stx":"simple","a":{"href":"./Category:Foo"},"sa":{"href":"Category:Foo"}}'/>
!! end
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
!! test
6. Categories and newlines: migrateTrailingCategories dom pass should not migrate categories not preceded by newlines
+!! options
+parsoid=wt2html
!! wikitext
* a [[Category:Foo]]
!! html/parsoid
</p>
!! end
-# html2wt localizes the "Category" namespace.
-# XXX the <link> element needs an empty data-parsoid attribute, or
-# else the html2html test fails because spaces are inserted.
+# We used to, but no longer wt2wt this test since the default serializer
+# will normalize all categories to serialize on their own line.
+# This wikitext usage is going to be fairly uncommon in production and
+# selser will take care of preventing whitespace insertion if this
+# occurs in an article.
+#
+# html2html disabled for the same reason (whitespace insertion between
+# x and y).
+#
+# html2wt disabled because it localizes the "Category" namespace.
!! test
Link prefix/suffixes aren't applied to category links
!! options
-parsoid=wt2html,wt2wt,html2html
+parsoid=wt2html
language=is
!! wikitext
x[[Category:Foo]]y
blah
!! endarticle
+## We used to, but no longer wt2wt this test since the default serializer
+## will normalize all categories to serialize on their own line.
+## This wikitext usage is going to be fairly uncommon in production and
+## selser will take care of preserving formatting in those scenarios.
!! test
Don't convert blue categorylinks to another variant (bug 33210)
!! options
-language=zh cat
+cat
+language=zh
+parsoid=wt2html
!! wikitext
[[A]][[Category:分类]]
-!! html
+!! html/php
<a href="/wiki/Category:%E5%88%86%E7%B1%BB" title="Category:分类">分类</a>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="A" title="A">A</a></p>
+<link rel="mw:PageProp/Category" href="Category:分类"/>
!! end
-
!! test
Stripping -{}- tags (language variants)
!! options
__TOC__ foo
-__TOC__ bar
+__TOC__
+ bar
!! end
#### --------------- HTML tags ---------------
<p><b><a href="http://cscott.net">http://cscott.net</a>x</b></p>
<p><a href="http://cscott.net">http://cscott.net</a>x</p>
!! wikitext
-http://cscott.net<nowiki/>'''foo'''
+http://cscott.net'''foo'''
http://cscott.net<b>foo</b>
-'''http://cscott.net<nowiki/>'''
+'''http://cscott.net'''
'''http://cscott.net '''
!! test
T75121: Infer extension name from typeOf if data-mw is not present
!! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
!! html/parsoid
<div typeOf="mw:Extension/foo"></div>
!! wikitext
!! test
Never serialize a-tag as html, regardless of what data-parsoid has to say
!! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
!! html/parsoid
<a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{"stx":"html"}'>Foo</a>
!! wikitext
!! test
Never serialize a-tag as html, no matter what attributes it has
!! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
!! html/parsoid
<a bad='true' href='http://boo.org'><img src='http://boohoo.org' /></a>
!! wikitext
# Tests spec'ing wikitext serialization norms |
# --------------------------------------------
+!! test
+1. Categories should always be serialized on their own line
+!! options
+parsoid=html2wt
+!! html/parsoid
+foo<link rel="mw:PageProp/Category" href="./Category:Foo">bar
+!! wikitext
+foo
+[[Category:Foo]]
+bar
+!! end
+
+!! test
+2. Categories that are part of templates should not introduce a line break
+!! wikitext
+foo {{echo|<span>bar</span> [[Category:baz]]}} bar
+!! html/parsoid
+<p>foo <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<span>bar</span> [[Category:baz]]"}},"i":0}}]}'>bar</span><span about="#mwt1"> </span><link rel="mw:PageProp/Category" href="./Category:Baz" about="#mwt1" data-parsoid='{"stx":"simple","a":{"href":"./Category:Baz"},"sa":{"href":"Category:baz"}}'/> bar</p>
+!! end
+
!! test
Lists: Add space after bullets
!! options
if ( $this->needsDB() && $this->db ) {
// Clean up open transactions
while ( $this->db->trxLevel() > 0 ) {
- $this->db->rollback();
+ $this->db->rollback( __METHOD__, 'flush' );
}
}
if ( $this->needsDB() && $this->db ) {
// Clean up open transactions
while ( $this->db->trxLevel() > 0 ) {
- $this->db->rollback();
+ $this->db->rollback( __METHOD__, 'flush' );
}
}
$this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
}
+ public function testSyncMerge() {
+ $key = wfRandomString();
+ $value = wfRandomString();
+ $func = function () use ( $value ) {
+ return $value;
+ };
+
+ // XXX: DeferredUpdates bound to transactions in CLI mode
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
+ $this->cache->merge( $key, $func );
+
+ // Set in tier 1
+ $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
+ // Not yet set in tier 2
+ $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' );
+
+ $dbw->commit();
+
+ // Set in tier 2
+ $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
+
+ $key = wfRandomString();
+
+ $dbw->begin();
+ $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC );
+
+ // Set in tier 1
+ $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
+ // Also set in tier 2
+ $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
+
+ $dbw->commit();
+ }
+
public function testSetDelayed() {
$key = wfRandomString();
$value = wfRandomString();