From: jenkins-bot Date: Thu, 14 Sep 2017 02:35:09 +0000 (+0000) Subject: Merge "Improve flaky SiteStatsTest" X-Git-Tag: 1.31.0-rc.0~2104 X-Git-Url: http://git.cyclocoop.org/ecrire?a=commitdiff_plain;h=655931210678d0090cd60a8463f2bb6d95099b0e;hp=d0b093d4bdc5d28eda5d37f5163f0694bd189234;p=lhc%2Fweb%2Fwiklou.git Merge "Improve flaky SiteStatsTest" --- diff --git a/RELEASE-NOTES-1.30 b/RELEASE-NOTES-1.30 index 67a449a312..8517a8f0de 100644 --- a/RELEASE-NOTES-1.30 +++ b/RELEASE-NOTES-1.30 @@ -70,6 +70,9 @@ section). ** This is currently gated by $wgCommentTableSchemaMigrationStage. Most wikis can set this to MIGRATION_NEW and run maintenance/migrateComments.php as soon as any necessary extensions are updated. +* (T138166) Added ability for users to prohibit other users from sending them + emails with Special:Emailuser. Can be enabled by setting + $wgEnableUserEmailBlacklist to true. === External library changes in 1.30 === @@ -216,6 +219,8 @@ changes to languages because of Phabricator reports. * wfUsePHP() is deprecated. * wfFixSessionID() was removed. * wfShellExec() and related functions are deprecated, use Shell::command(). +* (T138166) SpecialEmailUser::getTarget() now requires a second argument, the sending + user object. Using the method without the second argument is deprecated. == Compatibility == MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for diff --git a/autoload.php b/autoload.php index 4448204a5e..61fd192f0d 100644 --- a/autoload.php +++ b/autoload.php @@ -421,8 +421,8 @@ $wgAutoloadLocalClasses = [ 'EditAction' => __DIR__ . '/includes/actions/EditAction.php', 'EditCLI' => __DIR__ . '/maintenance/edit.php', 'EditPage' => __DIR__ . '/includes/EditPage.php', - 'EditWatchlistCheckboxSeriesField' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php', - 'EditWatchlistNormalHTMLForm' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php', + 'EditWatchlistCheckboxSeriesField' => __DIR__ . '/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php', + 'EditWatchlistNormalHTMLForm' => __DIR__ . '/includes/specials/forms/EditWatchlistNormalHTMLForm.php', 'EmailConfirmation' => __DIR__ . '/includes/specials/SpecialConfirmemail.php', 'EmailInvalidation' => __DIR__ . '/includes/specials/SpecialEmailInvalidate.php', 'EmailNotification' => __DIR__ . '/includes/mail/EmailNotification.php', @@ -633,7 +633,7 @@ $wgAutoloadLocalClasses = [ 'ImageQueryPage' => __DIR__ . '/includes/specialpage/ImageQueryPage.php', 'ImportImages' => __DIR__ . '/maintenance/importImages.php', 'ImportLogFormatter' => __DIR__ . '/includes/logging/ImportLogFormatter.php', - 'ImportReporter' => __DIR__ . '/includes/specials/SpecialImport.php', + 'ImportReporter' => __DIR__ . '/includes/specials/helpers/ImportReporter.php', 'ImportSiteScripts' => __DIR__ . '/maintenance/importSiteScripts.php', 'ImportSites' => __DIR__ . '/maintenance/importSites.php', 'ImportSource' => __DIR__ . '/includes/import/ImportSource.php', @@ -746,8 +746,8 @@ $wgAutoloadLocalClasses = [ 'Languages' => __DIR__ . '/maintenance/language/languages.inc', 'LayeredParameterizedPassword' => __DIR__ . '/includes/password/LayeredParameterizedPassword.php', 'LegacyLogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php', - 'License' => __DIR__ . '/includes/Licenses.php', - 'Licenses' => __DIR__ . '/includes/Licenses.php', + 'License' => __DIR__ . '/includes/specials/helpers/License.php', + 'Licenses' => __DIR__ . '/includes/specials/formfields/Licenses.php', 'LinkBatch' => __DIR__ . '/includes/cache/LinkBatch.php', 'LinkCache' => __DIR__ . '/includes/cache/LinkCache.php', 'LinkFilter' => __DIR__ . '/includes/LinkFilter.php', @@ -1135,7 +1135,7 @@ $wgAutoloadLocalClasses = [ 'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php', 'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php', 'Preferences' => __DIR__ . '/includes/Preferences.php', - 'PreferencesForm' => __DIR__ . '/includes/Preferences.php', + 'PreferencesForm' => __DIR__ . '/includes/specials/forms/PreferencesForm.php', 'PrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', 'PreprocessDump' => __DIR__ . '/maintenance/preprocessDump.php', 'Preprocessor' => __DIR__ . '/includes/parser/Preprocessor.php', @@ -1532,14 +1532,14 @@ $wgAutoloadLocalClasses = [ 'UploadChunkVerificationException' => __DIR__ . '/includes/upload/UploadFromChunks.php', 'UploadChunkZeroLengthFileException' => __DIR__ . '/includes/upload/UploadFromChunks.php', 'UploadDumper' => __DIR__ . '/maintenance/dumpUploads.php', - 'UploadForm' => __DIR__ . '/includes/specials/SpecialUpload.php', + 'UploadForm' => __DIR__ . '/includes/specials/forms/UploadForm.php', 'UploadFromChunks' => __DIR__ . '/includes/upload/UploadFromChunks.php', 'UploadFromFile' => __DIR__ . '/includes/upload/UploadFromFile.php', 'UploadFromStash' => __DIR__ . '/includes/upload/UploadFromStash.php', 'UploadFromUrl' => __DIR__ . '/includes/upload/UploadFromUrl.php', 'UploadLogFormatter' => __DIR__ . '/includes/logging/UploadLogFormatter.php', 'UploadSourceAdapter' => __DIR__ . '/includes/import/UploadSourceAdapter.php', - 'UploadSourceField' => __DIR__ . '/includes/specials/SpecialUpload.php', + 'UploadSourceField' => __DIR__ . '/includes/specials/formfields/UploadSourceField.php', 'UploadStash' => __DIR__ . '/includes/upload/UploadStash.php', 'UploadStashBadPathException' => __DIR__ . '/includes/upload/UploadStash.php', 'UploadStashCleanup' => __DIR__ . '/maintenance/cleanupUploadStash.php', diff --git a/includes/CommentStore.php b/includes/CommentStore.php index 2ed21d1995..b8a31e6f02 100644 --- a/includes/CommentStore.php +++ b/includes/CommentStore.php @@ -367,26 +367,7 @@ class CommentStore { * @return CommentStoreComment */ public function createComment( IDatabase $dbw, $comment, array $data = null ) { - global $wgContLang; - - if ( !$comment instanceof CommentStoreComment ) { - if ( $data !== null ) { - foreach ( $data as $k => $v ) { - if ( substr( $k, 0, 1 ) === '_' ) { - throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' ); - } - } - } - if ( $comment instanceof Message ) { - $message = clone $comment; - $text = $message->inLanguage( $wgContLang ) // Avoid $wgForceUIMsgAsContentMsg - ->setInterfaceMessageFlag( true ) - ->text(); - $comment = new CommentStoreComment( null, $text, $message, $data ); - } else { - $comment = new CommentStoreComment( null, $comment, null, $data ); - } - } + $comment = CommentStoreComment::newUnsavedComment( $comment, $data ); # Truncate comment in a Unicode-sensitive manner $comment->text = $this->lang->truncate( $comment->text, self::MAX_COMMENT_LENGTH ); diff --git a/includes/CommentStoreComment.php b/includes/CommentStoreComment.php index afc1374223..3920ba083f 100644 --- a/includes/CommentStoreComment.php +++ b/includes/CommentStoreComment.php @@ -42,7 +42,7 @@ class CommentStoreComment { public $data; /** - * @private For use by CommentStore only + * @private For use by CommentStore only. Use self::newUnsavedComment() instead. * @param int|null $id * @param string $text * @param Message|null $message @@ -54,4 +54,39 @@ class CommentStoreComment { $this->message = $message ?: new RawMessage( '$1', [ $text ] ); $this->data = $data; } + + /** + * Create a new, unsaved CommentStoreComment + * + * @param string|Message|CommentStoreComment $comment Comment text or Message object. + * A CommentStoreComment is also accepted here, in which case it is returned unchanged. + * @param array|null $data Structured data to store. Keys beginning with '_' are reserved. + * Ignored if $comment is a CommentStoreComment. + * @return CommentStoreComment + */ + public static function newUnsavedComment( $comment, array $data = null ) { + global $wgContLang; + + if ( $comment instanceof CommentStoreComment ) { + return $comment; + } + + if ( $data !== null ) { + foreach ( $data as $k => $v ) { + if ( substr( $k, 0, 1 ) === '_' ) { + throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' ); + } + } + } + + if ( $comment instanceof Message ) { + $message = clone $comment; + $text = $message->inLanguage( $wgContLang ) // Avoid $wgForceUIMsgAsContentMsg + ->setInterfaceMessageFlag( true ) + ->text(); + return new CommentStoreComment( null, $text, $message, $data ); + } else { + return new CommentStoreComment( null, $comment, null, $data ); + } + } } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 5b77d16c4e..852cd08cf0 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1603,6 +1603,13 @@ $wgEnableEmail = true; */ $wgEnableUserEmail = true; +/** + * Set to true to enable user-to-user e-mail blacklist. + * + * @since 1.30 + */ +$wgEnableUserEmailBlacklist = false; + /** * If true put the sending user's email in a Reply-To header * instead of From (false). ($wgPasswordSender will be used as From.) @@ -6850,13 +6857,6 @@ $wgStructuredChangeFiltersShowPreference = false; */ $wgStructuredChangeFiltersEnableExperimentalViews = false; -/** - * Whether to allow users to use the experimental live update feature in the new RecentChanges UI - * - * Temporary variable during development and will be removed. - */ -$wgStructuredChangeFiltersEnableLiveUpdate = false; - /** * Whether to enable RCFilters app on Special:Watchlist * diff --git a/includes/EditPage.php b/includes/EditPage.php index 06a5cc3b62..9f3f586748 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -2915,7 +2915,7 @@ class EditPage { } } - $buttonLabel = $this->context->msg( $this->getSaveButtonLabel() )->text(); + $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text(); if ( $this->missingComment ) { $out->wrapWikiMsg( "
\n$1\n
", 'missingcommenttext' ); @@ -3794,7 +3794,7 @@ class EditPage { * @return string */ public function getPreviewText() { - global $wgRawHtml, $wgLang; + global $wgRawHtml; global $wgAllowUserCss, $wgAllowUserJs; $out = $this->context->getOutput(); @@ -3830,7 +3830,8 @@ class EditPage { # provide a anchor link to the editform $continueEditing = '' . - '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . + '[[#' . self::EDITFORM_ID . '|' . + $this->context->getLanguage()->getArrow() . ' ' . $this->context->msg( 'continue-editing' )->text() . ']]'; if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { @@ -4333,9 +4334,9 @@ class EditPage { * * @return string */ - private function getSaveButtonLabel() { + protected function getSubmitButtonLabel() { $labelAsPublish = - $this->mArticle->getContext()->getConfig()->get( 'EditSubmitButtonLabelPublish' ); + $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' ); // Can't use $this->isNew as that's also true if we're adding a new section to an extant page $newPage = !$this->mTitle->exists(); @@ -4360,7 +4361,7 @@ class EditPage { public function getEditButtons( &$tabindex ) { $buttons = []; - $buttonLabel = $this->context->msg( $this->getSaveButtonLabel() )->text(); + $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text(); $attribs = [ 'name' => 'wpSave', @@ -4448,11 +4449,10 @@ class EditPage { * @param string|array|bool $match Text (or array of texts) which triggered one or more filters */ public function spamPageWithContent( $match = false ) { - global $wgLang; $this->textbox2 = $this->textbox1; if ( is_array( $match ) ) { - $match = $wgLang->listToText( $match ); + $match = $this->context->getLanguage()->listToText( $match ); } $out = $this->context->getOutput(); $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) ); @@ -4640,19 +4640,20 @@ class EditPage { * @since 1.29 */ protected function addLongPageWarningHeader() { - global $wgMaxArticleSize, $wgLang; + global $wgMaxArticleSize; if ( $this->contentLength === false ) { $this->contentLength = strlen( $this->textbox1 ); } $out = $this->context->getOutput(); + $lang = $this->context->getLanguage(); if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) { $out->wrapWikiMsg( "
\n$1\n
", [ 'longpageerror', - $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ), - $wgLang->formatNum( $wgMaxArticleSize ) + $lang->formatNum( round( $this->contentLength / 1024, 3 ) ), + $lang->formatNum( $wgMaxArticleSize ) ] ); } else { @@ -4660,7 +4661,7 @@ class EditPage { $out->wrapWikiMsg( "
\n$1\n
", [ 'longpage-hint', - $wgLang->formatSize( strlen( $this->textbox1 ) ), + $lang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) ] ); @@ -4717,7 +4718,7 @@ class EditPage { protected function addExplainConflictHeader( OutputPage $out ) { $out->wrapWikiMsg( "
\n$1\n
", - [ 'explainconflict', $this->context->msg( $this->getSaveButtonLabel() )->text() ] + [ 'explainconflict', $this->context->msg( $this->getSubmitButtonLabel() )->text() ] ); } diff --git a/includes/Licenses.php b/includes/Licenses.php deleted file mode 100644 index 6467777bff..0000000000 --- a/includes/Licenses.php +++ /dev/null @@ -1,210 +0,0 @@ - - * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later - */ - -/** - * A License class for use on Special:Upload - */ -class Licenses extends HTMLFormField { - /** @var string */ - protected $msg; - - /** @var array */ - protected $licenses = []; - - /** @var string */ - protected $html; - /**#@-*/ - - /** - * @param array $params - */ - public function __construct( $params ) { - parent::__construct( $params ); - - $this->msg = empty( $params['licenses'] ) - ? wfMessage( 'licenses' )->inContentLanguage()->plain() - : $params['licenses']; - $this->selected = null; - - $this->makeLicenses(); - } - - /** - * @private - */ - protected function makeLicenses() { - $levels = []; - $lines = explode( "\n", $this->msg ); - - foreach ( $lines as $line ) { - if ( strpos( $line, '*' ) !== 0 ) { - continue; - } else { - list( $level, $line ) = $this->trimStars( $line ); - - if ( strpos( $line, '|' ) !== false ) { - $obj = new License( $line ); - $this->stackItem( $this->licenses, $levels, $obj ); - } else { - if ( $level < count( $levels ) ) { - $levels = array_slice( $levels, 0, $level ); - } - if ( $level == count( $levels ) ) { - $levels[$level - 1] = $line; - } elseif ( $level > count( $levels ) ) { - $levels[] = $line; - } - } - } - } - } - - /** - * @param string $str - * @return array - */ - protected function trimStars( $str ) { - $numStars = strspn( $str, '*' ); - return [ $numStars, ltrim( substr( $str, $numStars ), ' ' ) ]; - } - - /** - * @param array &$list - * @param array $path - * @param mixed $item - */ - protected function stackItem( &$list, $path, $item ) { - $position =& $list; - if ( $path ) { - foreach ( $path as $key ) { - $position =& $position[$key]; - } - } - $position[] = $item; - } - - /** - * @param array $tagset - * @param int $depth - */ - protected function makeHtml( $tagset, $depth = 0 ) { - foreach ( $tagset as $key => $val ) { - if ( is_array( $val ) ) { - $this->html .= $this->outputOption( - $key, '', - [ - 'disabled' => 'disabled', - 'style' => 'color: GrayText', // for MSIE - ], - $depth - ); - $this->makeHtml( $val, $depth + 1 ); - } else { - $this->html .= $this->outputOption( - $val->text, $val->template, - [ 'title' => '{{' . $val->template . '}}' ], - $depth - ); - } - } - } - - /** - * @param string $message - * @param string $value - * @param null|array $attribs - * @param int $depth - * @return string - */ - protected function outputOption( $message, $value, $attribs = null, $depth = 0 ) { - $msgObj = $this->msg( $message ); - $text = $msgObj->exists() ? $msgObj->text() : $message; - $attribs['value'] = $value; - if ( $value === $this->selected ) { - $attribs['selected'] = 'selected'; - } - - $val = str_repeat( /*   */ "\xc2\xa0", $depth * 2 ) . $text; - return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n"; - } - - /**#@-*/ - - /** - * Accessor for $this->licenses - * - * @return array - */ - public function getLicenses() { - return $this->licenses; - } - - /** - * Accessor for $this->html - * - * @param bool $value - * - * @return string - */ - public function getInputHTML( $value ) { - $this->selected = $value; - - $this->html = $this->outputOption( wfMessage( 'nolicense' )->text(), '', - (bool)$this->selected ? null : [ 'selected' => 'selected' ] ); - $this->makeHtml( $this->getLicenses() ); - - $attribs = [ - 'name' => $this->mName, - 'id' => $this->mID - ]; - if ( !empty( $this->mParams['disabled'] ) ) { - $attibs['disabled'] = 'disabled'; - } - - return Html::rawElement( 'select', $attribs, $this->html ); - } -} - -/** - * A License class for use on Special:Upload (represents a single type of license). - */ -class License { - /** @var string */ - public $template; - - /** @var string */ - public $text; - - /** - * @param string $str License name?? - */ - function __construct( $str ) { - list( $text, $template ) = explode( '|', strrev( $str ), 2 ); - - $this->template = strrev( $template ); - $this->text = strrev( $text ); - } -} diff --git a/includes/Linker.php b/includes/Linker.php index dccd99c73e..403b10a149 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -76,7 +76,7 @@ class Linker { * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18. * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead * - * @param Title $target Can currently only be a Title, but this may + * @param LinkTarget $target Can currently only be a LinkTarget, but this may * change to support Images, literal URLs, etc. * @param string $html The HTML contents of the element, i.e., * the link text. This is raw HTML and will not be escaped. If null, @@ -107,8 +107,8 @@ class Linker { public static function link( $target, $html = null, $customAttribs = [], $query = [], $options = [] ) { - if ( !$target instanceof Title ) { - wfWarn( __METHOD__ . ': Requires $target to be a Title object.', 2 ); + if ( !$target instanceof LinkTarget ) { + wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 ); return "$html"; } @@ -1175,7 +1175,7 @@ class Linker { $sectionTitle = Title::newFromText( '#' . $section ); } else { $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), - $title->getDBkey(), $section ); + $title->getDBkey(), Sanitizer::decodeCharReferences( $section ) ); } if ( $sectionTitle ) { $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' ); @@ -1291,9 +1291,7 @@ class Linker { if ( $target->getText() == '' && !$target->isExternal() && !$local && $title ) { - $newTarget = clone $title; - $newTarget->setFragment( '#' . $target->getFragment() ); - $target = $newTarget; + $target = $title->createFragmentTarget( $target->getFragment() ); } $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail; @@ -1321,7 +1319,7 @@ class Linker { * * @note This is only public for technical reasons. It's not intended for use outside Linker. * - * @param Title $title + * @param LinkTarget $linkTarget * @param string $text * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), * as used by WikiMap. @@ -1330,23 +1328,23 @@ class Linker { * @return string HTML link */ public static function makeCommentLink( - Title $title, $text, $wikiId = null, $options = [] + LinkTarget $linkTarget, $text, $wikiId = null, $options = [] ) { - if ( $wikiId !== null && !$title->isExternal() ) { + if ( $wikiId !== null && !$linkTarget->isExternal() ) { $link = self::makeExternalLink( WikiMap::getForeignURL( $wikiId, - $title->getNamespace() === 0 - ? $title->getDBkey() - : MWNamespace::getCanonicalName( $title->getNamespace() ) . ':' - . $title->getDBkey(), - $title->getFragment() + $linkTarget->getNamespace() === 0 + ? $linkTarget->getDBkey() + : MWNamespace::getCanonicalName( $linkTarget->getNamespace() ) . ':' + . $linkTarget->getDBkey(), + $linkTarget->getFragment() ), $text, /* escape = */ false // Already escaped ); } else { - $link = self::link( $title, $text, [], [], $options ); + $link = self::link( $linkTarget, $text, [], [], $options ); } return $link; diff --git a/includes/Preferences.php b/includes/Preferences.php index c29c4b9c85..0a9d70153f 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -1,7 +1,5 @@ 'tog-ccmeonemails', 'disabled' => $disableEmailPrefs, ]; + + if ( $config->get( 'EnableUserEmailBlacklist' ) + && !$disableEmailPrefs + && !(bool)$user->getOption( 'disablemail' ) + ) { + $lookup = CentralIdLookup::factory(); + $ids = $user->getOption( 'email-blacklist', [] ); + $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : []; + + $defaultPreferences['email-blacklist'] = [ + 'type' => 'usersmultiselect', + 'label-message' => 'email-blacklist-label', + 'section' => 'personal/email', + 'default' => implode( "\n", $names ), + ]; + } } if ( $config->get( 'EnotifWatchlist' ) ) { @@ -1365,7 +1379,7 @@ class Preferences { $htmlForm->setSubmitText( $context->msg( 'saveprefs' )->text() ); # Used message keys: 'accesskey-preferences-save', 'tooltip-preferences-save' $htmlForm->setSubmitTooltip( 'preferences-save' ); - $htmlForm->setSubmitID( 'prefsubmit' ); + $htmlForm->setSubmitID( 'prefcontrol' ); $htmlForm->setSubmitCallback( [ 'Preferences', 'tryFormSubmit' ] ); return $htmlForm; @@ -1635,123 +1649,3 @@ class Preferences { return $timeZoneList; } } - -/** Some tweaks to allow js prefs to work */ -class PreferencesForm extends HTMLForm { - // Override default value from HTMLForm - protected $mSubSectionBeforeFields = false; - - private $modifiedUser; - - /** - * @param User $user - */ - public function setModifiedUser( $user ) { - $this->modifiedUser = $user; - } - - /** - * @return User - */ - public function getModifiedUser() { - if ( $this->modifiedUser === null ) { - return $this->getUser(); - } else { - return $this->modifiedUser; - } - } - - /** - * Get extra parameters for the query string when redirecting after - * successful save. - * - * @return array - */ - public function getExtraSuccessRedirectParameters() { - return []; - } - - /** - * @param string $html - * @return string - */ - function wrapForm( $html ) { - $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html ); - - return parent::wrapForm( $html ); - } - - /** - * @return string - */ - function getButtons() { - $attrs = [ 'id' => 'mw-prefs-restoreprefs' ]; - - if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) { - return ''; - } - - $html = parent::getButtons(); - - if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) { - $t = $this->getTitle()->getSubpage( 'reset' ); - - $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); - $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(), - Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) ); - - $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html ); - } - - return $html; - } - - /** - * Separate multi-option preferences into multiple preferences, since we - * have to store them separately - * @param array $data - * @return array - */ - function filterDataForSubmit( $data ) { - foreach ( $this->mFlatFields as $fieldname => $field ) { - if ( $field instanceof HTMLNestedFilterable ) { - $info = $field->mParams; - $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; - foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) { - $data["$prefix$key"] = $value; - } - unset( $data[$fieldname] ); - } - } - - return $data; - } - - /** - * Get the whole body of the form. - * @return string - */ - function getBody() { - return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ); - } - - /** - * Get the "" for a given section key. Normally this is the - * prefs-$key message but we'll allow extensions to override it. - * @param string $key - * @return string - */ - function getLegend( $key ) { - $legend = parent::getLegend( $key ); - Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] ); - return $legend; - } - - /** - * Get the keys of each top level preference section. - * @return array of section keys - */ - function getPreferenceSections() { - return array_keys( array_filter( $this->mFieldTree, 'is_array' ) ); - } -} diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 7d17cd1065..a7f963a435 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -1203,8 +1203,6 @@ class Sanitizer { global $wgExperimentalHtmlIds; $options = (array)$options; - $id = self::decodeCharReferences( $id ); - if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) { $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id ); $id = trim( $id, '_' ); @@ -1313,8 +1311,6 @@ class Sanitizer { * @return string */ private static function escapeIdInternal( $id, $mode ) { - $id = self::decodeCharReferences( $id ); - switch ( $mode ) { case 'html5': $id = str_replace( ' ', '_', $id ); diff --git a/includes/TemplateParser.php b/includes/TemplateParser.php index 2759ff9baa..2293dabbd9 100644 --- a/includes/TemplateParser.php +++ b/includes/TemplateParser.php @@ -38,6 +38,13 @@ class TemplateParser { */ protected $forceRecompile = false; + /** + * @var int Compilation flags passed to LightnCandy + */ + // Do not add more flags here without discussion. + // If you do add more flags, be sure to update unit tests as well. + protected $compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION; + /** * @param string $templateDir * @param bool $forceRecompile @@ -47,6 +54,18 @@ class TemplateParser { $this->forceRecompile = $forceRecompile; } + /** + * Enable/disable the use of recursive partials. + * @param bool $enable + */ + public function enableRecursivePartials( $enable ) { + if ( $enable ) { + $this->compileFlags = $this->compileFlags | LightnCandy::FLAG_RUNTIMEPARTIAL; + } else { + $this->compileFlags = $this->compileFlags & ~LightnCandy::FLAG_RUNTIMEPARTIAL; + } + } + /** * Constructs the location of the the source Mustache template * @param string $templateName The name of the template @@ -73,11 +92,13 @@ class TemplateParser { * @throws RuntimeException */ protected function getTemplate( $templateName ) { + $templateKey = $templateName . '|' . $this->compileFlags; + // If a renderer has already been defined for this template, reuse it - if ( isset( $this->renderers[$templateName] ) && - is_callable( $this->renderers[$templateName] ) + if ( isset( $this->renderers[$templateKey] ) && + is_callable( $this->renderers[$templateKey] ) ) { - return $this->renderers[$templateName]; + return $this->renderers[$templateKey]; } $filename = $this->getTemplateFilename( $templateName ); @@ -90,7 +111,7 @@ class TemplateParser { $fileContents = file_get_contents( $filename ); // Generate a quick hash for cache invalidation - $fastHash = md5( $fileContents ); + $fastHash = md5( $this->compileFlags . '|' . $fileContents ); // Fetch a secret key for building a keyed hash of the PHP code $config = MediaWikiServices::getInstance()->getMainConfig(); @@ -127,7 +148,7 @@ class TemplateParser { if ( !is_callable( $renderer ) ) { throw new RuntimeException( "Requested template, {$templateName}, is not callable" ); } - $this->renderers[$templateName] = $renderer; + $this->renderers[$templateKey] = $renderer; return $renderer; } @@ -168,9 +189,7 @@ class TemplateParser { return LightnCandy::compile( $code, [ - // Do not add more flags here without discussion. - // If you do add more flags, be sure to update unit tests as well. - 'flags' => LightnCandy::FLAG_ERROR_EXCEPTION, + 'flags' => $this->compileFlags, 'basedir' => $this->templateDir, 'fileext' => '.mustache', ] diff --git a/includes/Xml.php b/includes/Xml.php index 16a5a9ddec..0091513125 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -493,7 +493,8 @@ class Xml { } /** - * Build a drop-down box from a textual list. + * Build a drop-down box from a textual list. This is a wrapper + * for Xml::listDropDownOptions() plus the XmlSelect class. * * @param string $name Name and id for the drop-down * @param string $list Correctly formatted text (newline delimited) to be @@ -507,60 +508,91 @@ class Xml { public static function listDropDown( $name = '', $list = '', $other = '', $selected = '', $class = '', $tabindex = null ) { - $optgroup = false; + $options = self::listDropDownOptions( $list, [ 'other' => $other ] ); + + $xmlSelect = new XmlSelect( $name, $name, $selected ); + $xmlSelect->addOptions( $options ); + + if ( $class ) { + $xmlSelect->setAttribute( 'class', $class ); + } + if ( $tabindex ) { + $xmlSelect->setAttribute( 'tabindex', $tabindex ); + } - $options = self::option( $other, 'other', $selected === 'other' ); + return $xmlSelect->getHTML(); + } + /** + * Build options for a drop-down box from a textual list. + * + * The result of this function can be passed to XmlSelect::addOptions() + * (to render a plain `' . "\n" . - '' . - '' . - '' . - '' . - '' . "\n" . + '', Xml::listDropDown( // name @@ -426,4 +429,52 @@ class XmlTest extends MediaWikiTestCase { ) ); } + + /** + * @covers Xml::listDropDownOptions + */ + public function testListDropDownOptions() { + $this->assertEquals( + [ + 'other reasons' => 'other', + 'Foo' => [ + 'Foo 1' => 'Foo 1', + 'Example' => 'Example', + ], + 'Bar' => [ + 'Bar 1' => 'Bar 1', + ], + ], + Xml::listDropDownOptions( + "* Foo\n** Foo 1\n** Example\n* Bar\n** Bar 1", + [ 'other' => 'other reasons' ] + ) + ); + } + + /** + * @covers Xml::listDropDownOptionsOoui + */ + public function testListDropDownOptionsOoui() { + $this->assertEquals( + [ + [ 'data' => 'other', 'label' => 'other reasons' ], + [ 'optgroup' => 'Foo' ], + [ 'data' => 'Foo 1', 'label' => 'Foo 1' ], + [ 'data' => 'Example', 'label' => 'Example' ], + [ 'optgroup' => 'Bar' ], + [ 'data' => 'Bar 1', 'label' => 'Bar 1' ], + ], + Xml::listDropDownOptionsOoui( [ + 'other reasons' => 'other', + 'Foo' => [ + 'Foo 1' => 'Foo 1', + 'Example' => 'Example', + ], + 'Bar' => [ + 'Bar 1' => 'Bar 1', + ], + ] ) + ); + } }