* (T141604) Extensions can now provide a better error message when their
maintenance scripts are run without the extension being installed.
* (T8948) Numeric sorting in categories is now supported by setting $wgCategoryCollation
- to uca-default-u-kn or uca-<langcode>-u-kn. If migrating from another
+ to 'uca-default-u-kn' or 'uca-<langcode>-u-kn'. If you can't use UCA collations,
+ a 'numeric' collation is also available. If migrating from another
collation, you will need to run the updateCollation.php maintenance script.
=== External library changes in 1.28 ===
==== Removed and replaced external libraries ====
=== Bug fixes in 1.28 ===
+* (T137264) SECURITY: XSS in unclosed internal links
+* (T133147) SECURITY: Escape '<' and ']]>' in inline <style> blocks
+* (T133147) SECURITY: Require login to preview user CSS pages
+* (T132926) SECURITY: Do not allow undeleting a revision deleted file if it is
+ the top file
+* (T129738) SECURITY: Make $wgBlockDisablesLogin also restrict logged in
+ permissions
+* (T129738) SECURITY: Make blocks log users out if $wgBlockDisablesLogin is true
+* (T139670) Move 'UserGetRights' call before application of
+ Session::getAllowedUserRights()
=== Action API changes in 1.28 ===
* Added 'maxarticlesize' property to action=query&meta=siteinfo which contains
=== Action API internal changes in 1.28 ===
* Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
interact with ApiParse and ApiExpandTemplates.
+* (T139565) SECURITY: API: Generate head items in the context of the given title
+* (T115333) SECURITY: Check read permission when loading page content in ApiParse
=== Languages updated in 1.28 ===
'EnhancedChangesList' => __DIR__ . '/includes/changes/EnhancedChangesList.php',
'EnotifNotifyJob' => __DIR__ . '/includes/jobqueue/jobs/EnotifNotifyJob.php',
'EnqueueJob' => __DIR__ . '/includes/jobqueue/jobs/EnqueueJob.php',
- 'EnqueueableDataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
+ 'EnqueueableDataUpdate' => __DIR__ . '/includes/deferred/EnqueueableDataUpdate.php',
'EraseArchivedFile' => __DIR__ . '/maintenance/eraseArchivedFile.php',
'ErrorPageError' => __DIR__ . '/includes/exception/ErrorPageError.php',
'EventRelayer' => __DIR__ . '/includes/libs/eventrelayer/EventRelayer.php',
'MwSql' => __DIR__ . '/maintenance/sql.php',
'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php',
'MySQLMasterPos' => __DIR__ . '/includes/db/DatabaseMysqlBase.php',
- 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
+ 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MySqlLockManager.php',
'MysqlInstaller' => __DIR__ . '/includes/installer/MysqlInstaller.php',
'MysqlUpdater' => __DIR__ . '/includes/installer/MysqlUpdater.php',
'NaiveForeignTitleFactory' => __DIR__ . '/includes/title/NaiveForeignTitleFactory.php',
'NullLockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php',
'NullRepo' => __DIR__ . '/includes/filerepo/NullRepo.php',
'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php',
+ 'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php',
'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php',
'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php',
'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php',
'PopulateRecentChangesSource' => __DIR__ . '/maintenance/populateRecentChangesSource.php',
'PopulateRevisionLength' => __DIR__ . '/maintenance/populateRevisionLength.php',
'PopulateRevisionSha1' => __DIR__ . '/maintenance/populateRevisionSha1.php',
- 'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
+ 'PostgreSqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/PostgreSqlLockManager.php',
'PostgresBlob' => __DIR__ . '/includes/db/DatabasePostgres.php',
'PostgresField' => __DIR__ . '/includes/db/DatabasePostgres.php',
'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
'PurgeAction' => __DIR__ . '/includes/actions/PurgeAction.php',
'PurgeChangedFiles' => __DIR__ . '/maintenance/purgeChangedFiles.php',
'PurgeChangedPages' => __DIR__ . '/maintenance/purgeChangedPages.php',
+ 'PurgeJobUtils' => __DIR__ . '/includes/jobqueue/utils/PurgeJobUtils.php',
'PurgeList' => __DIR__ . '/maintenance/purgeList.php',
'PurgeOldText' => __DIR__ . '/maintenance/purgeOldText.php',
'PurgeParserCache' => __DIR__ . '/maintenance/purgeParserCache.php',
},
"require-dev": {
"jakub-onderka/php-parallel-lint": "0.9.2",
- "justinrainbow/json-schema": "~1.6",
+ "justinrainbow/json-schema": "~3.0",
"mediawiki/mediawiki-codesniffer": "0.7.2",
"monolog/monolog": "~1.18.2",
"nikic/php-parser": "2.1.0",
$user: the User object that was created. (Parameter added in 1.7)
$byEmail: true when account was created "by email" (added in 1.12)
-'AddNewAccountApiForm': Allow modifying internal login form when creating an
-account via API.
-$apiModule: the ApiCreateAccount module calling
-$loginForm: the LoginForm used
-
-'AddNewAccountApiResult': Modify API output when creating a new account via API.
-$apiModule: the ApiCreateAccount module calling
-$loginForm: the LoginForm used
-&$result: associative array for API result data
-
'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom, rss,...)
are created. Can be used to omit specific feeds from being outputted. You must not use
this hook to add feeds, use OutputPage::addFeedLink() instead.
$suppressed: true if the error was suppressed via
error_reporting()/wfSuppressWarnings()
-'LoginAuthenticateAudit': A login attempt for a valid user account either
-succeeded or failed. No return data is accepted; this hook is for auditing only.
-$user: the User object being authenticated against
-$password: the password being submitted and found wanting
-$retval: a LoginForm class constant with authenticateUserData() return
- value (SUCCESS, WRONG_PASS, etc.).
-
'LoginFormValidErrorMessages': Called in LoginForm when a function gets valid
error messages. Allows to add additional error messages (except messages already
in LoginForm::$validErrorMessages).
* ('id' => block ID, 'autoIds' => array of autoblock IDs)
*/
public function insert( $dbw = null ) {
+ global $wgBlockDisablesLogin;
wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
if ( $dbw === null ) {
if ( $affected ) {
$auto_ipd_ids = $this->doRetroactiveAutoblock();
+
+ if ( $wgBlockDisablesLogin && $this->target instanceof User ) {
+ // Change user login token to force them to be logged out.
+ $this->target->setToken();
+ $this->target->saveSettings();
+ }
+
return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
}
/**
* Get/set whether the Block prevents a given action
- * @param string $action
- * @param bool|null $x
- * @return bool
+ *
+ * @param string $action Action to check
+ * @param bool|null $x Value for set, or null to just get value
+ * @return bool|null Null for unrecognized rights.
*/
public function prevents( $action, $x = null ) {
+ global $wgBlockDisablesLogin;
+ $res = null;
switch ( $action ) {
case 'edit':
# For now... <evil laugh>
- return true;
-
+ $res = true;
+ break;
case 'createaccount':
- return wfSetVar( $this->mCreateAccount, $x );
-
+ $res = wfSetVar( $this->mCreateAccount, $x );
+ break;
case 'sendemail':
- return wfSetVar( $this->mBlockEmail, $x );
-
+ $res = wfSetVar( $this->mBlockEmail, $x );
+ break;
case 'editownusertalk':
- return wfSetVar( $this->mDisableUsertalk, $x );
-
- default:
- return null;
+ $res = wfSetVar( $this->mDisableUsertalk, $x );
+ break;
+ case 'read':
+ $res = false;
+ break;
}
+ if ( !$res && $wgBlockDisablesLogin ) {
+ // If a block would disable login, then it should
+ // prevent any action that all users cannot do
+ $anon = new User;
+ $res = $anon->isAllowed( $action ) ? $res : true;
+ }
+
+ return $res;
}
/**
/**
* Full path on the web server where shared uploads can be found
*/
-$wgSharedUploadPath = "http://commons.wikimedia.org/shared/images";
+$wgSharedUploadPath = null;
/**
* Fetch commons image description pages and display them on the local wiki?
/**
* Path on the file system where shared uploads can be found.
*/
-$wgSharedUploadDirectory = "/var/www/wiki3/images";
+$wgSharedUploadDirectory = null;
/**
* DB name with metadata about shared directory.
*/
$wgUseMediaWikiUIEverywhere = false;
+/**
+ * Whether to label the store-to-database-and-show-to-others button in the editor
+ * as "Save page"/"Save changes" if false (the default) or, if true, instead as
+ * "Publish page"/"Publish changes".
+ *
+ * @since 1.28
+ */
+$wgEditButtonPublishNotSave = false;
+
/**
* Permit other namespaces in addition to the w3.org default.
*
* When $wgJobRunRate > 0, try to run jobs asynchronously, spawning a new process
* to handle the job execution, instead of blocking the request until the job
* execution finishes.
+ *
* @since 1.23
*/
-$wgRunJobsAsync = true;
+$wgRunJobsAsync = (
+ !function_exists( 'register_postsend_function' ) &&
+ !function_exists( 'fastcgi_finish_request' )
+);
/**
* Number of rows to update per job
/**
* Global configuration variable for Virtual REST Services.
- * Parameters for different services are to be declared inside
- * $wgVirtualRestConfig['modules'], which is to be treated as an associative
- * array. Global parameters will be merged with service-specific ones. The
- * result will then be passed to VirtualRESTService::__construct() in the
- * module.
+ *
+ * Use the 'path' key to define automatically mounted services. The value for this
+ * key is a map of path prefixes to service configuration. The later is an array of:
+ * - class : the fully qualified class name
+ * - options : map of arguments to the class constructor
+ * Such services will be available to handle queries under their path from the VRS
+ * singleton, e.g. MediaWikiServices::getInstance()->getVirtualRESTServiceClient();
+ *
+ * Auto-mounting example for Parsoid:
+ *
+ * $wgVirtualRestConfig['paths']['/parsoid/'] = [
+ * 'class' => 'ParsoidVirtualRESTService',
+ * 'options' => [
+ * 'url' => 'http://localhost:8000',
+ * 'prefix' => 'enwiki',
+ * 'domain' => 'en.wikipedia.org'
+ * ]
+ * ];
+ *
+ * Parameters for different services can also be declared inside the 'modules' value,
+ * which is to be treated as an associative array. The parameters in 'global' will be
+ * merged with service-specific ones. The result will then be passed to
+ * VirtualRESTService::__construct() in the module.
*
* Example config for Parsoid:
*
* @since 1.25
*/
$wgVirtualRestConfig = [
+ 'paths' => [],
'modules' => [],
'global' => [
# Timeout in seconds
* PHP version, and chosen database backend. The Wikimedia Foundation shares this data with
* MediaWiki developers to help guide future development efforts.
*
- * For details about what data is sent, see: https://www.mediawiki.org/wiki/Pingback
+ * For details about what data is sent, see: https://www.mediawiki.org/wiki/Manual:$wgPingback
*
* @var bool
* @since 1.28
*/
private $enableApiEditOverride = false;
+ /**
+ * @var IContextSource
+ */
+ protected $context;
+
/**
* @param Article $article
*/
$this->mArticle = $article;
$this->page = $article->getPage(); // model object
$this->mTitle = $article->getTitle();
+ $this->context = $article->getContext();
$this->contentModel = $this->mTitle->getContentModel();
return $this->mArticle;
}
+ /**
+ * @since 1.28
+ * @return IContextSource
+ */
+ public function getContext() {
+ return $this->context;
+ }
+
/**
* @since 1.19
* @return Title
* the newly-edited page.
*/
function edit() {
- global $wgOut, $wgRequest, $wgUser;
// Allow extensions to modify/prevent this form or submission
if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
return;
wfDebug( __METHOD__ . ": enter\n" );
+ $request = $this->context->getRequest();
+ $out = $this->context->getOutput();
// If they used redlink=1 and the page exists, redirect to the main article
- if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
- $wgOut->redirect( $this->mTitle->getFullURL() );
+ if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
+ $out->redirect( $this->mTitle->getFullURL() );
return;
}
- $this->importFormData( $wgRequest );
+ $this->importFormData( $request );
$this->firsttime = false;
if ( wfReadOnly() && $this->save ) {
wfDebug( __METHOD__ . ": User can't edit\n" );
// Auto-block user's IP if the account was "hard" blocked
if ( !wfReadOnly() ) {
- $user = $wgUser;
+ $user = $this->context->getUser();
DeferredUpdates::addCallableUpdate( function () use ( $user ) {
$user->spreadAnyEditBlock();
} );
* @return array
*/
protected function getEditPermissionErrors( $rigor = 'secure' ) {
- global $wgUser;
-
- $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
+ $user = $this->context->getUser();
+ $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
# Can this title be created?
if ( !$this->mTitle->exists() ) {
$permErrors = array_merge(
$permErrors,
wfArrayDiff2(
- $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
+ $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
$permErrors
)
);
* @throws PermissionsError
*/
protected function displayPermissionsError( array $permErrors ) {
- global $wgRequest, $wgOut;
-
- if ( $wgRequest->getBool( 'redlink' ) ) {
+ $out = $this->context->getOutput();
+ if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
// The edit page was reached via a red link.
// Redirect to the article page and let them click the edit tab if
// they really want a permission error.
- $wgOut->redirect( $this->mTitle->getFullURL() );
+ $out->redirect( $this->mTitle->getFullURL() );
return;
}
$this->displayViewSourcePage(
$content,
- $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
+ $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
);
}
* @param string $errorMessage additional wikitext error message to display
*/
protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
- global $wgOut;
+ $out = $this->context->getOutput();
+ Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
- Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
-
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->setPageTitle( wfMessage(
+ $out->setRobotPolicy( 'noindex,nofollow' );
+ $out->setPageTitle( wfMessage(
'viewsource-title',
$this->getContextTitle()->getPrefixedText()
) );
- $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
- $wgOut->addHTML( $this->editFormPageTop );
- $wgOut->addHTML( $this->editFormTextTop );
+ $out->addBacklinkSubtitle( $this->getContextTitle() );
+ $out->addHTML( $this->editFormPageTop );
+ $out->addHTML( $this->editFormTextTop );
if ( $errorMessage !== '' ) {
- $wgOut->addWikiText( $errorMessage );
- $wgOut->addHTML( "<hr />\n" );
+ $out->addWikiText( $errorMessage );
+ $out->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)
if ( !$this->firsttime ) {
$text = $this->textbox1;
- $wgOut->addWikiMsg( 'viewyourtext' );
+ $out->addWikiMsg( 'viewyourtext' );
} else {
try {
$text = $this->toEditText( $content );
# (e.g. for an old revision with a different model)
$text = $content->serialize();
}
- $wgOut->addWikiMsg( 'viewsourcetext' );
+ $out->addWikiMsg( 'viewsourcetext' );
}
- $wgOut->addHTML( $this->editFormTextBeforeContent );
+ $out->addHTML( $this->editFormTextBeforeContent );
$this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
- $wgOut->addHTML( $this->editFormTextAfterContent );
+ $out->addHTML( $this->editFormTextAfterContent );
- $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+ $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
Linker::formatTemplates( $this->getTemplates() ) ) );
- $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+ $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
- $wgOut->addHTML( $this->editFormTextBottom );
+ $out->addHTML( $this->editFormTextBottom );
if ( $this->mTitle->exists() ) {
- $wgOut->returnToMain( null, $this->mTitle );
+ $out->returnToMain( null, $this->mTitle );
}
}
* @return bool
*/
protected function previewOnOpen() {
- global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
- if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
+ global $wgPreviewOnOpenNamespaces;
+ $request = $this->context->getRequest();
+ if ( $request->getVal( 'preview' ) == 'yes' ) {
// Explicit override from request
return true;
- } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
+ } elseif ( $request->getVal( 'preview' ) == 'no' ) {
// Explicit override from request
return false;
} elseif ( $this->section == 'new' ) {
// Nothing *to* preview for new sections
return false;
- } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
- && $wgUser->getOption( 'previewonfirst' )
+ } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
+ && $this->context->getUser()->getOption( 'previewonfirst' )
) {
// Standard preference behavior
return true;
* @throws ErrorPageError
*/
function importFormData( &$request ) {
- global $wgContLang, $wgUser;
+ global $wgContLang;
# Section edit can come from either the form or a link
$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
$this->watchthis = $request->getCheck( 'wpWatchthis' );
# Don't force edit summaries when a user is editing their own user or talk page
+ $user = $this->context->getUser();
if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
- && $this->mTitle->getText() == $wgUser->getName()
+ && $this->mTitle->getText() == $user->getName()
) {
$this->allowBlankSummary = true;
} else {
$this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
- || !$wgUser->getOption( 'forceeditsummary' );
+ || !$user->getOption( 'forceeditsummary' );
}
$this->autoSumm = $request->getText( 'wpAutoSummary' );
* @return bool If the requested section is valid
*/
function initialiseForm() {
- global $wgUser;
$this->edittime = $this->page->getTimestamp();
$this->editRevId = $this->page->getLatest();
return false;
}
$this->textbox1 = $this->toEditText( $content );
+ $user = $this->context->getUser();
// activate checkboxes if user wants them to be always active
# Sort out the "watch" checkbox
- if ( $wgUser->getOption( 'watchdefault' ) ) {
+ if ( $user->getOption( 'watchdefault' ) ) {
# Watch all edits
$this->watchthis = true;
- } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
+ } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
# Watch creations
$this->watchthis = true;
- } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
+ } elseif ( $user->isWatched( $this->mTitle ) ) {
# Already watched
$this->watchthis = true;
}
- if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
+ if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
$this->minoredit = true;
}
if ( $this->textbox1 === false ) {
* @since 1.21
*/
protected function getContentObject( $def_content = null ) {
- global $wgOut, $wgRequest, $wgUser, $wgContLang;
+ global $wgContLang;
$content = false;
+ $request = $this->context->getRequest();
+ $user = $this->context->getUser();
// For message page not locally set, use the i18n message.
// For other non-existent articles, use preload text if any.
}
if ( $content === false ) {
# If requested, preload some text.
- $preload = $wgRequest->getVal( 'preload',
+ $preload = $request->getVal( 'preload',
// Custom preload text for new sections
$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
- $params = $wgRequest->getArray( 'preloadparams', [] );
+ $params = $request->getArray( 'preloadparams', [] );
$content = $this->getPreloadedContent( $preload, $params );
}
} else {
if ( $this->section != '' ) {
// Get section edit text (returns $def_text for invalid sections)
- $orig = $this->getOriginalContent( $wgUser );
+ $orig = $this->getOriginalContent( $user );
$content = $orig ? $orig->getSection( $this->section ) : null;
if ( !$content ) {
$content = $def_content;
}
} else {
- $undoafter = $wgRequest->getInt( 'undoafter' );
- $undo = $wgRequest->getInt( 'undo' );
+ $undoafter = $request->getInt( 'undoafter' );
+ $undo = $request->getInt( 'undo' );
if ( $undo > 0 && $undoafter > 0 ) {
$undorev = Revision::newFromId( $undo );
$undoMsg = 'failure';
} else {
$oldContent = $this->page->getContent( Revision::RAW );
- $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
- $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
+ $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
+ $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
if ( $newContent->equals( $oldContent ) ) {
# Tell the user that the undo results in no change,
// Messages: undo-success, undo-failure, undo-norev, undo-nochange
$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
- $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
+ $this->editFormPageTop .= $this->context->getOutput()->parse(
+ "<div class=\"{$class}\">" .
wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
}
if ( $content === false ) {
- $content = $this->getOriginalContent( $wgUser );
+ $content = $this->getOriginalContent( $user );
}
}
}
* @private
*/
function tokenOk( &$request ) {
- global $wgUser;
$token = $request->getVal( 'wpEditToken' );
- $this->mTokenOk = $wgUser->matchEditToken( $token );
- $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
+ $user = $this->context->getUser();
+ $this->mTokenOk = $user->matchEditToken( $token );
+ $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
return $this->mTokenOk;
}
$val = 'restored';
}
- $response = RequestContext::getMain()->getRequest()->response();
+ $response = $this->context->getRequest()->response();
$response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
'httpOnly' => false,
] );
/**
* Attempt submission
- * @param array $resultDetails See docs for $result in internalAttemptSave
+ * @param array|bool $resultDetails See docs for $result in internalAttemptSave
* @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
* @return Status The resulting status object.
*/
public function attemptSave( &$resultDetails = false ) {
- global $wgUser;
-
# Allow bots to exempt some edits from bot flagging
- $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
+ $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
$status = $this->internalAttemptSave( $resultDetails, $bot );
Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
* @return bool False, if output is done, true if rest of the form should be displayed
*/
private function handleStatus( Status $status, $resultDetails ) {
- global $wgUser, $wgOut;
-
/**
* @todo FIXME: once the interface for internalAttemptSave() is made
* nicer, this should use the message in $status
}
}
+ $out = $this->context->getOutput();
+
// "wpExtraQueryRedirect" is a hidden input to modify
// after save URL and is not used by actual edit form
- $request = RequestContext::getMain()->getRequest();
+ $request = $this->context->getRequest();
$extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
switch ( $status->value ) {
case self::AS_CANNOT_USE_CUSTOM_MODEL:
case self::AS_PARSE_ERROR:
- $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
+ $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
return true;
case self::AS_SUCCESS_NEW_ARTICLE:
}
}
$anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
- $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
+ $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
return false;
case self::AS_SUCCESS_UPDATE:
}
}
- $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
+ $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
return false;
case self::AS_SPAM_ERROR:
return false;
case self::AS_BLOCKED_PAGE_FOR_USER:
- throw new UserBlockedError( $wgUser->getBlock() );
+ throw new UserBlockedError( $this->context->getUser()->getBlock() );
case self::AS_IMAGE_REDIRECT_ANON:
case self::AS_IMAGE_REDIRECT_LOGGED:
// Run new style post-section-merge edit filter
if ( !Hooks::run( 'EditFilterMergedContent',
- [ $this->mArticle->getContext(), $content, $status, $this->summary,
+ [ $this->context, $content, $status, $this->summary,
$user, $this->minoredit ] )
) {
# Error messages etc. could be handled within the hook...
* time.
*/
function internalAttemptSave( &$result, $bot = false ) {
- global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
- global $wgContentHandlerUseDB;
+ global $wgParser, $wgMaxArticleSize, $wgContentHandlerUseDB;
$status = Status::newGood();
+ $user = $this->context->getUser();
+ $request = $this->context->getRequest();
if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
return $status;
}
- $spam = $wgRequest->getText( 'wpAntispam' );
+ $spam = $request->getText( 'wpAntispam' );
if ( $spam !== '' ) {
wfDebugLog(
'SimpleAntiSpam',
- $wgUser->getName() .
+ $user->getName() .
' editing "' .
$this->mTitle->getPrefixedText() .
'" submitted bogus field "' .
# Check image redirect
if ( $this->mTitle->getNamespace() == NS_FILE &&
$textbox_content->isRedirect() &&
- !$wgUser->isAllowed( 'upload' )
+ !$user->isAllowed( 'upload' )
) {
- $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
+ $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
$status->setResult( false, $code );
return $status;
}
if ( $match !== false ) {
$result['spam'] = $match;
- $ip = $wgRequest->getIP();
+ $ip = $request->getIP();
$pdbk = $this->mTitle->getPrefixedDBkey();
$match = str_replace( "\n", '', $match );
wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
return $status;
}
- if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
+ if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
// Auto-block user's IP if the account was "hard" blocked
if ( !wfReadOnly() ) {
- $wgUser->spreadAnyEditBlock();
+ $user->spreadAnyEditBlock();
}
# Check block state against master, thus 'false'.
$status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
return $status;
}
- if ( !$wgUser->isAllowed( 'edit' ) ) {
- if ( $wgUser->isAnon() ) {
+ if ( !$user->isAllowed( 'edit' ) ) {
+ if ( $user->isAnon() ) {
$status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
return $status;
} else {
$status->fatal( 'editpage-cannot-use-custom-model' );
$status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
return $status;
- } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
+ } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
$status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
return $status;
if ( $this->changeTags ) {
$changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
- $this->changeTags, $wgUser );
+ $this->changeTags, $user );
if ( !$changeTagsStatus->isOK() ) {
$changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
return $changeTagsStatus;
$status->value = self::AS_READ_ONLY_PAGE;
return $status;
}
- if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
+ if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) ) {
$status->fatal( 'actionthrottledtext' );
$status->value = self::AS_RATE_LIMITED;
return $status;
if ( $new ) {
// Late check for create permission, just in case *PARANOIA*
- if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
+ if ( !$this->mTitle->userCan( 'create', $user ) ) {
$status->fatal( 'nocreatetext' );
$status->value = self::AS_NO_CREATE_PERMISSION;
wfDebug( __METHOD__ . ": no create permission\n" );
return $status;
}
- if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
+ if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
return $status;
}
) {
$this->isConflict = true;
if ( $this->section == 'new' ) {
- if ( $this->page->getUserText() == $wgUser->getName() &&
+ if ( $this->page->getUserText() == $user->getName() &&
$this->page->getComment() == $this->newSectionSummary()
) {
// Probably a duplicate submission of a new comment.
} elseif ( $this->section == ''
&& Revision::userWasLastToEdit(
DB_MASTER, $this->mTitle->getArticleID(),
- $wgUser->getId(), $this->edittime
+ $user->getId(), $this->edittime
)
) {
# Suppress edit conflict with self, except for section edits where merging is required.
return $status;
}
- if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
+ if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
return $status;
}
return $status;
}
} elseif ( !$this->allowBlankSummary
- && !$content->equals( $this->getOriginalContent( $wgUser ) )
+ && !$content->equals( $this->getOriginalContent( $user ) )
&& !$content->isRedirect()
&& md5( $this->summary ) == $this->autoSumm
) {
$this->summary,
$flags,
false,
- $wgUser,
+ $user,
$content->getDefaultFormat(),
$this->changeTags
);
$result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
if ( $result['nullEdit'] ) {
// We don't know if it was a null edit until now, so increment here
- $wgUser->pingLimiter( 'linkpurge' );
+ $user->pingLimiter( 'linkpurge' );
}
$result['redirect'] = $content->isRedirect();
// If the content model changed, add a log entry
if ( $changingContentModel ) {
$this->addContentModelChangeLogEntry(
- $wgUser,
+ $user,
$new ? false : $oldContentModel,
$this->contentModel,
$this->summary
* Register the change of watch status
*/
protected function updateWatchlist() {
- global $wgUser;
+ $user = $this->context->getUser();
- if ( !$wgUser->isLoggedIn() ) {
+ if ( !$user->isLoggedIn() ) {
return;
}
- $user = $wgUser;
$title = $this->mTitle;
$watch = $this->watchthis;
// Do this in its own transaction to reduce contention...
}
function setHeaders() {
- global $wgOut, $wgUser, $wgAjaxEditStash;
+ global $wgAjaxEditStash;
+
+ $out = $this->context->getOutput();
+ $user = $this->context->getUser();
- $wgOut->addModules( 'mediawiki.action.edit' );
- $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
+ $out->addModules( 'mediawiki.action.edit' );
+ $out->addModuleStyles( 'mediawiki.action.edit.styles' );
- if ( $wgUser->getOption( 'showtoolbar' ) ) {
+ if ( $user->getOption( 'showtoolbar' ) ) {
// The addition of default buttons is handled by getEditToolbar() which
// has its own dependency on this module. The call here ensures the module
// is loaded in time (it has position "top") for other modules to register
// buttons (e.g. extensions, gadgets, user scripts).
- $wgOut->addModules( 'mediawiki.toolbar' );
+ $out->addModules( 'mediawiki.toolbar' );
}
- if ( $wgUser->getOption( 'uselivepreview' ) ) {
- $wgOut->addModules( 'mediawiki.action.edit.preview' );
+ if ( $user->getOption( 'uselivepreview' ) ) {
+ $out->addModules( 'mediawiki.action.edit.preview' );
}
- if ( $wgUser->getOption( 'useeditwarning' ) ) {
- $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
+ if ( $user->getOption( 'useeditwarning' ) ) {
+ $out->addModules( 'mediawiki.action.edit.editWarning' );
}
# Enabled article-related sidebar, toplinks, etc.
- $wgOut->setArticleRelated( true );
+ $out->setArticleRelated( true );
$contextTitle = $this->getContextTitle();
if ( $this->isConflict ) {
if ( $displayTitle === false ) {
$displayTitle = $contextTitle->getPrefixedText();
}
- $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
+ $out->setPageTitle( wfMessage( $msg, $displayTitle ) );
# Transmit the name of the message to JavaScript for live preview
# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
- $wgOut->addJsConfigVars( [
+ $out->addJsConfigVars( [
'wgEditMessage' => $msg,
'wgAjaxEditStash' => $wgAjaxEditStash,
] );
* Show all applicable editing introductions
*/
protected function showIntro() {
- global $wgOut, $wgUser;
if ( $this->suppressIntro ) {
return;
}
+ $out = $this->context->getOutput();
$namespace = $this->mTitle->getNamespace();
if ( $namespace == NS_MEDIAWIKI ) {
# Show a warning if editing an interface message
- $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
+ $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
# If this is a default message (but not css or js),
# show a hint that it is translatable on translatewiki.net
if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
) {
$defaultMessageText = $this->mTitle->getDefaultMessageText();
if ( $defaultMessageText !== false ) {
- $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
+ $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
'translateinterface' );
}
}
# there must be a description url to show a hint to shared repo
if ( $descUrl ) {
if ( !$this->mTitle->exists() ) {
- $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
+ $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
] );
} else {
- $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
+ $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
] );
}
$ip = User::isIP( $username );
$block = Block::newFromTarget( $user, $user );
if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
- $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
+ $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
[ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
} elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
# Show log extract if the user is currently blocked
LogEventsList::showLogExtract(
- $wgOut,
+ $out,
'block',
MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
'',
$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
wfMessage( 'helppage' )->inContentLanguage()->text()
) );
- if ( $wgUser->isLoggedIn() ) {
- $wgOut->wrapWikiMsg(
+ if ( $this->context->getUser()->isLoggedIn() ) {
+ $out->wrapWikiMsg(
// Suppress the external link icon, consider the help url an internal one
"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
[
]
);
} else {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
// Suppress the external link icon, consider the help url an internal one
"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
[
}
# Give a notice if the user is editing a deleted/moved page...
if ( !$this->mTitle->exists() ) {
- LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
+ LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
'',
[
'lim' => 10,
if ( $this->editintro ) {
$title = Title::newFromText( $this->editintro );
if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
- global $wgOut;
// Added using template syntax, to take <noinclude>'s into account.
- $wgOut->addWikiTextTitleTidy(
+ $this->context->getOutput()->addWikiTextTitleTidy(
'<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
$this->mTitle
);
* content.
*
* @param string|null|bool $text Text to unserialize
- * @return Content The content object created from $text. If $text was false
+ * @return Content|bool|null The content object created from $text. If $text was false
* or null, false resp. null will be returned instead.
*
* @throws MWException If unserializing the text results in a Content
* use the EditPage::showEditForm:fields hook instead.
*/
function showEditForm( $formCallback = null ) {
- global $wgOut, $wgUser;
-
# need to parse the preview early so that we know which templates are used,
# otherwise users with "show preview after edit box" will get a blank list
# we parse this near the beginning so that setHeaders can do the title
$previewOutput = $this->getPreviewText();
}
- Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
+ $out = $this->context->getOutput();
+ Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$out ] );
$this->setHeaders();
return;
}
- $wgOut->addHTML( $this->editFormPageTop );
+ $out->addHTML( $this->editFormPageTop );
- if ( $wgUser->getOption( 'previewontop' ) ) {
+ $user = $this->context->getUser();
+ if ( $user->getOption( 'previewontop' ) ) {
$this->displayPreviewArea( $previewOutput, true );
}
- $wgOut->addHTML( $this->editFormTextTop );
+ $out->addHTML( $this->editFormTextTop );
$showToolbar = true;
if ( $this->wasDeletedSinceLastEdit() ) {
// Add an confirmation checkbox and explanation.
$showToolbar = false;
} else {
- $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
+ $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
'deletedwhileediting' );
}
}
// @todo add EditForm plugin interface and use it here!
// search for textarea1 and textares2, and allow EditForm to override all uses.
- $wgOut->addHTML( Html::openElement(
+ $out->addHTML( Html::openElement(
'form',
[
'id' => self::EDITFORM_ID,
if ( is_callable( $formCallback ) ) {
wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
- call_user_func_array( $formCallback, [ &$wgOut ] );
+ call_user_func_array( $formCallback, [ &$out ] );
}
// Add an empty field to trip up spambots
- $wgOut->addHTML(
+ $out->addHTML(
Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
. Html::rawElement(
'label',
. Xml::closeElement( 'div' )
);
- Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
+ Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$out ] );
// Put these up at the top to ensure they aren't lost on early form submission
$this->showFormBeforeText();
$key = $comment === ''
? 'confirmrecreate-noreason'
: 'confirmrecreate';
- $wgOut->addHTML(
+ $out->addHTML(
'<div class="mw-confirm-recreate">' .
wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
# When the summary is hidden, also hide them on preview/show changes
if ( $this->nosummary ) {
- $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
+ $out->addHTML( Html::hidden( 'nosummary', true ) );
}
# If a blank edit summary was previously provided, and the appropriate
# For a bit more sophisticated detection of blank summaries, hash the
# automatic one and pass that in the hidden field wpAutoSummary.
if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
- $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
+ $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
}
if ( $this->undidRev ) {
- $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
+ $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
}
if ( $this->selfRedirect ) {
- $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
+ $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
}
if ( $this->hasPresetSummary ) {
}
$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
- $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
+ $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
- $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
- $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
+ $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
+ $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
- $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
- $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
+ $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
+ $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
if ( $this->section == 'new' ) {
$this->showSummaryInput( true, $this->summary );
- $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
+ $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
}
- $wgOut->addHTML( $this->editFormTextBeforeContent );
+ $out->addHTML( $this->editFormTextBeforeContent );
- if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
- $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
+ if ( !$this->isCssJsSubpage && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
+ $out->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
}
if ( $this->blankArticle ) {
- $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
+ $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
}
if ( $this->isConflict ) {
$this->showContentForm();
}
- $wgOut->addHTML( $this->editFormTextAfterContent );
+ $out->addHTML( $this->editFormTextAfterContent );
$this->showStandardInputs();
$this->showEditTools();
- $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
+ $out->addHTML( $this->editFormTextAfterTools . "\n" );
- $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+ $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
- $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
+ $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
if ( $this->mParserOutput ) {
- $wgOut->setLimitReportData( $this->mParserOutput->getLimitReportData() );
+ $out->setLimitReportData( $this->mParserOutput->getLimitReportData() );
}
- $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+ $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
if ( $this->isConflict ) {
try {
$this->contentFormat,
$ex->getMessage()
);
- $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+ $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
}
}
} else {
$mode = 'text';
}
- $wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
+ $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
// Marker for detecting truncated form data. This must be the last
// parameter sent in order to be of use, so do not move me.
- $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
- $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
+ $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
+ $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
- if ( !$wgUser->getOption( 'previewontop' ) ) {
+ if ( !$user->getOption( 'previewontop' ) ) {
$this->displayPreviewArea( $previewOutput, false );
}
* @return bool
*/
protected function showHeader() {
- global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
- global $wgAllowUserCss, $wgAllowUserJs;
+ global $wgMaxArticleSize, $wgAllowUserCss, $wgAllowUserJs;
+
+ $out = $this->context->getOutput();
+ $user = $this->context->getUser();
if ( $this->mTitle->isTalkPage() ) {
- $wgOut->addWikiMsg( 'talkpagetext' );
+ $out->addWikiMsg( 'talkpagetext' );
}
// Add edit notices
$editNotices = $this->mTitle->getEditNotices( $this->oldid );
if ( count( $editNotices ) ) {
- $wgOut->addHTML( implode( "\n", $editNotices ) );
+ $out->addHTML( implode( "\n", $editNotices ) );
} else {
$msg = wfMessage( 'editnotice-notext' );
if ( !$msg->isDisabled() ) {
- $wgOut->addHTML(
+ $out->addHTML(
'<div class="mw-editnotice-notext">'
. $msg->parseAsBlock()
. '</div>'
}
if ( $this->isConflict ) {
- $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
+ $out->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
$this->editRevId = $this->page->getLatest();
} else {
if ( $this->section != '' && !$this->isSectionEditSupported() ) {
// We use $this->section to much before this and getVal('wgSection') directly in other places
// at this point we can't reset $this->section to '' to fallback to non-section editing.
// Someone is welcome to try refactoring though
- $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
+ $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
return false;
}
}
if ( $this->missingComment ) {
- $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
+ $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
}
if ( $this->missingSummary && $this->section != 'new' ) {
- $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
+ $out->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
}
if ( $this->missingSummary && $this->section == 'new' ) {
- $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
+ $out->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
}
if ( $this->blankArticle ) {
- $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
+ $out->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
}
if ( $this->selfRedirect ) {
- $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
+ $out->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
}
if ( $this->hookError !== '' ) {
- $wgOut->addWikiText( $this->hookError );
+ $out->addWikiText( $this->hookError );
}
if ( !$this->checkUnicodeCompliantBrowser() ) {
- $wgOut->addWikiMsg( 'nonunicodebrowser' );
+ $out->addWikiMsg( 'nonunicodebrowser' );
}
if ( $this->section != 'new' ) {
if ( $revision ) {
// Let sysop know that this will make private content public if saved
- if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
- $wgOut->wrapWikiMsg(
+ if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
+ $out->wrapWikiMsg(
"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
'rev-deleted-text-permission'
);
} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
'rev-deleted-text-view'
);
if ( !$revision->isCurrent() ) {
$this->mArticle->setOldSubtitle( $revision->getId() );
- $wgOut->addWikiMsg( 'editingold' );
+ $out->addWikiMsg( 'editingold' );
}
} elseif ( $this->mTitle->exists() ) {
// Something went wrong
- $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
+ $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
[ 'missing-revision', $this->oldid ] );
}
}
}
if ( wfReadOnly() ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
"<div id=\"mw-read-only-warning\">\n$1\n</div>",
[ 'readonlywarning', wfReadOnlyReason() ]
);
- } elseif ( $wgUser->isAnon() ) {
+ } elseif ( $user->isAnon() ) {
if ( $this->formtype != 'preview' ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
"<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
[ 'anoneditwarning',
// Log-in link
]
);
} else {
- $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
+ $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
'anonpreviewwarning'
);
}
if ( $this->isCssJsSubpage ) {
# Check the skin exists
if ( $this->isWrongCaseCssJsPage ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
"<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
[ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
);
}
- if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
+ if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
+ $out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
+ $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
+ );
if ( $this->formtype !== 'preview' ) {
if ( $this->isCssSubpage && $wgAllowUserCss ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
[ 'usercssyoucanpreview' ]
);
}
if ( $this->isJsSubpage && $wgAllowUserJs ) {
- $wgOut->wrapWikiMsg(
+ $out->wrapWikiMsg(
"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
[ 'userjsyoucanpreview' ]
);
# Then it must be protected based on static groups (regular)
$noticeMsg = 'protectedpagewarning';
}
- LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
+ LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
[ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
}
if ( $this->mTitle->isCascadeProtected() ) {
}
}
$notice .= '</div>';
- $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
+ $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
}
if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
- LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
+ LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
[ 'lim' => 1,
'showIfEmpty' => false,
'msgKey' => [ 'titleprotectedwarning' ],
$this->contentLength = strlen( $this->textbox1 );
}
+ $lang = $this->context->getLanguage();
if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
- $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
+ $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
[
'longpageerror',
- $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
- $wgLang->formatNum( $wgMaxArticleSize )
+ $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
+ $lang->formatNum( $wgMaxArticleSize )
]
);
} else {
if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
- $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
+ $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
[
'longpage-hint',
- $wgLang->formatSize( strlen( $this->textbox1 ) ),
+ $lang->formatSize( strlen( $this->textbox1 ) ),
strlen( $this->textbox1 )
]
);
* @param string $summary The text of the summary to display
*/
protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
- global $wgOut;
# Add a class if 'missingsummary' is triggered to allow styling of the summary line
$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
if ( $isSubjectPreview ) {
[ 'class' => $summaryClass ],
[]
);
- $wgOut->addHTML( "{$label} {$input}" );
+ $this->context->getOutput()->addHTML( "{$label} {$input}" );
}
/**
}
protected function showFormBeforeText() {
- global $wgOut;
$section = htmlspecialchars( $this->section );
- $wgOut->addHTML( <<<HTML
+ $out = $this->context->getOutput();
+ $out->addHTML( <<<HTML
<input type='hidden' value="{$section}" name="wpSection"/>
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
HTML
);
if ( !$this->checkUnicodeCompliantBrowser() ) {
- $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
+ $out->addHTML( Html::hidden( 'safemode', '1' ) );
}
}
protected function showFormAfterText() {
- global $wgOut, $wgUser;
/**
* To make it harder for someone to slip a user a page
* which submits an edit form to the wiki without their
* include the constant suffix to prevent editing from
* broken text-mangling proxies.
*/
- $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
+ $token = $this->context->getUser()->getEditToken();
+ $this->context->getOutput()->addHTML(
+ "\n" . Html::hidden( "wpEditToken", $token ) . "\n"
+ );
}
/**
}
protected function showTextbox( $text, $name, $customAttribs = [] ) {
- global $wgOut, $wgUser;
-
$wikitext = $this->safeUnicodeOutput( $text );
if ( strval( $wikitext ) !== '' ) {
// Ensure there's a newline at the end, otherwise adding lines
$wikitext .= "\n";
}
+ $user = $this->context->getUser();
$attribs = $customAttribs + [
'accesskey' => ',',
'id' => $name,
- 'cols' => $wgUser->getIntOption( 'cols' ),
- 'rows' => $wgUser->getIntOption( 'rows' ),
+ 'cols' => $user->getIntOption( 'cols' ),
+ 'rows' => $user->getIntOption( 'rows' ),
// Avoid PHP notices when appending preferences
// (appending allows customAttribs['style'] to still work).
'style' => ''
$attribs['lang'] = $pageLang->getHtmlCode();
$attribs['dir'] = $pageLang->getDir();
- $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
+ $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
}
protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
- global $wgOut;
$classes = [];
if ( $isOnTop ) {
$classes[] = 'ontop';
$attribs['style'] = 'display: none;';
}
- $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
+ $out = $this->context->getOutput();
+ $out->addHTML( Xml::openElement( 'div', $attribs ) );
if ( $this->formtype == 'preview' ) {
$this->showPreview( $previewOutput );
$pageViewLang = $this->mTitle->getPageViewLanguage();
$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
'class' => 'mw-content-' . $pageViewLang->getDir() ];
- $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
+ $out->addHTML( Html::rawElement( 'div', $attribs ) );
}
- $wgOut->addHTML( '</div>' );
+ $out->addHTML( '</div>' );
if ( $this->formtype == 'diff' ) {
try {
$this->contentFormat,
$ex->getMessage()
);
- $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+ $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
}
}
}
* @param string $text The HTML to be output for the preview.
*/
protected function showPreview( $text ) {
- global $wgOut;
if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
$this->mArticle->openShowCategory();
}
+ $out = $this->context->getOutput();
# This hook seems slightly odd here, but makes things more
# consistent for extensions.
- Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
- $wgOut->addHTML( $text );
+ Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
+ $out->addHTML( $text );
if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
$this->mArticle->closeShowCategory();
}
* save and then make a comparison.
*/
function showDiff() {
- global $wgUser, $wgContLang, $wgOut;
+ global $wgContLang;
$oldtitlemsg = 'currentrev';
# if message does not exist, show diff against the preloaded default
ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
- $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
- $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
+ $user = $this->context->getUser();
+ $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
+ $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
}
if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
$difftext = '';
}
- $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
+ $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
}
/**
protected function showHeaderCopyrightWarning() {
$msg = 'editpage-head-copy-warn';
if ( !wfMessage( $msg )->isDisabled() ) {
- global $wgOut;
- $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
+ $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
'editpage-head-copy-warn' );
}
}
$msg = 'editpage-tos-summary';
Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
if ( !wfMessage( $msg )->isDisabled() ) {
- global $wgOut;
- $wgOut->addHTML( '<div class="mw-tos-summary">' );
- $wgOut->addWikiMsg( $msg );
- $wgOut->addHTML( '</div>' );
+ $out = $this->context->getOutput();
+ $out->addHTML( '<div class="mw-tos-summary">' );
+ $out->addWikiMsg( $msg );
+ $out->addHTML( '</div>' );
}
}
protected function showEditTools() {
- global $wgOut;
- $wgOut->addHTML( '<div class="mw-editTools">' .
+ $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
wfMessage( 'edittools' )->inContentLanguage()->parse() .
'</div>' );
}
}
protected function showStandardInputs( &$tabindex = 2 ) {
- global $wgOut;
- $wgOut->addHTML( "<div class='editOptions'>\n" );
+ $out = $this->context->getOutput();
+ $out->addHTML( "<div class='editOptions'>\n" );
if ( $this->section != 'new' ) {
$this->showSummaryInput( false, $this->summary );
- $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
+ $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
}
$checkboxes = $this->getCheckboxes( $tabindex,
[ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
- $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
+ $out->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
// Show copyright warning.
- $wgOut->addWikiText( $this->getCopywarn() );
- $wgOut->addHTML( $this->editFormTextAfterWarn );
+ $out->addWikiText( $this->getCopywarn() );
+ $out->addHTML( $this->editFormTextAfterWarn );
- $wgOut->addHTML( "<div class='editButtons'>\n" );
- $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
+ $out->addHTML( "<div class='editButtons'>\n" );
+ $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
$cancel = $this->getCancelLink();
if ( $cancel !== '' ) {
wfMessage( 'word-separator' )->escaped() .
wfMessage( 'newwindow' )->parse();
- $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
- $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
- $wgOut->addHTML( "</div><!-- editButtons -->\n" );
+ $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
+ $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
+ $out->addHTML( "</div><!-- editButtons -->\n" );
- Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
+ Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
- $wgOut->addHTML( "</div><!-- editOptions -->\n" );
+ $out->addHTML( "</div><!-- editOptions -->\n" );
}
/**
* If you want to use another entry point to this function, be careful.
*/
protected function showConflict() {
- global $wgOut;
-
- if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
- $stats = $wgOut->getContext()->getStats();
+ $out = $this->context->getOutput();
+ if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$out ] ) ) {
+ $stats = $this->context->getStats();
$stats->increment( 'edit.failures.conflict' );
// Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
if (
$stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
}
- $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+ $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
$content1 = $this->toEditContent( $this->textbox1 );
$content2 = $this->toEditContent( $this->textbox2 );
wfMessage( 'storedversion' )->text()
);
- $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+ $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
$this->showTextbox2();
}
}
* @return string
*/
function getPreviewText() {
- global $wgOut, $wgRawHtml, $wgLang;
- global $wgAllowUserCss, $wgAllowUserJs;
+ global $wgRawHtml, $wgAllowUserCss, $wgAllowUserJs;
- $stats = $wgOut->getContext()->getStats();
+ $stats = $this->context->getStats();
+ $out = $this->context->getOutput();
if ( $wgRawHtml && !$this->mTokenOk ) {
// Could be an offsite preview attempt. This is very unsafe if
// Do not put big scary notice, if previewing the empty
// string, which happens when you initially edit
// a category page, due to automatic preview-on-open.
- $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
+ $parsedNote = $out->parse( "<div class='previewnote'>" .
wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
}
$stats->increment( 'edit.failures.session_loss' );
# provide a anchor link to the editform
$continueEditing = '<span class="mw-continue-editing">' .
- '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
+ '[[#' . self::EDITFORM_ID . '|' . $this->context->getLanguage()->getArrow() . ' ' .
wfMessage( 'continue-editing' )->text() . ']]</span>';
if ( $this->mTriedSave && !$this->mTokenOk ) {
if ( $this->mTokenOkExceptSuffix ) {
$parserOutput = $parserResult['parserOutput'];
$previewHTML = $parserResult['html'];
$this->mParserOutput = $parserOutput;
- $wgOut->addParserOutputMetadata( $parserOutput );
+ $out->addParserOutputMetadata( $parserOutput );
if ( count( $parserOutput->getWarnings() ) ) {
$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
$previewhead = "<div class='previewnote'>\n" .
'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
- $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
+ $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
$pageViewLang = $this->mTitle->getPageViewLanguage();
$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
* @return ParserOptions
*/
protected function getPreviewParserOptions() {
- $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
+ $parserOptions = $this->page->makeParserOptions( $this->context );
$parserOptions->setIsPreview( true );
$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
$parserOptions->enableLimitReport();
* Parse the page for a preview. Subclasses may override this class, in order
* to parse with different options, or to otherwise modify the preview HTML.
*
- * @param Content @content The page content
- * @return Associative array with keys:
+ * @param Content $content The page content
+ * @return array with keys:
* - parserOutput: The ParserOutput object
* - html: The HTML to be displayed
*/
protected function doPreviewParse( Content $content ) {
- global $wgUser;
+ $user = $this->context->getUser();
$parserOptions = $this->getPreviewParserOptions();
- $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+ $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
$scopedCallback = $parserOptions->setupFakeRevision(
- $this->mTitle, $pstContent, $wgUser );
+ $this->mTitle, $pstContent, $user );
$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
ScopedCallback::consume( $scopedCallback );
$parserOutput->setEditSectionTokens( false ); // no section edit links
* @return array
*/
public function getCheckboxes( &$tabindex, $checked ) {
- global $wgUser, $wgUseMediaWikiUIEverywhere;
+ global $wgUseMediaWikiUIEverywhere;
$checkboxes = [];
+ $user = $this->context->getUser();
// don't show the minor edit checkbox if it's a new page or section
if ( !$this->isNew ) {
$checkboxes['minor'] = '';
$minorLabel = wfMessage( 'minoredit' )->parse();
- if ( $wgUser->isAllowed( 'minoredit' ) ) {
+ if ( $user->isAllowed( 'minoredit' ) ) {
$attribs = [
'tabindex' => ++$tabindex,
'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
$watchLabel = wfMessage( 'watchthis' )->parse();
$checkboxes['watch'] = '';
- if ( $wgUser->isLoggedIn() ) {
+ if ( $user->isLoggedIn() ) {
$attribs = [
'tabindex' => ++$tabindex,
'accesskey' => wfMessage( 'accesskey-watch' )->text(),
public function getEditButtons( &$tabindex ) {
$buttons = [];
+ $labelAsPublish = $this->mArticle->getContext()->getConfig()->get( 'EditButtonPublishNotSave' );
+ if ( $labelAsPublish ) {
+ $buttonLabelKey = $this->isNew ? 'publishpage' : 'publishchanges';
+ } else {
+ $buttonLabelKey = $this->isNew ? 'savearticle' : 'savechanges';
+ }
+ $buttonLabel = wfMessage( $buttonLabelKey )->text();
$attribs = [
'id' => 'wpSave',
'name' => 'wpSave',
'tabindex' => ++$tabindex,
] + Linker::tooltipAndAccesskeyAttribs( 'save' );
- $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
- $attribs, [ 'mw-ui-constructive' ] );
+ $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-constructive' ] );
++$tabindex; // use the same for preview and live preview
$attribs = [
* they have attempted to edit a nonexistent section.
*/
function noSuchSectionPage() {
- global $wgOut;
-
- $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
+ $out = $this->context->getOutput();
+ $out->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
$res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
- $wgOut->addHTML( $res );
+ $out->addHTML( $res );
- $wgOut->returnToMain( false, $this->mTitle );
+ $out->returnToMain( false, $this->mTitle );
}
/**
* @param string|array|bool $match Text (or array of texts) which triggered one or more filters
*/
public function spamPageWithContent( $match = false ) {
- global $wgOut, $wgLang;
$this->textbox2 = $this->textbox1;
if ( is_array( $match ) ) {
- $match = $wgLang->listToText( $match );
+ $match = $this->context->getLanguage()->listToText( $match );
}
- $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
+ $out = $this->context->getOutput();
+ $out->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
- $wgOut->addHTML( '<div id="spamprotected">' );
- $wgOut->addWikiMsg( 'spamprotectiontext' );
+ $out->addHTML( '<div id="spamprotected">' );
+ $out->addWikiMsg( 'spamprotectiontext' );
if ( $match ) {
- $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
+ $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
}
- $wgOut->addHTML( '</div>' );
+ $out->addHTML( '</div>' );
- $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+ $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
$this->showDiff();
- $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+ $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
$this->showTextbox2();
- $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
+ $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
}
/**
* @return bool
*/
private function checkUnicodeCompliantBrowser() {
- global $wgBrowserBlackList, $wgRequest;
+ global $wgBrowserBlackList;
- $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
+ $currentbrowser = $this->context->getRequest()->getHeader( 'User-Agent' );
if ( $currentbrowser === false ) {
// No User-Agent header sent? Trust it by default...
return true;
);
$page = WikiPage::factory( $title );
$dbw = wfGetDB( DB_MASTER );
- try {
- $dbw->startAtomic( __METHOD__ );
- // delete the associated article first
- $error = '';
- $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user );
- // doDeleteArticleReal() returns a non-fatal error status if the page
- // or revision is missing, so check for isOK() rather than isGood()
- if ( $deleteStatus->isOK() ) {
- $status = $file->delete( $reason, $suppress, $user );
- if ( $status->isOK() ) {
- $status->value = $deleteStatus->value; // log id
- $dbw->endAtomic( __METHOD__ );
- } else {
- // Page deleted but file still there? rollback page delete
- wfGetLBFactory()->rollbackMasterChanges( __METHOD__ );
- }
- } else {
- // Done; nothing changed
+ $dbw->startAtomic( __METHOD__ );
+ // delete the associated article first
+ $error = '';
+ $deleteStatus = $page->doDeleteArticleReal( $reason, $suppress, 0, false, $error, $user );
+ // doDeleteArticleReal() returns a non-fatal error status if the page
+ // or revision is missing, so check for isOK() rather than isGood()
+ if ( $deleteStatus->isOK() ) {
+ $status = $file->delete( $reason, $suppress, $user );
+ if ( $status->isOK() ) {
+ $status->value = $deleteStatus->value; // log id
$dbw->endAtomic( __METHOD__ );
+ } else {
+ // Page deleted but file still there? rollback page delete
+ wfGetLBFactory()->rollbackMasterChanges( __METHOD__ );
}
- } catch ( Exception $e ) {
- // Rollback before returning to prevent UI from displaying
- // incorrect "View or restore N deleted edits?"
- $dbw->rollback( __METHOD__ );
- throw $e;
+ } else {
+ // Done; nothing changed
+ $dbw->endAtomic( __METHOD__ );
}
}
* @return string Raw HTML
*/
public static function inlineStyle( $contents, $media = 'all' ) {
+ // Don't escape '>' since that is used
+ // as direct child selector.
+ // Remember, in css, there is no "x" for hexadecimal escapes, and
+ // the space immediately after an escape sequence is swallowed.
+ $contents = strtr( $contents, [
+ '<' => '\3C ',
+ // CDATA end tag for good measure, but the main security
+ // is from escaping the '<'.
+ ']]>' => '\5D\5D\3E '
+ ] );
+
if ( preg_match( '/[<&]/', $contents ) ) {
$contents = "/*<![CDATA[*/$contents/*]]>*/";
}
// may still be a wikipage redirect to another article or URL.
$article = $this->initializeArticle();
if ( is_object( $article ) ) {
+ $url = $request->getFullRequestURL(); // requested URL
+ if (
+ $request->getMethod() === 'GET' &&
+ $url === $article->getTitle()->getCanonicalURL() &&
+ $article->checkTouched() &&
+ $output->checkLastModified( $article->getTouched() )
+ ) {
+ wfDebug( __METHOD__ . ": done 304\n" );
+ return;
+ }
$this->performAction( $article, $requestTitle );
} elseif ( is_string( $article ) ) {
$output->redirect( $article );
*/
public function triggerJobs() {
$jobRunRate = $this->config->get( 'JobRunRate' );
- if ( $jobRunRate <= 0 || wfReadOnly() ) {
- return;
- } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
+ if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
return; // recursion guard
+ } elseif ( $jobRunRate <= 0 || wfReadOnly() ) {
+ return;
}
if ( $jobRunRate < 1 ) {
$query, $this->config->get( 'SecretKey' ) );
$errno = $errstr = null;
- $info = wfParseUrl( $this->config->get( 'Server' ) );
+ $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
MediaWiki\suppressWarnings();
$host = $info['host'];
$port = 80;
return;
}
- $url = wfAppendQuery( wfScript( 'index' ), $query );
+ $special = SpecialPageFactory::getPage( 'RunJobs' );
+ $url = $special->getPageTitle()->getCanonicalURL( $query );
$req = (
"POST $url HTTP/1.1\r\n" .
"Host: {$info['host']}\r\n" .
$runJobsLogger->info( "Running $n job(s) via '$url'" );
// Send a cron API request to be performed in the background.
// Give up if this takes too long to send (which should be rare).
- stream_set_timeout( $sock, 1 );
+ stream_set_timeout( $sock, 2 );
$bytes = fwrite( $sock, $req );
if ( $bytes !== strlen( $req ) ) {
$runJobsLogger->error( "Failed to start cron API (socket write error)" );
use SkinFactory;
use TitleFormatter;
use TitleParser;
+use VirtualRESTServiceClient;
use MediaWiki\Interwiki\InterwikiLookup;
/**
return $this->getService( 'TitleParser' );
}
+ /**
+ * @since 1.28
+ * @return VirtualRESTServiceClient
+ */
+ public function getVirtualRESTServiceClient() {
+ return $this->getService( 'VirtualRESTServiceClient' );
+ }
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service getter here, don't forget to add a test
// case for it in MediaWikiServicesTest::provideGetters() and in
if ( $protected ) {
# Protect the redirect title as the title used to be...
- $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
- [
- 'pr_page' => $redirid,
- 'pr_type' => 'pr_type',
- 'pr_level' => 'pr_level',
- 'pr_cascade' => 'pr_cascade',
- 'pr_user' => 'pr_user',
- 'pr_expiry' => 'pr_expiry'
- ],
+ $res = $dbw->select(
+ 'page_restrictions',
+ '*',
[ 'pr_page' => $pageid ],
__METHOD__,
- [ 'IGNORE' ]
+ 'FOR UPDATE'
);
+ $rowsInsert = [];
+ foreach ( $res as $row ) {
+ $rowsInsert[] = [
+ 'pr_page' => $redirid,
+ 'pr_type' => $row->pr_type,
+ 'pr_level' => $row->pr_level,
+ 'pr_cascade' => $row->pr_cascade,
+ 'pr_user' => $row->pr_user,
+ 'pr_expiry' => $row->pr_expiry
+ ];
+ }
+ $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
// Build comment for log
$comment = wfMessage(
private function isUserJsPreview() {
return $this->getConfig()->get( 'AllowUserJs' )
- && $this->getUser()->isLoggedIn()
&& $this->getTitle()
&& $this->getTitle()->isJsSubpage()
&& $this->userCanPreview();
private function isUserCssPreview() {
return $this->getConfig()->get( 'AllowUserCss' )
- && $this->getUser()->isLoggedIn()
&& $this->getTitle()
&& $this->getTitle()->isCssSubpage()
&& $this->userCanPreview();
}
$user = $this->getUser();
+
+ if ( !$user->isLoggedIn() ) {
+ // Anons have predictable edit tokens
+ return false;
+ }
if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
return false;
}
*
* This is public so we can display it in the installer
*
+ * Developers: If you're adding a new piece of data to this, please ensure
+ * that you update https://www.mediawiki.org/wiki/Manual:$wgPingback
+ *
* @return array
*/
public function getSystemInfo() {
return $services->getService( '_MediaWikiTitleCodec' );
},
+ 'VirtualRESTServiceClient' => function( MediaWikiServices $services ) {
+ $config = $services->getMainConfig()->get( 'VirtualRestConfig' );
+
+ $vrsClient = new VirtualRESTServiceClient( new MultiHttpClient( [] ) );
+ foreach ( $config['paths'] as $prefix => $serviceConfig ) {
+ $class = $serviceConfig['class'];
+ // Merge in the global defaults
+ $constructArg = isset( $serviceConfig['options'] )
+ ? $serviceConfig['options']
+ : [];
+ $constructArg += $config['global'];
+ // Make the VRS service available at the mount point
+ $vrsClient->mount( $prefix, [ 'class' => $class, 'config' => $constructArg ] );
+ }
+
+ return $vrsClient;
+ },
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service here, don't forget to add a getter function
// in the MediaWikiServices class. The convenience getter should just call
* @return array List of errors
*/
private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
+ global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
// Account creation blocks handled at userlogin.
// Unblocking handled in SpecialUnblock
if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
return $errors;
}
- global $wgEmailConfirmToEdit;
+ // Optimize for a very common case
+ if ( $action === 'read' && !$wgBlockDisablesLogin ) {
+ return $errors;
+ }
if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
$errors[] = [ 'confirmedittext' ];
$checks = [
'checkPermissionHooks',
'checkReadPermissions',
+ 'checkUserBlock', // for wgBlockDisablesLogin
];
# Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
# here as it will lead to duplicate error messages. This is okay to do
/** @var Content $pstContent */
private $pstContent = null;
+ private function checkReadPermissions( Title $title ) {
+ if ( !$title->userCan( 'read', $this->getUser() ) ) {
+ $this->dieUsage( "You don't have permission to view this page", 'permissiondenied' );
+ }
+ }
+
public function execute() {
// The data is hot but user-dependent, like page views, so we set vary cookies
$this->getMain()->setCacheMode( 'anon-public-user-private' );
if ( !$rev ) {
$this->dieUsage( "There is no revision ID $oldid", 'missingrev' );
}
+
+ $this->checkReadPermissions( $rev->getTitle() );
if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
$this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' );
}
if ( !$titleObj || !$titleObj->exists() ) {
$this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
}
+
+ $this->checkReadPermissions( $titleObj );
$wgTitle = $titleObj;
if ( isset( $prop['revid'] ) ) {
],
'attachedwiki' => null,
'users' => [
- ApiBase::PARAM_TYPE => 'user',
ApiBase::PARAM_ISMULTI => true
],
'token' => [
$code = $overrideCode;
}
if ( $moreExtraData ) {
+ $extraData = $extraData ?: [];
$extraData += $moreExtraData;
}
$this->dieUsage( $msg, $code, 0, $extraData );
]
},
"apihelp-main-param-action": "Kurį veiksmą atlikti.",
+ "apihelp-main-param-curtimestamp": "Prie rezultato pridėti dabartinę laiko žymę.",
"apihelp-block-description": "Blokuoti vartotoją.",
"apihelp-block-param-reason": "Blokavimo priežastis.",
"apihelp-block-param-nocreate": "Neleisti kurti paskyrų.",
"apihelp-edit-param-text": "Puslapio turinys.",
"apihelp-edit-param-minor": "Smulkus pakeitimas.",
"apihelp-edit-param-notminor": "Nesmulkus pakeitimas.",
+ "apihelp-edit-param-createonly": "Neredaguoti puslapio jei jis jau egzistuoja.",
+ "apihelp-edit-param-watch": "Pridėti puslapį į dabartinio vartotojo stebimųjų sąrašą.",
+ "apihelp-edit-param-unwatch": "Pašalinti puslapį iš dabartinio vartotojo stebimųjų sąrašo.",
"apihelp-edit-param-redirect": "Automatiškai išspręsti peradresavimus.",
"apihelp-edit-example-edit": "Redaguoti puslapį.",
"apihelp-emailuser-description": "Siųsti el. laišką naudotojui.",
+ "apihelp-emailuser-param-target": "El. laiško gavėjas.",
"apihelp-expandtemplates-param-title": "Puslapio pavadinimas.",
+ "apihelp-feedcontributions-param-feedformat": "Srauto formatas.",
+ "apihelp-feedcontributions-param-year": "Nuo metų (ir anksčiau).",
+ "apihelp-feedcontributions-param-month": "Nuo mėnesio (ir anksčiau).",
+ "apihelp-feedcontributions-param-tagfilter": "Filtruoti įnašus, kurie turi šias žymes.",
+ "apihelp-feedcontributions-param-deletedonly": "Rodyti tik ištrintus įnašus.",
+ "apihelp-feedcontributions-param-hideminor": "Slėpti nedidelius pakeitimus.",
+ "apihelp-feedrecentchanges-param-feedformat": "Srauto formatas.",
+ "apihelp-feedrecentchanges-param-from": "Rodyti pakeitimus nuo tada.",
"apihelp-feedrecentchanges-param-hideminor": "Slėpti smulkius pakeitimus.",
"apihelp-feedrecentchanges-param-hidebots": "Slėpti robotų pakeitimus.",
"apihelp-feedrecentchanges-param-hideanons": "Slėpti vartotojų anonimų pakeitimus.",
"apihelp-feedrecentchanges-param-hideliu": "Slėpti užsiregistravusių vartotojų pakeitimus.",
"apihelp-feedrecentchanges-param-hidemyself": "Slėpti pakeitimus, atliktus dabartinio vartotojo.",
"apihelp-feedrecentchanges-param-tagfilter": "Filtruoti pagal žymę.",
+ "apihelp-feedrecentchanges-param-target": "Rodyti tik keitimus puslapiuose, pasiekiamuose iš šio puslapio.",
"apihelp-feedrecentchanges-example-simple": "Parodyti naujausius keitimus.",
+ "apihelp-feedwatchlist-param-feedformat": "Srauto formatas.",
"apihelp-filerevert-param-comment": "Įkėlimo komentaras.",
+ "apihelp-help-example-recursive": "Visa pagalba viename puslapyje.",
+ "apihelp-help-example-help": "Pačio pagalbos modulio pagalba.",
+ "apihelp-imagerotate-description": "Pasukti viena ar daugiau paveikslėlių.",
+ "apihelp-imagerotate-param-rotation": "Kiek laipsnių pasukti paveikslėlį pagal laikrodžio rodyklę.",
+ "apihelp-imagerotate-example-generator": "Pasukti visus paveikslėlius <kbd>Category:Flip</kbd> <kbd>180</kbd> laipsnių.",
"apihelp-import-param-xml": "XML failas įkeltas.",
"apihelp-login-param-name": "Vartotojo vardas.",
"apihelp-login-param-password": "Slaptažodis.",
"apihelp-login-param-domain": "Domenas (neprivaloma).",
+ "apihelp-login-example-login": "Prisijungti.",
+ "apihelp-logout-description": "Atsijungti ir išvalyti sesijos duomenis.",
+ "apihelp-logout-example-logout": "Atjungti dabartinė vartotoją.",
+ "apihelp-managetags-example-delete": "Ištrinti <kbd>vandlaism</kbd> žymę su priežastimi <kbd>Misspelt</kbd>",
+ "apihelp-managetags-example-activate": "Aktyvuoti žymę pavadinimu <kbd>spam</kbd> su priežastimi <kbd>For use in edit patrolling</kbd>",
+ "apihelp-managetags-example-deactivate": "Išjungti žymę pavadinimu <kbd>spam</kbd> su priežastimi <kbd>No longer required</kbd>",
+ "apihelp-mergehistory-description": "Sujungti puslapio istorijas.",
+ "apihelp-mergehistory-param-reason": "Istorijos sujungimo priežastis.",
+ "apihelp-mergehistory-example-merge": "Sujungti visą <kbd>Oldpage</kbd> istoriją į <kbd>Newpage</kbd>.",
+ "apihelp-move-description": "Perkelti puslapį.",
+ "apihelp-move-param-to": "Pavadinimas, į kuri pervadinamas puslapis.",
+ "apihelp-move-param-reason": "Pervadinimo priežastis.",
+ "apihelp-move-param-movetalk": "Pervadinti aptarimo puslapį, jei jis egzistuoja.",
+ "apihelp-move-param-noredirect": "Nekurti nukreipimo.",
+ "apihelp-move-param-watch": "Pridėti puslapį ir nukreipimą į dabartinio vartotojo stebimųjų sąrašą.",
+ "apihelp-move-param-unwatch": "Pašalinti puslapį ir nukreipimą iš dabartinio vartotojo stebimųjų sąrašo.",
+ "apihelp-move-param-ignorewarnings": "Ignuoruoti bet kokius įspėjimus.",
+ "apihelp-move-example-move": "Perkelti <kbd>Badtitle</kbd> į <kbd>Goodtitle</kbd> nepaliekant nukreipimo.",
+ "apihelp-opensearch-description": "Ieškoti viki naudojant OpenSearch protokolą.",
+ "apihelp-opensearch-param-limit": "Maksimalus grąžinamas rezultatų skaičius.",
+ "apihelp-opensearch-example-te": "Rasti puslapius prasidedančius su <kbd>Te</kbd>.",
+ "apihelp-options-example-reset": "Nustatyti visus pageidavimus iš naujo.",
+ "apihelp-options-example-change": "Keisti <kbd>skin</kbd> ir <kbd>hideminor</kbd> pageidavimus.",
+ "apihelp-options-example-complex": "Nustatyti visus pageidavimus iš naujo, tada nustatyti <kbd>skin</kbd> ir <kbd>nickname</kbd>.",
+ "apihelp-paraminfo-description": "Gauti informaciją apie API modulius.",
+ "apihelp-protect-example-protect": "Apsaugoti puslapį.",
+ "apihelp-query+allcategories-param-dir": "Rūšiavimo kryptis.",
+ "apihelp-query+allcategories-param-min": "Gražinti tik kategorijas, kuriuose yra bent tiek narių.",
+ "apihelp-query+allcategories-param-max": "Gražinti tik kategorijas, kuriuose yra iki tiek narių.",
+ "apihelp-query+allcategories-param-limit": "Kiek kategorijų gražinti.",
+ "apihelp-query+allcategories-paramvalue-prop-size": "Prideda puslapių kategorijoje skaičių.",
"apihelp-query+alldeletedrevisions-example-user": "Sąrašas paskutinių 50 ištrintų indėlių pagal vartotoją\n<kbd>Pavyzdys</kbd>.",
+ "apihelp-query+alllinks-paramvalue-prop-title": "Prideda nuorodos pavadinimą.",
+ "apihelp-query+alllinks-param-limit": "Kiek objektų iš viso gražinti.",
+ "apihelp-query+allmessages-param-lang": "Gražinti pranešimus šia kalba.",
"apihelp-query+allrevisions-param-namespace": "Rodyti puslapius tik šioje vardų srityje.",
"apihelp-query+backlinks-example-simple": "Rodyti nuorodas <kbd>Pagrindinis puslapis</kbd>.",
+ "apihelp-query+blocks-paramvalue-prop-id": "Prideda bloko ID.",
+ "apihelp-query+blocks-paramvalue-prop-user": "Prideda užblokuoto vartotojo vardą.",
+ "apihelp-query+blocks-paramvalue-prop-userid": "Prideda užblokuoto vartotojo ID.",
+ "apihelp-query+blocks-paramvalue-prop-by": "Prideda užblokuoto vartotojo vardą.",
+ "apihelp-query+blocks-paramvalue-prop-byid": "Prideda užblokuoto vartotojo ID.",
+ "apihelp-query+blocks-paramvalue-prop-timestamp": "Prideda blokavimo laiko žymę.",
+ "apihelp-query+blocks-paramvalue-prop-expiry": "Prideda blokavimo pabaigos laiko žymes.",
+ "apihelp-query+blocks-paramvalue-prop-reason": "Prideda blokavimo priežastį.",
+ "apihelp-query+blocks-paramvalue-prop-range": "Prideda blokavimo paveiktų IP adresų diapazoną.",
"apihelp-query+watchlist-paramvalue-type-external": "Išoriniai keitimai.",
"apihelp-query+watchlist-paramvalue-type-new": "Puslapio sukūrimai.",
"apihelp-stashedit-param-title": "Puslapio pavadinimas buvo redaguotas.",
switch ( $collationName ) {
case 'uppercase':
return new UppercaseCollation;
+ case 'numeric':
+ return new NumericUppercaseCollation;
case 'identity':
return new IdentityCollation;
case 'uca-default':
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Collation that orders text with numbers "naturally", so that 'Foo 1' < 'Foo 2' < 'Foo 12'.
+ *
+ * Note that this only works in terms of sequences of digits, and the behavior for decimal fractions
+ * or pretty-formatted numbers may be unexpected.
+ *
+ * @since 1.28
+ */
+class NumericUppercaseCollation extends UppercaseCollation {
+ public function getSortKey( $string ) {
+ $sortkey = parent::getSortKey( $string );
+
+ // For each sequence of digits, insert the digit '0' and then the length of the sequence
+ // (encoded in two bytes) before it. That's all folks, it sorts correctly now! The '0' ensures
+ // correct position (where digits would normally sort), then the length will be compared putting
+ // shorter numbers before longer ones; if identical, then the characters will be compared, which
+ // generates the correct results for numbers of equal length.
+ $sortkey = preg_replace_callback( '/\d+/', function ( $matches ) {
+ $len = strlen( $matches[0] );
+ // This allows sequences of up to 65536 numeric characters to be handled correctly. One byte
+ // would allow only for 256, which doesn't feel future-proof.
+ $prefix = chr( floor( $len / 256 ) ) . chr( $len % 256 );
+ return '0' . $prefix . $matches[0];
+ }, $sortkey );
+
+ return $sortkey;
+ }
+
+ public function getFirstLetter( $string ) {
+ if ( preg_match( '/^\d/', $string ) ) {
+ // Note that we pass 0 and 9 as normal params, not numParams(). This only works for 0-9
+ // and not localised digits, so we don't want them to be converted.
+ return wfMessage( 'category-header-numerals' )->params( 0, 9 )->text();
+ } else {
+ return parent::getFirstLetter( $string );
+ }
+ }
+}
return $this;
}
- return new static( $this->beautifyJSON() );
+ return new static( self::normalizeLineEndings( $this->beautifyJSON() ) );
}
/**
}
}
+ /**
+ * Do a "\r\n" -> "\n" and "\r" -> "\n" transformation
+ * as well as trim trailing whitespace
+ *
+ * This was formerly part of Parser::preSaveTransform, but
+ * for non-wikitext content models they probably still want
+ * to normalize line endings without all of the other PST
+ * changes.
+ *
+ * @since 1.28
+ * @param $text
+ * @return string
+ */
+ public static function normalizeLineEndings( $text ) {
+ return str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
+ }
+
/**
* Returns a Content object with pre-save transformations applied.
- * This implementation just trims trailing whitespace and normalizes newlines.
+ *
+ * At a minimum, subclasses should make sure to call TextContent::normalizeLineEndings()
+ * either directly or part of Parser::preSaveTransform().
*
* @param Title $title
* @param User $user
*/
public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
$text = $this->getNativeData();
- $pst = rtrim( $text );
- $pst = str_replace( [ "\r\n", "\r" ], "\n", $pst );
+ $pst = self::normalizeLineEndings( $text );
return ( $text === $pst ) ? $this : new static( $pst, $this->getModel() );
}
$text = $this->getNativeData();
$pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
- rtrim( $pst );
return ( $text === $pst ) ? $this : new static( $pst );
}
*/
public static function changePrefix( $prefix ) {
global $wgDBprefix;
- wfGetLBFactory()->forEachLB( [ 'CloneDatabase', 'changeLBPrefix' ], [ $prefix ] );
+ wfGetLBFactory()->forEachLB( function( $lb ) use ( $prefix ) {
+ $lb->forEachOpenConnection( function ( $db ) use ( $prefix ) {
+ $db->tablePrefix( $prefix );
+ } );
+ } );
$wgDBprefix = $prefix;
}
-
- /**
- * @param LoadBalancer $lb
- * @param string $prefix
- * @return void
- */
- public static function changeLBPrefix( $lb, $prefix ) {
- $lb->forEachOpenConnection( [ 'CloneDatabase', 'changeDBPrefix' ], [ $prefix ] );
- }
-
- /**
- * @param DatabaseBase $db
- * @param string $prefix
- * @return void
- */
- public static function changeDBPrefix( $db, $prefix ) {
- $db->tablePrefix( $prefix );
- }
}
abstract class DatabaseBase implements IDatabase {
/** Number of times to re-try an operation in case of deadlock */
const DEADLOCK_TRIES = 4;
-
/** Minimum time to wait before retry, in microseconds */
const DEADLOCK_DELAY_MIN = 500000;
-
/** Maximum time to wait before retry */
const DEADLOCK_DELAY_MAX = 1500000;
+ /** How long before it is worth doing a dummy query to test the connection */
+ const PING_TTL = 1.0;
+
+ /** @var string SQL query */
protected $mLastQuery = '';
+ /** @var bool */
protected $mDoneWrites = false;
+ /** @var string|bool */
protected $mPHPError = false;
-
- protected $mServer, $mUser, $mPassword, $mDBname;
+ /** @var string */
+ protected $mServer;
+ /** @var string */
+ protected $mUser;
+ /** @var string */
+ protected $mPassword;
+ /** @var string */
+ protected $mDBname;
/** @var BagOStuff APC cache */
protected $srvCache;
/** @var resource Database connection */
protected $mConn = null;
+ /** @var bool */
protected $mOpened = false;
/** @var array[] List of (callable, method name) */
/** @var bool Whether to suppress triggering of post-commit callbacks */
protected $suppressPostCommitCallbacks = false;
+ /** @var string */
protected $mTablePrefix;
+ /** @var string */
protected $mSchema;
+ /** @var integer */
protected $mFlags;
+ /** @var bool */
protected $mForeign;
+ /** @var array */
protected $mLBInfo = [];
+ /** @var bool|null */
protected $mDefaultBigSelects = null;
+ /** @var array|bool */
protected $mSchemaVars = false;
/** @var array */
protected $mSessionVars = [];
-
+ /** @var array|null */
protected $preparedArgs;
-
+ /** @var string|bool|null Stashed value of html_errors INI setting */
protected $htmlErrors;
-
+ /** @var string */
protected $delimiter = ';';
/**
*/
protected $allViews = null;
+ /** @var float UNIX timestamp */
+ protected $lastPing = 0.0;
+
/** @var TransactionProfiler */
protected $trxProfiler;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->mLastQuery = $sql;
- $isWriteQuery = $this->isWriteQuery( $sql );
- if ( $isWriteQuery ) {
+ $isWrite = $this->isWriteQuery( $sql );
+ if ( $isWrite ) {
$reason = $this->getReadOnlyReason();
if ( $reason !== false ) {
throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
# Keep track of whether the transaction has write queries pending
- if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) {
+ if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
$this->mTrxDoneWrites = true;
$this->getTransactionProfiler()->transactionWritingIn(
$this->mServer, $this->mDBname, $this->mTrxShortId );
}
- $isMaster = !is_null( $this->getLBInfo( 'master' ) );
- # generalizeSQL will probably cut down the query to reasonable
- # logging size most of the time. The substr is really just a sanity check.
- if ( $isMaster ) {
- $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query-master';
- } else {
- $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query';
- }
- # Include query transaction state
- $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
-
- $profiler = Profiler::instance();
- if ( !$profiler instanceof ProfilerStub ) {
- $totalProfSection = $profiler->scopedProfileIn( $totalProf );
- $queryProfSection = $profiler->scopedProfileIn( $queryProf );
- }
-
if ( $this->debug() ) {
wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
}
# Avoid fatals if close() was called
$this->assertOpen();
- # Do the query and handle errors
- $startTime = microtime( true );
- $ret = $this->doQuery( $commentedSql );
- $queryRuntime = microtime( true ) - $startTime;
- # Log the query time and feed it into the DB trx profiler
- $this->getTransactionProfiler()->recordQueryCompletion(
- $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
-
- MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+ # Send the query to the server
+ $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
# Try reconnecting if the connection was lost
if ( false === $ret && $this->wasErrorReissuable() ) {
$this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
} else {
# Should be safe to silently retry the query
- $startTime = microtime( true );
- $ret = $this->doQuery( $commentedSql );
- $queryRuntime = microtime( true ) - $startTime;
- # Log the query time and feed it into the DB trx profiler
- $this->getTransactionProfiler()->recordQueryCompletion(
- $queryProf, $startTime, $isWriteQuery, $this->affectedRows() );
+ $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
}
} else {
wfDebug( "Failed\n" );
$res = $this->resultObject( $ret );
- // Destroy profile sections in the opposite order to their creation
- ScopedCallback::consume( $queryProfSection );
- ScopedCallback::consume( $totalProfSection );
+ return $res;
+ }
- if ( $isWriteQuery && $this->mTrxLevel ) {
- $this->mTrxWriteDuration += $queryRuntime;
- $this->mTrxWriteCallers[] = $fname;
+ private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ # generalizeSQL() will probably cut down the query to reasonable
+ # logging size most of the time. The substr is really just a sanity check.
+ if ( $isMaster ) {
+ $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+ } else {
+ $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
}
- return $res;
+ # Include query transaction state
+ $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
+
+ $profiler = Profiler::instance();
+ if ( !( $profiler instanceof ProfilerStub ) ) {
+ $queryProfSection = $profiler->scopedProfileIn( $queryProf );
+ }
+
+ $startTime = microtime( true );
+ $ret = $this->doQuery( $commentedSql );
+ $queryRuntime = microtime( true ) - $startTime;
+
+ unset( $queryProfSection ); // profile out (if set)
+
+ if ( $ret !== false ) {
+ $this->lastPing = $startTime;
+ if ( $isWrite && $this->mTrxLevel ) {
+ $this->mTrxWriteDuration += $queryRuntime;
+ $this->mTrxWriteCallers[] = $fname;
+ }
+ }
+
+ $this->getTransactionProfiler()->recordQueryCompletion(
+ $queryProf, $startTime, $isWrite, $this->affectedRows()
+ );
+ MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+
+ return $ret;
}
private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
}
public function ping() {
+ if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
+ return true;
+ }
try {
// This will reconnect if possible, or error out if not
$this->query( "SELECT 1 AS ping", __METHOD__ );
* @return bool
*/
protected function reconnect() {
- # Stub. Not essential to override.
- return true;
+ $this->closeConnection();
+ $this->mOpened = false;
+ $this->mConn = false;
+ try {
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ $this->lastPing = microtime( true );
+ $ok = true;
+ } catch ( DBConnectionError $e ) {
+ $ok = false;
+ }
+
+ return $ok;
}
public function getSessionLagStatus() {
return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
}
- function reconnect() {
- $this->closeConnection();
- $this->mOpened = false;
- $this->mConn = false;
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
-
- return true;
- }
-
function getLag() {
if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
return $this->getLagFromPtHeartbeat();
public function approveMasterChanges( array $options ) {
$limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $limit ) {
- // If atomic section or explicit transactions are still open, some caller must have
+ // If atomic sections or explicit transactions are still open, some caller must have
// caught an exception but failed to properly rollback any changes. Detect that and
// throw and error (causing rollback).
if ( $conn->explicitTrxActive() ) {
wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
);
}
+ // If a connection sits idle while slow queries execute on another, that connection
+ // may end up dropped before the commit round is reached. Ping servers to detect this.
+ if ( $conn->writesOrCallbacksPending() && !$conn->ping() ) {
+ throw new DBTransactionError(
+ $conn,
+ "A connection to the {$conn->getDBname()} database was lost before commit."
+ );
+ }
} );
}
return $remaining;
}
}
-
-/**
- * Interface that marks a DataUpdate as enqueuable via the JobQueue
- *
- * Such updates must be representable using IJobSpecification, so that
- * they can be serialized into jobs and enqueued for later execution
- *
- * @since 1.27
- */
-interface EnqueueableDataUpdate {
- /**
- * @return array (wiki => wiki ID, job => IJobSpecification)
- */
- public function getAsJobSpecification();
-}
--- /dev/null
+<?php
+/**
+ * Interface that marks a DataUpdate as enqueuable via the JobQueue
+ *
+ * Such updates must be representable using IJobSpecification, so that
+ * they can be serialized into jobs and enqueued for later execution
+ *
+ * @since 1.27
+ */
+interface EnqueueableDataUpdate {
+ /**
+ * @return array (wiki => wiki ID, job => IJobSpecification)
+ */
+ public function getAsJobSpecification();
+}
* @param array $cats
*/
function invalidateCategories( $cats ) {
- $this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
+ PurgeJobUtils::invalidatePages( $this->mDb, NS_CATEGORY, array_keys( $cats ) );
}
/**
* @param array $images
*/
function invalidateImageDescriptions( $images ) {
- $this->invalidatePages( NS_FILE, array_keys( $images ) );
+ PurgeJobUtils::invalidatePages( $this->mDb, NS_FILE, array_keys( $images ) );
}
/**
$this->mHasTransaction = false;
}
}
-
- /**
- * Invalidate the cache of a list of pages from a single namespace.
- * This is intended for use by subclasses.
- *
- * @param int $namespace Namespace number
- * @param array $dbkeys
- */
- protected function invalidatePages( $namespace, array $dbkeys ) {
- if ( $dbkeys === [] ) {
- return;
- }
-
- $dbw = $this->mDb;
- $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $namespace, $dbkeys ) {
- /**
- * Determine which pages need to be updated
- * This is necessary to prevent the job queue from smashing the DB with
- * large numbers of concurrent invalidations of the same page
- */
- $now = $dbw->timestamp();
- $ids = $dbw->selectFieldValues( 'page',
- 'page_id',
- [
- 'page_namespace' => $namespace,
- 'page_title' => $dbkeys,
- 'page_touched < ' . $dbw->addQuotes( $now )
- ],
- __METHOD__
- );
-
- if ( $ids === [] ) {
- return;
- }
-
- /**
- * Do the update
- * We still need the page_touched condition, in case the row has changed since
- * the non-locking select above.
- */
- $dbw->update( 'page',
- [ 'page_touched' => $now ],
- [
- 'page_id' => $ids,
- 'page_touched < ' . $dbw->addQuotes( $now )
- ], __METHOD__
- );
- } );
- }
}
* @param string $lockDb
* @return IDatabase
* @throws DBError
+ * @throws UnexpectedValueException
*/
protected function getConnection( $lockDb ) {
if ( !isset( $this->conns[$lockDb] ) ) {
- $db = null;
if ( $lockDb === 'localDBMaster' ) {
- $db = $this->getLocalLB()->getConnection( DB_MASTER, [], $this->domain );
+ $lb = $this->getLocalLB();
+ $db = $lb->getConnection( DB_MASTER, [], $this->domain );
+ # Do not mess with settings if the LoadBalancer is the main singleton
+ # to avoid clobbering the settings of handles from wfGetDB( DB_MASTER ).
+ $init = ( wfGetLB() !== $lb );
} elseif ( isset( $this->dbServers[$lockDb] ) ) {
$config = $this->dbServers[$lockDb];
$db = DatabaseBase::factory( $config['type'], $config );
+ $init = true;
+ } else {
+ throw new UnexpectedValueException( "No server called '$lockDb'." );
}
- if ( !$db ) {
- return null; // config error?
+
+ if ( $init ) {
+ $db->clearFlag( DBO_TRX );
+ # If the connection drops, try to avoid letting the DB rollback
+ # and release the locks before the file operations are finished.
+ # This won't handle the case of DB server restarts however.
+ $options = [];
+ if ( $this->lockExpiry > 0 ) {
+ $options['connTimeout'] = $this->lockExpiry;
+ }
+ $db->setSessionOptions( $options );
+ $this->initConnection( $lockDb, $db );
}
+
$this->conns[$lockDb] = $db;
- $this->conns[$lockDb]->clearFlag( DBO_TRX );
- # If the connection drops, try to avoid letting the DB rollback
- # and release the locks before the file operations are finished.
- # This won't handle the case of DB server restarts however.
- $options = [];
- if ( $this->lockExpiry > 0 ) {
- $options['connTimeout'] = $this->lockExpiry;
- }
- $this->conns[$lockDb]->setSessionOptions( $options );
- $this->initConnection( $lockDb, $this->conns[$lockDb] );
- }
- if ( !$this->conns[$lockDb]->trxLevel() ) {
- $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
}
return $this->conns[$lockDb];
}
}
}
-
-/**
- * MySQL version of DBLockManager that supports shared locks.
- *
- * All lock servers must have the innodb table defined in locking/filelocks.sql.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class MySqlLockManager extends DBLockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- protected function getLocalLB() {
- // Use a separate connection so releaseAllLocks() doesn't rollback the main trx
- return wfGetLBFactory()->newMainLB( $this->domain );
- }
-
- protected function initConnection( $lockDb, IDatabase $db ) {
- # Let this transaction see lock rows from other transactions
- $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
- }
-
- /**
- * Get a connection to a lock DB and acquire locks on $paths.
- * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
- *
- * @see DBLockManager::getLocksOnServer()
- * @param string $lockSrv
- * @param array $paths
- * @param string $type
- * @return Status
- */
- protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
-
- $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
-
- $keys = []; // list of hash keys for the paths
- $data = []; // list of rows to insert
- $checkEXKeys = []; // list of hash keys that this has no EX lock on
- # Build up values for INSERT clause
- foreach ( $paths as $path ) {
- $key = $this->sha1Base36Absolute( $path );
- $keys[] = $key;
- $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
- if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
- $checkEXKeys[] = $key;
- }
- }
-
- # Block new writers (both EX and SH locks leave entries here)...
- $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
- # Actually do the locking queries...
- if ( $type == self::LOCK_SH ) { // reader locks
- $blocked = false;
- # Bail if there are any existing writers...
- if ( count( $checkEXKeys ) ) {
- $blocked = $db->selectField( 'filelocks_exclusive', '1',
- [ 'fle_key' => $checkEXKeys ],
- __METHOD__
- );
- }
- # Other prospective writers that haven't yet updated filelocks_exclusive
- # will recheck filelocks_shared after doing so and bail due to this entry.
- } else { // writer locks
- $encSession = $db->addQuotes( $this->session );
- # Bail if there are any existing writers...
- # This may detect readers, but the safe check for them is below.
- # Note: if two writers come at the same time, both bail :)
- $blocked = $db->selectField( 'filelocks_shared', '1',
- [ 'fls_key' => $keys, "fls_session != $encSession" ],
- __METHOD__
- );
- if ( !$blocked ) {
- # Build up values for INSERT clause
- $data = [];
- foreach ( $keys as $key ) {
- $data[] = [ 'fle_key' => $key ];
- }
- # Block new readers/writers...
- $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
- # Bail if there are any existing readers...
- $blocked = $db->selectField( 'filelocks_shared', '1',
- [ 'fls_key' => $keys, "fls_session != $encSession" ],
- __METHOD__
- );
- }
- }
-
- if ( $blocked ) {
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- $status = Status::newGood();
-
- foreach ( $this->conns as $lockDb => $db ) {
- if ( $db->trxLevel() ) { // in transaction
- try {
- $db->rollback( __METHOD__ ); // finish transaction and kill any rows
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
- }
-
- return $status;
- }
-}
-
-/**
- * PostgreSQL version of DBLockManager that supports shared locks.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * @ingroup LockManager
- */
-class PostgreSqlLockManager extends DBLockManager {
- /** @var array Mapping of lock types to the type actually used */
- protected $lockTypeMap = [
- self::LOCK_SH => self::LOCK_SH,
- self::LOCK_UW => self::LOCK_SH,
- self::LOCK_EX => self::LOCK_EX
- ];
-
- protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
- $status = Status::newGood();
- if ( !count( $paths ) ) {
- return $status; // nothing to lock
- }
-
- $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
- $bigints = array_unique( array_map(
- function ( $key ) {
- return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
- },
- array_map( [ $this, 'sha1Base16Absolute' ], $paths )
- ) );
-
- // Try to acquire all the locks...
- $fields = [];
- foreach ( $bigints as $bigint ) {
- $fields[] = ( $type == self::LOCK_SH )
- ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
- : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
- }
- $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
- $row = $res->fetchRow();
-
- if ( in_array( 'f', $row ) ) {
- // Release any acquired locks if some could not be acquired...
- $fields = [];
- foreach ( $row as $kbigint => $ok ) {
- if ( $ok === 't' ) { // locked
- $bigint = substr( $kbigint, 1 ); // strip off the "K"
- $fields[] = ( $type == self::LOCK_SH )
- ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
- : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
- }
- }
- if ( count( $fields ) ) {
- $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
- }
- foreach ( $paths as $path ) {
- $status->fatal( 'lockmanager-fail-acquirelock', $path );
- }
- }
-
- return $status;
- }
-
- /**
- * @see QuorumLockManager::releaseAllLocks()
- * @return Status
- */
- protected function releaseAllLocks() {
- $status = Status::newGood();
-
- foreach ( $this->conns as $lockDb => $db ) {
- try {
- $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
- } catch ( DBError $e ) {
- $status->fatal( 'lockmanager-fail-db-release', $lockDb );
- }
- }
-
- return $status;
- }
-}
--- /dev/null
+<?php
+/**
+ * MySQL version of DBLockManager that supports shared locks.
+ *
+ * All lock servers must have the innodb table defined in locking/filelocks.sql.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class MySqlLockManager extends DBLockManager {
+ /** @var array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = [
+ self::LOCK_SH => self::LOCK_SH,
+ self::LOCK_UW => self::LOCK_SH,
+ self::LOCK_EX => self::LOCK_EX
+ ];
+
+ protected function getLocalLB() {
+ // Use a separate connection so releaseAllLocks() doesn't rollback the main trx
+ return wfGetLBFactory()->newMainLB( $this->domain );
+ }
+
+ protected function initConnection( $lockDb, IDatabase $db ) {
+ # Let this transaction see lock rows from other transactions
+ $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
+ # Do everything in a transaction as it all gets rolled back eventually
+ $db->startAtomic( __CLASS__ );
+ }
+
+ /**
+ * Get a connection to a lock DB and acquire locks on $paths.
+ * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118.
+ *
+ * @see DBLockManager::getLocksOnServer()
+ * @param string $lockSrv
+ * @param array $paths
+ * @param string $type
+ * @return Status
+ */
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+
+ $keys = []; // list of hash keys for the paths
+ $data = []; // list of rows to insert
+ $checkEXKeys = []; // list of hash keys that this has no EX lock on
+ # Build up values for INSERT clause
+ foreach ( $paths as $path ) {
+ $key = $this->sha1Base36Absolute( $path );
+ $keys[] = $key;
+ $data[] = [ 'fls_key' => $key, 'fls_session' => $this->session ];
+ if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
+ $checkEXKeys[] = $key;
+ }
+ }
+
+ # Block new writers (both EX and SH locks leave entries here)...
+ $db->insert( 'filelocks_shared', $data, __METHOD__, [ 'IGNORE' ] );
+ # Actually do the locking queries...
+ if ( $type == self::LOCK_SH ) { // reader locks
+ $blocked = false;
+ # Bail if there are any existing writers...
+ if ( count( $checkEXKeys ) ) {
+ $blocked = $db->selectField( 'filelocks_exclusive', '1',
+ [ 'fle_key' => $checkEXKeys ],
+ __METHOD__
+ );
+ }
+ # Other prospective writers that haven't yet updated filelocks_exclusive
+ # will recheck filelocks_shared after doing so and bail due to this entry.
+ } else { // writer locks
+ $encSession = $db->addQuotes( $this->session );
+ # Bail if there are any existing writers...
+ # This may detect readers, but the safe check for them is below.
+ # Note: if two writers come at the same time, both bail :)
+ $blocked = $db->selectField( 'filelocks_shared', '1',
+ [ 'fls_key' => $keys, "fls_session != $encSession" ],
+ __METHOD__
+ );
+ if ( !$blocked ) {
+ # Build up values for INSERT clause
+ $data = [];
+ foreach ( $keys as $key ) {
+ $data[] = [ 'fle_key' => $key ];
+ }
+ # Block new readers/writers...
+ $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
+ # Bail if there are any existing readers...
+ $blocked = $db->selectField( 'filelocks_shared', '1',
+ [ 'fls_key' => $keys, "fls_session != $encSession" ],
+ __METHOD__
+ );
+ }
+ }
+
+ if ( $blocked ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockDb => $db ) {
+ if ( $db->trxLevel() ) { // in transaction
+ try {
+ $db->rollback( __METHOD__ ); // finish transaction and kill any rows
+ } catch ( DBError $e ) {
+ $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+ }
+ }
+ }
+
+ return $status;
+ }
+}
--- /dev/null
+<?php
+/**
+ * PostgreSQL version of DBLockManager that supports shared locks.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * @ingroup LockManager
+ */
+class PostgreSqlLockManager extends DBLockManager {
+ /** @var array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = [
+ self::LOCK_SH => self::LOCK_SH,
+ self::LOCK_UW => self::LOCK_SH,
+ self::LOCK_EX => self::LOCK_EX
+ ];
+
+ protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+ if ( !count( $paths ) ) {
+ return $status; // nothing to lock
+ }
+
+ $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
+ $bigints = array_unique( array_map(
+ function ( $key ) {
+ return Wikimedia\base_convert( substr( $key, 0, 15 ), 16, 10 );
+ },
+ array_map( [ $this, 'sha1Base16Absolute' ], $paths )
+ ) );
+
+ // Try to acquire all the locks...
+ $fields = [];
+ foreach ( $bigints as $bigint ) {
+ $fields[] = ( $type == self::LOCK_SH )
+ ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint"
+ : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint";
+ }
+ $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+ $row = $res->fetchRow();
+
+ if ( in_array( 'f', $row ) ) {
+ // Release any acquired locks if some could not be acquired...
+ $fields = [];
+ foreach ( $row as $kbigint => $ok ) {
+ if ( $ok === 't' ) { // locked
+ $bigint = substr( $kbigint, 1 ); // strip off the "K"
+ $fields[] = ( $type == self::LOCK_SH )
+ ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})"
+ : "pg_advisory_unlock({$db->addQuotes( $bigint )})";
+ }
+ }
+ if ( count( $fields ) ) {
+ $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ );
+ }
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock', $path );
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ $status = Status::newGood();
+
+ foreach ( $this->conns as $lockDb => $db ) {
+ try {
+ $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ );
+ } catch ( DBError $e ) {
+ $status->fatal( 'lockmanager-fail-db-release', $lockDb );
+ }
+ }
+
+ return $status;
+ }
+}
protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
$status = Status::newGood();
+ $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
$server = $this->lockServers[$lockSrv];
$conn = $this->redisPool->getConnection( $server );
if ( !$conn ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+ foreach ( $pathList as $path ) {
$status->fatal( 'lockmanager-fail-acquirelock', $path );
}
}
if ( $res === false ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+ foreach ( $pathList as $path ) {
$status->fatal( 'lockmanager-fail-acquirelock', $path );
}
} else {
protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
$status = Status::newGood();
+ $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
$server = $this->lockServers[$lockSrv];
$conn = $this->redisPool->getConnection( $server );
if ( !$conn ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+ foreach ( $pathList as $path ) {
$status->fatal( 'lockmanager-fail-releaselock', $path );
}
}
if ( $res === false ) {
- foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+ foreach ( $pathList as $path ) {
$status->fatal( 'lockmanager-fail-releaselock', $path );
}
} else {
}
protected function doDBInserts() {
+ $now = time();
$dbw = $this->file->repo->getMasterDB();
- $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
$encUserId = $dbw->addQuotes( $this->user->getId() );
$encReason = $dbw->addQuotes( $this->reason );
$encGroup = $dbw->addQuotes( 'deleted' );
}
if ( $deleteCurrent ) {
- $concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
- $where = [ 'img_name' => $this->file->getName() ];
- $dbw->insertSelect( 'filearchive', 'image',
+ $dbw->insertSelect(
+ 'filearchive',
+ 'image',
[
'fa_storage_group' => $encGroup,
'fa_storage_key' => $dbw->conditional(
[ 'img_sha1' => '' ],
$dbw->addQuotes( '' ),
- $concat
+ $dbw->buildConcat( [ "img_sha1", $encExt ] )
),
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_user' => 'img_user',
'fa_user_text' => 'img_user_text',
'fa_timestamp' => 'img_timestamp',
- 'fa_sha1' => 'img_sha1',
- ], $where, __METHOD__ );
+ 'fa_sha1' => 'img_sha1'
+ ],
+ [ 'img_name' => $this->file->getName() ],
+ __METHOD__
+ );
}
if ( count( $oldRels ) ) {
- $concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
- $where = [
- 'oi_name' => $this->file->getName(),
- 'oi_archive_name' => array_keys( $oldRels ) ];
- $dbw->insertSelect( 'filearchive', 'oldimage',
+ $res = $dbw->select(
+ 'oldimage',
+ OldLocalFile::selectFields(),
[
- 'fa_storage_group' => $encGroup,
- 'fa_storage_key' => $dbw->conditional(
- [ 'oi_sha1' => '' ],
- $dbw->addQuotes( '' ),
- $concat
- ),
- 'fa_deleted_user' => $encUserId,
- 'fa_deleted_timestamp' => $encTimestamp,
- 'fa_deleted_reason' => $encReason,
- 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
-
- 'fa_name' => 'oi_name',
- 'fa_archive_name' => 'oi_archive_name',
- 'fa_size' => 'oi_size',
- 'fa_width' => 'oi_width',
- 'fa_height' => 'oi_height',
- 'fa_metadata' => 'oi_metadata',
- 'fa_bits' => 'oi_bits',
- 'fa_media_type' => 'oi_media_type',
- 'fa_major_mime' => 'oi_major_mime',
- 'fa_minor_mime' => 'oi_minor_mime',
- 'fa_description' => 'oi_description',
- 'fa_user' => 'oi_user',
- 'fa_user_text' => 'oi_user_text',
- 'fa_timestamp' => 'oi_timestamp',
- 'fa_sha1' => 'oi_sha1',
- ], $where, __METHOD__ );
+ 'oi_name' => $this->file->getName(),
+ 'oi_archive_name' => array_keys( $oldRels )
+ ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+ $rowsInsert = [];
+ foreach ( $res as $row ) {
+ $rowsInsert[] = [
+ // Deletion-specific fields
+ 'fa_storage_group' => 'deleted',
+ 'fa_storage_key' => ( $row->oi_sha1 === '' )
+ ? ''
+ : "{$row->oi_sha1}{$dotExt}",
+ 'fa_deleted_user' => $this->user->getId(),
+ 'fa_deleted_timestamp' => $dbw->timestamp( $now ),
+ 'fa_deleted_reason' => $this->reason,
+ // Counterpart fields
+ 'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
+ 'fa_name' => $row->oi_name,
+ 'fa_archive_name' => $row->oi_archive_name,
+ 'fa_size' => $row->oi_size,
+ 'fa_width' => $row->oi_width,
+ 'fa_height' => $row->oi_height,
+ 'fa_metadata' => $row->oi_metadata,
+ 'fa_bits' => $row->oi_bits,
+ 'fa_media_type' => $row->oi_media_type,
+ 'fa_major_mime' => $row->oi_major_mime,
+ 'fa_minor_mime' => $row->oi_minor_mime,
+ 'fa_description' => $row->oi_description,
+ 'fa_user' => $row->oi_user,
+ 'fa_user_text' => $row->oi_user_text,
+ 'fa_timestamp' => $row->oi_timestamp,
+ 'fa_sha1' => $row->oi_sha1
+ ];
+ }
+
+ $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
}
}
// The live (current) version cannot be hidden!
if ( !$this->unsuppress && $row->fa_deleted ) {
- $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
- $this->cleanupBatch[] = $row->fa_storage_key;
+ $status->fatal( 'undeleterevdel' );
+ $this->file->unlock();
+ return $status;
}
} else {
$archiveName = $row->fa_archive_name;
$this->mFieldTree = $loadedDescriptor;
}
+ /**
+ * @param string $fieldname
+ * @return bool
+ */
+ public function hasField( $fieldname ) {
+ return isset( $this->mFlatFields[$fieldname] );
+ }
+
+ /**
+ * @param string $fieldname
+ * @return HTMLFormField
+ * @throws DomainException on invalid field name
+ */
+ public function getField( $fieldname ) {
+ if ( !$this->hasField( $fieldname ) ) {
+ throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
+ }
+ return $this->mFlatFields[$fieldname];
+ }
+
/**
* Set format in which to display the form
*
trait HTMLFormElement {
protected $hideIf = null;
+ protected $modules = null;
public function initializeHTMLFormElement( array $config = [] ) {
// Properties
$this->hideIf = isset( $config['hideIf'] ) ? $config['hideIf'] : null;
+ $this->modules = isset( $config['modules'] ) ? $config['modules'] : [];
// Initialization
if ( $this->hideIf ) {
$this->addClasses( [ 'mw-htmlform-hide-if' ] );
}
+ if ( $this->modules ) {
+ // JS code must be able to read this before infusing (before OOjs UI is even loaded),
+ // so we put this in a separate attribute (not with the rest of the config).
+ // And it's not needed anymore after infusing, so we don't put it in JS config at all.
+ $this->setAttributes( [ 'data-mw-modules' => implode( ',', $this->modules ) ] );
+ }
$this->registerConfigCallback( function( &$config ) {
if ( $this->hideIf !== null ) {
$config['hideIf'] = $this->hideIf;
* @return bool
*/
public function canDisplayErrors() {
- return true;
+ return $this->hasVisibleOutput();
}
/**
$this->mFilterCallback = $params['filter-callback'];
}
- if ( isset( $params['flatlist'] ) ) {
- $this->mClass .= ' mw-htmlform-flatlist';
- }
-
if ( isset( $params['hidelabel'] ) ) {
$this->mShowEmptyLabels = false;
}
'infusable' => $infusable,
];
+ $preloadModules = false;
+
if ( $infusable && $this->shouldInfuseOOUI() ) {
- $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+ $preloadModules = true;
$config['classes'][] = 'mw-htmlform-field-autoinfuse';
}
}
if ( $this->mHideIf ) {
- $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+ $preloadModules = true;
$config['hideIf'] = $this->mHideIf;
}
+ $config['modules'] = $this->getOOUIModules();
+
+ if ( $preloadModules ) {
+ $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
+ $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
+ }
+
return $this->getFieldLayoutOOUI( $inputField, $config );
}
return $this->getHelpText() !== null;
}
+ /**
+ * Get the list of extra ResourceLoader modules which must be loaded client-side before it's
+ * possible to infuse this field's OOjs UI widget.
+ *
+ * @return string[]
+ */
+ protected function getOOUIModules() {
+ return [];
+ }
+
/**
* Get the complete raw fields for the input, including help text,
* labels, and whatever.
* Multi-select field
*/
class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable {
+ /**
+ * @param array $params
+ * In adition to the usual HTMLFormField parameters, this can take the following fields:
+ * - dropdown: If given, the options will be displayed inside a dropdown with a text field that
+ * can be used to filter them. This is desirable mostly for very long lists of options.
+ * This only works for users with JavaScript support and falls back to the list of checkboxes.
+ * - flatlist: If given, the options will be displayed on a single line (wrapping to following
+ * lines if necessary), rather than each one on a line of its own. This is desirable mostly
+ * for very short lists of concisely labelled options.
+ */
+ public function __construct( $params ) {
+ parent::__construct( $params );
+
+ // For backwards compatibility, also handle the old way with 'cssclass' => 'mw-chosen'
+ if ( isset( $params['dropdown'] ) || strpos( $this->mClass, 'mw-chosen' ) !== false ) {
+ $this->mClass .= ' mw-htmlform-dropdown';
+ }
+
+ if ( isset( $params['flatlist'] ) ) {
+ $this->mClass .= ' mw-htmlform-flatlist';
+ }
+ }
+
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
}
function getInputHTML( $value ) {
+ if ( isset( $this->mParams['dropdown'] ) ) {
+ $this->mParent->getOutput()->addModules( 'jquery.chosen' );
+ }
+
$value = HTMLFormField::forceToStringRecursive( $value );
$html = $this->formatOptions( $this->getOptions(), $value );
* Radio checkbox fields.
*/
class HTMLRadioField extends HTMLFormField {
+ /**
+ * @param array $params
+ * In adition to the usual HTMLFormField parameters, this can take the following fields:
+ * - flatlist: If given, the options will be displayed on a single line (wrapping to following
+ * lines if necessary), rather than each one on a line of its own. This is desirable mostly
+ * for very short lists of concisely labelled options.
+ */
+ public function __construct( $params ) {
+ parent::__construct( $params );
+
+ if ( isset( $params['flatlist'] ) ) {
+ $this->mClass .= ' mw-htmlform-flatlist';
+ }
+ }
+
function validate( $value, $alldata ) {
$p = parent::validate( $value, $alldata );
] );
}
+ protected function getOOUIModules() {
+ // FIXME: NamespaceInputWidget should be in its own module (probably?)
+ return [ 'mediawiki.widgets' ];
+ }
+
protected function shouldInfuseOOUI() {
return true;
}
}
protected function getInputWidget( $params ) {
- $this->mParent->getOutput()->addModules( 'mediawiki.widgets' );
if ( $this->mParams['namespace'] !== false ) {
$params['namespace'] = $this->mParams['namespace'];
}
return true;
}
+ protected function getOOUIModules() {
+ // FIXME: TitleInputWidget should be in its own module
+ return [ 'mediawiki.widgets' ];
+ }
+
public function getInputHtml( $value ) {
// add mw-searchInput class to enable search suggestions for non-OOUI, too
$this->mClass .= 'mw-searchInput';
}
protected function getInputWidget( $params ) {
- $this->mParent->getOutput()->addModules( 'mediawiki.widgets.UserInputWidget' );
-
return new UserInputWidget( $params );
}
return true;
}
+ protected function getOOUIModules() {
+ return [ 'mediawiki.widgets.UserInputWidget' ];
+ }
+
public function getInputHtml( $value ) {
// add the required module and css class for user suggestions in non-OOUI mode
$this->mParent->getOutput()->addModules( 'mediawiki.userSuggest' );
"config-env-php": "A PHP verziója: $1",
"config-env-hhvm": "HHVM verziója: $1",
"config-unicode-using-intl": "A rendszer Unicode normalizálására az [http://pecl.php.net/intl intl PECL kiterjesztést] használja.",
- "config-unicode-pure-php-warning": "'''Figyelmeztetés''': Az Unicode normalizáláshoz szükséges [http://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el, helyette a lassú, PHP alapú implementáció lesz használva.\nHa nagy látogatottságú oldalt üzemeltetsz, itt találhatsz további információkat [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations a témáról].",
- "config-unicode-update-warning": "'''Figyelmeztetés''': Az Unicode normalizáláshoz szükséges burkolókönyvtár [http://site.icu-project.org/ ICU projekt] függvénykönyvtárának régebbi változatát használja.\nHa ügyelni kívánsz a Unicode használatára, fontold meg a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations frissítését].",
+ "config-unicode-pure-php-warning": "<strong>Figyelmeztetés:</strong> Az Unicode normalizáláshoz szükséges [http://pecl.php.net/intl intl PECL kiterjesztés] nem érhető el, helyette a lassú, PHP-alapú implementáció lesz használatban.\nHa nagy látogatottságú oldalt üzemeltetsz, [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations itt] találhatsz további információkat a témáról.",
+ "config-unicode-update-warning": "<strong>Figyelmeztetés:</strong> Az Unicode normalizáláshoz szükséges burkolókönyvtár [http://site.icu-project.org/ ICU projekt] függvénykönyvtárának régebbi változatát használja.\nHa ügyelni kívánsz a Unicode használatára, fontold meg a [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations frissítését].",
"config-no-db": "Nem sikerült egyetlen használható adatbázis-illesztőprogramot sem találni. Telepítened kell egyet a PHP-hez.\nA következő {{PLURAL:$2|adatbázistípus támogatott|adatbázistípusok támogatottak}}: $1.\n\nHa a PHP-t magad fordítottad, konfiguráld újra úgy, hogy engedélyezve legyen egy adatbáziskliens, pl. a <code>./configure --with-mysql</code> parancs használatával.\nHa a PHP-t Debian vagy Ubuntu csomaggal telepítetted, akkor szükséged lesz például a php5-mysql csomagra is.",
"config-outdated-sqlite": "<strong>Figyelmeztetés:</strong> SQLite $1 verziód van, ami alacsonyabb a legalább szükséges $2 verziónál. Az SQLite nem lesz elérhető.",
"config-no-fts3": "'''Figyelmeztetés''': Az SQLite [//sqlite.org/fts3.html FTS3 modul] nélkül lett fordítva, a keresési funkciók nem fognak működni ezen a rendszeren.",
"config-ns-site-name": "Ugyanaz, mint a wiki neve: $1",
"config-ns-other": "Más (meg kell adni)",
"config-ns-other-default": "SajátWiki",
- "config-project-namespace-help": "A Wikipédia példáját követve számos wiki elkülöníti egy '''projekt névtérbe''' az irányelveit a tartalommal rendelkező lapoktól\nAz ebben a névtérben található lapok nevei egy előtaggal kezdődnek, amit itt adhatsz meg.\nÁltalában az előtag a wiki nevéből származik, de nem tartalmazhat írásjeleket, például „#”-t vagy „:”-t.",
+ "config-project-namespace-help": "A Wikipédia példáját követve számos wiki elkülöníti az irányelveit a tartalmi lapoktól egy '''projektnévtérbe'''.\nAz ebben a névtérben található lapok nevei egy előtaggal kezdődnek, amit itt adhatsz meg.\nÁltalában az előtag a wiki nevéből származik, de nem tartalmazhat írásjeleket, például „#”-t vagy „:”-t.",
"config-ns-invalid": "A megadott névtér („<nowiki>$1</nowiki>”) érvénytelen.\nVálassz másik projektnévteret!",
"config-ns-conflict": "A megadott névtér („<nowiki>$1</nowiki>”) ütközik az egyik alapértelmezett MediaWiki-névtérrel.\nVálassz másik projektnévteret!",
"config-admin-box": "Adminisztrátori fiók",
"config-logo": "A logó URL-címe:",
"config-logo-help": "A MediaWiki alapértelmezett felülete helyet ad egy 135×160 pixeles logónak a bal felső sarokban.\nTölts fel egy megfelelő méretű képet, majd írd be ide az URL-címét!\n\nHasználhatsz <code>$wgStylePath</code>-t vagy <code>$wgScriptPath</code>-t, ha más méretű a logód.\n\nHa nem szeretnél logót használni, egyszerűen hagyd üresen a mezőt.",
"config-instantcommons": "Instant Commons engedélyezése",
- "config-instantcommons-help": "Az [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] lehetővé teszi, hogy a wikin használhassák a [https://commons.wikimedia.org/ Wikimedia Commons] oldalon található képeket, hangokat és más médiafájlokat.\nA használatához a MediaWikinek internethozzáférésre van szüksége.\n\nA funkcióról és hogy hogyan állítható be más wikik esetén [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos a kézikönyvben] találhatsz további információkat.",
+ "config-instantcommons-help": "Az [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] lehetővé teszi, hogy a wikin használhassák a [https://commons.wikimedia.org/ Wikimédia Commons] oldalon található képeket, hangokat és más médiafájlokat.\nA használatához a MediaWikinek internet-hozzáférésre van szüksége.\n\nA funkcióról és hogy hogyan állítható be más wikik esetén [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos a kézikönyvben] találhatsz további információkat.",
"config-cc-error": "A Creative Commons-licencválasztó nem tért vissza eredménnyel.\nAdd meg kézzel a licencet.",
"config-cc-again": "Válassz újra…",
"config-cc-not-chosen": "Válaszd ki a kívánt Creative Commons licencet, majd kattints a „proceed”!",
"config-install-mainpage": "Kezdőlap létrehozása az alapértelmezett tartalommal",
"config-install-extension-tables": "Táblák létrehozása az engedélyezett kiterjesztésekhez",
"config-install-mainpage-failed": "Nemsikerült létrehozni a kezdőlapot: $1",
- "config-install-done": "'''Gratulálunk!'''\nA MediaWiki telepítése sikeresen befejeződött.\n\nA telepítő elkészítette a <code>LocalSettings.php</code> fájlt, amely tartalmazza az összes beállítást.\n\nEzt le kell tölteni, majd elhelyezni a wiki telepítési könyvtárába (az a könyvtár, ahol az index.php is található).\n\nA letöltés automatikusan elindul. Ha mégsem indulna el, vagy megszakítottad, az alábbi linkre kattintva újra letöltheted:\n\n$3\n\n'''Megjegyzés''': Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.\n\nHa végeztél a fájl elhelyezésével, '''[$2 beléphetsz a wikibe]'''.",
+ "config-install-done": "<strong>Gratulálunk!</strong>\nA MediaWiki telepítése sikeresen befejeződött.\n\nA telepítő elkészítette a <code>LocalSettings.php</code> fájlt, amely tartalmazza az összes beállítást.\n\nEzt le kell tölteni, majd elhelyezni a wiki telepítési könyvtárába (az a könyvtár, ahol az index.php is található).\n\nA letöltés automatikusan elindul. Ha mégsem indulna el, vagy megszakítottad, az alábbi linkre kattintva újra letöltheted:\n\n$3\n\n<strong>Megjegyzés:</strong> Ha ezt most nem teszed meg, és kilépsz a telepítésből, az elkészített konfigurációs fájlt nem tudod elérni a későbbiekben.\n\nHa végeztél a fájl elhelyezésével, <strong>[$2 beléphetsz a wikibe]</strong>.",
"config-download-localsettings": "<code>LocalSettings.php</code> letöltése",
"config-help": "segítség",
"config-help-tooltip": "kattints a kibontáshoz",
"config-nofile": "\"$1\" fájl nem található. Törölve lett?",
"config-extension-link": "Tudtad, hogy a wikid támogat [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions kiterjesztéseket]?\n\nBöngészhetsz [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category kiterjesztéseket kategóriánként] vagy válogathatsz a [https://www.mediawiki.org/wiki/Extension_Matrix kiterjesztésmátrixból] az összes kiterjesztés áttekintéséhez.",
- "mainpagetext": "'''A MediaWiki telepítése sikeresen befejeződött.'''",
+ "mainpagetext": "<strong>A MediaWiki telepítése sikeresen befejeződött.</strong>",
"mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]"
}
"config-install-user": "Kuriamas duomenų bazės naudotojas",
"config-install-user-alreadyexists": "Naudotojas „$1“ jau yra",
"config-install-user-create-failed": "Nepavyko sukurti naudotojo „$1“: $2",
+ "config-install-user-missing": "Nurodytas vartotojas „$1“ neegzistuoja.",
+ "config-install-user-missing-create": "Nurodytas vartotojas „$1“ neegzistuoja.\nPrašome pažymėti „sukurti paskyrą“ laukelį žemiau jei norite jį sukurti.",
"config-install-tables": "Kuriamos lentelės",
+ "config-install-tables-exist": "<strong>Įspėjimas:</strong> MediaWiki lentelės, atrodo, jau egzistuoja.\nKūrimas praleidžiamas.",
+ "config-install-tables-failed": "<strong>Klaida:</strong> Lentelės sukūrimas nepavyko dėl šios klaidos: $1",
+ "config-install-interwiki-list": "Nepavyko perskaityti failo <code>interwiki.list</code>.",
+ "config-install-interwiki-exists": "<strong>Įspėjimas:</strong> Interwiki lentelė, atrodo, jau turi įrašų.\nNumatytasis sąrašas praleidžiamas.",
"config-install-stats": "Inicijuojamos statistikos",
"config-install-keys": "Generuojami slapti raktai",
"config-install-sysop": "Administratoriaus vartotojo paskyra kuriama",
"config-help": "pagalba",
"config-help-tooltip": "spustelėkite išplėtimui",
"config-nofile": "Failas \"$1\" nerastas. Ar jis buvo ištrintas?",
- "mainpagetext": "'''MediaWiki sėkmingai įdiegta.'''",
+ "mainpagetext": "<strong>MediaWiki sėkmingai įdiegta.</strong>",
"mainpagedocfooter": "Informacijos apie wiki programinės įrangos naudojimą, ieškokite [https://meta.wikimedia.org/wiki/Help:Contents žinyne].\n\n== Pradžiai ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigūracijos nustatymų sąrašas]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki DUK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki pranešimai paštu apie naujas versijas]"
}
"config-subscribe": "Theo dõi [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce danh sách thư thông báo phát hành].",
"config-subscribe-help": "thông báo an ninh.\nBạn nên đồng ý với nó và cập nhật bản cài đặt MediaWiki của bạn khi phiên bản mới xuất hiện.",
"config-subscribe-noemail": "Bạn đã cố gắng để đăng ký vào danh sách thư thông báo phát hành mà không cung cấp một địa chỉ thư điện tử nào cả.\nVui lòng cung cấp một địa chỉ thư điện tử nếu bạn muốn đăng ký vào danh sách thư.",
+ "config-pingback": "Chia sẻ dữ liệu về bản cài đặt này với nhóm phát triển MediaWiki.",
"config-almost-done": "Bạn gần như đã hoàn tất!\nBây giờ bạn có thể bỏ qua cấu hình còn lại và cài đặt wiki ngay bây giờ.",
"config-optional-continue": "Hỏi tôi về thêm chi tiết.",
"config-optional-skip": "Chán quá, cài đặt wiki rỗi.",
count( $rowSet ) + count( $rowList ) - count( $rows )
);
} catch ( DBError $e ) {
- if ( $flags & self::QOS_ATOMIC ) {
- $dbw->rollback( $method );
- }
- throw $e;
+ $this->throwDBException( $e );
}
if ( $flags & self::QOS_ATOMIC ) {
$dbw->endAtomic( $method );
protected function doPop() {
$dbw = $this->getMasterDB();
try {
- $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
$autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
$dbw->clearFlag( DBO_TRX ); // make each query its own transaction
$scopedReset = new ScopedCallback( function () use ( $dbw, $autoTrx ) {
$dbw = $this->getMasterDB();
try {
- $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
$autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
$dbw->clearFlag( DBO_TRX ); // make each query its own transaction
$scopedReset = new ScopedCallback( function () use ( $dbw, $autoTrx ) {
* @ingroup JobQueue
*/
+use MediaWiki\MediaWikiServices;
use MediaWiki\Logger\LoggerFactory;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
}
// Flush any pending DB writes for sanity
- wfGetLBFactory()->commitAll( __METHOD__ );
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory->commitAll( __METHOD__ );
// Catch huge single updates that lead to slave lag
$trxProfiler = Profiler::instance()->getTransactionProfiler();
$backoffs = $this->syncBackoffDeltas( $backoffs, $backoffDeltas, $wait );
}
+ $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
$info = $this->executeJob( $job, $stats, $popTime );
if ( $info['status'] !== false || !$job->allowRetries() ) {
$group->ack( $job ); // succeeded or job cannot be retried
+ $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
}
// Back off of certain jobs for a while (for throttling and for errors)
$timePassed = microtime( true ) - $lastCheckTime;
if ( $timePassed >= self::LAG_CHECK_PERIOD || $timePassed < 0 ) {
try {
- wfGetLBFactory()->waitForReplication( [
+ $lbFactory->waitForReplication( [
'ifWritesSince' => $lastCheckTime,
'timeout' => self::MAX_ALLOWED_LAG
] );
$msg = $job->toString() . " STARTING";
$this->logger->debug( $msg );
$this->debugCallback( $msg );
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
// Run the job...
$rssStart = $this->getMaxRssKb();
// 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( __METHOD__ );
+ $lbFactory->commitAll( __METHOD__ );
// Clear out title cache data from prior snapshots
LinkCache::singleton()->clear();
$timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
$lb->waitForAll( $pos );
}
- $fname = __METHOD__;
- // Re-ping all masters with transactions. This throws DBError if some
- // connection died while waiting on locks/slaves, triggering a rollback.
- wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $fname ) {
- $lb->forEachOpenConnection( function( IDatabase $conn ) use ( $fname ) {
- if ( $conn->writesOrCallbacksPending() ) {
- $conn->ping();
- }
- } );
- } );
-
// Actually commit the DB master changes
wfGetLBFactory()->commitMasterChanges( __METHOD__ );
--- /dev/null
+<?php
+/**
+ * Base code for update jobs that put some secondary data extracted
+ * from article content into the database.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+class PurgeJobUtils {
+ /**
+ * Invalidate the cache of a list of pages from a single namespace.
+ * This is intended for use by subclasses.
+ *
+ * @param IDatabase $dbw
+ * @param int $namespace Namespace number
+ * @param array $dbkeys
+ */
+ public static function invalidatePages( IDatabase $dbw, $namespace, array $dbkeys ) {
+ if ( $dbkeys === [] ) {
+ return;
+ }
+
+ $dbw->onTransactionPreCommitOrIdle( function() use ( $dbw, $namespace, $dbkeys ) {
+ // Determine which pages need to be updated.
+ // This is necessary to prevent the job queue from smashing the DB with
+ // large numbers of concurrent invalidations of the same page.
+ $now = $dbw->timestamp();
+ $ids = $dbw->selectFieldValues(
+ 'page',
+ 'page_id',
+ [
+ 'page_namespace' => $namespace,
+ 'page_title' => $dbkeys,
+ 'page_touched < ' . $dbw->addQuotes( $now )
+ ],
+ __METHOD__
+ );
+
+ if ( $ids === [] ) {
+ return;
+ }
+
+ // Do the update.
+ // We still need the page_touched condition, in case the row has changed since
+ // the non-locking select above.
+ $dbw->update(
+ 'page',
+ [ 'page_touched' => $now ],
+ [
+ 'page_id' => $ids,
+ 'page_touched < ' . $dbw->addQuotes( $now )
+ ],
+ __METHOD__
+ );
+ } );
+ }
+}
*/
class VirtualRESTServiceClient {
/** @var MultiHttpClient */
- protected $http;
- /** @var VirtualRESTService[] Map of (prefix => VirtualRESTService) */
- protected $instances = [];
+ private $http;
+ /** @var array Map of (prefix => VirtualRESTService|array) */
+ private $instances = [];
const VALID_MOUNT_REGEX = '#^/[0-9a-z]+/([0-9a-z]+/)*$#';
/**
* Map a prefix to service handler
*
+ * If $instance is in array, it must have these keys:
+ * - class : string; fully qualified VirtualRESTService class name
+ * - config : array; map of parameters that is the first __construct() argument
+ *
* @param string $prefix Virtual path
- * @param VirtualRESTService $instance
+ * @param VirtualRESTService|array $instance Service or info to yield the service
*/
- public function mount( $prefix, VirtualRESTService $instance ) {
+ public function mount( $prefix, $instance ) {
if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
throw new UnexpectedValueException( "Invalid service mount point '$prefix'." );
} elseif ( isset( $this->instances[$prefix] ) ) {
throw new UnexpectedValueException( "A service is already mounted on '$prefix'." );
}
+ if ( !( $instance instanceof VirtualRESTService ) ) {
+ if ( !isset( $instance['class'] ) || !isset( $instance['config'] ) ) {
+ throw new UnexpectedValueException( "Missing 'class' or 'config' ('$prefix')." );
+ }
+ }
$this->instances[$prefix] = $instance;
}
};
$matches = []; // matching prefixes (mount points)
- foreach ( $this->instances as $prefix => $service ) {
+ foreach ( $this->instances as $prefix => $unused ) {
if ( strpos( $path, $prefix ) === 0 ) {
$matches[] = $prefix;
}
usort( $matches, $cmpFunc );
// Return the most specific prefix and corresponding service
- return isset( $matches[0] )
- ? [ $matches[0], $this->instances[$matches[0]] ]
+ return $matches
+ ? [ $matches[0], $this->getInstance( $matches[0] ) ]
: [ null, null ];
}
// defer the original or to set a proxy response to the original.
$newReplaceReqsByService = [];
foreach ( $replaceReqsByService as $prefix => $servReqs ) {
- $service = $this->instances[$prefix];
+ $service = $this->getInstance( $prefix );
foreach ( $service->onRequests( $servReqs, $idFunc ) as $index => $req ) {
// Services use unique IDs for replacement requests
if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
$checkReqIndexesByPrefix[$prefix][$index] = 1;
}
}
- // Update index of requests to inspect for replacement
- $replaceReqsByService = $newReplaceReqsByService;
// Run the actual work HTTP requests
foreach ( $this->http->runMulti( $executeReqs ) as $index => $ranReq ) {
$doneReqs[$index] = $ranReq;
// forced by setting 'response' rather than actually be sent over the wire.
$newReplaceReqsByService = [];
foreach ( $checkReqIndexesByPrefix as $prefix => $servReqIndexes ) {
- $service = $this->instances[$prefix];
+ $service = $this->getInstance( $prefix );
// $doneReqs actually has the requests (with 'response' set)
$servReqs = array_intersect_key( $doneReqs, $servReqIndexes );
foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) {
return $responses;
}
+
+ /**
+ * @param string $prefix
+ * @return VirtualRESTService
+ */
+ private function getInstance( $prefix ) {
+ if ( !isset( $this->instances[$prefix] ) ) {
+ throw new RunTimeException( "No service registered at prefix '{$prefix}'." );
+ }
+
+ if ( !( $this->instances[$prefix] instanceof VirtualRESTService ) ) {
+ $config = $this->instances[$prefix]['config'];
+ $class = $this->instances[$prefix]['class'];
+ $service = new $class( $config );
+ if ( !( $service instanceof VirtualRESTService ) ) {
+ throw new UnexpectedValueException( "Registered service has the wrong class." );
+ }
+ $this->instances[$prefix] = $service;
+ }
+
+ return $this->instances[$prefix];
+ }
}
* @ingroup Cache
*/
+use \MediaWiki\MediaWikiServices;
+
/**
* Class to store objects in the database
*
if ( isset( $dataRows[$key] ) ) { // HIT?
$row = $dataRows[$key];
$this->debug( "get: retrieved data; expiry time is " . $row->exptime );
+ $db = null;
try {
$db = $this->getDB( $row->serverIndex );
if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
$values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) );
}
} catch ( DBQueryError $e ) {
- $this->handleWriteError( $e, $row->serverIndex );
+ $this->handleWriteError( $e, $db, $row->serverIndex );
}
} else { // MISS
$this->debug( 'get: no matching rows' );
$result = true;
$exptime = (int)$expiry;
foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
$result = false;
continue;
}
__METHOD__
);
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
$result = false;
}
protected function cas( $casToken, $key, $value, $exptime = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
$exptime = intval( $exptime );
__METHOD__
);
} catch ( DBQueryError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
return false;
}
public function delete( $key ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
$db->delete(
[ 'keyname' => $key ],
__METHOD__ );
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
return false;
}
public function incr( $key, $step = 1 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
$step = intval( $step );
$newValue = null;
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
return null;
}
public function changeTTL( $key, $expiry = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
$db->update(
return false;
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
return false;
}
*/
public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
$dbTimestamp = $db->timestamp( $timestamp );
}
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
return false;
}
}
*/
public function deleteAll() {
for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+ $db = null;
try {
$db = $this->getDB( $serverIndex );
for ( $i = 0; $i < $this->shards; $i++ ) {
$db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
}
} catch ( DBError $e ) {
- $this->handleWriteError( $e, $serverIndex );
+ $this->handleWriteError( $e, $db, $serverIndex );
return false;
}
}
* Handle a DBQueryError which occurred during a write operation.
*
* @param DBError $exception
+ * @param IDatabase|null $db DB handle or null if connection failed
* @param int $serverIndex
+ * @throws Exception
*/
- protected function handleWriteError( DBError $exception, $serverIndex ) {
- if ( $exception instanceof DBConnectionError ) {
+ protected function handleWriteError( DBError $exception, IDatabase $db = null, $serverIndex ) {
+ if ( !$db ) {
$this->markServerDown( $exception, $serverIndex );
- }
- if ( $exception->db && $exception->db->wasReadOnlyError() ) {
- if ( $exception->db->trxLevel() ) {
- try {
- $exception->db->rollback( __METHOD__ );
- } catch ( DBError $e ) {
- }
+ } elseif ( $db->wasReadOnlyError() ) {
+ if ( $db->trxLevel() && $this->usesMainDB() ) {
+ // Errors like deadlocks and connection drops already cause rollback.
+ // For consistency, we have no choice but to throw an error and trigger
+ // complete rollback if the main DB is also being used as the cache DB.
+ throw $exception;
}
}
* @param DBError $exception
* @param int $serverIndex
*/
- protected function markServerDown( $exception, $serverIndex ) {
+ protected function markServerDown( DBError $exception, $serverIndex ) {
unset( $this->conns[$serverIndex] ); // bug T103435
if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
}
}
+ /**
+ * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
+ */
+ protected function usesMainDB() {
+ return !$this->serverInfos;
+ }
+
protected function waitForSlaves() {
- if ( !$this->serverInfos ) {
+ if ( $this->usesMainDB() ) {
// Main LB is used; wait for any slaves to catch up
try {
- wfGetLBFactory()->waitForReplication( [ 'wiki' => wfWikiID() ] );
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] );
return true;
} catch ( DBReplicationWaitError $e ) {
return false;
}
}
- # Is it client cached?
- if ( $outputPage->checkLastModified( $timestamp ) ) {
- wfDebug( __METHOD__ . ": done 304\n" );
-
- return;
- # Try file cache
- } elseif ( $wgUseFileCache && $this->tryFileCache() ) {
+ # Try to stream the output from file cache
+ if ( $wgUseFileCache && $this->tryFileCache() ) {
wfDebug( __METHOD__ . ": done file cache\n" );
# tell wgOut that output is taken care of
$outputPage->disable();
/**
* Loads page_touched and returns a value indicating if it should be used
- * @return bool True if not a redirect
+ * @return bool True if this page exists and is not a redirect
*/
public function checkTouched() {
if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
- return !$this->mIsRedirect;
+ return ( $this->mId && !$this->mIsRedirect );
}
/**
$revisionId = $revision->insertOn( $dbw );
// Update page_latest and friends to reflect the new revision
if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
- $dbw->rollback( __METHOD__ ); // sanity; this should never happen
throw new MWException( "Failed to update page row to use new revision." );
}
$revisionId = $revision->insertOn( $dbw );
// Update the page record with revision data
if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
- $dbw->rollback( __METHOD__ ); // sanity; this should never happen
throw new MWException( "Failed to update page row to use new revision." );
}
return $status;
}
+ // Given the lock above, we can be confident in the title and page ID values
+ $namespace = $this->getTitle()->getNamespace();
+ $dbKey = $this->getTitle()->getDBkey();
+
// At this point we are now comitted to returning an OK
// status unless some DB query error or other exception comes up.
// This way callers don't have to call rollback() if $status is bad
$bitfield = 'rev_deleted';
}
- /**
- * For now, shunt the revision data into the archive table.
- * Text is *not* removed from the text table; bulk storage
- * is left intact to avoid breaking block-compression or
- * immutable storage schemes.
- *
- * For backwards compatibility, note that some older archive
- * table entries will have ar_text and ar_flags fields still.
- *
- * In the future, we may keep revisions and mark them with
- * the rev_deleted field, which is reserved for this purpose.
- */
-
- $row = [
- 'ar_namespace' => 'page_namespace',
- 'ar_title' => 'page_title',
- 'ar_comment' => 'rev_comment',
- 'ar_user' => 'rev_user',
- 'ar_user_text' => 'rev_user_text',
- 'ar_timestamp' => 'rev_timestamp',
- 'ar_minor_edit' => 'rev_minor_edit',
- 'ar_rev_id' => 'rev_id',
- 'ar_parent_id' => 'rev_parent_id',
- 'ar_text_id' => 'rev_text_id',
- 'ar_text' => '\'\'', // Be explicit to appease
- 'ar_flags' => '\'\'', // MySQL's "strict mode"...
- 'ar_len' => 'rev_len',
- 'ar_page_id' => 'page_id',
- 'ar_deleted' => $bitfield,
- 'ar_sha1' => 'rev_sha1',
- ];
+ // For now, shunt the revision data into the archive table.
+ // Text is *not* removed from the text table; bulk storage
+ // is left intact to avoid breaking block-compression or
+ // immutable storage schemes.
+ // In the future, we may keep revisions and mark them with
+ // the rev_deleted field, which is reserved for this purpose.
- if ( $wgContentHandlerUseDB ) {
- $row['ar_content_model'] = 'rev_content_model';
- $row['ar_content_format'] = 'rev_content_format';
- }
-
- // Copy all the page revisions into the archive table
- $dbw->insertSelect(
- 'archive',
- [ 'page', 'revision' ],
- $row,
- [
- 'page_id' => $id,
- 'page_id = rev_page'
- ],
- __METHOD__
+ // Get all of the page revisions
+ $res = $dbw->select(
+ 'revision',
+ Revision::selectFields(),
+ [ 'rev_page' => $id ],
+ __METHOD__,
+ 'FOR UPDATE'
);
+ // Build their equivalent archive rows
+ $rowsInsert = [];
+ foreach ( $res as $row ) {
+ $rowInsert = [
+ 'ar_namespace' => $namespace,
+ 'ar_title' => $dbKey,
+ 'ar_comment' => $row->rev_comment,
+ 'ar_user' => $row->rev_user,
+ 'ar_user_text' => $row->rev_user_text,
+ 'ar_timestamp' => $row->rev_timestamp,
+ 'ar_minor_edit' => $row->rev_minor_edit,
+ 'ar_rev_id' => $row->rev_id,
+ 'ar_parent_id' => $row->rev_parent_id,
+ 'ar_text_id' => $row->rev_text_id,
+ 'ar_text' => '',
+ 'ar_flags' => '',
+ 'ar_len' => $row->rev_len,
+ 'ar_page_id' => $id,
+ 'ar_deleted' => $bitfield,
+ 'ar_sha1' => $row->rev_sha1,
+ ];
+ if ( $wgContentHandlerUseDB ) {
+ $rowInsert['ar_content_model'] = $row->rev_content_model;
+ $rowInsert['ar_content_format'] = $row->rev_content_format;
+ }
+ $rowsInsert[] = $rowInsert;
+ }
+ // Copy them into the archive table
+ $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
// Save this so we can pass it to the ArticleDeleteComplete hook.
$archivedRevisionCount = $dbw->affectedRows();
$might_be_img = true;
$text = $m[2];
if ( strpos( $m[1], '%' ) !== false ) {
- $m[1] = rawurldecode( $m[1] );
+ $m[1] = str_replace( [ '<', '>' ], [ '<', '>' ], rawurldecode( $m[1] ) );
}
$trail = "";
} else { # Invalid form; output directly
$this->startParse( $title, $options, self::OT_WIKI, $clearState );
$this->setUser( $user );
- $text = str_replace( [ "\r\n", "\r" ], "\n", $text );
+ // We still normalize line endings for backwards-compatibility
+ // with other code that just calls PST, but this should already
+ // be handled in TextContent subclasses
+ $text = TextContent::normalizeLineEndings( $text );
+
if ( $options->getPreSaveTransform() ) {
$text = $this->pstPass2( $text, $user );
}
$text = preg_replace( $p2, '[[\\1]]', $text );
}
- # Trim trailing whitespace
- $text = rtrim( $text );
-
return $text;
}
$form->setAction( $this->getFullTitle()->getFullURL( $this->getPreservedParams() ) );
$form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
$form->addHiddenField( 'authAction', $this->authAction );
- $form->suppressDefaultSubmit( !$this->needsSubmitButton( $formDescriptor ) );
+ $form->suppressDefaultSubmit( !$this->needsSubmitButton( $requests ) );
return $form;
}
}
/**
- * Returns true if the form has fields which take values. If all available providers use the
- * redirect flow, the form might contain nothing but submit buttons, in which case we should
- * not add an extra submit button which does nothing.
+ * Returns true if the form built from the given AuthenticationRequests has fields which take
+ * values. If all available providers use the redirect flow, the form might contain nothing
+ * but submit buttons, in which case we should not add an extra submit button which does nothing.
*
- * @param array $formDescriptor A HTMLForm descriptor
+ * @param AuthenticationRequest[] $requests An array of AuthenticationRequests from which the
+ * form will be built
* @return bool
*/
- protected function needsSubmitButton( $formDescriptor ) {
- return (bool)array_filter( $formDescriptor, function ( $item ) {
- $class = false;
- if ( array_key_exists( 'class', $item ) ) {
- $class = $item['class'];
- } elseif ( array_key_exists( 'type', $item ) ) {
- $class = HTMLForm::$typeMappings[$item['type']];
+ protected function needsSubmitButton( array $requests ) {
+ foreach ( $requests as $req ) {
+ if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED &&
+ $this->doesRequestNeedsSubmitButton( $req )
+ ) {
+ return true;
}
- return !is_a( $class, \HTMLInfoField::class, true ) &&
- !is_a( $class, \HTMLSubmitField::class, true );
- } );
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the given AuthenticationRequest needs a submit button or not.
+ *
+ * @param AuthenticationRequest $req The request to check
+ * @return bool
+ */
+ protected function doesRequestNeedsSubmitButton( AuthenticationRequest $req ) {
+ foreach ( $req->getFieldInfo() as $field => $info ) {
+ if ( $info['type'] === 'button' ) {
+ return false;
+ }
+ }
+ return true;
}
/**
$this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook
// this will call onAuthChangeFormFields()
$formDescriptor = static::fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
- $this->postProcessFormDescriptor( $formDescriptor );
+ $this->postProcessFormDescriptor( $formDescriptor, $requests );
$context = $this->getContext();
if ( $context->getRequest() !== $this->getRequest() ) {
$form->setId( 'userlogin2' );
}
- // add pre/post text
- // header used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
- // should be above the error message but HTMLForm doesn't support that
- $form->addHeaderText( $fakeTemplate->get( 'header' ) );
-
- // FIXME the old form used this for error/warning messages which does not play well with
- // HTMLForm (maybe it could with a subclass?); for now only display it for signups
- // (where the JS username validation needs it) and alway empty
- if ( $this->isSignup() ) {
- // used by the mediawiki.special.userlogin.signup.js module
- $statusAreaAttribs = [ 'id' => 'mw-createacct-status-area' ];
- // $statusAreaAttribs += $msg ? [ 'class' => "{$msgType}box" ] : [ 'style' => 'display: none;' ];
- $form->addHeaderText( Html::element( 'div', $statusAreaAttribs ) );
- }
-
- // header used by MobileFrontend
- $form->addHeaderText( $fakeTemplate->get( 'formheader' ) );
-
- // blank signup footer for site customization
- if ( $this->isSignup() && $this->showExtraInformation() ) {
- // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise
- $signupendMsg = $this->msg( 'signupend' );
- $signupendHttpsMsg = $this->msg( 'signupend-https' );
- if ( !$signupendMsg->isDisabled() ) {
- $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
- ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
- $form->addPostText( Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ) );
- }
- }
-
// warning header for non-standard workflows (e.g. security reauthentication)
if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
$reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
$this->msg( $reauthMessage )->params( $this->getUser()->getName() )->parse() ) );
}
- if ( !$this->isSignup() && $this->showExtraInformation() ) {
- $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
- if ( $passwordReset->isAllowed( $this->getUser() ) ) {
- $form->addFooterText( Html::rawElement(
- 'div',
- [ 'class' => 'mw-ui-vform-field mw-form-related-link-container' ],
- Linker::link(
- SpecialPage::getTitleFor( 'PasswordReset' ),
- $this->msg( 'userlogin-resetpassword-link' )->escaped()
- )
- ) );
- }
-
- // Don't show a "create account" link if the user can't.
- if ( $this->showCreateAccountLink() ) {
- // link to the other action
- $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
- $linkq = $this->getReturnToQueryStringFragment();
- // Pass any language selection on to the mode switch link
- if ( $wgLoginLanguageSelector && $this->mLanguage ) {
- $linkq .= '&uselang=' . $this->mLanguage;
- }
-
- $loggedIn = $this->getUser()->isLoggedIn();
- $createOrLoginHtml = Html::rawElement( 'div',
- [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
- 'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
- ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
- . Html::element( 'a',
- [
- 'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
- 'href' => $linkTitle->getLocalURL( $linkq ),
- 'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
- 'tabindex' => 100,
- ],
- $this->msg(
- ( $this->getUser()->isLoggedIn() ?
- 'userlogin-createanother' :
- 'userlogin-joinproject'
- ) )->escaped()
- )
- );
- $form->addFooterText( $createOrLoginHtml );
- }
- }
-
$form->suppressDefaultSubmit();
$this->authForm = $form;
array $requests, array $fieldInfo, array &$formDescriptor, $action
) {
$coreFieldDescriptors = $this->getFieldDefinitions( $this->fakeTemplate );
- $specialFields = array_merge( [ 'extraInput', 'linkcontainer', 'entryError' ],
+ $specialFields = array_merge( [ 'extraInput' ],
array_keys( $this->fakeTemplate->getExtraInputDefinitions() ) );
// keep the ordering from getCoreFieldDescriptors() where there is no explicit weight
$formDescriptor[$fieldName] : [];
// remove everything that is not in the fieldinfo, is not marked as a supplemental field
- // to something in the fieldinfo, and is not a generic or B/C field or a submit button
+ // to something in the fieldinfo, is not B/C for the pre-AuthManager templates,
+ // and is not an info field or a submit button
if (
!isset( $fieldInfo[$fieldName] )
&& (
!isset( $coreField['baseField'] )
|| !isset( $fieldInfo[$coreField['baseField']] )
- ) && !in_array( $fieldName, $specialFields, true )
- && ( !isset( $coreField['type'] ) || $coreField['type'] !== 'submit' )
+ )
+ && !in_array( $fieldName, $specialFields, true )
+ && (
+ !isset( $coreField['type'] )
+ || !in_array( $coreField['type'], [ 'submit', 'info' ], true )
+ )
) {
$coreFieldDescriptors[$fieldName] = null;
continue;
* @return array
*/
protected function getFieldDefinitions( $template ) {
- global $wgEmailConfirmToEdit;
+ global $wgEmailConfirmToEdit, $wgLoginLanguageSelector;
$isLoggedIn = $this->getUser()->isLoggedIn();
$continuePart = $this->isContinued() ? 'continue-' : '';
$anotherPart = $isLoggedIn ? 'another-' : '';
- $expiration = $this->getRequest()->getSession()->getProvider()
- ->getRememberUserDuration();
+ $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
$expirationDays = ceil( $expiration / ( 3600 * 24 ) );
$secureLoginLink = '';
if ( $this->mSecureLoginUrl ) {
if ( $this->isSignup() ) {
$fieldDefinitions = [
+ 'statusarea' => [
+ // used by the mediawiki.special.userlogin.signup.js module for error display
+ // FIXME merge this with HTMLForm's normal status (error) area
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
+ 'weight' => -105,
+ ],
'username' => [
'label-message' => 'userlogin-yourname',
// FIXME help-message does not match old formatting
],
];
}
+
$fieldDefinitions['username'] += [
'type' => 'text',
'name' => 'wpName',
// 'required' => true,
];
+ if ( $template->get( 'header' ) || $template->get( 'formheader' ) ) {
+ // B/C for old extensions that haven't been converted to AuthManager (or have been
+ // but somebody is using the old version) and still use templates via the
+ // UserCreateForm/UserLoginForm hook.
+ // 'header' used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
+ // 'formheader' used by MobileFrontend
+ $fieldDefinitions['header'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $template->get( 'header' ) ?: $template->get( 'formheader' ),
+ 'weight' => - 110,
+ ];
+ }
if ( $this->mEntryError ) {
$fieldDefinitions['entryError'] = [
'type' => 'info',
'weight' => -100,
];
}
-
if ( !$this->showExtraInformation() ) {
- unset( $fieldDefinitions['linkcontainer'] );
+ unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
+ }
+ if ( $this->isSignup() && $this->showExtraInformation() ) {
+ // blank signup footer for site customization
+ // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
+ $signupendMsg = $this->msg( 'signupend' );
+ $signupendHttpsMsg = $this->msg( 'signupend-https' );
+ if ( !$signupendMsg->isDisabled() ) {
+ $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
+ $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
+ ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
+ $fieldDefinitions['signupend'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
+ 'weight' => 225,
+ ];
+ }
+ }
+ if ( !$this->isSignup() && $this->showExtraInformation() ) {
+ $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
+ if ( $passwordReset->isAllowed( $this->getUser() ) ) {
+ $fieldDefinitions['passwordReset'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'cssclass' => 'mw-form-related-link-container',
+ 'default' => Linker::link(
+ SpecialPage::getTitleFor( 'PasswordReset' ),
+ $this->msg( 'userlogin-resetpassword-link' )->escaped()
+ ),
+ 'weight' => 230,
+ ];
+ }
+
+ // Don't show a "create account" link if the user can't.
+ if ( $this->showCreateAccountLink() ) {
+ // link to the other action
+ $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
+ $linkq = $this->getReturnToQueryStringFragment();
+ // Pass any language selection on to the mode switch link
+ if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+ $linkq .= '&uselang=' . $this->mLanguage;
+ }
+ $loggedIn = $this->getUser()->isLoggedIn();
+
+ $fieldDefinitions['createOrLogin'] = [
+ 'type' => 'info',
+ 'raw' => true,
+ 'linkQuery' => $linkq,
+ 'default' => function ( $params ) use ( $loggedIn, $linkTitle ) {
+ return Html::rawElement( 'div',
+ [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
+ 'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
+ ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
+ . Html::element( 'a',
+ [
+ 'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
+ 'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
+ 'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
+ 'tabindex' => 100,
+ ],
+ $this->msg(
+ $loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
+ )->escaped()
+ )
+ );
+ },
+ 'weight' => 235,
+ ];
+ }
}
$fieldDefinitions = $this->getBCFieldDefinitions( $fieldDefinitions, $template );
/**
* @param array $formDescriptor
*/
- protected function postProcessFormDescriptor( &$formDescriptor ) {
+ protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
// Pre-fill username (if not creating an account, T46775).
if (
isset( $formDescriptor['username'] ) &&
// don't show a submit button if there is nothing to submit (i.e. the only form content
// is other submit buttons, for redirect flows)
- if ( !$this->needsSubmitButton( $formDescriptor ) ) {
+ if ( !$this->needsSubmitButton( $requests ) ) {
unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
}
return $form;
}
- protected function needsSubmitButton( $formDescriptor ) {
+ protected function needsSubmitButton( array $requests ) {
// Change/remove forms show are built from a single AuthenticationRequest and do not allow
// for redirect flow; they always need a submit button.
return true;
} else {
$out->addWikiMsg( 'accountcreatedtext', $user->getName() );
}
- $out->addReturnTo( $this->getPageTitle() );
+
+ $rt = Title::newFromText( $this->mReturnTo );
+ $out->addReturnTo(
+ ( $rt && !$rt->isExternal() ) ? $rt : $this->getPageTitle(),
+ wfCgiToArray( $this->mReturnToQuery )
+ );
return;
}
}
'buttontype' => 'submit',
'buttonname' => 'addcat',
'buttondefault' => $this->msg( 'export-addcat' )->text(),
+ 'hide-if' => [ '===', 'exportall', '1' ],
],
];
if ( $config->get( 'ExportFromNamespaces' ) ) {
'buttontype' => 'submit',
'buttonname' => 'addns',
'buttondefault' => $this->msg( 'export-addns' )->text(),
+ 'hide-if' => [ '===', 'exportall', '1' ],
],
];
}
'nodata' => true,
'rows' => 10,
'default' => $page,
+ 'hide-if' => [ '===', 'exportall', '1' ],
],
];
$uiCode = $this->getLanguage()->getCode();
$proposed = $base->getSubpage( $uiCode );
- if ( $uiCode !== $this->getConfig()->get( 'LanguageCode' ) && $proposed && $proposed->exists() ) {
+ if ( $proposed && $proposed->exists() && $uiCode !== $base->getPageLanguage()->getCode() ) {
return $proposed;
} elseif ( $provided && $provided->exists() ) {
return $provided;
public function execute( $par = '' ) {
$this->getOutput()->disable();
-
if ( wfReadOnly() ) {
// HTTP 423 Locked
HttpStatus::header( 423 );
public function getRights() {
if ( is_null( $this->mRights ) ) {
$this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+ Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
// Deny any rights denied by the user's session, unless this
// endpoint has no sessions.
}
}
- Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
// Force reindexation of rights when a hook has unset one of them
$this->mRights = array_values( array_unique( $this->mRights ) );
+
+ // If block disables login, we should also remove any
+ // extra rights blocked users might have, in case the
+ // blocked user has a pre-existing session (T129738).
+ // This is checked here for cases where people only call
+ // $user->isAllowed(). It is also checked in Title::checkUserBlock()
+ // to give a better error message in the common case.
+ $config = RequestContext::getMain()->getConfig();
+ if (
+ $this->isLoggedIn() &&
+ $config->get( 'BlockDisablesLogin' ) &&
+ $this->isBlocked()
+ ) {
+ $anon = new User;
+ $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
+ }
}
return $this->mRights;
}
$noPass = PasswordFactory::newInvalidPassword()->toString();
$dbw = wfGetDB( DB_MASTER );
- $inWrite = $dbw->writesOrCallbacksPending();
$seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
$dbw->insert( 'user',
[
[ 'IGNORE' ]
);
if ( !$dbw->affectedRows() ) {
- // The queries below cannot happen in the same REPEATABLE-READ snapshot.
- // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
- if ( $inWrite ) {
- // Can't commit due to pending writes that may need atomicity.
- // This may cause some lock contention unlike the case below.
- $options = [ 'LOCK IN SHARE MODE' ];
- $flags = self::READ_LOCKING;
- } else {
- // Often, this case happens early in views before any writes when
- // using CentralAuth. It's should be OK to commit and break the snapshot.
- $dbw->commit( __METHOD__, 'flush' );
- $options = [];
- $flags = self::READ_LATEST;
- }
- $this->mId = $dbw->selectField( 'user', 'user_id',
- [ 'user_name' => $this->mName ], __METHOD__, $options );
+ // Use locking reads to bypass any REPEATABLE-READ snapshot.
+ $this->mId = $dbw->selectField(
+ 'user',
+ 'user_id',
+ [ 'user_name' => $this->mName ],
+ __METHOD__,
+ [ 'LOCK IN SHARE MODE' ]
+ );
$loaded = false;
if ( $this->mId ) {
- if ( $this->loadFromDatabase( $flags ) ) {
+ if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
$loaded = true;
}
}
* @file
* @ingroup Maintenance
*/
+use \MediaWiki\MediaWikiServices;
+
class BatchRowWriter {
/**
* @var IDatabase $db The database to write to
* names to update values to apply to the row.
*/
public function write( array $updates ) {
- $this->db->begin();
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
foreach ( $updates as $update ) {
$this->db->update(
);
}
- $this->db->commit();
- wfGetLBFactory()->waitForReplication();
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
}
}
* xin n (month number) in Iranian calendar
* xiy y (two digit year) in Iranian calendar
* xiY Y (full year) in Iranian calendar
+ * xit t (days in month) in Iranian calendar
*
* xjj j (day number) in Hebrew calendar
* xjF F (month name) in Hebrew calendar
}
$num = substr( $iranian[0], -2 );
break;
+ case 'xit':
+ $usedIranianYear = true;
+ if ( !$iranian ) {
+ $iranian = self::tsToIranian( $ts );
+ }
+ $num = self::$IRANIAN_DAYS[$iranian[1] - 1];
+ break;
case 'a':
$usedAMPM = true;
$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
"passwordreset-emailtext-user": "Gebruiker $1 op die webtuiste {{SITENAME}} het u gebruikersgegewens vir {{SITENAME}} ($4) opgevra.\nDie volgende {{PLURAL:$3|gebruiker is|gebruikers is}} aan die e-posadres gekoppel:\n\n$2\n\n{{PLURAL:$3|Die tydelike wagwoord verval|Hierdie tydelike wagwoorde verval}} oor {{PLURAL:$5|een dag|$5 dae}}.\nMeld asseblief aan en verander u wagwoord nou. As u dit nie versoek het nie, of as u die oorspronklike wagwoord nog ken en dit nie wil verander nie, ignoreer die berig en hou aan om u ou wagwoord te gebruik.",
"passwordreset-emailelement": "Gebruikersnaam: \n$1\n\nTydelike wagwoord: \n$2",
"passwordreset-emailsentemail": "'n E-pos is gestuur om u wagwoord te herstel.",
- "passwordreset-emailsent-capture": "'n E-pos vir die herstel van 'n wagwoord is gestuur. Dit word hieronder vertoon.",
- "passwordreset-emailerror-capture": "'n E-pos vir die herstel van 'n wagwoord is saamgestel. Dit word hieronder vertoon. Die uitstuur daarvan na die {{GENDER:$2|gebruiker}} het egter gefaal: $1",
"changeemail": "Wysig E-posadres",
"changeemail-header": "Wysig rekening se e-posadres",
"changeemail-no-info": "U moet aangemeld wees om regstreeks toegang tot die bladsy te kry.",
"undo-nochange": "Die wysiging is klaarblyklik reeds teruggerol.",
"undo-summary": "Rol weergawe $1 deur [[Special:Contributions/$2|$2]] ([[User talk:$2|bespreek]]) terug.",
"undo-summary-username-hidden": "Rol weergawe $1 deur 'n versteekte gebruiker terug",
- "cantcreateaccounttitle": "Kan nie rekening skep nie",
"cantcreateaccount-text": "Die registrasie van nuwe rekeninge vanaf die IP-adres ('''$1''') is geblok deur [[User:$3|$3]].\n\nDie rede verskaf deur $3 is ''$2''",
"viewpagelogs": "Bekyk logboeke vir hierdie bladsy",
"nohistory": "Daar is geen wysigingsgeskiedenis vir hierdie bladsy nie.",
"mediastatistics-header-total": "Alle lêers",
"json-error-syntax": "Sintaksfout",
"headline-anchor-title": "Skakel na die afdeling",
- "special-characters-group-latin": "Latyns",
- "special-characters-group-latinextended": "Latyns uitgebreid",
+ "special-characters-group-latin": "Latyn",
+ "special-characters-group-latinextended": "Latyn uitgebrei",
"special-characters-group-ipa": "IFA",
"special-characters-group-symbols": "Simbole",
"special-characters-group-greek": "Grieks",
+ "special-characters-group-greekextended": "Grieks uitgebrei",
"special-characters-group-cyrillic": "Cyrillies",
"special-characters-group-arabic": "Arabies",
"special-characters-group-arabicextended": "Arabies uitgebrei",
"special-characters-group-gujarati": "Gujarati",
"special-characters-group-devanagari": "Devanagari",
"special-characters-group-thai": "Thai",
- "special-characters-group-lao": "Lao",
+ "special-characters-group-lao": "Laosiaans",
"special-characters-group-khmer": "Khmer",
"special-characters-title-minus": "minusteken",
"mw-widgets-dateinput-no-date": "Geen datum gekies nie",
"activeusers-hidebots": "أخف البوتات",
"activeusers-hidesysops": "أخف الإداريين",
"activeusers-noresult": "لم يعثر على أي مستخدمين",
- "activeusers-submit": "لعرض المستخدمين النشطين",
+ "activeusers-submit": "عرض المستخدمين النشطين",
"listgrouprights": "صلاحيات مجموعات المستخدمين",
"listgrouprights-summary": "التالي قائمة بمجموعات المستخدمين المعرفة في هذا الويكي، بصلاحياتهم المصاحبة.\nربما تكون هناك [[{{MediaWiki:Listgrouprights-helppage}}|معلومات إضافية]] حول الصلاحيات المنفردة.",
"listgrouprights-key": "عنوان:\n* <span class=\"listgrouprights-granted\">صلاحية ممنوحة</span>\n* <span class=\"listgrouprights-revoked\">صلاحية مسحوبة</span>",
"tagline": "Зьвесткі з {{GRAMMAR:родны|{{SITENAME}}}}",
"help": "Дапамога",
"search": "Пошук",
- "search-ignored-headings": " #<!-- не зьмяняйце гэты радок --> <pre>\n# Загалоўкі, якія мусіць ігнараваць пошукавы рухавік.\n# Зьмены будуць ужытыя па наступным індэксаваньні старонкі.\n# Вы можаце змусіць пераіндэксаваць старонку пустым рэдагаваньнем.\n# Сынтакс наступны:\n# * Усё, што пачынаецца з \"#\" — камэнтар\n# * Усякі непусты радок — загаловак, які трэба ігнараваць\nКрыніцы\nВонкавыя спасылкі\nГлядзіце таксама\n #</pre> <!-- не зьмяняйце гэты радок -->",
+ "search-ignored-headings": " #<!-- не зьмяняйце гэты радок --> <pre>\n# Загалоўкі, якія мусіць ігнараваць пошукавы рухавік.\n# Зьмены будуць ужытыя па наступным індэксаваньні старонкі.\n# Вы можаце змусіць пераіндэксаваць старонку пустым рэдагаваньнем.\n# Сынтакс наступны:\n# * Усё, што пачынаецца з «#» і да канца радку — камэнтар\n# * Усякі непусты радок — загаловак, які трэба ігнараваць\nКрыніцы\nВонкавыя спасылкі\nГлядзіце таксама\n #</pre> <!-- не зьмяняйце гэты радок -->",
"searchbutton": "Пошук",
"go": "Старонка",
"searcharticle": "Старонка",
"grant-group-high-volume": "Выкананьне дзеяньняў з высокай інтэнсіўнасьцю",
"grant-group-customization": "Налады і перавагі",
"grant-group-administration": "Выкананьне адміністрацыйных дзеяньняў",
+ "grant-group-private-information": "Доступ да прыватных зьвестак пра вас",
"grant-group-other": "Розная актыўнасьць",
"grant-blockusers": "Блякаваньне і разблякаваньне ўдзельнікаў",
"grant-createaccount": "Стварыць рахункі",
"grant-highvolume": "Рэдагаваньне з высокай інтэнсіўнасьцю",
"grant-oversight": "Хаваньне ўдзельнікаў і вэрсіяў старонак",
"grant-patrol": "Патруляваньне зьменаў старонак",
+ "grant-privateinfo": "Доступ да прыватных зьвестак",
"grant-protect": "Абарона і зьняцьце абароны старонак",
"grant-rollback": "Адкат зьменаў старонак",
"grant-sendemail": "Адпраўка лістоў электроннай пошты іншым удзельнікам",
"uploadstash-errclear": "Не атрымалася ачысьціць файлы.",
"uploadstash-refresh": "Абнавіць сьпіс файлаў.",
"uploadstash-thumbnail": "прагляд мініятуры",
+ "uploadstash-exception": "Не магу захаваць загрузку ў сховішчы ($1): «$2».",
"invalid-chunk-offset": "Няслушнае зрушэньне фрагмэнту",
"img-auth-accessdenied": "Доступ забаронены",
"img-auth-nopathinfo": "Адсутнічае PATH_INFO.\nВаш сэрвэр не ўстаноўлены на пропуск гэтай інфармацыі.\nМагчма, ён працуе праз CGI і не падтрымлівае img_auth.\nГлядзіце https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
"undeletedrevisions": "{{PLURAL:$1|адноўленая $1 вэрсія|адноўленыя $1 вэрсіі|адноўленыя $1 вэрсіяў}}",
"undeletedrevisions-files": "адноўленыя $1 {{PLURAL:$1|вэрсія|вэрсіі|вэрсіяў}} і $2 {{PLURAL:$2|файл|файлы|файлаў}}",
"undeletedfiles": "{{PLURAL:$1|адноўлены $1 файл|адноўленыя $1 файлы|адноўленыя $1 файлаў}}",
- "cannotundelete": "Ð\9fамÑ\8bлка аднаÑ\9eленÑ\8cня:\n$1",
+ "cannotundelete": "Ð\9dекаÑ\82оÑ\80Ñ\8bÑ\8f або Ñ\9eÑ\81е аднаÑ\9eленÑ\8cнÑ\96 не бÑ\8bлÑ\96 вÑ\8bкананÑ\8bя:\n$1",
"undeletedpage": "'''Старонка $1 была адноўленая'''\n\nГлядзіце [[Special:Log/delete|журнал выдаленьняў]] для прагляду апошніх выдаленьняў і аднаўненьняў.",
"undelete-header": "Глядзіце [[Special:Log/delete|журнал выдаленьняў]] для прагляду апошніх выдаленьняў.",
"undelete-search-title": "Пошук выдаленых старонак",
"sp-contributions-newbies-sub": "Унёсак пачынаючых",
"sp-contributions-newbies-title": "Унёсак удзельнікаў з новых рахункаў",
"sp-contributions-blocklog": "журнал блякаваньняў",
- "sp-contributions-suppresslog": "выдалены ўнёсак удзельніка",
- "sp-contributions-deleted": "выдалены ўнёсак удзельніка",
+ "sp-contributions-suppresslog": "выдалены ўнёсак {{GENDER:$1|удзельніка|удзельніцы}}",
+ "sp-contributions-deleted": "выдалены ўнёсак {{GENDER:$1|удзельніка|удзельніцы}}",
"sp-contributions-uploads": "загрузкі",
"sp-contributions-logs": "журналы падзеяў",
"sp-contributions-talk": "гутаркі",
"log-action-filter-import": "Тып імпарту:",
"log-action-filter-move": "Тып пераносу:",
"log-action-filter-all": "Усе",
+ "log-action-filter-block-block": "Заблякаваць",
+ "log-action-filter-block-reblock": "Зьмяненьне блякаваньня",
+ "log-action-filter-block-unblock": "Разблякаваць",
+ "log-action-filter-contentmodel-change": "Зьмена мадэлі зьместу",
+ "log-action-filter-delete-delete": "Выдаленьне старонкі",
+ "log-action-filter-delete-restore": "Аднаўленьне старонкі",
+ "log-action-filter-delete-event": "Выдаленьне журналу",
+ "log-action-filter-delete-revision": "Выдаленьне вэрсіі",
+ "log-action-filter-managetags-create": "Стварэньне цэтлікаў",
+ "log-action-filter-managetags-delete": "Выдаленьне цэтлікаў",
+ "log-action-filter-managetags-activate": "Актывацыя цэтлікаў",
+ "log-action-filter-managetags-deactivate": "Дэактывацыя цэтлікаў",
+ "log-action-filter-newusers-autocreate": "Аўтаматычнае стварэньне",
+ "log-action-filter-patrol-autopatrol": "Аўтаматычнае патруляваньне",
+ "log-action-filter-protect-protect": "Абарона",
+ "log-action-filter-protect-unprotect": "Зьняцьце абароны",
+ "log-action-filter-rights-autopromote": "Аўтаматычнае зьмяненьне",
+ "log-action-filter-upload-upload": "Новая загрузка",
+ "authmanager-realname-label": "Сапраўднае імя",
+ "authmanager-provider-temporarypassword": "Часовы пароль",
"changecredentials": "Зьмена ўліковых зьвестак",
"removecredentials": "Выдаленьне ўліковых зьвестак",
"removecredentials-submit": "Выдаліць уліковыя зьвесткі",
"tagline": "З пляцоўкі {{SITENAME}}",
"help": "Даведка",
"search": "Знайсці",
+ "search-ignored-headings": " #<!-- не змяняйце гэты радок --> <pre>\n# Загалоўкі, якія будзе ігнараваць рухавік пошуку.\n# Змены набудуць моц па наступным індэксаванні старонкі.\n# Вы можаце змусіць пераіндэксаванне старонкі, зрабіўшы пустое рэдагаванне.\n# Сінтаксіс наступны:\n# * Усё ад сімвала \"#\" да канца радка - каментарый.\n# * Кожны непусты радок - дакладны загаловак, які трэба ігнараваць, з рэгістрам і інш.\nКрыніцы\nСпасылкі\nГл. таксама\n #</pre> <!-- не змяняйце гэты радок -->",
"searchbutton": "Знайсці",
"go": "Пераход",
"searcharticle": "Артыкул",
"nocookiesnew": "Рахунак быў створаны, але ў сістэму вы не ўвайшлі. {{SITENAME}} карыстаецца квіткамі (кукі), каб апрацоўваць уваходы ўдзельнікаў, а гэтая функцыянальнасць адключана ў вашым браўзеры. Уключыце квіткі ў браўзеры, тады ўваходзьце са сваімі новымі імем удзельніка і паролем.",
"nocookieslogin": "{{SITENAME}} карыстаецца квіткамі (кукі), каб пазнаваць удзельнікаў. У вашым браўзеры квіткі не дазволены. Дазвольце іх працу і паспрабуйце ізноў.",
"nocookiesfornew": "Уліковы запіс карыстальніка не быў створаны, бо мы не змаглі пацвердзіць яго крыніцы. \nУпэўніцеся, што кукі ўключаныя, абнавіце старонку і паспрабуйце яшчэ раз.",
- "createacct-loginerror": "Уліковы запіс быў паспяхова створаны, але Вы не змаглі аўтарызавацц аўтаматычна. Калі ласка, перайдзіце да старонкі [[Адмысловае:Імя_ўдзельніка|ручной аўтарызацыі]].",
+ "createacct-loginerror": "Уліковы запіс быў паспяхова створаны, але Вы не змаглі аўтарызавацца аўтаматычна. Калі ласка, перайдзіце да старонкі [[Special:UserLogin|ручной аўтарызацыі]].",
"noname": "Вы не вызначылі правільнага імя ўдзельніка.",
"loginsuccesstitle": "Паспяховы ўваход у сістэму",
"loginsuccess": "<strong>Цяпер Вы ўвайшлі на {{SITENAME}} як \"$1\".</strong>",
"passwordreset-emailelement": "Імя ўдзельніка: \n$1\n\nЧасовы пароль: \n$2",
"passwordreset-emailsentemail": "Калі гэты адрас электроннай пошты злучаны з вашым уліковым запісам, будзе адпраўлены ліст пра скід пароля.",
"passwordreset-emailsentusername": "Калі ёсць адрас электроннай пошты, злучаны з гэтым імем удзельніка, то будзе дасланы ліст пра скід пароля.",
+ "passwordreset-emailsent-capture2": "{{PLURAL:$1|Электронны ліст|электронныя лісты}} скіду пароля адпраўлены. {{PLURAL:$1|Імя ўдзельніка і пароль|Спіс імён удзельнікаў і паролі}} паказаны ніжэй.",
"passwordreset-invalideamil": "Няслушны адрас электроннай пошты",
"passwordreset-nodata": "Не былі пададзены ні імя ўдзельніка, ні адрас электроннай пошты",
"changeemail": "Змяніць або выдаліць адрас электроннай пошты",
"lineno": "Xeta $1:",
"compareselectedversions": "Rewizyonanê weçineyan pêver ke",
"showhideselectedversions": "Revizyonanê weçinıtan bımocne/bınımne",
- "editundo": "peyser bıgê",
+ "editundo": "peyser bıya",
"diff-empty": "(Babetna niyo)",
"diff-multi-sameuser": "(Terefê eyni karberi ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})",
"diff-multi-otherusers": "(Terefê {{PLURAL:$2|yew karberi|$2 karberan}} ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})",
"versionrequiredtext": "ये पाना प्रयोग गर्नका लागि MediaWiki $1 संस्करण चाहिन्छ ।\nहेर [[Special:Version|version page]]",
"ok": "भयो",
"retrievedfrom": " \"$1\" बठे निकालिया",
- "youhavenewmessages": "तमखी लेखा($3)मी $1 ($2) छ ।",
+ "youhavenewmessages": "तमखी लेखा($3)मी $1($2) छ ।",
"youhavenewmessagesfromusers": "तमखी लेखा {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्तान}}($2)बठे$1",
"youhavenewmessagesmanyusers": "तमलाई धेरै प्रयोगकर्ताहरू($2) बठे $1 छ ।",
"newmessageslinkplural": "{{PLURAL:$1|एक नौलो रैबार|999=नौला रैबारहरू}}",
"title-invalid-interwiki": "अनुरोध गरियाको शिर्षकमी अन्तर विकि लिङ्क छ जइलाई शिर्षकमी प्रयोग गद्द नाइपाइनो ।",
"title-invalid-talk-namespace": "निवेदन गरियाको पानाको शिर्षकले उपलब्ध नभएका कुरडी पानालाई सन्दर्भको रूपमी राख्याको छ ।",
"title-invalid-characters": "निवेदन गरियाको यै पानाको शिर्षकमी अवैध अक्षर रयाको छः \"$1\" ।",
+ "title-invalid-leading-colon": "निवेदन गरिया पृष्ठको शिर्षकको शुरूमी अवैध कोलोन रया छ ।",
+ "perfcached": "तलका डाटाहरू क्याचमी रया कुराहरू हुन्। अपटुडेट नहुन लाई सक्दान। बर्ति {{PLURAL:$1|नतिजा|$1 नतिजाहरू}} क्याचमी उपलब्ध छ।",
+ "perfcachedts": "तलतिरको आँकडा क्याच हो र $1 पहिला अद्यतन गरिया थ्यो। येई क्याचमी उपलब्ध {{PLURAL:$4|एउटा कारण हो|$4 कारणहरू हुन्}}।",
"viewsource": "स्रोत हेर",
"viewsource-title": " $1 को स्रोत हेर",
"actionthrottled": "कार्य रोकिईयो",
"prefs-rc": "नौला परिवर्तनहरू",
"prefs-watchlist": "मेरो ध्यान सूची",
"prefs-editwatchlist": "अवलोकनसूची सम्पादन",
+ "prefs-editwatchlist-label": "आफना अवलोकनसूचीमी रया इन्ट्रीलाई सम्पादन गर:",
+ "prefs-editwatchlist-edit": "आफना अवलोकनसूचीमी रया शीर्षकलाई धेकाउन्या तथा हटाउन्या",
"prefs-editwatchlist-raw": "कच्चा अवलोकनसूची सम्पादन गद्दा",
"prefs-editwatchlist-clear": "तमरो अवलोकनसूची मेटा",
"prefs-watchlist-days": "ध्यान सूचीमी धेकाउने दिनहरू:",
+ "prefs-watchlist-days-max": "भौत $1 {{PLURAL:$1|दिन|दिन}}",
+ "prefs-watchlist-edits": "उच्चतम परिवर्तन संख्या बढाइएको निगरानी सूचीमी धकाउनका लागि :",
"prefs-watchlist-edits-max": "सबै है ज्यादा संख्या : १०००",
"prefs-watchlist-token": "अवलोकन सूची टोकन:",
"prefs-misc": "साधारण",
"prefs-resetpass": "पासवर्ड परिवर्तन गर",
- "prefs-changeemail": "à¤\87मà¥\87ल परिवरà¥\8dतन à¤\97रà¥\8dनà¥\8dया",
+ "prefs-changeemail": "à¤\87मà¥\87ल ठà¥\87à¤\97ाना बदà¥\87ल वा हà¤\9fा",
"prefs-setemail": "इमेल ठेगाना प्रविष्ट गर्न्या",
"prefs-email": "इमेल विकल्पहरू",
"prefs-rendering": "स्वरुप",
"saveprefs": "संग्रह",
+ "restoreprefs": "सबै पूर्वनिर्धारित स्थिती कायम गर्ने(सबै खण्डहरूमी)",
"prefs-editing": "सम्पादन",
"rows": "हरफहरू :",
"columns": "स्तम्भहरू :",
"right-sendemail": "अन्य प्रयोगकर्तानलाई इमेल पठाउन्या",
"grant-editmycssjs": "तमरो प्रयोगकर्ता CSS/JavaScript सम्पादन गर",
"grant-editmyoptions": "तमरा प्रयोगकर्ता अभिरूचीहरूलाई सम्पादन गर",
+ "grant-editmywatchlist": "तमरो अवलोकनसूची सम्पादन गर",
+ "grant-editpage": "भैरया पृष्ठहरू सम्पादन गर",
+ "grant-editprotected": "सुरक्षित पृष्ठ सम्पादन",
+ "grant-highvolume": "उच्च मात्रा सम्पादन",
+ "grant-basic": "आधारभूत अधिकार",
+ "grant-viewdeleted": "नयाँ फाइलहरू अपलोड\nसम्पादन गर",
+ "grant-viewmywatchlist": "आफनो अबलोकन सुची हेर",
"newuserlogpage": "प्रयोगकर्ता श्रृजना लग",
"action-move-subpages": "यै पानाको रे यैका उपपानाको नाम बदल्न्या",
"action-unwatchedpages": "कसैले ध्यान नराख्याका पाननको सूची हेद्या",
"linkaccounts-submit": "Link accounts",
"unlinkaccounts": "Unlink accounts",
"unlinkaccounts-success": "The account was unlinked.",
- "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?"
+ "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?",
+ "userjsispublic": "Please note: JavaScript subpages should not contain confidential data as they are viewable by other users.",
+ "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users."
}
"actionfailed": "Ago malsukcesis",
"deletedtext": "\"$1\" estas forigita.\nVidu la paĝon $2 por registro de lastatempaj forigoj.",
"dellogpage": "Protokolo pri forigoj",
- "dellogpagetext": "Jen listo de la plej lastaj forigoj el la datumaro.\nĈiuj tempoj sekvas la horzonon UTC.",
+ "dellogpagetext": "Jen listo de la plej lastaj forigoj.",
"deletionlog": "protokolo pri forigoj",
"reverted": "Malfaris al antaŭa revisio",
"deletecomment": "Kialo:",
"undo-success": "این ویرایش را میتوان خنثی کرد.\nلطفاً تفاوت زیر را بررسی کنید تا تأیید کنید که این چیزی است که میخواهید انجام دهید، سپس تغییرات زیر را ذخیره کنید تا خنثیسازی ویرایش را به پایان ببرید.",
"undo-failure": "به علت تعارض با ویرایشهای میانی، این ویرایش را نمیتوان خنثی کرد.",
"undo-norev": "این ویرایش را نمیتوان خنثی کرد چون وجود ندارد یا حذف شدهاست.",
- "undo-nochange": "به نظر میرسد ویرایش از پیش واگردانی شده است.",
+ "undo-nochange": "به نظر میرسد ویرایش از پیش خنثیسازی شده است.",
"undo-summary": "خنثیسازی ویرایش $1 توسط [[Special:Contributions/$2|$2]] ([[User talk:$2|بحث]])",
"undo-summary-username-hidden": "خنثیسازی نسخهٔ $1 به دست یک کاربر پنهانشده",
"cantcreateaccount-text": "امكان ساختن حساب کاربری از این این نشانی آیپی ('''$1''') توسط [[User:$3|$3]] سلب شده است.\n\nدلیل ارائه شده توسط $3 چنین است: $2",
"shortpages": "Pages courtes",
"longpages": "Pages longues",
"deadendpages": "Pages en impasse",
- "deadendpagestext": "Les pages suivantes ne contiennent aucun lien vers d'autres pages du wiki.",
+ "deadendpagestext": "Les pages suivantes ne contiennent aucun lien vers d'autres pages dans le wiki {{SITENAME}}.",
"protectedpages": "Pages protégées",
"protectedpages-indef": "Uniquement les protections indéfinies",
"protectedpages-summary": "Cette page liste les pages existantes actuellement protégées. Pour une liste des titres protégés contre la création, voir [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
"move": "Renommer",
"movethispage": "Renommer cette page",
"unusedimagestext": "Les fichiers suivants existent, mais ne sont inclus dans aucune page.\nVeuillez noter que d’autres sites peuvent accéder à ces fichiers à l’aide de liens directs (URLs), et donc qu’un fichier peut être listé ici alors qu’il est utilisé par ces sites.",
- "unusedcategoriestext": "Les catégories suivantes existent mais aucune page ou catégorie ne les utilise.",
+ "unusedcategoriestext": "Les pages de catégories suivantes existent, mais aucune page ou catégorie ne les utilise.",
"notargettitle": "Pas de cible",
"notargettext": "Vous n'avez pas indiqué une page ou un utilisateur sur lequel vous souhaitez effectuer cette action.",
"nopagetitle": "Page cible inexistante",
"querypage-disabled": "Cette page spéciale est désactivée pour des raisons de performances.",
"apihelp": "Aide de l’API",
"apihelp-no-such-module": "Le module « $1 » est introuvable.",
- "apisandbox": "Bac à sable API",
+ "apisandbox": "Bac à sable de l'API",
"apisandbox-jsonly": "Le bac à sable de l'API nécessite JavaScript",
"apisandbox-api-disabled": "L'API est désactivé sur ce site.",
"apisandbox-intro": "Utilisez cette page pour expérimenter l’<strong>API webservice de MediaWiki</strong>.\nReportez-vous à [[mw:API:Main page|la documentation de l’API]] pour plus de détails sur l’utilisation de l’API. Exemple: [https://www.mediawiki.org/wiki/API#A_simple_example obtenir le contenu d'une page principale]. Choisissez une option pour voir d'autres exemples.",
"emailccsubject": "Copie de votre message à $1 : $2",
"emailsent": "Courriel envoyé",
"emailsenttext": "Votre message a été envoyé par courriel.",
- "emailuserfooter": "Ce courriel a été envoyé par « $1 » à « $2 » par la fonction « {{int:emailuser}} » de {{SITENAME}}.",
- "usermessage-summary": "A laissé un message système.",
+ "emailuserfooter": "Ce courriel a été {{GENDER:$1|envoyé}} par « $1 » à « {{GENDER:$2|$2}} » par la fonction « {{int:emailuser}} » de {{SITENAME}}.",
+ "usermessage-summary": "Laisser un message système.",
"usermessage-editor": "Messager du système",
"watchlist": "Liste de suivi",
"mywatchlist": "Liste de suivi",
"unwatch": "Ne plus suivre",
"unwatchthispage": "Ne plus suivre",
"notanarticle": "Ce n'est pas une page de contenu",
- "notvisiblerev": "La version a été supprimée",
+ "notvisiblerev": "La dernière version relue par un utilisateur différent, a été supprimée",
"watchlist-details": "{{PLURAL:$1|$1 page|$1 pages}} dans votre liste de suivi, sans compter les pages de discussion.",
"wlheader-enotif": "La notification par courriel est activée.",
- "wlheader-showupdated": "Les pages qui ont été modifiées depuis votre dernière visite sont affichées en '''gras'''.",
+ "wlheader-showupdated": "Les pages qui ont été modifiées depuis votre dernière visite sont affichées en <strong>gras</strong>.",
"wlnote": "Ci-dessous {{PLURAL:$1|figure la dernière modification effectuée|figurent les <strong>$1</strong> dernières modifications effectuées}} durant {{PLURAL:$2|la dernière heure|les <strong>$2</strong> dernières heures}}, jusqu'au $3, $4.",
"wlshowlast": "Montrer les dernières $1 heures, les derniers $2 jours",
"watchlist-hide": "Masquer",
"listingcontinuesabbrev": "samb.",
"index-category": "Kaca kaindhèksan",
"noindex-category": "Kaca ora kaindhèksan",
- "broken-file-category": "Kaca mawa pranala berkas rusak",
+ "broken-file-category": "Kaca mawa pranala barkas rusak",
"about": "Bab",
"article": "Kaca isi",
"newwindow": "(buka mawa jendhéla anyar)",
"no-null-revision": "Ora isa nggawe revisi 'null' anyar kanggo kaca \"$1\"",
"badtitle": "Sesirah ala",
"badtitletext": "Sesirahing kaca sing dikarepaké ora sah, suwung, utawa salah nggayut nyang sesirah antarabasa utawa antarawiki.\nIku mungkin ngandhut pralambang siji utawa luwih sing ora kena dianggo tumrap sesirah iki.",
- "perfcached": "Data iki mung dijupuk saka papan singgahan lan mungkin ora kaanyaran. Maksimum {{PLURAL:$1|sak asil|$1 asil}} sumadhiya nèng papan singgahan.",
- "perfcachedts": "Data iki mung dijupuk saka papan singgahan lan mungkin dianyari pungkasan $1. Maksimum {{PLURAL:$4|sak asil|$4 asil}} sumadhiya nèng papan singgahan.",
+ "perfcached": "Data ing ngisor iki kasimpen ing telih lan mungkin durung dianyari. Paling akèh ana {{PLURAL:$1|sakasil|$1 kasil}} sumadhiya ing telih iku.",
+ "perfcachedts": "Data ing ngisor iki kasimpen ing telih, lan pungkasan dianyari $1. Paling akèh ana {{PLURAL:$4|sakasil|$4 kasil}} sumadhiya ing telih iku.",
"querypage-no-updates": "Update saka kaca iki lagi dipatèni. Data sing ana ing kéné saiki ora bisa bakal dibalèni unggah manèh.",
"viewsource": "Deleng sumber",
"viewsource-title": "Delok sumberé $1",
"anoneditwarning": "<strong>Penget:</strong> Panjenengan boten mlebet log. Alamat IP Panjenengan badhe katingal dening publik manawi Panjenengan ngayahi ewah-ewahan. Manawi Panjenengan <strong>[$1 mlebet log]</strong> utawai <strong>[$2 damel akun]</strong>, suntingan Panjenengan badhe kaatribusekaken dhumateng nama pangangge Panjenengan, lan rupi-rupi kauntungan sanesipun.",
"anonpreviewwarning": "''Sampéyan durung mlebu log. Nyimpen bakal nyathet alamat IP Sampéyan nèng riwayat sunting kaca iki.''",
"missingsummary": "'''Pènget:''' Panjenengan ora nglebokaké ringkesan panyuntingan. Menawa panjenengan mencèt tombol Simpen manèh, suntingan panjenengan bakal kasimpen tanpa ringkesan panyuntingan.",
+ "selfredirect": "<strong>Pélik:</strong> Sampéyan ngalih kaca iki iya nyang kaca iki dhéwé.\nSampéyan mungkin salah wènèh tujuan kanggo alihan utawa salah mbesut kaca.\nYèn sampéyan ngeklik \"{{int:savearticle}}\" manèh, kaca alihan bakal digawé.",
"missingcommenttext": "Mangga isi tanggapan ing ngisor iki.",
"missingcommentheader": "'''Pangéling:''' Sampéyan durung nyadhiyakaké judhul/jejer kanggo tanggepan iki.\nYèn Sampéyan klik \"{{int:savearticle}}\" manèh, suntingan Sampéyan bakal kasimpen tanpa kuwi.",
"summary-preview": "Pratuduh tingkesan:",
"searchrelated": "magepokan",
"searchall": "kabèh",
"showingresults": "Ing ngisor iki dituduhaké {{PLURAL:$1|'''1''' kasil|'''$1''' kasil}}, wiwitané saking #<strong>$2</strong>.",
+ "showingresultsinrange": "Nuduhaké nganti {{PLURAL:$1|<strong>1</strong> kasil|<strong>$1</strong> kasil}} sajeroning penthangan #<strong>$2</strong> tekan #<strong>$3</strong>.",
"search-showingresults": "{{PLURAL:$4|Asil <strong>$1</strong> dari <strong>$3</strong>|Asil <strong>$1 - $2</strong> saking <strong>$3</strong>}}",
"search-nonefound": "Ora ana kasil sing cocog karo pitakonan (''query'').",
"powersearch-legend": "Panggolèkan sabanjuré (''advance search'')",
"prefs-searchoptions": "Golèk",
"prefs-namespaces": "Ruang jeneng / Bilik jeneng",
"default": "baku",
- "prefs-files": "Berkas",
+ "prefs-files": "Barkas",
"prefs-custom-css": "CSS priangga",
"prefs-custom-js": "JavaScript priangga",
"prefs-common-css-js": "CSS/JS didumaké kanggo kabèh kulit:",
"right-move-rootuserpages": "Ngalih kaca panganggo oyod",
"right-movefile": "Mindhah berkas",
"right-suppressredirect": "Aja nggawé pangalihan saka kaca sing lawas yèn mindhah sawijining kaca",
- "right-upload": "Ngunggahaké berkas-berkas",
+ "right-upload": "Unggah barkas",
"right-reupload": "Tindhihana sawijining berkas sing wis ana",
"right-reupload-own": "Nimpa sawijining berkas sing wis ana lan diunggahaké déning panganggo sing padha",
"right-reupload-shared": "Timpanana berkas-berkas ing khazanah binagi sacara lokal",
"upload-prohibited": "Jenis berkas sing dilarang: $1.",
"uploadlogpage": "Log pangunggahan",
"uploadlogpagetext": "Ing ngisor iki kapacak log pangunggahan berkas sing anyar dhéwé.\nMangga mirsani [[Special:NewFiles|galeri berkas-berkas anyar]] kanggo pratélan visual.",
- "filename": "Jeneng berkas",
+ "filename": "Jeneng barkas",
"filedesc": "Tingkesan",
"fileuploadsummary": "Ringkesan:",
"filereuploadsummary": "Owah-owahan berkas:",
"file-deleted-duplicate": "Sawijining berkas persis berkas iki ([[:$1]]) wis tau dibusak. Mangga panjenengan priksani sajarah pambusakan berkas kasebut sadurungé nerusaké ngunggahaké berkas kuwi manèh.",
"uploadwarning": "Pèngetan pangunggahan berkas",
"uploadwarning-text": "Mangga owah katrangan berkas nèng ngisor lan coba manèh.",
- "savefile": "Simpen berkas",
+ "savefile": "Simpen barkas",
"uploaddisabled": "Nuwun sèwu, fasilitas pangunggahan dipatèni.",
"copyuploaddisabled": "Ngunggah mawa URL dipatèni.",
"uploaddisabledtext": "Pangunggahan berkas ora diidinaké.",
"uploadscripted": "Berkas iki ngandhut HTML utawa kode sing bisa diinterpretasi salah déning panjlajah wèb.",
"uploadvirus": "Berkas iki ngamot virus! Détil: $1",
"uploadjava": "Berkas kuwi berkas ZIP sing kaisi berkas .class Java.\nNgungga berkas Java ora dililakaké amarga bisa nyebabaké ngluwèhaké wates kamanan.",
- "upload-source": "Berkas sumber",
+ "upload-source": "Barkas sumber",
"sourcefilename": "Jeneng berkas sumber:",
"sourceurl": "URL sumber:",
"destfilename": "Jeneng berkas sing dituju",
"brokenredirectstext": "Pengalihan ing ngisor iki tumuju menyang kaca sing ora ana:",
"brokenredirects-edit": "besut",
"brokenredirects-delete": "busak",
- "withoutinterwiki": "Kaca tanpa pranala antarbasa",
- "withoutinterwiki-summary": "Kaca-kaca iki ora nduwé pranala menyang vèrsi ing basa liyané:",
+ "withoutinterwiki": "Kaca tanpa pranala basa",
+ "withoutinterwiki-summary": "Kaca-kaca ing ngisor iki ora nggayut nyang vèrsi basa liyané.",
"withoutinterwiki-legend": "Préfiks",
"withoutinterwiki-submit": "Tuduhna",
"fewestrevisions": "Artikel mawa owah-owahan sithik dhéwé",
"mostlinkedtemplates": "Kaca paling akèh transklusi",
"mostcategories": "Kaca sing kategoriné akèh dhéwé",
"mostimages": "Berkas sing kerep dhéwé dienggo",
- "mostinterwikis": "Halaman dengan interwiki terbanyak",
+ "mostinterwikis": "Kaca mawa interwiki paling akèh",
"mostrevisions": "Kaca mawa pangowahan sing akèh dhéwé",
"prefixindex": "Kabèh kaca mawa ater-ater",
"prefixindex-namespace": "Kabèh kaca mawa ater-ater (bilik jeneng $1)",
"shortpages": "Kaca cendhak",
"longpages": "Kaca dawa",
"deadendpages": "Kaca-kaca buntu (tanpa pranala)",
- "deadendpagestext": "kaca-kaca iki ora nduwé pranala tekan ngendi waé ing wiki iki..",
+ "deadendpagestext": "Kaca-kaca ing ngisor iki ora nggayut nyang kaca liya ing {{SITENAME}}.",
"protectedpages": "Kaca sing direksa",
"protectedpages-indef": "Namung pangreksan ora langgeng waé",
"protectedpages-cascade": "Amung kaca rineksan kang runtut",
"delete-legend": "Busak",
"historywarning": "'''Pènget''': Kaca sing bakal panjenengan busak ana sajarahé kanthi $1 {{PLURAL:$1|révisi|révisi}}:",
"confirmdeletetext": "Panjenengan bakal mbusak kaca utawa berkas iki minangka permanèn karo kabèh sajarahé saka basis data. Pastèkna dhisik menawa panjenengan pancèn nggayuh iki, ngerti kabèh akibat lan konsekwènsiné, lan apa sing bakal panjenengan tumindak iku cocog karo [[{{MediaWiki:Policy-url}}|kawicaksanan {{SITENAME}}]].",
- "actioncomplete": "Proses tuntas",
+ "actioncomplete": "Kasil diayahi",
"actionfailed": "Tindakan gagal",
- "deletedtext": "\"$1\" sampun kabusak. Coba pirsani $2 kanggé log paling énggal kaca ingkang kabusak.",
+ "deletedtext": "\"$1\" wis dibusak. \nDelenga $2 minangka rekamaning busak-busakan pungkasan.",
"dellogpage": "Log busak",
"dellogpagetext": "Ing ngisor iki kapacak log pambusakan kaca sing anyar dhéwé.",
"deletionlog": "Cathetan sing dibusak",
"whatlinkshere": "Sing nggayut mréné",
"whatlinkshere-title": "Kaca mawa pranala nggayut \"$1\"",
"whatlinkshere-page": "Kaca:",
- "linkshere": "Kaca-kaca iki nduwé pranala menyang '''[[:$1]]''':",
+ "linkshere": "Kaca-kaca ing ngisor iki nggayut nyang '''[[:$1]]''':",
"nolinkshere": "Ora ana kaca sing nduwé pranala menyang '''[[:$1]]'''.",
"nolinkshere-ns": " Ora ana kaca sing nduwé pranala menyang '''[[:$1]]''' ing bilik jeneng sing kapilih.",
"isredirect": "kaca lih-lihan",
"import-interwiki-history": "Tuladen kabèh vèrsi lawas saka kaca iki",
"import-interwiki-templates": "Katutna kabèh cithakan",
"import-interwiki-submit": "Impor",
- "import-upload-filename": "Jeneng berkas:",
+ "import-upload-filename": "Jeneng barkas:",
"import-comment": "Komentar:",
"importtext": "Mangga èkspor berkas saka wiki sumber nganggo [[Special:Export|prangkat èkspor]].\nSimpen nèng komputer Sampéyan lan unggaha nèng kéné.",
"importstart": "Ngimpor kaca...",
"exif-subjectlocation": "Lokasi subjèk",
"exif-exposureindex": "Indhèks pajanan",
"exif-sensingmethod": "Métodhe pangindran",
- "exif-filesource": "Sumber berkas",
+ "exif-filesource": "Sumber barkas",
"exif-scenetype": "Tipe panyawangan",
"exif-customrendered": "Prosès nggawé gambar",
"exif-exposuremode": "Modhe pajanan",
"redirect-user": "ID panganggo",
"redirect-page": "ID kaca",
"redirect-revision": "Revisi kaca",
- "redirect-file": "Jeneng berkas",
+ "redirect-file": "Jeneng barkas",
"redirect-not-exists": "Nilai ora ditemokaké",
"fileduplicatesearch": "Golèk berkas duplikat",
"fileduplicatesearch-summary": "Golèk duplikat berkas adhedhasar biji hash-é.",
"recentchanges-feed-description": "Verfollegt mat dësem Feed déi rezent Ännerungen op {{SITENAME}}.",
"recentchanges-label-newpage": "Mat dëser Ännerung gouf eng nei Säit ugeluecht",
"recentchanges-label-minor": "Dëst ass eng kleng Ännerung",
- "recentchanges-label-bot": "Dës Ännerung gouf vun engem Bot gemaacht",
+ "recentchanges-label-bot": "Dës Ännerung gouf vun engem Bot gemaach",
"recentchanges-label-unpatrolled": "Dës Ännerung gouf nach net nogekuckt",
"recentchanges-label-plusminus": "D'Gréisst vun der Säit huet sech ëm déi Zuel vu Bytes geännert",
"recentchanges-legend-heading": "<strong>Legend:</strong>",
"show": "Rodyti",
"minoreditletter": "S",
"newpageletter": "N",
- "boteditletter": "R",
+ "boteditletter": "r",
"number_of_watching_users_pageview": "[$1 {{PLURAL:$1|stebintis naudotojas|stebintys naudotojai|stebinčių naudotojų}}]",
"rc_categories": "Riboti kategorijoms (atskirkite su „|“)",
"rc_categories_any": "Bet kuris iš pasirinktųjų",
"log-action-filter-rights-autopromote": "Automatinis keitimas",
"log-action-filter-upload-upload": "Naujas įkėlimas",
"log-action-filter-upload-overwrite": "Kelti iš naujo",
+ "authmanager-create-disabled": "Paskyros kūrimas yra išjungtas.",
+ "authmanager-create-from-login": "Norėdami sukurti paskyrą užpildykite laukelius žemiau.",
"authmanager-authplugin-setpass-failed-title": "Slaptažodžio keitimas nepavyko",
"authmanager-authplugin-setpass-bad-domain": "Negalimas domenas.",
"authmanager-autocreate-noperm": "Automatinis paskyros kūrimas neleidžiamas.",
"botpasswords-label-resetpassword": "Atiestatīt paroli",
"botpasswords-label-restrictions": "Lietošanas ierobežojumi:",
"botpasswords-label-grants-column": "Piešķirts",
+ "botpasswords-created-title": "Bota parole izveidota",
+ "botpasswords-updated-title": "Bota parole atjaunināta",
"botpasswords-deleted-title": "Bota parole dzēsta",
"resetpass_forbidden": "Paroles nav iespējams nomainīt",
"resetpass-no-info": "Jums ir nepieciešams ieiet, lai tūlīt piekļūtu šai lapai.",
"passwordreset-emailsentemail": "Paroles atiestatīšanas e-pasts ir nosūtīts.",
"passwordreset-nosuchcaller": "Izsaucējs nepastāv: $1",
"passwordreset-invalideamil": "Nederīga e-pasta adrese",
- "changeemail": "Mainīt e-pasta adresi",
+ "changeemail": "Mainīt vai noņemt e-pasta adresi",
"changeemail-header": "Mainīt konta e-pasta adresi",
"changeemail-oldemail": "Pašreizējā e-pasta adrese:",
"changeemail-newemail": "Jaunā e-pasta adrese:",
"mergehistory-submit": "Apvienot versijas",
"mergehistory-empty": "Neviena versija nevar tikt apvienota",
"mergehistory-fail": "Nav iespējams apvienot hronoloģiju, lūdzu, pārbaudiet vēlreiz lapu un laika parametrus.",
+ "mergehistory-fail-bad-timestamp": "Laika zīmogs ir nederīgs.",
+ "mergehistory-fail-invalid-source": "Avota lapa ir nederīga.",
+ "mergehistory-fail-invalid-dest": "Mērķa lapa ir nederīga.",
"mergehistory-no-source": "Avota lapa $1 nepastāv.",
"mergehistory-no-destination": "Mērķa lapa $1 nepastāv.",
"mergehistory-invalid-source": "Avota lapas nosaukumam jābūt derīgam.",
"sp-contributions-newbies": "Rādīt jauno lietotāju devumu",
"sp-contributions-newbies-sub": "Jaunie lietotāji",
"sp-contributions-blocklog": "Bloķēšanas reģistrs",
- "sp-contributions-deleted": "dzēstais dalībnieka devums",
+ "sp-contributions-deleted": "dzēstais {{GENDER:$1|dalībnieka|dalībnieces}} devums",
"sp-contributions-uploads": "augšupielādes",
"sp-contributions-logs": "reģistri",
"sp-contributions-talk": "diskusija",
"Milicevic01",
"Macofe",
"Nemo bis",
- "Matma Rex"
+ "Matma Rex",
+ "Kaldari"
]
},
"tog-underline": "Потцртување на врски:",
"hidden-category-category": "Скриени категории",
"category-subcat-count": "{{PLURAL:$2|Оваа категорија ја содржи само следнава поткатегорија.|Оваа категорија {{PLURAL:$1|ја содржи следнава поткатегорија|ги содржи следниве $1 поткатегории}} од вкупно $2.}}",
"category-subcat-count-limited": "Оваа категорија {{PLURAL:$1|ја содржи следнава поткатегорија|ги содржи следниве $1 поткатегории}}.",
- "category-article-count": "{{#ifeq:$2|Оваа категорија содржи само една страница.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 страници во категоријата.}}",
+ "category-article-count": "{{PLURAL:$2|Оваа категорија содржи само една страница.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 страници во категоријата.}}",
"category-article-count-limited": "{{PLURAL:$1|Следната страница е|Следните $1 страници се}} во оваа категорија.",
- "category-file-count": "{{#ifeq:$2|Оваа категорија содржи само една податотека.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 податотеки во категоријата.}}",
+ "category-file-count": "{{PLURAL:$2|Оваа категорија содржи само една податотека.|{{PLURAL:$1|Прикажана е една|Прикажани се $1}} од вкупно $2 податотеки во категоријата.}}",
"category-file-count-limited": "{{PLURAL:$1|Следнава податотека е|Следниве $1 податотеки се}} во оваа категорија.",
"listingcontinuesabbrev": "продолжува",
"index-category": "Индексирани страници",
"shown-title": "စာမျက်နှာတစ်ခုလျှင် ရလဒ် $1 {{PLURAL:$1|ခု|ခု}} ပြရန်",
"viewprevnext": "($1 {{int:မှ}} $2) အထိကြား ရလဒ် ($3) ခုကို ကြည့်ရန်",
"searchmenu-exists": "'''ဤဝီကီတွင် \"[[:$1]]\" အမည်နှင့် စာမျက်နှာတစ်ခုရှိသည်။'''",
- "searchmenu-new": "<strong>á\80¤á\80\9dá\80®á\80\80á\80®á\80\90á\80½á\80\84á\80º \"[[:$1]]\" á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80á\80¯ á\80\96á\80\94á\80ºá\80\90á\80®á\80¸á\80\95á\80«!</strong> {{PLURAL:$2|0=|á\80\9eá\80\84á\80·á\80ºá\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯á\80\94á\80¾á\80\84á\80·á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80·á\80ºá\80\95á\80«á\81\8b|á\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯ á\80\9bá\80\9cá\80\92á\80ºá\80\99á\80»á\80¬á\80¸á\80\80á\80á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80ºá\80«ပါ။}}",
+ "searchmenu-new": "<strong>á\80¤á\80\9dá\80®á\80\80á\80®á\80\90á\80½á\80\84á\80º \"[[:$1]]\" á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80á\80¯ á\80\96á\80\94á\80ºá\80\90á\80®á\80¸á\80\95á\80«!</strong> {{PLURAL:$2|0=|á\80\9eá\80\84á\80·á\80ºá\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯á\80\94á\80¾á\80\84á\80·á\80º á\80\85á\80¬á\80\99á\80»á\80\80á\80ºá\80\94á\80¾á\80¬á\80\80á\80á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80·á\80ºá\80\95á\80«á\81\8b|á\80\9bá\80¾á\80¬á\80\96á\80½á\80±á\80\99á\80¾á\80¯ á\80\9bá\80\9cá\80\92á\80ºá\80\99á\80»á\80¬á\80¸á\80\80á\80á\80¯á\80\9cá\80\8aá\80ºá\80¸ á\80\80á\80¼á\80\8aá\80·á\80ºပါ။}}",
"searchprofile-articles": "မာတိကာစာမျက်နှာများ",
"searchprofile-images": "မာလတီမီဒီယာ",
"searchprofile-everything": "အားလုံး",
"expand_templates_remove_comments": "Kommentaren rutnehmen",
"expand_templates_generate_xml": "XML-Parser-Boom wiesen",
"expand_templates_preview": "Vörschau",
- "pagelang-language": "Spraak"
+ "pagelang-language": "Spraak",
+ "special-characters-group-latin": "Latiensch",
+ "special-characters-group-latinextended": "Latiensch verwiedert",
+ "special-characters-group-ipa": "Internatschonal Phoneetsch Alphabet",
+ "special-characters-group-symbols": "Symbolen",
+ "special-characters-group-greek": "Greeksch",
+ "special-characters-group-greekextended": "Greeksch verwiedert",
+ "special-characters-group-cyrillic": "Kyrillisch",
+ "special-characters-group-arabic": "Araabsch",
+ "special-characters-group-arabicextended": "Araabsch verwiedert",
+ "special-characters-group-persian": "Persisch",
+ "special-characters-group-hebrew": "Hebrääsch",
+ "special-characters-group-bangla": "Bengaalsch",
+ "special-characters-group-tamil": "Tamilsch",
+ "special-characters-group-telugu": "Telugu",
+ "special-characters-group-sinhala": "Singaleesch",
+ "special-characters-group-gujarati": "Gujarati",
+ "special-characters-group-devanagari": "Devanagari",
+ "special-characters-group-thai": "Thailändsch",
+ "special-characters-group-lao": "Laotisch",
+ "special-characters-group-khmer": "Khmer"
}
"recreate": "Attopprett",
"confirm_purge_button": "OK",
"confirm-purge-top": "Vil du slette tenarane sin mellomlagra versjon av denne sida?",
- "confirm-purge-bottom": "Reinsing av ei side slettar mellomlageret og tvinger fram den nyaste versjonen.",
+ "confirm-purge-bottom": "Reinsing av ei side slettar mellomlageret og tvingar fram den nyaste versjonen.",
"confirm-watch-button": "OK",
"confirm-watch-top": "Legg denne sida til i overvakingslista di?",
"confirm-unwatch-button": "OK",
"passwordreset-emailtext-user": "L'utilizaire $1 sus {{SITENAME}} a demandat una reïnicializacion de vòstre senhal per {{SITENAME}} ($4). {{PLURAL:$3|Lo compte d'utilizaire seguent es associat|Los comptes d'utilizaires seguents son associats}} a aquesta adreça de corrièr electronic :\n\n$2\n\n{{PLURAL:$3|Aqueste senhal temporari expirarà|Aquestes senhals temporaris expiraràn}} dins {{PLURAL:$5|un jorn|$5 jorns}}. Ara, vos cal vos connectar e causir un senhal novèl. Se aquesta demanda proven pas de vos, o que vos sètz remembrat de vòstre senhal inicial, e que lo volètz pas mai modificar, podètz ignorar aqueste messatge e contunhar d'utilizar vòstre ancian senhal.",
"passwordreset-emailelement": "Utilizaire: \n$1\n\nSenhal temporari: \n$2",
"passwordreset-emailsentemail": "Un corrièr electronic de reïnicializacion de senhal es estat mandat.",
- "passwordreset-emailsent-capture": "Un corrièr electronic de reïnicializacion senhal es estat mandat, qu'es afichat çaijós.",
- "passwordreset-emailerror-capture": "Un corrièr electronic de reïnicializacion de senhal es estat generat, qu'es afichat çaijós, mas lo mandadís a l'{{GENDER:$2|utilizaire}} a fracassat : $1",
"changeemail": "Cambiar o suprimir l'adreça electronica",
"changeemail-header": "Cambiar l'adreça electronica del compte",
"changeemail-no-info": "Vos cal èsser connectat per aver accès a aquesta pagina.",
"minoredit": "Aquò es un cambiament menor",
"watchthis": "Seguir aquesta pagina",
"savearticle": "Salvar",
+ "publishchanges": "Publicar las modificacions",
"preview": "Previsualizar",
- "showpreview": "Previsualizacion",
+ "showpreview": "Previsualizar",
"showdiff": "Veire los cambiaments",
"blankarticle": "<strong>Atencion :</strong> La pagina que creatz es voida.\nSe clicatz tornarmai sus « {{int:savearticle}} », la pagina serà creada sens cap de contengut.",
"anoneditwarning": "<strong>Atencion :<strong> sètz pas connectat.\nVòstra adreça IP serà visibla per tot lo monde se fasètz de modificacions. Se <strong>[$1 vos connectatz]</strong> o <strong>[$2 creatz un compte]</strong>, vòstras modificacions seràn atribuidas a vòstre nom d’utilizaire, entre autres avantatges.",
"undo-nochange": "Sembla que la modificacion es ja estada anullada.",
"undo-summary": "Anullacion de las modificacions $1 de [[Special:Contributions/$2|$2]] ([[User talk:$2|discutir]] | [[Special:Contributions/$2|{{MediaWiki:Contribslink}}]])",
"undo-summary-username-hidden": "Anullar la revision $1 per un utilizaire amagat",
- "cantcreateaccounttitle": "Podètz pas crear de compte.",
"cantcreateaccount-text": "La creacion de compte dempuèi aquesta adreça IP ('''$1''') es estada blocada per [[User:$3|$3]].\n\nLa rason balhada per $3 èra ''$2''.",
"cantcreateaccount-range-text": "La creacion de compte dempuèi las adreças IP dins la plaja <strong>$1</strong>, que compren vòstra agreça IP (<strong>$4</strong>) son estadas blocadas per [[User:$3|$3]].\n\nLo motiu provesit per $3 es <em>$2</em>",
"viewpagelogs": "Vejatz las operacions per aquesta pagina",
"tooltip-ca-nstab-category": "Vejatz la pagina de la categoria",
"tooltip-minoredit": "Marcar mas modificacions coma un cambiament menor",
"tooltip-save": "Salvar vòstras modificacions",
+ "tooltip-publish": "Publicar vòstras modificacions",
"tooltip-preview": "Mercé de previsualizar vòstras modificacions abans de salvar!",
"tooltip-diff": "Aficha los cambiaments qu'avètz aportats al tèxte",
"tooltip-compareselectedversions": "Afichar las diferéncias entre doas versions d'aquesta pagina",
"botpasswords-insert-failed": "Falhou ao adicionar o nome do robô \"$1\". Já foi adicionado?",
"botpasswords-update-failed": "Falha ao atualizar o nome do robô \"$1\". Será que foi eliminado?",
"botpasswords-created-title": "Criada palavra-passe para o robô",
- "botpasswords-created-body": "O robô palavra-passe para o nome do robô \"$1\" do utilizador \"$2\" foi criado.",
+ "botpasswords-created-body": "A palavra-passe de robô para o robô \"$1\" do utilizador \"$2\" foi criada.",
"botpasswords-updated-title": "A palavra-passe de robô foi actualizada.",
"botpasswords-updated-body": "O robô palavra-passe para o nome do robô \"$1\" do utilizador \"$2\" foi atualizado.",
"botpasswords-deleted-title": "Palavra-passe de robô eliminada",
"prefs-watchlist-token": "వీక్షణాజాబితా టోకెను:",
"prefs-misc": "ఇతరత్రా",
"prefs-resetpass": "సంకేతపదాన్ని మార్చుకోండి",
- "prefs-changeemail": "ఈ-మెయిలు చిరునామా మార్పు",
+ "prefs-changeemail": "ఈ-మెయిలు చిరునామా మార్పు లేదా తొలగింపు",
"prefs-setemail": "ఓ ఈమెయిల్ చిరునామా ఇవ్వండి",
"prefs-email": "ఈ-మెయిల్ ఎంపికలు",
"prefs-rendering": "రూపురేఖలు",
"saveprefs": "భద్రపరచు",
"restoreprefs": "అప్రమేయ అమరికలను పునఃస్థాపించు (అన్ని విభాగాల్లోనూ)",
- "prefs-editing": "సవరిసà±\8dà°¤à±\81à°¨à±\8dనారు",
+ "prefs-editing": "దిదà±\8dà°¦à±\81బాà°\9fà±\8dà°²ు",
"rows": "అడ్డు వరుసలు:",
"columns": "నిలువు వరుసలు:",
"searchresultshead": "వెతుకు",
- "stub-threshold": "<a href=\"#\" class=\"stub\">మొలక లింకు</a> ఫార్మాటింగు కొరకు హద్దు (బైట్లు):",
+ "stub-threshold": "మొలక లింకు ఫార్మాటింగు కొరకు హద్దు ($1):",
"stub-threshold-sample-link": "నమూనా",
"stub-threshold-disabled": "అచేతనం",
"recentchangesdays": "ఇటీవలి మార్పులు లో చూపించవలసిన రోజులు:",
"prefs-help-recentchangescount": "ఇది ఇటీవలి మార్పులు, పేజీ చరిత్రలు, మరియు చిట్టాలకు వర్తిస్తుంది.",
"prefs-help-watchlist-token2": "మీ వీక్షణజాబితా యొక్క జాలవడ్డింపుకు చెందిన రహస్య తాళమిది.\nఈ తాళం తెలిసిన ఎవరైనా మీ వీక్షణజాబితాను చదవగలుగుతారు. అందుచేత దీన్ని ఎవరికీ ఇవ్వకండి.\n[[Special:ResetTokens|దాన్ని మార్చాలంటే ఇక్కడ నొక్కండి]].",
"savedprefs": "మీ అభిరుచులను భద్రపరిచాం.",
+ "savedrights": "{{GENDER:$1|$1}} వాడుకరి హక్కులను భద్రపరచాం.",
"timezonelegend": "కాల మండలం:",
"localtime": "స్థానిక సమయం:",
"timezoneuseserverdefault": "వికీ అప్రమేయాన్ని ఉపయోగించు ($1)",
"badsig": "సంతకం చెల్లనిది.\nHTML ట్యాగులను ఒకసారి సరిచూసుకోండి.",
"badsiglength": "మీ సంతకం చాలా పెద్దగా ఉంది.\nఇది తప్పనిసరిగా $1 {{PLURAL:$1|అక్షరం|అక్షరాల}} లోపులోనే ఉండాలి.",
"yourgender": "మిమ్మల్ని మీరు ఎలా వర్ణించుకుంటారు?",
- "gender-unknown": "à°µà±\86à°²à±\8dలడిà°\82à°\9aడానిà°\95à°¿ à°¨à±\87à°¨à±\81 à°\87à°·à±\8dà°\9fపడà°\9fà±\8dà°²à±\87à°¦à±\81",
+ "gender-unknown": "సాఫà±\8dà°\9fà±\81à°µà±\87à°°à±\81 మిమà±\8dమలà±\8dని à°\89à°²à±\8dà°²à±\87à°\96à°¿à°\82à°\9aà±\87 à°¸à°\82దరà±\8dà°à°\82à°²à±\8b, à°µà±\80à°²à±\88à°¨à°\82తవరà°\95à±\81 లిà°\82à°\97 à°¤à°\9fà°¸à±\8dథతనà±\81 à°\85వలà°\82బిసà±\8dà°¤à±\81à°\82ది",
"gender-male": "అతను వికీ పేజీలను సరిదిద్దుతాడు",
"gender-female": "ఆమె వికీ పేజీలను సరిదిద్దుతుంది",
"prefs-help-gender": "ఈ అభిరుచిని అమర్చుకోవడం ఐచ్చికం.\nమిమ్మల్ని సంబోధించేప్పుడూ మిమ్మల్ని పేర్కొనేప్పుడూ వ్యాకరణపరంగా సరైన లింగాన్ని వాడటానికి ఈ విలువ ఉపయోగపడుతుంది.\nఈ సమాచారం బహిరంగం.",
"prefs-tokenwatchlist": "టోకెన్",
"prefs-diffs": "తేడాలు",
"prefs-help-prefershttps": "ఈ అభిరుచి మీరు పైసారి లాగినైనపుడు అమలౌతుంది.",
+ "prefswarning-warning": "మీ అభిరుచులలో మీరు చేసిన మార్పులను ఇంకా భద్రపరచలేదు. మీరు \"$1\" ను నొక్కకుండా ఈ పేజీని వదలి వెళ్తే, మీ అభిరుచులు భద్రం కావు.",
"prefs-tabs-navigation-hint": "చిట్కా: ట్యాబుల జాబితాలో ఓ ట్యాబు నుండి మరోదానికి వెళ్ళేందుకు కుడి ఎడమ బాణాల కీలను వాడవచ్చు.",
"userrights": "వాడుకరి హక్కుల నిర్వహణ",
"userrights-lookup-user": "వాడుకరి సమూహాలను నిర్వహించండి",
"userrights-user-editname": "వాడుకరిపేరును ఇవ్వండి:",
- "editusergroup": "వాడుకరి గుంపులను మార్చు",
- "editinguser": "వాడుకరి '''[[User:$1|$1]]''' $2 యొక్క వాడుకరి హక్కులను మారుస్తున్నారు",
+ "editusergroup": "{{GENDER:$1|వాడుకరి}} గుంపులను మార్చు",
+ "editinguser": "{{GENDER:$1|వాడుకరి}} <strong>[[వాడుకరి:$1|$1]]</strong> $2 యొక్క వాడుకరి హక్కులను మారుస్తున్నారు",
"userrights-editusergroup": "వాడుకరి సమూహాలను మార్చండి",
- "saveusergroups": "వాడుకరి గుంపులను భద్రపరచు",
+ "saveusergroups": "{{GENDER:$1|వాడుకరి}} గుంపులను భద్రపరచు",
"userrights-groupsmember": "సభ్యులు:",
"userrights-groupsmember-auto": "సంభావిత సభ్యులు:",
"userrights-groups-help": "ఈ వాడుకరి ఏయే గుంపులలో ఉండాలో మీరు మార్చవచ్చు.\n* టిక్కు పెట్టివుంటే ఆ గుంపులో ఈ వాడుకరి ఉన్నట్టు.\n* టిక్కు లేకుంటే ఆ గుంపులో ఈ వాడుకరి లేనట్టు.\n* * ఉంటే ఒకసారి ఆ గుంపుని చేర్చాక మీరు తీసివేయలేరు, లేదా తీసివేసాక తిరిగి చేర్చలేరు.",
"userrights-changeable-col": "మీరు మార్చదగిన గుంపులు",
"userrights-unchangeable-col": "మీరు మార్చలేని గుంపులు",
"userrights-conflict": "వాడుకరి హక్కుల మార్పులలో ఘర్షణ! మీ మార్పులను సమీక్షించి, నిర్ధారించండి.",
- "userrights-removed-self": "à°®à±\80 à°¹à°\95à±\8dà°\95à±\81లనà±\81 à°®à±\80à°°à±\81 విà°\9cయవà°\82à°¤à°\82à°\97à°¾ à°¤à±\8aà°²à°\97à°¿à°\82à°\9aà±\81à°\95à±\81à°¨à±\8dనారà±\81. తదà±\8dవారా, à°\88 à°ªà±\87à°\9cà±\80ని à°\9aà±\82డడానిà°\95à°¿ à°®à±\80à°\95à±\81 à°\87à°\95 à°\85à°¨à±\81మతి à°²à±\87à°¦ు.",
+ "userrights-removed-self": "à°®à±\80 à°¹à°\95à±\8dà°\95à±\81లనà±\81 à°®à±\80à°°à±\81 à°¤à±\8aà°²à°\97à°¿à°\82à°\9aà±\81à°\95à±\81à°¨à±\8dనారà±\81. à°\87à°\95, à°®à±\80à°°à±\80 à°ªà±\87à°\9cà±\80ని à°\9aà±\82à°¡à°²à±\87à°°ు.",
"group": "గుంపు:",
"group-user": "వాడుకరులు",
"group-autoconfirmed": "ఆటోమాటిగ్గా నిర్ధారించబడిన వాడుకరులు",
"right-managechangetags": "డేటాబేసులో [[Special:Tags|ట్యాగుల]]ను సృష్టించడం, తొలగించడం",
"right-applychangetags": "తన మార్పులతో [[Special:Tags|ట్యాగుల]]ను ఆపాదించడం",
"right-changetags": "విడి కూర్పులకు, చిట్టా పద్దులకు ఏవైనా [[Special:Tags|ట్యాగుల]]ను చేర్చడం, తొలగించడం",
+ "right-deletechangetags": "[[ప్రత్యేక:Tags|ట్యాగులను]] డేటాబేసు నుండి తొలగించు",
+ "grant-generic": "\"$1\" హక్కుల కట్ట",
+ "grant-group-email": "ఈమెయిలు పంపించడం",
+ "grant-group-administration": "నిర్వాహక చర్యలు చేపట్టడం",
+ "grant-group-private-information": "మీ గోపనీయ డేటాను చూడడం",
"newuserlogpage": "కొత్త వాడుకరుల చిట్టా",
"newuserlogpagetext": "ఇది వాడుకరి నమోదుల చిట్టా.",
"rightslog": "వాడుకరుల హక్కుల మార్పుల చిట్టా",
"changepassword-throttled": "Bạn đã thử đăng nhập gần đây nhiều lần quá. Xin chờ $1 trước khi bạn thử lần nữa.",
"botpasswords": "Mật khẩu Bot",
"botpasswords-summary": "<em>Mật khẩu bot</em> cho phép truy cập một tài khoản người dùng qua API mà không sử dụng thông tin chứng nhận chính của tài khoản. Các quyền người dùng có thể bị hạn chế khi đăng nhập dùng mật khẩu bot.\n\nNếu bạn không hiểu tại sao cần sử dụng mật khẩu bot, có lẽ bạn không nên sử dụng nó. Không ai bao giờ có lý do chính đáng để yêu cầu bạn tạo ra một mật khẩu bot và cung cấp nó cho họ.",
- "botpasswords-disabled": "Mật khẩu Bot bị vô hiệu hoá.",
+ "botpasswords-disabled": "Mật khẩu bot bị vô hiệu hóa.",
"botpasswords-no-central-id": "Để sử dụng mật khẩu bot, bạn phải đăng nhập vào một tài khoản tập trung.",
"botpasswords-existing": "Mật khẩu bot hiện tại",
"botpasswords-createnew": "Tạo một mật khẩu mới bot",
"grant-group-high-volume": "Hoạt động với tần số cao",
"grant-group-customization": "Tùy biến và tùy chọn",
"grant-group-administration": "Thực hiện các hành động bảo quản",
+ "grant-group-private-information": "Truy cập dữ liệu cá nhân của bạn",
"grant-group-other": "Hoạt động khác",
"grant-blockusers": "Cấm và bỏ cấm người dùng",
"grant-createaccount": "Mở tài khoản",
"grant-highvolume": "Sửa đổi tốc độ cao",
"grant-oversight": "Ẩn người dùng và phiên bản",
"grant-patrol": "Tuần tra các thay đổi trang",
+ "grant-privateinfo": "Truy cập dữ liệu cá nhân",
"grant-protect": "Khóa và mở khóa các trang",
"grant-rollback": "Lùi một loạt thay đổi vào một trang",
"grant-sendemail": "Gửi thư điện tử cho người dùng khác",
"uploadstash-errclear": "Việc dọn sạch các tập tin bị thất bại.",
"uploadstash-refresh": "Làm mới danh sách tập tin",
"uploadstash-thumbnail": "xem hình thu nhỏ",
+ "uploadstash-exception": "Không thể lưu tập tin vào hàng đợi tải lên ($1): “$2”.",
"invalid-chunk-offset": "Khúc lệch (chunk offset) không hợp lệ",
"img-auth-accessdenied": "Không cho phép truy cập",
"img-auth-nopathinfo": "Thiếu PATH_INFO.\nMáy chủ của bạn không được thiết lập để truyền thông tin này.\nCó thể do nó dựa trên CGI và không hỗ trợ img_auth.\nXem [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization hướng dẫn điều khiển truy cập hình ảnh].",
"watchnologin": "Chưa đăng nhập",
"addwatch": "Thêm vào danh sách theo dõi",
"addedwatchtext": "“[[:$1]]” cùng trang thảo luận đã vào [[Special:Watchlist|danh sách theo dõi]] của bạn.",
+ "addedwatchtext-talk": "“[[:$1]]” cùng trang đi kèm đã vào [[Special:Watchlist|danh sách theo dõi]] của bạn.",
"addedwatchtext-short": "Trang “$1” đã được thêm vào danh sách theo dõi của bạn.",
"removewatch": "Gỡ khỏi danh sách theo dõi",
"removedwatchtext": "“[[:$1]]” cùng trang thảo luận đã được đưa ra khỏi [[Special:Watchlist|danh sách theo dõi]] của bạn.",
+ "removedwatchtext-talk": "“[[:$1]]” cùng trang đi kèm đã được đưa ra khỏi [[Special:Watchlist|danh sách theo dõi]] của bạn.",
"removedwatchtext-short": "Trang “$1” đã được xóa khỏi danh sách theo dõi của bạn.",
"watch": "Theo dõi",
"watchthispage": "Theo dõi trang này",
"undeletehistorynoadmin": "Trang này đã bị xóa.\nLý do xóa trang được hiển thị dưới đây, cùng với thông tin về những người đã sửa đổi trang này trước khi bị xóa.\nChỉ có bảo quản viên mới xem được văn bản đầy đủ của những phiên bản trang bị xóa.",
"undelete-revision": "Phiên bản đã xóa của $1 (vào lúc $4 tại $5) do $3 sửa đổi:",
"undeleterevision-missing": "Phiên bản này không hợp lệ hay không tồn tại. Đây có thể là một địa chỉ sai, hoặc là phiên bản đã được phục hồi hoặc đã xóa khỏi kho lưu trữ.",
+ "undeleterevision-duplicate-revid": "Không thể phục hồi {{PLURAL:$1|một phiên bản|$1 phiên bản}} vì <code>rev_id</code> của {{PLURAL:$1|nó|chúng}} đã được sử dụng.",
"undelete-nodiff": "Không tìm thấy phiên bản cũ hơn.",
"undeletebtn": "Phục hồi",
"undeletelink": "xem lại/phục hồi",
"undeletedrevisions": "$1 phiên bản được phục hồi",
"undeletedrevisions-files": "$1 phiên bản và $2 tập tin đã được phục hồi",
"undeletedfiles": "$1 tập tin đã được phục hồi",
- "cannotundelete": "Phục hồi thất bại:\n$1",
+ "cannotundelete": "Phục hồi bị thất bại một phần hoặc hoàn toàn:\n$1",
"undeletedpage": "'''$1 đã được khôi phục'''\n\nXem nhật trình xóa và phục hồi các trang gần đây tại [[Special:Log/delete|nhật trình xóa]].",
"undelete-header": "Xem các trang bị xóa gần đây tại [[Special:Log/delete|nhật trình xóa]].",
"undelete-search-title": "Tìm kiếm trang đã bị xóa",
"sp-contributions-newbies-sub": "Các thành viên mới",
"sp-contributions-newbies-title": "Đóng góp của các thành viên mới",
"sp-contributions-blocklog": "nhật trình cấm",
- "sp-contributions-suppresslog": "đóng góp của người dùng đã bị xóa hẳn",
- "sp-contributions-deleted": "đóng góp đã bị xóa của thành viên",
+ "sp-contributions-suppresslog": "đóng góp của {{GENDER:$1}}người dùng đã bị xóa hẳn",
+ "sp-contributions-deleted": "đóng góp đã bị xóa của {{GENDER:$1}}thành viên",
"sp-contributions-uploads": "tập tin tải lên",
"sp-contributions-logs": "nhật trình",
"sp-contributions-talk": "thảo luận",
NS_MEDIA => 'Médiá',
NS_SPECIAL => 'Špeciálne',
NS_TALK => 'Diskusia',
- NS_USER => 'Redaktor',
- NS_USER_TALK => 'Diskusia_s_redaktorom',
+ NS_USER => 'Užívateľ',
+ NS_USER_TALK => 'Diskusia_s_užívateľom',
NS_PROJECT_TALK => 'Diskusia_k_{{GRAMMAR:datív|$1}}',
NS_FILE => 'Súbor',
NS_FILE_TALK => 'Diskusia_k_súboru',
$namespaceAliases = [
"Komentár" => NS_TALK,
+ 'Redaktor' => NS_USER,
+ 'Diskusia_s_redaktorom' => NS_USER_TALK,
"Komentár_k_redaktorovi" => NS_USER_TALK,
"Komentár_k_Wikipédii" => NS_PROJECT_TALK,
'Obrázok' => NS_FILE,
"Komentár_k_MediaWiki" => NS_MEDIAWIKI_TALK,
];
+$namespaceGenderAliases = [
+ NS_USER => [ 'male' => 'Užívateľ', 'female' => 'Užívateľka' ],
+ NS_USER_TALK => [ 'male' => 'Diskusia_s_užívateľom', 'female' => 'Diskusia_s_užívateľkou' ],
+];
+
$separatorTransformTable = [
',' => "\xc2\xa0",
'.' => ','
$this->db->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+ $this->commitTransaction( $this->db, __METHOD__ );
+
/* Call LinksDeletionUpdate to delete outgoing links from the old title,
* and update category counts.
*
* accidentally introduce an assumption of title validity to the code we
* are calling.
*/
- $update = new LinksDeletionUpdate( $wikiPage );
- $update->doUpdate();
- $this->commitTransaction( $this->db, __METHOD__ );
+ $updates = [ new LinksDeletionUpdate( $wikiPage ) ];
+ DataUpdate::runUpdates( $updates );
return true;
}
$this->addArg( 'path', 'Path to extension.json/skin.json file.', true );
}
public function execute() {
- if ( !class_exists( 'JsonSchema\Uri\UriRetriever' ) ) {
+ if ( !class_exists( 'JsonSchema\Validato' ) ) {
$this->error( 'The JsonSchema library cannot be found, please install it through composer.', 1 );
}
$this->output( "Warning: $path is using a deprecated schema, and should be updated to "
. ExtensionRegistry::MANIFEST_VERSION . "\n" );
}
- $retriever = new JsonSchema\Uri\UriRetriever();
- $schema = $retriever->retrieve( 'file://' . $schemaPath );
-
- $validator = new JsonSchema\Validator();
- $validator->check( $data, $schema );
+ $validator = new JsonSchema\Validator;
+ $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
if ( $validator->isValid() ) {
$this->output( "$path validates against the version $version schema!\n" );
} else {
}
} else if ( $collapsible.parent().is( 'li' ) &&
- $collapsible.parent().children( '.mw-collapsible' ).size() === 1 &&
- $collapsible.find( '> .mw-collapsible-toggle' ).size() === 0
+ $collapsible.parent().children( '.mw-collapsible' ).length === 1 &&
+ $collapsible.find( '> .mw-collapsible-toggle' ).length === 0
) {
// special case of one collapsible in <li> tag
$toggleLink = buildDefaultToggleLink();
// Standalone icons
//
// Markup:
- // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok">OK</div><br/>
- // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok mw-ui-button mw-ui-progressive">OK</div><br/>
+ // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok">OK</div><br>
+ // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok mw-ui-button mw-ui-progressive">OK</div><br>
// <button class="mw-ui-icon mw-ui-icon-ok mw-ui-icon-element mw-ui-button mw-ui-quiet" title="">Close</button>
//
// Styleguide 6.1.1.
margin-right: @iconGutterWidth;
}
}
-}
+
+ // Icons small for elements like indicators
+ //
+ // Markup:
+ // <div class="mw-ui-icon mw-ui-icon-small mw-ui-icon-help"></div>
+ //
+ // Styleguide 6.1.3
+ &.mw-ui-icon-small:before {
+ background-size: 66.67% auto; // 66.67% of 24px equals 16px
+ }
+}
\ No newline at end of file
$.each( response.query.pages, function ( index, page ) {
var title = new ForeignTitle( page.title ).getPrefixedText();
cache.existenceCache[ title ] = !page.missing;
+ if ( !queue[ title ] ) {
+ // Debugging for T139130
+ throw new Error( 'No queue for "' + title + '", requested "' + titles.join( '|' ) + '"' );
+ }
queue[ title ].resolve( cache.existenceCache[ title ] );
} );
} );
* HTMLForm enhancements:
* Infuse some OOjs UI HTMLForm fields (those which benefit from always being infused).
*/
-( function ( mw ) {
+( function ( mw, $ ) {
mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- var $oouiNodes, modules;
+ var $oouiNodes, modules, extraModules;
$oouiNodes = $root.find( '.mw-htmlform-field-autoinfuse' );
if ( $oouiNodes.length ) {
// The modules are preloaded (added server-side in HTMLFormField, and the individual fields
// which need extra ones), but this module doesn't depend on them. Wait until they're loaded.
modules = [ 'mediawiki.htmlform.ooui' ];
- if ( $oouiNodes.filter( '.mw-htmlform-field-HTMLTitleTextField' ).length ) {
- // FIXME: TitleInputWidget should be in its own module
- modules.push( 'mediawiki.widgets' );
- }
- if ( $oouiNodes.filter( '.mw-htmlform-field-HTMLUserTextField' ).length ) {
- modules.push( 'mediawiki.widgets.UserInputWidget' );
- }
- if (
- $oouiNodes.filter( '.mw-htmlform-field-HTMLSelectNamespace' ).length ||
- $oouiNodes.filter( '.mw-htmlform-field-HTMLSelectNamespaceWithButton' ).length
- ) {
- // FIXME: NamespaceInputWidget should be in its own module (probably?)
- modules.push( 'mediawiki.widgets' );
- }
+ $oouiNodes.each( function () {
+ var data = $( this ).data( 'mw-modules' );
+ if ( data ) {
+ // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
+ extraModules = data.split( ',' );
+ modules.push.apply( modules, extraModules );
+ }
+ } );
mw.loader.using( modules ).done( function () {
$oouiNodes.each( function () {
OO.ui.infuse( this );
} );
-}( mediaWiki ) );
+}( mediaWiki, jQuery ) );
mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
$root.find( '.mw-htmlform-hide-if' ).each( function () {
- var v, i, fields, test, func, spec, self, modules,
+ var v, i, fields, test, func, spec, self, modules, data,extraModules,
$el = $( this );
modules = [];
if ( $el.is( '[data-ooui]' ) ) {
modules.push( 'mediawiki.htmlform.ooui' );
- if ( $el.filter( '.mw-htmlform-field-HTMLTitleTextField' ).length ) {
- // FIXME: TitleInputWidget should be in its own module
- modules.push( 'mediawiki.widgets' );
- }
- if ( $el.filter( '.mw-htmlform-field-HTMLUserTextField' ).length ) {
- modules.push( 'mediawiki.widgets.UserInputWidget' );
- }
- if (
- $el.filter( '.mw-htmlform-field-HTMLSelectNamespace' ).length ||
- $el.filter( '.mw-htmlform-field-HTMLSelectNamespaceWithButton' ).length
- ) {
- // FIXME: NamespaceInputWidget should be in its own module (probably?)
- modules.push( 'mediawiki.widgets' );
+ data = $el.data( 'mw-modules' );
+ if ( data ) {
+ // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer
+ extraModules = data.split( ',' );
+ modules.push.apply( modules, extraModules );
}
}
function addMulti( $oldContainer, $container ) {
var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ),
- oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ),
+ oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen|mw-htmlform-dropdown)/g, '' ),
$select = $( '<select>' ),
dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
oldClass = $.trim( oldClass );
}
mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- if ( $root.find( '.mw-chosen' ).length ) {
+ if ( $root.find( '.mw-htmlform-dropdown' ).length ) {
mw.loader.using( 'jquery.chosen', function () {
- $root.find( '.mw-chosen' ).each( function () {
+ $root.find( '.mw-htmlform-dropdown' ).each( function () {
var type = this.nodeName.toLowerCase(),
$converted = convertCheckboxesToMulti( $( this ), type );
$converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
* State machine:
*
* - `registered`:
- * The module is known to the system but not yet requested.
+ * The module is known to the system but not yet required.
* Meta data is registered via mw.loader#register. Calls to that method are
* generated server-side by the startup module.
* - `loading`:
- * The module is requested through mw.loader (either directly or as dependency of
- * another module). The client will be fetching module contents from the server.
+ * The module was required through mw.loader (either directly or as dependency of
+ * another module). The client will fetch module contents from the server.
* The contents are then stashed in the registry via mw.loader#implement.
* - `loaded`:
- * The module has been requested from the server and stashed via mw.loader#implement.
- * If the module has no more dependencies in-fight, the module will be executed
- * right away. Otherwise execution is deferred, controlled via #handlePending.
+ * The module has been loaded from the server and stashed via mw.loader#implement.
+ * If the module has no more dependencies in-flight, the module will be executed
+ * immediately. Otherwise execution is deferred, controlled via #handlePending.
* - `executing`:
* The module is being executed.
* - `ready`:
//
sources = {},
- // List of modules which will be loaded as when ready
- batch = [],
-
- // Pending queueModuleScript() requests
+ // For queueModuleScript()
handlingPendingRequests = false,
pendingRequests = [],
/**
* List of callback jobs waiting for modules to be ready.
*
- * Jobs are created by #request() and run by #handlePending().
+ * Jobs are created by #enqueue() and run by #handlePending().
*
* Typically when a job is created for a module, the job's dependencies contain
- * both the module being requested and all its recursive dependencies.
+ * both the required module and all its recursive dependencies.
*
* Format:
*
}
/**
- * Adds all dependencies to the queue with optional callbacks to be run
- * when the dependencies are ready or fail
+ * Add one or more modules to the module load queue.
+ *
+ * See also #work().
*
* @private
* @param {string|string[]} dependencies Module name or array of string module names
* @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute when any dependency fails
*/
- function request( dependencies, ready, error ) {
+ function enqueue( dependencies, ready, error ) {
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
dependencies = [ dependencies ];
}
/**
- * Load modules from load.php
+ * Make a network request to load modules from the server.
*
* @private
* @param {Object} moduleMap Module map, see #buildModulesString
* @param {string} sourceLoadScript URL of load.php
*/
function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
- var request = $.extend(
+ var query = $.extend(
{ modules: buildModulesString( moduleMap ) },
currReqBase
);
- request = sortQuery( request );
- addScript( sourceLoadScript + '?' + $.param( request ) );
+ query = sortQuery( query );
+ addScript( sourceLoadScript + '?' + $.param( query ) );
}
/**
} );
}
+ /**
+ * Create network requests for a batch of modules.
+ *
+ * This is an internal method for #work(). This must not be called directly
+ * unless the modules are already registered, and no request is in progress,
+ * and the module state has already been set to `loading`.
+ *
+ * @private
+ * @param {string[]} batch
+ */
+ function batchRequest( batch ) {
+ var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
+ source, group, i, modules, sourceLoadScript,
+ currReqBase, currReqBaseLength, moduleMap, l,
+ lastDotIndex, prefix, suffix, bytesAdded;
+
+ if ( !batch.length ) {
+ return;
+ }
+
+ // Always order modules alphabetically to help reduce cache
+ // misses for otherwise identical content.
+ batch.sort();
+
+ // Build a list of query parameters common to all requests
+ reqBase = {
+ skin: mw.config.get( 'skin' ),
+ lang: mw.config.get( 'wgUserLanguage' ),
+ debug: mw.config.get( 'debug' )
+ };
+ maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+
+ // Split module list by source and by group.
+ splits = {};
+ for ( b = 0; b < batch.length; b++ ) {
+ bSource = registry[ batch[ b ] ].source;
+ bGroup = registry[ batch[ b ] ].group;
+ if ( !hasOwn.call( splits, bSource ) ) {
+ splits[ bSource ] = {};
+ }
+ if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+ splits[ bSource ][ bGroup ] = [];
+ }
+ bSourceGroup = splits[ bSource ][ bGroup ];
+ bSourceGroup.push( batch[ b ] );
+ }
+
+ for ( source in splits ) {
+
+ sourceLoadScript = sources[ source ];
+
+ for ( group in splits[ source ] ) {
+
+ // Cache access to currently selected list of
+ // modules for this group from this source.
+ modules = splits[ source ][ group ];
+
+ currReqBase = $.extend( {
+ version: getCombinedVersion( modules )
+ }, reqBase );
+ // For user modules append a user name to the query string.
+ if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
+ currReqBase.user = mw.config.get( 'wgUserName' );
+ }
+ currReqBaseLength = $.param( currReqBase ).length;
+ // We may need to split up the request to honor the query string length limit,
+ // so build it piece by piece.
+ l = currReqBaseLength + 9; // '&modules='.length == 9
+
+ moduleMap = {}; // { prefix: [ suffixes ] }
+
+ for ( i = 0; i < modules.length; i++ ) {
+ // Determine how many bytes this module would add to the query string
+ lastDotIndex = modules[ i ].lastIndexOf( '.' );
+
+ // If lastDotIndex is -1, substr() returns an empty string
+ prefix = modules[ i ].substr( 0, lastDotIndex );
+ suffix = modules[ i ].slice( lastDotIndex + 1 );
+
+ bytesAdded = hasOwn.call( moduleMap, prefix )
+ ? suffix.length + 3 // '%2C'.length == 3
+ : modules[ i ].length + 3; // '%7C'.length == 3
+
+ // If the url would become too long, create a new one,
+ // but don't create empty requests
+ if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
+ // This url would become too long, create a new one, and start the old one
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
+ moduleMap = {};
+ l = currReqBaseLength + 9;
+ mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
+ }
+ if ( !hasOwn.call( moduleMap, prefix ) ) {
+ moduleMap[ prefix ] = [];
+ }
+ moduleMap[ prefix ].push( suffix );
+ l += bytesAdded;
+ }
+ // If there's anything left in moduleMap, request that too
+ if ( !$.isEmptyObject( moduleMap ) ) {
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
+ }
+ }
+ }
+ }
+
/* Public Members */
return {
/**
addStyleTag: newStyleTag,
/**
- * Batch-request queued dependencies from the server.
+ * Start loading of all queued module dependencies.
*
* @protected
*/
work: function () {
- var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
- source, concatSource, origBatch, group, i, modules, sourceLoadScript,
- currReqBase, currReqBaseLength, moduleMap, l,
- lastDotIndex, prefix, suffix, bytesAdded;
-
- // Build a list of request parameters common to all requests.
- reqBase = {
- skin: mw.config.get( 'skin' ),
- lang: mw.config.get( 'wgUserLanguage' ),
- debug: mw.config.get( 'debug' )
- };
- // Split module batch by source and by group.
- splits = {};
- maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+ var q, batch, concatSource, origBatch;
+
+ batch = [];
// Appends a list of modules from the queue to the batch
for ( q = 0; q < queue.length; q++ ) {
- // Only request modules which are registered
+ // Only load modules which are registered
if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
// Prevent duplicate entries
if ( $.inArray( queue[ q ], batch ) === -1 ) {
}
}
- // Early exit if there's nothing to load...
- if ( !batch.length ) {
- return;
- }
-
- // The queue has been processed into the batch, clear up the queue.
+ // Now that the queue has been processed into a batch, clear up the queue.
+ // This MUST happen before we initiate any network request. Else it's possible
+ // that a script will be locally cached, instantly load, and work the queue
+ // again; all before we've cleared it causing each request to include modules
+ // which are already loaded.
queue = [];
- // Always order modules alphabetically to help reduce cache
- // misses for otherwise identical content.
- batch.sort();
-
- // Split batch by source and by group.
- for ( b = 0; b < batch.length; b++ ) {
- bSource = registry[ batch[ b ] ].source;
- bGroup = registry[ batch[ b ] ].group;
- if ( !hasOwn.call( splits, bSource ) ) {
- splits[ bSource ] = {};
- }
- if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
- splits[ bSource ][ bGroup ] = [];
- }
- bSourceGroup = splits[ bSource ][ bGroup ];
- bSourceGroup.push( batch[ b ] );
- }
-
- // Clear the batch - this MUST happen before we append any
- // script elements to the body or it's possible that a script
- // will be locally cached, instantly load, and work the batch
- // again, all before we've cleared it causing each request to
- // include modules which are already loaded.
- batch = [];
-
- for ( source in splits ) {
-
- sourceLoadScript = sources[ source ];
-
- for ( group in splits[ source ] ) {
-
- // Cache access to currently selected list of
- // modules for this group from this source.
- modules = splits[ source ][ group ];
-
- currReqBase = $.extend( {
- version: getCombinedVersion( modules )
- }, reqBase );
- // For user modules append a user name to the request.
- if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
- currReqBase.user = mw.config.get( 'wgUserName' );
- }
- currReqBaseLength = $.param( currReqBase ).length;
- // We may need to split up the request to honor the query string length limit,
- // so build it piece by piece.
- l = currReqBaseLength + 9; // '&modules='.length == 9
-
- moduleMap = {}; // { prefix: [ suffixes ] }
-
- for ( i = 0; i < modules.length; i++ ) {
- // Determine how many bytes this module would add to the query string
- lastDotIndex = modules[ i ].lastIndexOf( '.' );
-
- // If lastDotIndex is -1, substr() returns an empty string
- prefix = modules[ i ].substr( 0, lastDotIndex );
- suffix = modules[ i ].slice( lastDotIndex + 1 );
-
- bytesAdded = hasOwn.call( moduleMap, prefix )
- ? suffix.length + 3 // '%2C'.length == 3
- : modules[ i ].length + 3; // '%7C'.length == 3
-
- // If the request would become too long, create a new one,
- // but don't create empty requests
- if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
- // This request would become too long, create a new one
- // and fire off the old one
- doRequest( moduleMap, currReqBase, sourceLoadScript );
- moduleMap = {};
- l = currReqBaseLength + 9;
- mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
- }
- if ( !hasOwn.call( moduleMap, prefix ) ) {
- moduleMap[ prefix ] = [];
- }
- moduleMap[ prefix ].push( suffix );
- l += bytesAdded;
- }
- // If there's anything left in moduleMap, request that too
- if ( !$.isEmptyObject( moduleMap ) ) {
- doRequest( moduleMap, currReqBase, sourceLoadScript );
- }
- }
- }
+ batchRequest( batch );
},
/**
/**
* Implement a module given the components that make up the module.
*
- * When #load or #using requests one or more modules, the server
+ * When #load() or #using() requests one or more modules, the server
* response contain calls to this function.
*
* @param {string} module Name of module
dependencies
);
} else {
- // Not all dependencies are ready: queue up a request
- request( dependencies, function () {
+ // Not all dependencies are ready, add to the load queue
+ enqueue( dependencies, function () {
deferred.resolve( mw.loader.require );
}, deferred.reject );
}
if ( allReady( filtered ) || anyFailed( filtered ) ) {
return;
}
- // Since some modules are not yet ready, queue up a request.
- request( filtered, undefined, undefined );
+ // Some modules are not yet ready, add to module load queue.
+ enqueue( filtered, undefined, undefined );
},
/**
!! wikitext
{{echo|[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]}}
!! html
-<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>[Main_Page bar]</p>
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>[Main Page bar]</p>
!! end
!! test
!!end
-!!test
+!! test
4. Indent-Pre and extension tags
!! wikitext
- a <gallery>
-File:foobar.jpg
-</gallery>
-!! html
- a <ul class="gallery mw-gallery-traditional">
- <li class="gallerybox" style="width: 155px"><div style="width: 155px">
- <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
- <div class="gallerytext">
- </div>
- </div></li>
-</ul>
-
-!! html+tidy
-<p>a</p>
-<ul class="gallery mw-gallery-traditional">
-<li class="gallerybox" style="width: 155px">
-<div style="width: 155px">
-<div class="thumb" style="width: 150px;">
-<div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div>
-</div>
-<div class="gallerytext"></div>
-</div>
-</li>
-</ul>
-!!end
+ a <tag />
+!! html/php
+ a <pre>
+NULL
+array (
+)
+</pre>
-!! test
-Table wikitext syntax outside wiki-tables
-!! wikitext
-a
-! not a table heading
-|- not a table row
-| not a table cell
-| class="foo bar" | baz
-b
-|}
-|-
-c
-!! html
-<p>a
-! not a table heading
-|- not a table row
-| not a table cell
-| class="foo bar" | baz
-b
-|}
-|-
-c
-</p>
+!! html/parsoid
+ a <pre typeof="mw:Extension/tag" about="#mwt2" data-parsoid='{}' data-mw='{"name":"tag","attrs":{},"body":null}'></pre>
!! end
!!test
</p>
!!end
+# Since Parsoid is starting to emit canonical wikitext for links,
+# [http://example.com http://example.com] will not RT back to that
+# form anymore.
!! test
External links: URL in text
+!! options
+parsoid=wt2html
!! wikitext
URL in text: [http://example.com http://example.com]
-!! html
+!! html/php
<p>URL in text: <a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
</p>
+!! html/parsoid
+<p>URL in text: <a rel="mw:ExtLink" href="http://example.com">http://example.com</a></p>
!! end
!! test
Link containing % (not as a hex sequence)
!! wikitext
[[7% Solution]]
+[[7% Solution|7%25 Solution]]
!! html/php
<p><a href="/wiki/7%25_Solution" title="7% Solution">7% Solution</a>
+<a href="/wiki/7%25_Solution" title="7% Solution">7%25 Solution</a>
</p>
!! html/parsoid
-<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a></p>
+<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a>
+<a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7%25 Solution</a></p>
!! end
# note that the parsoid HTML is identical to the previous test output,
parsoid=wt2wt,wt2html,html2html
!! wikitext
[[7%25 Solution]]
+[[7%25 Solution|7%25 Solution]]
!! html/php
<p><a href="/wiki/7%25_Solution" title="7% Solution">7% Solution</a>
+<a href="/wiki/7%25_Solution" title="7% Solution">7%25 Solution</a>
</p>
!! html/parsoid
-<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a></p>
+<p><a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7% Solution</a>
+<a rel="mw:WikiLink" href="./7%25_Solution" title="7% Solution">7%25 Solution</a></p>
!!end
!! test
<p><a rel="mw:WikiLink" href="./User:Foo/Test/63636:Bar" title="User:Foo/Test/63636:Bar">Test</a></p>
!! end
+## Mainly a sanity check for Parsoid
+!! test
+Handle title parsing for subpages
+!! options
+title=[[/123123]]
+!! wikitext
+123
+!! html/parsoid
+<p>123</p>
+!! end
+
+## FIXME: Add a working php section here
+!! test
+Link to a subpage from a namespace other than main
+!! options
+title=[[User:test]]
+!! wikitext
+[[/123]]
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./User:Test/123" title="User:Test/123" data-parsoid='{"stx":"simple","a":{"href":"./User:Test/123"},"sa":{"href":"/123"}}'>/123</a></p>
+!! end
+
!! test
Purely hash wikilink
!! options
!! test
Parsoid bug 53221: Wikilinks should be properly entity-escaped
!! options
-parsoid=html2wt
+parsoid={ "modes": ["html2wt"], "suppressErrors": true }
!! html/parsoid
<p>He&nbsp;llo <a href="Foo" rel="mw:WikiLink">He&nbsp;llo</a></p>
<p>He&nbsp;llo <a href="He&nbsp;llo" rel="mw:WikiLink">He&nbsp;llo</a></p>
!! wikitext
He&nbsp;llo [[Foo|He&nbsp;llo]]
-He&nbsp;llo [[He&nbsp;llo]]
+He&nbsp;llo He&nbsp;llo
+!! html/php
+<p>He&nbsp;llo <a href="/wiki/Foo" title="Foo">He&nbsp;llo</a>
+</p><p>He&nbsp;llo He&nbsp;llo
+</p>
!! end
+# html2wt will fail because of title normalization without data-parsoid
!! test
Parsoid: handle constructor well
+!! options
+parsoid=wt2html,wt2wt
!! wikitext
[[constructor]]
</p><p><a href="/index.php?title=Constructor:foo&action=edit&redlink=1" class="new" title="Constructor:foo (page does not exist)">constructor:foo</a>
</p>
!! html/parsoid
-<p><a rel="mw:WikiLink" href="./Constructor" title="Constructor" data-parsoid="{"stx":"simple","a":{"href":"./Constructor"},"sa":{"href":"constructor"}}">constructor</a></p>
+<p><a rel="mw:WikiLink" href="./Constructor" title="Constructor" data-parsoid='{"stx":"simple","a":{"href":"./Constructor"},"sa":{"href":"constructor"}}'>constructor</a></p>
-<p><a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid="{"stx":"simple","a":{"href":"./Foo"},"sa":{"href":"constructor:foo"}}">constructor:foo</a></p>
+<p><a rel="mw:WikiLink" href="./Constructor:foo" title="Constructor:foo" data-parsoid='{"stx":"simple","a":{"href":"./Constructor:foo"},"sa":{"href":"constructor:foo"}}'>constructor:foo</a></p>
!! end
!! article
!! wikitext
{{DISPLAYTITLE:''{{PAGENAME}}''}}
!! html/parsoid
-<meta property="mw:PageProp/displaytitle" content="Main_Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"src":"{{DISPLAYTITLE:''{{PAGENAME}}''}}"}' data-mw='{"attribs":[[{"txt":"content"},{"html":"<i data-parsoid='{\"dsr\":[15,31,2,2]}'><span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[]],\"dsr\":[17,29,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"PAGENAME\",\"function\":\"pagename\"},\"params\":{},\"i\":0}}]}'>Main_Page</span></i>"}]]}'/>
+<meta property="mw:PageProp/displaytitle" content="Main Page" about="#mwt2" typeof="mw:ExpandedAttrs" data-parsoid='{"src":"{{DISPLAYTITLE:''{{PAGENAME}}''}}"}' data-mw='{"attribs":[[{"txt":"content"},{"html":"<i data-parsoid='{\"dsr\":[15,31,2,2]}'><span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[]],\"dsr\":[17,29,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"PAGENAME\",\"function\":\"pagename\"},\"params\":{},\"i\":0}}]}'>Main Page</span></i>"}]]}'/>
!! end
!! test
<p>foo bar baz
</p>
!! html/parsoid
-<p>foo <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"''"},"params":{},"i":0}}]}'>bar</span> baz
+<p>foo <span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"''","href":"./Template:''"},"params":{},"i":0}}]}'>bar</span> baz
</p>
!! end
</tbody></table>
!!end
+!! test
+Table wikitext syntax outside wiki-tables
+!! wikitext
+a
+|+ not a caption
+! not a table heading
+|- not a table row
+| not a table cell
+| class="foo bar" | baz
+b
+|}
+|-
+c
+!! html
+<p>a
+|+ not a caption
+! not a table heading
+|- not a table row
+| not a table cell
+| class="foo bar" | baz
+b
+|}
+|-
+c
+</p>
+!! end
+
###
### Testing parsing of templates where a template arg
### has the same name as the template itself.
<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"& < > \""}]}' data-mw='{"caption":"&amp; &lt; > \""}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
!! end
+!! test
+Entities in file name and attributes
+!! wikitext
+[[File:7%25 solution.gif|manualthumb=7%25 solution.gif|link=7%25 solution|[[7%25 solution]]]]
+!! html/php
+<p><a href="/index.php?title=Special:Upload&wpDestFile=7%25_solution.gif" class="new" title="File:7% solution.gif">7% solution</a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Error mw:Image" data-parsoid='{"optList":[{"ck":"bogus","ak":"manualthumb=7%25 solution.gif"},{"ck":"link","ak":"link=7%25 solution"},{"ck":"caption","ak":"[[7%25 solution]]"}]}' data-mw='{"errors":[{"key":"missing-image","message":"This image does not exist."}],"caption":"<a rel=\"mw:WikiLink\" href=\"./7%25_solution\" title=\"7% solution\" data-parsoid='{\"stx\":\"simple\",\"a\":{\"href\":\"./7%25_solution\"},\"sa\":{\"href\":\"7%25 solution\"},\"dsr\":[74,91,2,2]}'>7% solution</a>"}'><a href="./7%25_solution" data-parsoid='{"a":{"href":"./7%25_solution"},"sa":{"href":"link=7%25 solution"}}'><img resource="./File:7%25_solution.gif" src="./Special:FilePath/7%25_solution.gif" height="220" width="220" data-parsoid='{"a":{"resource":"./File:7%25_solution.gif","height":"220","width":"220"},"sa":{"resource":"File:7%25 solution.gif"}}'/></a></span></p>
+!! end
+
!! test
BUG 499: Alt text should have Ӓ, not &1234;
!! wikitext
<link rel="mw:PageProp/Category" href="./Category:Baz" data-parsoid='{"stx":"simple","a":{"href":"./Category:Baz"},"sa":{"href":"Category:Baz"}}'/>
!! end
+!! test
+Category links with multiple namespaces
+!! wikitext
+[[Category:Project:Foo]]
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:Project:Foo" />
+!! end
+
!! test
Parsoid: Serialize link to category page with colon escape
!! options
</p>
!! end
+# This is wt2html only in Parsoid because we add <nowiki>
+# because of {{..}} and we don't expect to fix that to
+# eliminate the nowikis selective for {{..}} markup.
!! test
Non-transclusion because of too many up levels
!! options
subpage title=[[Subpage test/L1/L2/L3]]
+parsoid=wt2html
!! wikitext
{{../../../../More than parent}}
!! html
</p>
!! end
+# Since Parsoid is starting to emit canonical wikitext for links,
+# [http://example.com http://example.com] will not RT back to that
+# form anymore.
!! test
Proper conversion of text in external links
!! options
language=sr variant=sr-ec
+parsoid=wt2html
!! wikitext
http://www.google.com
gopher://www.google.com
[https://www.google.com irc://www.google.com]
[ftp://www.google.com www.google.com/ftp://dir]
[//www.google.com www.google.com]
-!! html
+!! html/php
<p><a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a>
<a rel="nofollow" class="external free" href="gopher://www.google.com">gopher://www.google.com</a>
<a rel="nofollow" class="external free" href="http://www.google.com">http://www.google.com</a>
<a rel="nofollow" class="external text" href="ftp://www.google.com">www.гоогле.цом/фтп://дир</a>
<a rel="nofollow" class="external text" href="//www.google.com">www.гоогле.цом</a>
</p>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://www.google.com">http://www.google.com</a>
+<a rel="mw:ExtLink" href="gopher://www.google.com">gopher://www.google.com</a>
+<a rel="mw:ExtLink" href="http://www.google.com">http://www.google.com</a>
+<a rel="mw:ExtLink" href="gopher://www.google.com">gopher://www.google.com</a>
+<a rel="mw:ExtLink" href="https://www.google.com">irc://www.google.com</a>
+<a rel="mw:ExtLink" href="ftp://www.google.com">www.гоогле.цом/фтп://дир</a>
+<a rel="mw:ExtLink" href="//www.google.com">www.гоогле.цом</a></p>
!! end
!! test
# -----------------------------------------------------------------
# The following section of tests are primarily to spec requirements
-# around serialization of new/edited content.
+# around Parsoid's serialization (old, new, edited content)
#
# All these tests are marked Parsoid html2wt and html2html only
# ----------------------------------------------------------------
+!! test
+Ignore rel attribute in a-tags during serialization to url-links
+!! options
+parsoid=html2wt
+!! html/parsoid
+<a href='http://en.wikipedia.org/wiki/Foobar'>http://en.wikipedia.org/wiki/Foobar</a>
+<a href='http://en.wikipedia.org/wiki/Foobar' rel='mw:ExtLink'>http://en.wikipedia.org/wiki/Foobar</a>
+<a href='http://en.wikipedia.org/wiki/Foobar' rel='mw:WikiLink'>http://en.wikipedia.org/wiki/Foobar</a>
+!! wikitext
+http://en.wikipedia.org/wiki/Foobar
+http://en.wikipedia.org/wiki/Foobar
+http://en.wikipedia.org/wiki/Foobar
+!! end
+
# 'mi' is a localinterwiki prefix as well as a language
!! test
Serialize interwiki links pointing to the current wiki as plain wiki links (bug 65869)
!! html/parsoid
<a rel="mw:WikiLink" href="./Foo" title="Foo" data-parsoid='{}'>Foo</a>
<a rel="mw:WikiLink" href="./Foo" title="Foo">Foo</a>
+<a href="//en.wikipedia.org/wiki/Foo">//en.wikipedia.org/wiki/Foo</a>
+<a href="http://en.wikipedia.org/wiki/Foo">http://en.wikipedia.org/wiki/Foo</a>
+<a href="//en.wikipedia.org/wiki/Foo_bar">//en.wikipedia.org/wiki/Foo bar</a>
!! wikitext
[[Foo]]
[[Foo]]
+[[:en:Foo|//en.wikipedia.org/wiki/Foo]]
+http://en.wikipedia.org/wiki/Foo
+[[:en:Foo_bar|//en.wikipedia.org/wiki/Foo bar]]
!! end
!! test
[[File:Foobar.jpg|link=]]
!! end
+!! test
+Image: Invalid title as link
+!! wikitext
+[[File:Foobar.jpg|link=<]]
+!! html/php
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="link=<"><img alt="link=<" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! html/parsoid
+<p><span class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"link","ak":"link=<"}]}' data-mw='{"caption":"link=&lt;"}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"}}'/></a></span></p>
+!! end
+
!! test
Lists: Serialize correctly even when list content is wrapped in p-tags (like VE does)
!! options
</div>
</div>
!! end
+
+!! test
+unclosed internal link XSS (T137264)
+!! wikitext
+[[#%3Cscript%3Ealert(1)%3C/script%3E|
+!! html
+<p>[[#<script>alert(1)</script>|
+</p>
+!! end
'_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
'TitleParser' => [ 'TitleParser', TitleParser::class ],
+ 'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ]
];
}
$this->assertEquals( $expectedNative, $converted->getNativeData() );
}
}
+
+ /**
+ * @covers TextContent::normalizeLineEndings
+ * @dataProvider provideNormalizeLineEndings
+ */
+ public function testNormalizeLineEndings( $input, $expected ) {
+ $this->assertEquals( $expected, TextContent::normalizeLineEndings( $input ) );
+ }
+
+ public static function provideNormalizeLineEndings() {
+ return [
+ [
+ "Foo\r\nbar",
+ "Foo\nbar"
+ ],
+ [
+ "Foo\rbar",
+ "Foo\nbar"
+ ],
+ [
+ "Foobar\n ",
+ "Foobar"
+ ]
+ ];
+ }
+
}
*/
class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @covers CachedBagOStuff::__construct
+ * @covers CachedBagOStuff::doGet
+ */
public function testGetFromBackend() {
$backend = new HashBagOStuff;
$cache = new CachedBagOStuff( $backend );
$this->assertEquals( 'bar', $cache->get( 'foo' ), 'cached' );
}
+ /**
+ * @covers CachedBagOStuff::set
+ * @covers CachedBagOStuff::delete
+ */
public function testSetAndDelete() {
$backend = new HashBagOStuff;
$cache = new CachedBagOStuff( $backend );
}
}
+ /**
+ * @covers CachedBagOStuff::set
+ * @covers CachedBagOStuff::delete
+ */
public function testWriteCacheOnly() {
$backend = new HashBagOStuff;
$cache = new CachedBagOStuff( $backend );
$this->assertEquals( 'old', $cache->get( 'foo' ) ); // Reloaded from backend
}
+ /**
+ * @covers CachedBagOStuff::doGet
+ */
public function testCacheBackendMisses() {
$backend = new HashBagOStuff;
$cache = new CachedBagOStuff( $backend );
*/
class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @covers HashBagOStuff::delete
+ */
public function testDelete() {
$cache = new HashBagOStuff();
for ( $i = 0; $i < 10; $i++ ) {
}
}
+ /**
+ * @covers HashBagOStuff::clear
+ */
public function testClear() {
$cache = new HashBagOStuff();
for ( $i = 0; $i < 10; $i++ ) {
}
}
+ /**
+ * @covers HashBagOStuff::doGet
+ * @covers HashBagOStuff::expire
+ */
public function testExpire() {
$cache = new HashBagOStuff();
$cacheInternal = TestingAccessWrapper::newFromObject( $cache );
/**
* Ensure maxKeys eviction prefers keeping new keys.
+ *
+ * @covers HashBagOStuff::__construct
+ * @covers HashBagOStuff::set
*/
public function testEvictionAdd() {
$cache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
/**
* Ensure maxKeys eviction prefers recently set keys
* even if the keys pre-exist.
+ *
+ * @covers HashBagOStuff::__construct
+ * @covers HashBagOStuff::set
*/
public function testEvictionSet() {
$cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
/**
* Ensure maxKeys eviction prefers recently retrieved keys (LRU).
+ *
+ * @covers HashBagOStuff::__construct
+ * @covers HashBagOStuff::doGet
+ * @covers HashBagOStuff::hasKey
*/
public function testEvictionGet() {
$cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
] );
}
+ /**
+ * @covers MultiWriteBagOStuff::set
+ * @covers MultiWriteBagOStuff::doWrite
+ */
public function testSetImmediate() {
$key = wfRandomString();
$value = wfRandomString();
$this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
}
+ /**
+ * @covers MultiWriteBagOStuff
+ */
public function testSyncMerge() {
$key = wfRandomString();
$value = wfRandomString();
$dbw->commit();
}
+ /**
+ * @covers MultiWriteBagOStuff::set
+ */
public function testSetDelayed() {
$key = wfRandomString();
$value = wfRandomString();
$this->assertNotContains( 'nukeworld', $rights );
}
+ /**
+ * @covers User::getRights
+ */
+ public function testUserGetRightsHooks() {
+ $user = new User;
+ $user->addGroup( 'unittesters' );
+ $user->addGroup( 'testwriters' );
+ $userWrapper = TestingAccessWrapper::newFromObject( $user );
+
+ $rights = $user->getRights();
+ $this->assertContains( 'test', $rights, 'sanity check' );
+ $this->assertContains( 'runtest', $rights, 'sanity check' );
+ $this->assertContains( 'writetest', $rights, 'sanity check' );
+ $this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
+
+ // Add a hook manipluating the rights
+ $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserGetRights' => [ function ( $user, &$rights ) {
+ $rights[] = 'nukeworld';
+ $rights = array_diff( $rights, [ 'writetest' ] );
+ } ] ] );
+
+ $userWrapper->mRights = null;
+ $rights = $user->getRights();
+ $this->assertContains( 'test', $rights );
+ $this->assertContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertContains( 'nukeworld', $rights );
+
+ // Add a Session that limits rights
+ $mock = $this->getMockBuilder( stdclass::class )
+ ->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
+ ->getMock();
+ $mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] );
+ $mock->method( 'getSessionId' )->willReturn(
+ new MediaWiki\Session\SessionId( str_repeat( 'X', 32 ) )
+ );
+ $session = MediaWiki\Session\TestUtils::getDummySession( $mock );
+ $mockRequest = $this->getMockBuilder( FauxRequest::class )
+ ->setMethods( [ 'getSession' ] )
+ ->getMock();
+ $mockRequest->method( 'getSession' )->willReturn( $session );
+ $userWrapper->mRequest = $mockRequest;
+
+ $userWrapper->mRights = null;
+ $rights = $user->getRights();
+ $this->assertContains( 'test', $rights );
+ $this->assertNotContains( 'runtest', $rights );
+ $this->assertNotContains( 'writetest', $rights );
+ $this->assertNotContains( 'nukeworld', $rights );
+ }
+
/**
* @dataProvider provideGetGroupsWithPermission
* @covers User::getGroupsWithPermission
$version <= ExtensionRegistry::MANIFEST_VERSION,
"$path is using a non-supported schema version"
);
- $retriever = new JsonSchema\Uri\UriRetriever();
- $schema = $retriever->retrieve( 'file://' . $schemaPath );
- $validator = new JsonSchema\Validator();
- $validator->check( $data, $schema );
+ $validator = new JsonSchema\Validator;
+ $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
if ( $validator->isValid() ) {
// All good.
$this->assertTrue( true );