* (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',
'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',
'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;
}
/**
*/
$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
* 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/*]]>*/";
}
*/
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)" );
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 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'] ) ) {
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];
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__ );
}
/**
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
*
* @return bool
*/
public function canDisplayErrors() {
- return true;
+ return $this->hasVisibleOutput();
}
/**
* 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.
+ */
+ 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';
+ }
+ }
+
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 );
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__
+ );
+ } );
+ }
+}
* @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;
$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;
$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;
}
}
* 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';
"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."
}
"linkaccounts-submit": "Text of the main submit button on [[Special:LinkAccounts]] (when there is one)",
"unlinkaccounts": "Title of the special page [[Special:UnlinkAccounts]] which allows the user to remove linked remote accounts.",
"unlinkaccounts-success": "Account unlinking form success message",
- "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}."
+ "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}.",
+ "userjsispublic": "A reminder to users that Javascript subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .js. See also {{msg-mw|usercssispublic}}.",
+ "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}"
}
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 ] );
} );
} );
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 );
},
/**
</div>
</div>
!! end
+
+!! test
+unclosed internal link XSS (T137264)
+!! wikitext
+[[#%3Cscript%3Ealert(1)%3C/script%3E|
+!! html
+<p>[[#<script>alert(1)</script>|
+</p>
+!! end
$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 );