diff options
Diffstat (limited to 'includes')
220 files changed, 3272 insertions, 2535 deletions
diff --git a/includes/Defines.php b/includes/Defines.php index 31fa397d7262..d825bcab123d 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -124,14 +124,35 @@ define( 'RC_CATEGORIZE', 6 ); /** @{ * Article edit flags */ +/** Article is assumed to be non-existent, fail if it exists. */ define( 'EDIT_NEW', 1 ); + +/** Article is assumed to be pre-existing, fail if it doesn't exist. */ define( 'EDIT_UPDATE', 2 ); + +/** Mark this edit minor, if the user is allowed to do so */ define( 'EDIT_MINOR', 4 ); -define( 'EDIT_SUPPRESS_RC', 8 ); + +/** Do not notify other users (e.g. via RecentChanges or watchlist) */ +define( 'EDIT_SILENT', 8 ); + +/** @deprecated since 1.44, use EDIT_SILENT instead */ +define( 'EDIT_SUPPRESS_RC', EDIT_SILENT ); + +/** Mark the edit a "bot" edit regardless of user rights */ define( 'EDIT_FORCE_BOT', 16 ); -define( 'EDIT_DEFER_UPDATES', 32 ); // Unused since 1.27 + +/** @deprecated since 1.27, updates are always deferred */ +define( 'EDIT_DEFER_UPDATES', 32 ); + +/** Fill in blank summaries with generated text where possible */ define( 'EDIT_AUTOSUMMARY', 64 ); + +/** Signal that the page retrieve/save cycle happened entirely in this request. */ define( 'EDIT_INTERNAL', 128 ); + +/** The edit is a side effect and does not represent an active user contribution. */ +define( 'EDIT_IMPLICIT', 256 ); /** @} */ /** @{ diff --git a/includes/DomainEvent/DomainEvent.php b/includes/DomainEvent/DomainEvent.php index 1ebefb05c583..4012057c6044 100644 --- a/includes/DomainEvent/DomainEvent.php +++ b/includes/DomainEvent/DomainEvent.php @@ -33,15 +33,32 @@ abstract class DomainEvent { private string $eventType = self::ANY; private array $compatibleWithTypes = [ self::ANY ]; private ConvertibleTimestamp $timestamp; + private bool $isReconciliationRequest; /** * @stable to call + * * @param string|ConvertibleTimestamp|false $timestamp + * @param bool $isReconciliationRequest see isReconciliationRequest() */ - public function __construct( $timestamp = false ) { + public function __construct( $timestamp = false, bool $isReconciliationRequest = false ) { $this->timestamp = $timestamp instanceof ConvertibleTimestamp ? $timestamp : MWTimestamp::getInstance( $timestamp ); + + $this->isReconciliationRequest = $isReconciliationRequest; + } + + /** + * Determines whether this is a reconciliation event, triggered artificially + * in order to give listeners an opportunity to catch up on missed events or + * recreate corrupted data. + * + * Reconciliation requests are typically issued by maintenance scripts, + * but can also be caused by user actions such as null-edits. + */ + public function isReconciliationRequest(): bool { + return $this->isReconciliationRequest; } /** diff --git a/includes/Feed/FeedItem.php b/includes/Feed/FeedItem.php index bcfbcb76cdd0..9254c2c812ee 100644 --- a/includes/Feed/FeedItem.php +++ b/includes/Feed/FeedItem.php @@ -102,7 +102,7 @@ class FeedItem { /** * Encode $string so that it can be safely embedded in a XML document, * returning `null` if $string was `null`. - * @since 1.44 + * @since 1.44 (also backported to 1.39.12, 1.42.6 and 1.43.1) */ public function xmlEncodeNullable( ?string $string ): ?string { return $string !== null ? $this->xmlEncode( $string ) : null; diff --git a/includes/MainConfigSchema.php b/includes/MainConfigSchema.php index 0c8e3b8382cf..c32892be60f9 100644 --- a/includes/MainConfigSchema.php +++ b/includes/MainConfigSchema.php @@ -5384,6 +5384,7 @@ class MainConfigSchema { "src" => null, "url" => "https://www.mediawiki.org/", "alt" => "Powered by MediaWiki", + "lang" => "en", ] ], ], @@ -6996,7 +6997,8 @@ class MainConfigSchema { LocalUserRegistrationProvider::TYPE => [ 'class' => LocalUserRegistrationProvider::class, 'services' => [ - 'UserFactory' + 'UserFactory', + 'ConnectionProvider', ] ] ], diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index df8dc7beb039..2abcf3b8e7e8 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -38,9 +38,11 @@ use MediaWiki\Block\BlockManager; use MediaWiki\Block\BlockPermissionCheckerFactory; use MediaWiki\Block\BlockRestrictionStore; use MediaWiki\Block\BlockRestrictionStoreFactory; +use MediaWiki\Block\BlockTargetFactory; use MediaWiki\Block\BlockUserFactory; use MediaWiki\Block\BlockUtils; use MediaWiki\Block\BlockUtilsFactory; +use MediaWiki\Block\CrossWikiBlockTargetFactory; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Block\DatabaseBlockStoreFactory; use MediaWiki\Block\HideUserUtils; @@ -87,6 +89,7 @@ use MediaWiki\Json\JsonCodec; use MediaWiki\Language\FormatterFactory; use MediaWiki\Language\Language; use MediaWiki\Language\LanguageCode; +use MediaWiki\Language\MessageParser; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\Languages\LanguageFactory; use MediaWiki\Languages\LanguageFallback; @@ -184,6 +187,7 @@ use MediaWiki\User\Registration\UserRegistrationLookup; use MediaWiki\User\TalkPageNotificationManager; use MediaWiki\User\TempUser\RealTempUserConfig; use MediaWiki\User\TempUser\TempUserCreator; +use MediaWiki\User\TempUser\TempUserDetailsLookup; use MediaWiki\User\UserEditTracker; use MediaWiki\User\UserFactory; use MediaWiki\User\UserGroupManager; @@ -829,6 +833,13 @@ class MediaWikiServices extends ServiceContainer { } /** + * @since 1.44 + */ + public function getBlockTargetFactory(): BlockTargetFactory { + return $this->getService( 'BlockTargetFactory' ); + } + + /** * @since 1.36 */ public function getBlockUserFactory(): BlockUserFactory { @@ -836,6 +847,7 @@ class MediaWikiServices extends ServiceContainer { } /** + * @deprecated since 1.44 * @since 1.36 */ public function getBlockUtils(): BlockUtils { @@ -843,6 +855,7 @@ class MediaWikiServices extends ServiceContainer { } /** + * @deprecated since 1.44 * @since 1.42 */ public function getBlockUtilsFactory(): BlockUtilsFactory { @@ -1030,6 +1043,13 @@ class MediaWikiServices extends ServiceContainer { } /** + * @since 1.44 + */ + public function getCrossWikiBlockTargetFactory(): CrossWikiBlockTargetFactory { + return $this->getService( 'CrossWikiBlockTargetFactory' ); + } + + /** * @since 1.36 */ public function getDatabaseBlockStore(): DatabaseBlockStore { @@ -1468,6 +1488,13 @@ class MediaWikiServices extends ServiceContainer { } /** + * @since 1.44 + */ + public function getMessageParser(): MessageParser { + return $this->getService( 'MessageParser' ); + } + + /** * @since 1.42 * @unstable * @return BagOStuff @@ -1988,6 +2015,13 @@ class MediaWikiServices extends ServiceContainer { } /** + * @since 1.44 + */ + public function getTempUserDetailsLookup(): TempUserDetailsLookup { + return $this->getService( 'TempUserDetailsLookup' ); + } + + /** * @since 1.36 */ public function getTidy(): TidyDriverBase { diff --git a/includes/Notification/NotificationService.php b/includes/Notification/NotificationService.php index 703b4ecb5b25..b3545d346f13 100644 --- a/includes/Notification/NotificationService.php +++ b/includes/Notification/NotificationService.php @@ -9,7 +9,7 @@ use Wikimedia\Message\MessageSpecifier; use Wikimedia\ObjectFactory\ObjectFactory; /** - * Notify users about things occuring. + * Notify users about things occurring. * * @since 1.44 * @unstable @@ -54,14 +54,17 @@ class NotificationService { } /** - * Notify users about an event occuring. This method allows providing custom notification data to + * Notify users about an event occurring. This method allows providing custom notification data to * be handled by extensions, and defining multiple recipients. */ public function notify( Notification $notification, RecipientSet $recipients ): void { $handlers = $this->getHandlers(); $handler = $handlers[$notification->getType()] ?? $handlers['*'] ?? null; - if ( !$handler ) { - throw new RuntimeException( "No handler defined for notification type \"{$notification->getType()}\"" ); + if ( $handler === null ) { + $this->logger->warning( "No handler defined for notification type {type}", [ + 'type' => $notification->getType(), + ] ); + return; } $handler->notify( $notification, $recipients ); } diff --git a/includes/OutputTransform/Stages/DeduplicateStyles.php b/includes/OutputTransform/Stages/DeduplicateStyles.php index 692e5933a910..f80e70f1896d 100644 --- a/includes/OutputTransform/Stages/DeduplicateStyles.php +++ b/includes/OutputTransform/Stages/DeduplicateStyles.php @@ -2,11 +2,12 @@ namespace MediaWiki\OutputTransform\Stages; -use MediaWiki\Html\Html; +use MediaWiki\Html\HtmlHelper; use MediaWiki\OutputTransform\ContentTextTransformStage; use MediaWiki\Parser\ParserOptions; use MediaWiki\Parser\ParserOutput; -use MediaWiki\Parser\Sanitizer; +use Wikimedia\RemexHtml\Serializer\SerializerNode; +use Wikimedia\RemexHtml\Tokenizer\PlainAttributes; /** * Generates a list of unique style links @@ -20,28 +21,32 @@ class DeduplicateStyles extends ContentTextTransformStage { protected function transformText( string $text, ParserOutput $po, ?ParserOptions $popts, array &$options ): string { $seen = []; - return preg_replace_callback( '#<style\s+([^>]*data-mw-deduplicate\s*=[\'"][^>]*)>.*?</style>#s', - static function ( $m ) use ( &$seen ) { - $attr = Sanitizer::decodeTagAttributes( $m[1] ); - if ( !isset( $attr['data-mw-deduplicate'] ) ) { - return $m[0]; - } - - $key = $attr['data-mw-deduplicate']; + return HtmlHelper::modifyElements( + $text, + static function ( SerializerNode $node ): bool { + return $node->name === 'style' && + ( $node->attrs['data-mw-deduplicate'] ?? '' ) !== ''; + }, + static function ( SerializerNode $node ) use ( &$seen ): SerializerNode { + $key = $node->attrs['data-mw-deduplicate']; if ( !isset( $seen[$key] ) ) { $seen[$key] = true; - - return $m[0]; + return $node; } - // We were going to use an empty <style> here, but there // was concern that would be too much overhead for browsers. // So let's hope a <link> with a non-standard rel and href isn't // going to be misinterpreted or mangled by any subsequent processing. - return Html::element( 'link', [ + $node->name = 'link'; + $node->attrs = new PlainAttributes( [ 'rel' => 'mw-deduplicated-inline-style', 'href' => "mw-data:" . wfUrlencode( $key ), ] ); - }, $text ); + $node->children = []; + $node->void = true; + return $node; + }, + $options['isParsoidContent'] ?? false + ); } } diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index 3824e16fcbdc..652f95326544 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -310,6 +310,17 @@ class PermissionManager { return $this->userCan( $action, $user, $page, self::RIGOR_QUICK ); } + /** @var array For use by deprecated getPermissionErrors() only */ + private const BLOCK_CODES = [ + 'blockedtext' => true, + 'blockedtext-partial' => true, + 'autoblockedtext' => true, + 'systemblockedtext' => true, + 'blockedtext-composite' => true, + 'blockedtext-tempuser' => true, + 'autoblockedtext-tempuser' => true, + ]; + /** * Can $user perform $action on a page? * @@ -350,6 +361,11 @@ class PermissionManager { $key = $keyOrMsg instanceof MessageSpecifier ? $keyOrMsg->getKey() : $keyOrMsg; // Remove the errors being ignored. if ( !in_array( $key, $ignoreErrors ) ) { + // Remove modern block info that is not expected by users of this legacy API + if ( isset( self::BLOCK_CODES[ $key ] ) && $keyOrMsg instanceof MessageSpecifier ) { + $params = $keyOrMsg->getParams(); + $keyOrMsg = $key; + } $result[] = [ $keyOrMsg, ...$params ]; } } @@ -802,6 +818,8 @@ class PermissionManager { ); if ( $block ) { + $status->setBlock( $block ); + // @todo FIXME: Pass the relevant context into this function. $context = RequestContext::getMain(); $messages = $this->blockErrorFormatter->getMessages( @@ -811,10 +829,7 @@ class PermissionManager { ); foreach ( $messages as $message ) { - // TODO: We can pass $message directly once getPermissionErrors() is removed. - // For now we store the message key as a string here out of overabundance of caution, - // because there is a test case verifying that block messages use strings in that format. - $status->fatal( $message->getKey(), ...$message->getParams() ); + $status->fatal( $message ); } } } diff --git a/includes/Permissions/UserAuthority.php b/includes/Permissions/UserAuthority.php index fca1534344bb..a138eaccbf0b 100644 --- a/includes/Permissions/UserAuthority.php +++ b/includes/Permissions/UserAuthority.php @@ -324,17 +324,6 @@ class UserAuthority implements Authority { return !$status || $status->isOK(); } - // See ApiBase::BLOCK_CODE_MAP - private const BLOCK_CODES = [ - 'blockedtext', - 'blockedtext-partial', - 'autoblockedtext', - 'systemblockedtext', - 'blockedtext-composite', - 'blockedtext-tempuser', - 'autoblockedtext-tempuser', - ]; - /** * @param string $rigor * @param string $action @@ -376,32 +365,13 @@ class UserAuthority implements Authority { $rigor ); - if ( $tempStatus->isGood() ) { - // Nothing to merge, return early - return $status->isOK(); - } - - // Instead of `$status->merge( $tempStatus )`, process the messages like this to ensure that - // the resulting status contains Message objects instead of strings+arrays, and thus does not - // trigger wikitext escaping in a legacy code path. See T368821 for more information about - // that behavior, and see T306494 for the specific bug this fixes. - foreach ( $tempStatus->getMessages() as $msg ) { - $status->fatal( $msg ); + if ( !$tempStatus->isGood() ) { + $status->merge( $tempStatus ); } - foreach ( self::BLOCK_CODES as $code ) { - // HACK: Detect whether the permission was denied because the user is blocked. - // A similar hack exists in ApiBase::BLOCK_CODE_MAP. - // When permission checking logic is moved out of PermissionManager, - // we can record the block info directly when first checking the block, - // rather than doing that here. - if ( $tempStatus->hasMessage( $code ) ) { - $block = $this->getBlock(); - if ( $block ) { - $status->setBlock( $block ); - } - break; - } + $block = $tempStatus->getBlock(); + if ( $block ) { + $status->setBlock( $block ); } return $status->isOK(); diff --git a/includes/Rest/i18n/ru.json b/includes/Rest/i18n/ru.json index 2107369dbcf7..a0f272955d89 100644 --- a/includes/Rest/i18n/ru.json +++ b/includes/Rest/i18n/ru.json @@ -8,7 +8,8 @@ "Katunchik", "Megakott", "Okras", - "Vlad5250" + "Vlad5250", + "Yurina Tatiana" ] }, "rest-prefix-mismatch": "Запрашиваемый путь ($1) не найден внутри базового пути REST API ($2)", @@ -68,5 +69,11 @@ "rest-param-desc-comment": "Причина редактирования страницы. Чтобы комментарий был размещён на сервере, используйте \"комментарий: null\".", "rest-param-desc-contentmodel": "Формат содержимого страницы: вики-текст (по умолчанию), css, javascript, json или text.", "rest-param-desc-update-latest": "Информация о новейшей версии страницы. Вы можете получить эту информацию из источника источника страницы.", - "rest-param-desc-create-title": "Заголовок страницы. Посетите отдельные вики для ознакомления с политикой относительно форматов и символов заголовков страниц." + "rest-param-desc-create-title": "Заголовок страницы. Посетите отдельные вики для ознакомления с политикой относительно форматов и символов заголовков страниц.", + "rest-param-desc-language-links-title": "Заголовок вики-страницы", + "rest-param-desc-media-file-title": "Заголовок вики-страницы", + "rest-param-desc-media-links-title": "Заголовок вики-страницы", + "rest-param-desc-pagehistory-count-title": "Заголовок вики-страницы", + "rest-param-desc-pagehistory-title": "Заголовок вики-страницы", + "rest-param-desc-search-q": "Условия поиска" } diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index a54f06e3c6d1..831d45bbaff2 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -54,9 +54,11 @@ use MediaWiki\Block\BlockManager; use MediaWiki\Block\BlockPermissionCheckerFactory; use MediaWiki\Block\BlockRestrictionStore; use MediaWiki\Block\BlockRestrictionStoreFactory; +use MediaWiki\Block\BlockTargetFactory; use MediaWiki\Block\BlockUserFactory; use MediaWiki\Block\BlockUtils; use MediaWiki\Block\BlockUtilsFactory; +use MediaWiki\Block\CrossWikiBlockTargetFactory; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Block\DatabaseBlockStoreFactory; @@ -117,6 +119,7 @@ use MediaWiki\Language\FormatterFactory; use MediaWiki\Language\Language; use MediaWiki\Language\LanguageCode; use MediaWiki\Language\LazyLocalizationContext; +use MediaWiki\Language\MessageParser; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\Languages\LanguageEventIngress; use MediaWiki\Languages\LanguageFactory; @@ -245,6 +248,7 @@ use MediaWiki\User\Registration\UserRegistrationLookup; use MediaWiki\User\TalkPageNotificationManager; use MediaWiki\User\TempUser\RealTempUserConfig; use MediaWiki\User\TempUser\TempUserCreator; +use MediaWiki\User\TempUser\TempUserDetailsLookup; use MediaWiki\User\UserEditTracker; use MediaWiki\User\UserFactory; use MediaWiki\User\UserGroupManager; @@ -452,7 +456,7 @@ return [ BlockPermissionCheckerFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ), - $services->getBlockUtils() + $services->getBlockTargetFactory() ); }, @@ -466,6 +470,10 @@ return [ ); }, + 'BlockTargetFactory' => static function ( MediaWikiServices $services ): BlockTargetFactory { + return $services->getCrossWikiBlockTargetFactory()->getFactory(); + }, + 'BlockUserFactory' => static function ( MediaWikiServices $services ): BlockUserFactory { return $services->getService( '_UserBlockCommandFactory' ); }, @@ -476,13 +484,7 @@ return [ 'BlockUtilsFactory' => static function ( MediaWikiServices $services ): BlockUtilsFactory { return new BlockUtilsFactory( - new ServiceOptions( - BlockUtilsFactory::CONSTRUCTOR_OPTIONS, - $services->getMainConfig() - ), - $services->getActorStoreFactory(), - $services->getUserNameUtils(), - $services->getDBLoadBalancerFactory() + $services->getCrossWikiBlockTargetFactory() ); }, @@ -682,6 +684,14 @@ return [ return RequestTimeout::singleton()->createCriticalSectionProvider( $limit ); }, + 'CrossWikiBlockTargetFactory' => static function ( MediaWikiServices $services ): CrossWikiBlockTargetFactory { + return new CrossWikiBlockTargetFactory( + new ServiceOptions( CrossWikiBlockTargetFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ), + $services->getActorStoreFactory(), + $services->getUserNameUtils() + ); + }, + 'DatabaseBlockStore' => static function ( MediaWikiServices $services ): DatabaseBlockStore { return $services->getDatabaseBlockStoreFactory()->getDatabaseBlockStore( DatabaseBlock::LOCAL ); }, @@ -701,7 +711,7 @@ return [ $services->getReadOnlyMode(), $services->getUserFactory(), $services->getTempUserConfig(), - $services->getBlockUtilsFactory(), + $services->getCrossWikiBlockTargetFactory(), $services->getAutoblockExemptionList() ); }, @@ -877,7 +887,7 @@ return [ 'FormatterFactory' => static function ( MediaWikiServices $services ): FormatterFactory { return new FormatterFactory( - $services->getMessageCache(), + $services->getMessageParser(), $services->getTitleFormatter(), $services->getHookContainer(), $services->getUserIdentityUtils(), @@ -1312,7 +1322,7 @@ return [ $services->getLanguageNameUtils(), $services->getLanguageFallback(), $services->getHookContainer(), - $services->getParserFactory() + $services->getMessageParser() ); }, @@ -1320,6 +1330,15 @@ return [ return new MessageFormatterFactory(); }, + 'MessageParser' => static function ( MediaWikiServices $services ): MessageParser { + return new MessageParser( + $services->getParserFactory(), + $services->getDefaultOutputPipeline(), + $services->getLanguageFactory(), + LoggerFactory::getInstance( 'MessageParser' ) + ); + }, + 'MicroStash' => static function ( MediaWikiServices $services ): BagOStuff { $mainConfig = $services->getMainConfig(); @@ -2345,6 +2364,13 @@ return [ ); }, + 'TempUserDetailsLookup' => static function ( MediaWikiServices $services ): TempUserDetailsLookup { + return new TempUserDetailsLookup( + $services->getTempUserConfig(), + $services->getUserRegistrationLookup() + ); + }, + 'Tidy' => static function ( MediaWikiServices $services ): TidyDriverBase { return new RemexDriver( new ServiceOptions( @@ -2828,7 +2854,7 @@ return [ new ServiceOptions( UserBlockCommandFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ), $services->getHookContainer(), $services->getBlockPermissionCheckerFactory(), - $services->getBlockUtils(), + $services->getBlockTargetFactory(), $services->getDatabaseBlockStore(), $services->getBlockRestrictionStore(), $services->getUserFactory(), diff --git a/includes/Status/StatusFormatter.php b/includes/Status/StatusFormatter.php index c3d0cfbfe63e..bda0c5afd104 100644 --- a/includes/Status/StatusFormatter.php +++ b/includes/Status/StatusFormatter.php @@ -22,11 +22,11 @@ namespace MediaWiki\Status; use MediaWiki\Api\ApiMessage; use MediaWiki\Language\Language; +use MediaWiki\Language\MessageParser; use MediaWiki\Language\RawMessage; use MediaWiki\Message\Message; use MediaWiki\Page\PageReferenceValue; use MediaWiki\StubObject\StubUserLang; -use MessageCache; use MessageLocalizer; use Psr\Log\LoggerInterface; use RuntimeException; @@ -45,16 +45,16 @@ use Wikimedia\Message\MessageSpecifier; class StatusFormatter { private MessageLocalizer $messageLocalizer; - private MessageCache $messageCache; + private MessageParser $messageParser; private LoggerInterface $logger; public function __construct( MessageLocalizer $messageLocalizer, - MessageCache $messageCache, + MessageParser $messageParser, LoggerInterface $logger ) { $this->messageLocalizer = $messageLocalizer; - $this->messageCache = $messageCache; + $this->messageParser = $messageParser; $this->logger = $logger; } @@ -351,7 +351,7 @@ class StatusFormatter { $lang = $options['lang'] ?? null; $text = $this->getWikiText( $status, $options ); - $out = $this->messageCache->parseWithPostprocessing( + $out = $this->messageParser->parse( $text, PageReferenceValue::localReference( NS_SPECIAL, 'Badtitle/StatusFormatter' ), /*linestart*/ true, diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index 941ff01b67e8..05c1f4db4d5c 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -1249,7 +1249,7 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { if ( $this->revision && $this->revision->getId() ) { if ( $this->revision->getId() === $revision->getId() ) { - return; // nothing to do! + $this->options['changed'] = false; // null-edit } else { throw new LogicException( 'Trying to re-use DerivedPageDataUpdater with revision ' @@ -1589,7 +1589,7 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { // TODO: MCR: check if *any* changed slot supports categories! if ( $this->rcWatchCategoryMembership && $this->getContentHandler( SlotRecord::MAIN )->supportsCategories() === true - && ( $event->isContentChange() || $event->isNew() ) + && ( $event->isContentChange() || $event->isCreation() ) && !$event->hasCause( PageUpdatedEvent::CAUSE_UNDELETE ) ) { // Note: jobs are pushed after deferred updates, so the job should be able to see @@ -1621,7 +1621,7 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { ( !$event->isContentChange() && !$event->hasCause( PageUpdatedEvent::CAUSE_MOVE ) ) ) { $good = 0; - } elseif ( $event->isNew() ) { + } elseif ( $event->isCreation() ) { $good = (int)$this->isCountable(); } elseif ( $this->options['oldcountable'] !== null ) { $good = (int)$this->isCountable() @@ -1633,7 +1633,7 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { $good = 0; } $edits = $event->isContentChange() ? 1 : 0; - $pages = $event->isNew() ? 1 : 0; + $pages = $event->isCreation() ? 1 : 0; DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ] @@ -1641,7 +1641,7 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { } ); // TODO: move onArticleCreate and onArticleEdit into a PageEventEmitter service - if ( $event->isNew() ) { + if ( $event->isCreation() ) { // Deferred update that adds a mw-recreated tag to edits that create new pages // which have an associated deletion log entry for the specific namespace/title combination // and which are not undeletes @@ -1701,6 +1701,14 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { PageUpdatedEvent::DEFAULT_FLAGS ); + $newRevision = $this->getRevision(); + $oldRevision = $this->getOldRevision(); + + if ( $oldRevision && $newRevision->getId() === $oldRevision->getId() ) { + // This is a null edit, flag it as a reconciliation request. + $flags[ PageUpdatedEvent::FLAG_RECONCILIATION_REQUEST ] = true; + } + /** @var UserIdentity $performer */ $performer = $this->options['triggeringUser'] ?? $this->user; @@ -1710,8 +1718,8 @@ class DerivedPageDataUpdater implements LoggerAwareInterface, PreparedUpdate { // @phan-suppress-next-line PhanTypeMismatchArgumentNullable $this->user is already set $performer, $this->getRevisionSlotsUpdate(), - $this->getRevision(), - $this->getOldRevision(), + $newRevision, + $oldRevision, $this->options['editResult'] ?? null, $this->options['tags'] ?? [], $flags, diff --git a/includes/Storage/PageUpdater.php b/includes/Storage/PageUpdater.php index 21f86f19b5b9..4041d38ffc6c 100644 --- a/includes/Storage/PageUpdater.php +++ b/includes/Storage/PageUpdater.php @@ -305,24 +305,9 @@ class PageUpdater implements PageUpdateCauses { * Flags passed in subsequent calls to this method as well as calls to prepareUpdate() * or saveRevision() are aggregated using bitwise OR. * - * Known flags: - * - * EDIT_NEW - * Create a new page, or fail with "edit-already-exists" if the page exists. - * EDIT_UPDATE - * Create a new revision, or fail with "edit-gone-missing" if the page does not exist. - * EDIT_MINOR - * Mark this revision as minor - * EDIT_SUPPRESS_RC - * Do not log the change in recentchanges - * EDIT_FORCE_BOT - * Mark the revision as automated ("bot edit") - * EDIT_AUTOSUMMARY - * Fill in blank summaries with generated text where possible - * EDIT_INTERNAL - * Signal that the page retrieve/save cycle happened entirely in this request. - * - * @param int $flags Bitfield + * @param int $flags Bitfield, see the EDIT_XXX constants such as EDIT_NEW + * or EDIT_FORCE_BOT. + * * @return $this */ public function setFlags( int $flags ) { @@ -413,19 +398,6 @@ class PageUpdater implements PageUpdateCauses { } /** - * Indicate that the page update was not explicitly performed by the user. - * - * @param bool $automated - * - * @return $this - * - * @since 1.44 - */ - public function setAutomated( bool $automated ) { - return $this->setHints( [ PageUpdatedEvent::FLAG_AUTOMATED => $automated ] ); - } - - /** * Whether to create a log entry for new page creations. * * @see $wgPageCreationLog @@ -831,15 +803,18 @@ class PageUpdater implements PageUpdateCauses { * revision history, such as the page getting renamed. * * @param CommentStoreComment|string $summary Edit summary + * @param int $flags Bitfield, will be combined with the flags set via setFlags(). + * Callers should use this to set the EDIT_SILENT and EDIT_MINOR flag + * if appropriate. The EDIT_UPDATE | EDIT_INTERNAL | EDIT_IMPLICIT + * flags will always be set. * * @return RevisionRecord The newly created dummy revision * * @since 1.44 */ - public function saveDummyRevision( $summary ) { - $flags = EDIT_UPDATE | EDIT_SUPPRESS_RC | EDIT_INTERNAL; + public function saveDummyRevision( $summary, int $flags = 0 ) { + $flags |= EDIT_UPDATE | EDIT_INTERNAL | EDIT_IMPLICIT; - $this->setAutomated( true ); $this->setForceEmptyRevision( true ); $rev = $this->saveRevision( $summary, $flags ); @@ -1358,7 +1333,7 @@ class PageUpdater implements PageUpdateCauses { [], [ PageUpdatedEvent::FLAG_SILENT => true, - PageUpdatedEvent::FLAG_AUTOMATED => true, + PageUpdatedEvent::FLAG_IMPLICIT => true, 'dispatchPageUpdatedEvent' => false, ] ); @@ -1500,11 +1475,22 @@ class PageUpdater implements PageUpdateCauses { // error-prone way is to reuse given old revision. $newRevisionRecord = $oldRev; + $this->prepareDerivedDataUpdater( + $wikiPage, + $newRevisionRecord, + $summary, + [], + [ 'changed' => false ] + ); + $status->warning( 'edit-no-change' ); // Update page_touched as updateRevisionOn() was not called. // Other cache updates are managed in WikiPage::onArticleEdit() // via WikiPage::doEditUpdates(). $this->getTitle()->invalidateCache( $now ); + + // Notify the dispatcher of the PageUpdatedEvent during the transaction round + $this->dispatchPageUpdatedEvent(); } // Schedule the secondary updates to run after the transaction round commits. @@ -1650,8 +1636,9 @@ class PageUpdater implements PageUpdateCauses { array $hintOverrides = [] ) { static $flagMap = [ - EDIT_SUPPRESS_RC => PageUpdatedEvent::FLAG_SILENT, - EDIT_FORCE_BOT => PageUpdatedEvent::FLAG_BOT + EDIT_SILENT => PageUpdatedEvent::FLAG_SILENT, + EDIT_FORCE_BOT => PageUpdatedEvent::FLAG_BOT, + EDIT_IMPLICIT => PageUpdatedEvent::FLAG_IMPLICIT, ]; $hints = $this->hints; @@ -1670,8 +1657,6 @@ class PageUpdater implements PageUpdateCauses { $hints['editResult'] = $editResult; if ( $editResult->isRevert() ) { - $hints[ PageUpdatedEvent::FLAG_REVERTED ] = true; - // Should the reverted tag update be scheduled right away? // The revert is approved if either patrolling is disabled or the // edit is patrolled or autopatrolled. diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 6ef7f8741bbe..6980b2d2023b 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -35,7 +35,6 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Message\Message; use MediaWiki\Page\PageIdentity; use MediaWiki\ParamValidator\TypeDef\NamespaceDef; -use MediaWiki\Permissions\Authority; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Permissions\PermissionStatus; use MediaWiki\Registration\ExtensionRegistry; @@ -75,8 +74,6 @@ use WikiPage; */ abstract class ApiBase extends ContextSource { - use ApiBlockInfoTrait; - /** @var HookContainer */ private $hookContainer; @@ -267,17 +264,6 @@ abstract class ApiBase extends ContextSource { /** @var stdClass[][] Cache for self::filterIDs() */ private static $filterIDsCache = []; - /** @var array Map of web UI block messages which magically gain machine-readable block info */ - private const BLOCK_CODE_MAP = [ - 'blockedtext' => true, - 'blockedtext-partial' => true, - 'autoblockedtext' => true, - 'systemblockedtext' => true, - 'blockedtext-composite' => true, - 'blockedtext-tempuser' => true, - 'autoblockedtext-tempuser' => true, - ]; - /** @var array Map of web UI block messages to corresponding API messages and codes */ private const MESSAGE_CODE_MAP = [ 'actionthrottled' => [ 'apierror-ratelimited', 'ratelimited' ], @@ -1377,35 +1363,6 @@ abstract class ApiBase extends ContextSource { } /** - * Add block info to block messages in a Status - * @since 1.33 - * @internal since 1.37, should become protected in the future. - * @param StatusValue $status - * @param Authority|null $user - */ - public function addBlockInfoToStatus( StatusValue $status, ?Authority $user = null ) { - if ( $status instanceof PermissionStatus ) { - $block = $status->getBlock(); - } else { - $user = $user ?: $this->getAuthority(); - $block = $user->getBlock(); - } - - if ( !$block ) { - return; - } - foreach ( $status->getMessages() as $msg ) { - if ( isset( self::BLOCK_CODE_MAP[$msg->getKey()] ) ) { - $status->replaceMessage( $msg->getKey(), ApiMessage::create( - Message::newFromSpecifier( $msg ), - $this->getBlockCode( $block ), - [ 'blockinfo' => $this->getBlockDetails( $block ) ] - ) ); - } - } - } - - /** * Call wfTransactionalTimeLimit() if this request was POSTed. * * @since 1.26 @@ -1601,11 +1558,7 @@ abstract class ApiBase extends ContextSource { $this->getRequest()->getIP() ); - $this->dieWithError( - $msg, - $this->getBlockCode( $block ), - [ 'blockinfo' => $this->getBlockDetails( $block ) ] - ); + $this->dieWithError( $msg ); } /** @@ -1650,8 +1603,6 @@ abstract class ApiBase extends ContextSource { $status = $newStatus; } - $this->addBlockInfoToStatus( $status ); - throw new ApiUsageException( $this, $status ); } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index c37b57cb5d5e..f1ac59a79574 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -25,9 +25,10 @@ namespace MediaWiki\Api; use MediaWiki\Block\AbstractBlock; use MediaWiki\Block\BlockActionInfo; use MediaWiki\Block\BlockPermissionCheckerFactory; +use MediaWiki\Block\BlockTarget; +use MediaWiki\Block\BlockTargetFactory; use MediaWiki\Block\BlockUser; use MediaWiki\Block\BlockUserFactory; -use MediaWiki\Block\BlockUtils; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Block\Restriction\ActionRestriction; @@ -41,7 +42,6 @@ use MediaWiki\Status\Status; use MediaWiki\Title\Title; use MediaWiki\Title\TitleFactory; use MediaWiki\User\Options\UserOptionsLookup; -use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityLookup; use MediaWiki\Watchlist\WatchedItemStoreInterface; use MediaWiki\Watchlist\WatchlistManager; @@ -64,9 +64,9 @@ class ApiBlock extends ApiBase { private TitleFactory $titleFactory; private UserIdentityLookup $userIdentityLookup; private WatchedItemStoreInterface $watchedItemStore; - private BlockUtils $blockUtils; private BlockActionInfo $blockActionInfo; private DatabaseBlockStore $blockStore; + private BlockTargetFactory $blockTargetFactory; public function __construct( ApiMain $main, @@ -76,7 +76,7 @@ class ApiBlock extends ApiBase { TitleFactory $titleFactory, UserIdentityLookup $userIdentityLookup, WatchedItemStoreInterface $watchedItemStore, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, BlockActionInfo $blockActionInfo, DatabaseBlockStore $blockStore, WatchlistManager $watchlistManager, @@ -89,7 +89,7 @@ class ApiBlock extends ApiBase { $this->titleFactory = $titleFactory; $this->userIdentityLookup = $userIdentityLookup; $this->watchedItemStore = $watchedItemStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->blockActionInfo = $blockActionInfo; $this->blockStore = $blockStore; @@ -127,12 +127,13 @@ class ApiBlock extends ApiBase { $status = $this->updateBlock( $block, $params ); } else { if ( $params['user'] !== null ) { - $target = $params['user']; + $target = $this->blockTargetFactory->newFromUser( $params['user'] ); } else { - $target = $this->userIdentityLookup->getUserIdentityByUserId( $params['userid'] ); - if ( !$target ) { + $targetUser = $this->userIdentityLookup->getUserIdentityByUserId( $params['userid'] ); + if ( !$targetUser ) { $this->dieWithError( [ 'apierror-nosuchuserid', $params['userid'] ], 'nosuchuserid' ); } + $target = $this->blockTargetFactory->newUserBlockTarget( $targetUser ); } if ( $params['newblock'] ) { $status = $this->insertBlock( $target, $params ); @@ -289,7 +290,7 @@ class ApiBlock extends ApiBase { /** * Insert a block * - * @param UserIdentity|string $target + * @param BlockTarget $target * @param array $params * @return Status */ @@ -320,6 +321,7 @@ class ApiBlock extends ApiBase { 'user' => [ ParamValidator::PARAM_TYPE => 'user', UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'userid' => [ ParamValidator::PARAM_TYPE => 'integer', diff --git a/includes/api/ApiBlockInfoHelper.php b/includes/api/ApiBlockInfoHelper.php new file mode 100644 index 000000000000..ad0fcf8814f7 --- /dev/null +++ b/includes/api/ApiBlockInfoHelper.php @@ -0,0 +1,123 @@ +<?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 + */ + +namespace MediaWiki\Api; + +use MediaWiki\Block\AbstractBlock; +use MediaWiki\Block\Block; +use MediaWiki\Block\CompositeBlock; +use MediaWiki\Block\DatabaseBlock; +use MediaWiki\Block\SystemBlock; +use MediaWiki\Language\Language; +use MediaWiki\User\UserIdentity; +use MediaWiki\Utils\MWTimestamp; + +/** + * Helper class for API modules that display block information. Intended for use via + * composition. + * + * @ingroup API + * @since 1.44 + */ +class ApiBlockInfoHelper { + + /** + * Get basic info about a given block + * + * @return array Array containing several keys: + * - blockid - ID of the block + * - blockedby - username of the blocker + * - blockedbyid - user ID of the blocker + * - blockreason - reason provided for the block + * - blockedtimestamp - timestamp for when the block was placed/modified + * - blockedtimestampformatted - blockedtimestamp, formatted for the current locale + * - blockexpiry - expiry time of the block + * - blockexpiryformatted - blockexpiry formatted for the current locale, omitted if infinite + * - blockexpiryrelative - relative time to blockexpiry (e.g. 'in 5 days'), omitted if infinite + * - blockpartial - block only applies to certain pages, namespaces and/or actions + * - systemblocktype - system block type, if any + * - blockcomponents - If the block is a composite block, this will be an array of block + * info arrays + */ + public function getBlockDetails( + Block $block, + Language $language, + UserIdentity $user + ) { + $blocker = $block->getBlocker(); + + $vals = []; + $vals['blockid'] = $block->getId(); + $vals['blockedby'] = $blocker ? $blocker->getName() : ''; + $vals['blockedbyid'] = $blocker ? $blocker->getId() : 0; + $vals['blockreason'] = $block->getReasonComment() + ->message->inLanguage( $language )->plain(); + $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->getTimestamp() ); + $expiry = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' ); + $vals['blockexpiry'] = $expiry; + $vals['blockpartial'] = !$block->isSitewide(); + $vals['blocknocreate'] = $block->isCreateAccountBlocked(); + $vals['blockanononly'] = !$block->isHardblock(); + if ( $block instanceof AbstractBlock ) { + $vals['blockemail'] = $block->isEmailBlocked(); + $vals['blockowntalk'] = !$block->isUsertalkEditAllowed(); + } + + // Formatted timestamps + $vals['blockedtimestampformatted'] = $language->formatExpiry( + $block->getTimestamp(), true, 'infinity', $user + ); + if ( $expiry !== 'infinite' ) { + $vals['blockexpiryformatted'] = $language->formatExpiry( + $expiry, true, 'infinity', $user + ); + $vals['blockexpiryrelative'] = $language->getHumanTimestamp( + new MWTimestamp( $expiry ), new MWTimestamp(), $user + ); + } + + if ( $block instanceof SystemBlock ) { + $vals['systemblocktype'] = $block->getSystemBlockType(); + } + + if ( $block instanceof CompositeBlock ) { + $components = []; + foreach ( $block->getOriginalBlocks() as $singleBlock ) { + $components[] = $this->getBlockDetails( $singleBlock, $language, $user ); + } + $vals['blockcomponents'] = $components; + } + + return $vals; + } + + /** + * Get the API error code, to be used in ApiMessage::create or ApiBase::dieWithError + * @param Block $block + * @return string + */ + public function getBlockCode( Block $block ): string { + if ( $block instanceof DatabaseBlock && $block->getType() === Block::TYPE_AUTO ) { + return 'autoblocked'; + } + return 'blocked'; + } + +} diff --git a/includes/api/ApiBlockInfoTrait.php b/includes/api/ApiBlockInfoTrait.php index 86e1c32753eb..467622d4f88b 100644 --- a/includes/api/ApiBlockInfoTrait.php +++ b/includes/api/ApiBlockInfoTrait.php @@ -20,14 +20,9 @@ namespace MediaWiki\Api; -use MediaWiki\Block\AbstractBlock; use MediaWiki\Block\Block; -use MediaWiki\Block\CompositeBlock; -use MediaWiki\Block\DatabaseBlock; -use MediaWiki\Block\SystemBlock; use MediaWiki\Language\Language; use MediaWiki\User\UserIdentity; -use MediaWiki\Utils\MWTimestamp; /** * @ingroup API @@ -57,54 +52,8 @@ trait ApiBlockInfoTrait { Block $block, $language = null ) { - $language ??= $this->getLanguage(); - - $blocker = $block->getBlocker(); - - $vals = []; - $vals['blockid'] = $block->getId(); - $vals['blockedby'] = $blocker ? $blocker->getName() : ''; - $vals['blockedbyid'] = $blocker ? $blocker->getId() : 0; - $vals['blockreason'] = $block->getReasonComment() - ->message->inLanguage( $language )->plain(); - $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->getTimestamp() ); - $expiry = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' ); - $vals['blockexpiry'] = $expiry; - $vals['blockpartial'] = !$block->isSitewide(); - $vals['blocknocreate'] = $block->isCreateAccountBlocked(); - $vals['blockanononly'] = !$block->isHardblock(); - if ( $block instanceof AbstractBlock ) { - $vals['blockemail'] = $block->isEmailBlocked(); - $vals['blockowntalk'] = !$block->isUsertalkEditAllowed(); - } - - $user = $this->getUser(); - // Formatted timestamps - $vals['blockedtimestampformatted'] = $language->formatExpiry( - $block->getTimestamp(), true, 'infinity', $user - ); - if ( $expiry !== 'infinite' ) { - $vals['blockexpiryformatted'] = $language->formatExpiry( - $expiry, true, 'infinity', $user - ); - $vals['blockexpiryrelative'] = $language->getHumanTimestamp( - new MWTimestamp( $expiry ), new MWTimestamp(), $user - ); - } - - if ( $block instanceof SystemBlock ) { - $vals['systemblocktype'] = $block->getSystemBlockType(); - } - - if ( $block instanceof CompositeBlock ) { - $components = []; - foreach ( $block->getOriginalBlocks() as $singleBlock ) { - $components[] = $this->getBlockDetails( $singleBlock, $language ); - } - $vals['blockcomponents'] = $components; - } - - return $vals; + return ( new ApiBlockInfoHelper )->getBlockDetails( + $block, $language ?? $this->getLanguage(), $this->getUser() ); } /** @@ -113,10 +62,7 @@ trait ApiBlockInfoTrait { * @return string */ private function getBlockCode( Block $block ): string { - if ( $block instanceof DatabaseBlock && $block->getType() === Block::TYPE_AUTO ) { - return 'autoblocked'; - } - return 'blocked'; + return ( new ApiBlockInfoHelper )->getBlockCode( $block ); } // region Methods required from ApiBase diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 3ed3cba5958c..cf4ce46b32de 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -641,11 +641,6 @@ class ApiEditPage extends ApiBase { case EditPage::AS_IMAGE_REDIRECT_LOGGED: $status->fatal( 'apierror-noimageredirect' ); break; - case EditPage::AS_CONTENT_TOO_BIG: - case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED: - $status->fatal( 'apierror-contenttoobig', - $this->getConfig()->get( MainConfigNames::MaxArticleSize ) ); - break; case EditPage::AS_READ_ONLY_PAGE_ANON: $status->fatal( 'apierror-noedit-anon' ); break; diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index dbb2bfa0082b..e28a7c94317f 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -333,7 +333,7 @@ class ApiMain extends ApiBase { 'TitleFactory', 'UserIdentityLookup', 'WatchedItemStore', - 'BlockUtils', + 'BlockTargetFactory', 'BlockActionInfo', 'DatabaseBlockStore', 'WatchlistManager', @@ -350,6 +350,7 @@ class ApiMain extends ApiBase { 'WatchlistManager', 'UserOptionsLookup', 'DatabaseBlockStore', + 'BlockTargetFactory', ] ], 'move' => [ diff --git a/includes/api/ApiMessageTrait.php b/includes/api/ApiMessageTrait.php index 3cc2a6720dc5..8f796101ce2b 100644 --- a/includes/api/ApiMessageTrait.php +++ b/includes/api/ApiMessageTrait.php @@ -35,6 +35,7 @@ trait ApiMessageTrait { * Compatibility code mappings for various MW messages. * @todo Ideally anything relying on this should be changed to use ApiMessage. * @var string[] + * @phpcs-require-sorted-array */ protected static $messageMap = [ 'actionthrottledtext' => 'ratelimited', @@ -54,14 +55,14 @@ trait ApiMessageTrait { 'cantrollback' => 'onlyauthor', 'confirmedittext' => 'confirmemail', 'content-not-allowed-here' => 'contentnotallowedhere', - 'deleteprotected' => 'cantedit', 'delete-toobig' => 'bigdelete', + 'deleteprotected' => 'cantedit', 'edit-conflict' => 'editconflict', 'imagenocrossnamespace' => 'nonfilenamespace', 'imagetypemismatch' => 'filetypemismatch', + 'import-noarticle' => 'badinterwiki', 'importbadinterwiki' => 'badinterwiki', 'importcantopen' => 'cantopenfile', - 'import-noarticle' => 'badinterwiki', 'importnofile' => 'nofile', 'importuploaderrorpartial' => 'partialupload', 'importuploaderrorsize' => 'filetoobig', @@ -71,6 +72,7 @@ trait ApiMessageTrait { 'ipb_cant_unblock' => 'cantunblock', 'ipb_expiry_invalid' => 'invalidexpiry', 'ip_range_invalid' => 'invalidrange', + 'longpageerror' => 'contenttoobig', 'mailnologin' => 'cantsend', 'markedaspatrollederror-noautopatrol' => 'noautopatrol', 'movenologintext' => 'cantmove-anon', @@ -94,8 +96,8 @@ trait ApiMessageTrait { 'systemblockedtext' => 'blocked', 'titleprotected' => 'protectedtitle', 'undo-failure' => 'undofailure', - 'userrights-nodatabase' => 'nosuchdatabase', 'userrights-no-interwiki' => 'nointerwikiuserrights', + 'userrights-nodatabase' => 'nosuchdatabase', ]; /** @var string|null */ diff --git a/includes/api/ApiOptionsBase.php b/includes/api/ApiOptionsBase.php index 0d49050ec423..d967e0c5ccc5 100644 --- a/includes/api/ApiOptionsBase.php +++ b/includes/api/ApiOptionsBase.php @@ -53,9 +53,6 @@ abstract class ApiOptionsBase extends ApiBase { /** @var string[]|null */ private $prefsKinds; - /** @var array */ - private $params; - public function __construct( ApiMain $main, string $action, @@ -304,7 +301,7 @@ abstract class ApiOptionsBase extends ApiBase { /** * Reset preferences of the specified kinds * - * @param string[] $kinds One or more types returned by UserOptionsManager::listOptionKinds() or 'all' + * @param string[] $kinds One or more types returned by PreferencesFactory::listResetKinds() or 'all' */ abstract protected function resetPreferences( array $kinds ); diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 63362c947965..236df4f14f14 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -545,7 +545,8 @@ class ApiQuery extends ApiBase { 'DBLoadBalancer', 'ReadOnlyMode', 'UrlUtils', - 'TempUserConfig' + 'TempUserConfig', + 'GroupPermissionsLookup', ] ], 'userinfo' => [ diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 9fa49b1b96f7..f983370b1597 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -22,6 +22,7 @@ namespace MediaWiki\Api; +use MediaWiki\MainConfigNames; use MediaWiki\Title\Title; use Wikimedia\ParamValidator\ParamValidator; use Wikimedia\ParamValidator\TypeDef\IntegerDef; @@ -33,8 +34,13 @@ use Wikimedia\ParamValidator\TypeDef\IntegerDef; */ class ApiQueryCategories extends ApiQueryGeneratorBase { + private int $migrationStage; + public function __construct( ApiQuery $query, string $moduleName ) { parent::__construct( $query, $moduleName, 'cl' ); + $this->migrationStage = $query->getConfig()->get( + MainConfigNames::CategoryLinksSchemaMigrationStage + ); } public function execute() { @@ -62,15 +68,22 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $prop = array_fill_keys( (array)$params['prop'], true ); $show = array_fill_keys( (array)$params['show'], true ); - $this->addFields( [ - 'cl_from', - 'cl_to' - ] ); - $this->addFieldsIf( [ 'cl_sortkey', 'cl_sortkey_prefix' ], isset( $prop['sortkey'] ) ); $this->addFieldsIf( 'cl_timestamp', isset( $prop['timestamp'] ) ); $this->addTables( 'categorylinks' ); + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $titleField = 'cl_to'; + } else { + $this->addTables( 'linktarget' ); + $this->addJoinConds( [ 'linktarget' => [ 'JOIN', 'cl_target_id = lt_id ' ] ] ); + $this->addWhere( [ 'lt_namespace' => NS_CATEGORY ] ); + $titleField = 'lt_title'; + } + $this->addFields( [ + 'cl_from', + $titleField + ] ); $this->addWhereFld( 'cl_from', array_keys( $pages ) ); if ( $params['categories'] ) { $cats = []; @@ -86,7 +99,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { // No titles so no results return; } - $this->addWhereFld( 'cl_to', $cats ); + $this->addWhereFld( $titleField, $cats ); } if ( $params['continue'] !== null ) { @@ -95,7 +108,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $op = $params['dir'] == 'descending' ? '<=' : '>='; $this->addWhere( $db->buildComparison( $op, [ 'cl_from' => $cont[0], - 'cl_to' => $cont[1], + $titleField => $cont[1], ] ) ); } @@ -109,7 +122,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addJoinConds( [ 'page' => [ 'LEFT JOIN', [ 'page_namespace' => NS_CATEGORY, - 'page_title = cl_to' ] ], + 'page_title = ' . $titleField ] ], 'page_props' => [ 'LEFT JOIN', [ 'pp_page=page_id', 'pp_propname' => 'hiddencat' ] ] @@ -124,11 +137,11 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't order by cl_from if it's constant in the WHERE clause if ( count( $pages ) === 1 ) { - $this->addOption( 'ORDER BY', 'cl_to' . $sort ); + $this->addOption( 'ORDER BY', $titleField . $sort ); } else { $this->addOption( 'ORDER BY', [ 'cl_from' . $sort, - 'cl_to' . $sort + $titleField . $sort ] ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -141,11 +154,11 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->$titleField ); break; } - $title = Title::makeTitle( NS_CATEGORY, $row->cl_to ); + $title = Title::makeTitle( NS_CATEGORY, $row->$titleField ); $vals = []; ApiQueryBase::addTitleInfo( $vals, $title ); if ( isset( $prop['sortkey'] ) ) { @@ -161,7 +174,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $fit = $this->addPageSubItem( $row->cl_from, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->$titleField ); break; } } @@ -171,11 +184,11 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->$titleField ); break; } - $titles[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); + $titles[] = Title::makeTitle( NS_CATEGORY, $row->$titleField ); } $resultPageSet->populateFromTitles( $titles ); } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index f0314e686a97..18b86e6c8737 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -37,6 +37,7 @@ use Wikimedia\ParamValidator\TypeDef\IntegerDef; class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { private Collation $collation; + private int $migrationStage; public function __construct( ApiQuery $query, @@ -45,6 +46,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { ) { parent::__construct( $query, $moduleName, 'cm' ); $this->collation = $collationFactory->getCategoryCollation(); + $this->migrationStage = $query->getConfig()->get( + MainConfigNames::CategoryLinksSchemaMigrationStage + ); } public function execute() { @@ -100,8 +104,17 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addFieldsIf( 'cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp' ); $this->addTables( [ 'page', 'categorylinks' ] ); // must be in this order for 'USE INDEX' + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() ); + } else { + $this->addTables( 'linktarget' ); + $this->addJoinConds( [ 'linktarget' => [ 'JOIN', 'cl_target_id = lt_id ' ] ] ); + $this->addWhere( [ + 'lt_namespace' => NS_CATEGORY, + 'lt_title' => $categoryTitle->getDBkey(), + ] ); + } - $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() ); $queryTypes = $params['type']; $contWhere = false; diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 530f77a8b709..9382d43652c7 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -22,7 +22,6 @@ namespace MediaWiki\Api; -use MediaWiki\Block\Block; use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\EditPage\IntroMessageBuilder; use MediaWiki\EditPage\PreloadedContentBuilder; @@ -598,17 +597,6 @@ class ApiQueryInfo extends ApiQueryBase { $authority->definitelyCan( $action, $page, $status ); } - if ( $shouldAutoCreate ) { - // Additionally check for blocks on the session user, since checking the - // placeholder temp user won't find blocks against the IP address or other - // parts of the request: T357063 - $block = $this->getAuthority()->getBlock(); - if ( $block instanceof Block ) { - $status->setBlock( $block ); - } - } - $this->addBlockInfoToStatus( $status ); - $pageInfo['actions'][$action] = $errorFormatter->arrayFromStatus( $status ); } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 4fe733a09ffe..3b09bf9ad2e0 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -33,6 +33,7 @@ use MediaWiki\Languages\LanguageNameUtils; use MediaWiki\MainConfigNames; use MediaWiki\Parser\MagicWordFactory; use MediaWiki\Parser\ParserFactory; +use MediaWiki\Permissions\GroupPermissionsLookup; use MediaWiki\Registration\ExtensionRegistry; use MediaWiki\ResourceLoader\SkinModule; use MediaWiki\SiteStats\SiteStats; @@ -80,6 +81,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { private ReadOnlyMode $readOnlyMode; private UrlUtils $urlUtils; private TempUserConfig $tempUserConfig; + private GroupPermissionsLookup $groupPermissionsLookup; public function __construct( ApiQuery $query, @@ -100,7 +102,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { ILoadBalancer $loadBalancer, ReadOnlyMode $readOnlyMode, UrlUtils $urlUtils, - TempUserConfig $tempUserConfig + TempUserConfig $tempUserConfig, + GroupPermissionsLookup $groupPermissionsLookup ) { parent::__construct( $query, $moduleName, 'si' ); $this->userOptionsLookup = $userOptionsLookup; @@ -120,6 +123,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->readOnlyMode = $readOnlyMode; $this->urlUtils = $urlUtils; $this->tempUserConfig = $tempUserConfig; + $this->groupPermissionsLookup = $groupPermissionsLookup; } public function execute() { @@ -596,11 +600,13 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data = []; $result = $this->getResult(); - $allGroups = array_values( $this->userGroupManager->listAllGroups() ); - foreach ( $config->get( MainConfigNames::GroupPermissions ) as $group => $permissions ) { + $allGroups = $this->userGroupManager->listAllGroups(); + $allImplicitGroups = $this->userGroupManager->listAllImplicitGroups(); + foreach ( array_merge( $allImplicitGroups, $allGroups ) as $group ) { $arr = [ 'name' => $group, - 'rights' => array_keys( $permissions, true ), + 'rights' => $this->groupPermissionsLookup->getGrantedPermissions( $group ), + // TODO: Also expose the list of revoked permissions somehow. ]; if ( $numberInGroup ) { @@ -614,25 +620,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { } } - $groupArr = [ - 'add' => $config->get( MainConfigNames::AddGroups ), - 'remove' => $config->get( MainConfigNames::RemoveGroups ), - 'add-self' => $config->get( MainConfigNames::GroupsAddToSelf ), - 'remove-self' => $config->get( MainConfigNames::GroupsRemoveFromSelf ) - ]; + $groupArr = $this->userGroupManager->getGroupsChangeableByGroup( $group ); - foreach ( $groupArr as $type => $rights ) { - if ( isset( $rights[$group] ) ) { - if ( $rights[$group] === true ) { - $groups = $allGroups; - } else { - $groups = array_intersect( $rights[$group], $allGroups ); - } - if ( $groups ) { - $arr[$type] = $groups; - ApiResult::setArrayType( $arr[$type], 'BCarray' ); - ApiResult::setIndexedTagName( $arr[$type], 'group' ); - } + foreach ( $groupArr as $type => $groups ) { + $groups = array_values( array_intersect( $groups, $allGroups ) ); + if ( $groups ) { + $arr[$type] = $groups; + ApiResult::setArrayType( $arr[$type], 'BCarray' ); + ApiResult::setIndexedTagName( $arr[$type], 'group' ); } } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index 0fe7bbea6d3a..53da553bb205 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -22,9 +22,9 @@ namespace MediaWiki\Api; -use MediaWiki\Block\AbstractBlock; use MediaWiki\Block\Block; use MediaWiki\Block\BlockPermissionCheckerFactory; +use MediaWiki\Block\BlockTargetFactory; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Block\UnblockUserFactory; use MediaWiki\MainConfigNames; @@ -34,6 +34,7 @@ use MediaWiki\User\Options\UserOptionsLookup; use MediaWiki\User\UserIdentityLookup; use MediaWiki\Watchlist\WatchedItemStoreInterface; use MediaWiki\Watchlist\WatchlistManager; +use RuntimeException; use Wikimedia\ParamValidator\ParamValidator; use Wikimedia\ParamValidator\TypeDef\ExpiryDef; @@ -53,6 +54,7 @@ class ApiUnblock extends ApiBase { private UserIdentityLookup $userIdentityLookup; private WatchedItemStoreInterface $watchedItemStore; private DatabaseBlockStore $blockStore; + private BlockTargetFactory $blockTargetFactory; public function __construct( ApiMain $main, @@ -63,7 +65,8 @@ class ApiUnblock extends ApiBase { WatchedItemStoreInterface $watchedItemStore, WatchlistManager $watchlistManager, UserOptionsLookup $userOptionsLookup, - DatabaseBlockStore $blockStore + DatabaseBlockStore $blockStore, + BlockTargetFactory $blockTargetFactory ) { parent::__construct( $main, $action ); @@ -79,6 +82,7 @@ class ApiUnblock extends ApiBase { $this->watchlistManager = $watchlistManager; $this->userOptionsLookup = $userOptionsLookup; $this->blockStore = $blockStore; + $this->blockTargetFactory = $blockTargetFactory; } /** @@ -99,7 +103,7 @@ class ApiUnblock extends ApiBase { if ( !$identity ) { $this->dieWithError( [ 'apierror-nosuchuserid', $params['userid'] ], 'nosuchuserid' ); } - $params['user'] = $identity->getName(); + $params['user'] = $identity; } $blockToRemove = null; @@ -110,15 +114,12 @@ class ApiUnblock extends ApiBase { [ 'apierror-nosuchblockid', $params['id'] ], 'nosuchblockid' ); } - - if ( $blockToRemove->getType() === AbstractBlock::TYPE_AUTO ) { - $target = '#' . $params['id']; - } else { - $target = $blockToRemove->getTargetUserIdentity() - ?? $blockToRemove->getTargetName(); + $target = $blockToRemove->getRedactedTarget(); + if ( !$target ) { + throw new RuntimeException( 'Block has no target' ); } } else { - $target = $params['user']; + $target = $this->blockTargetFactory->newFromUser( $params['user'] ); } # T17810: blocked admins should have limited access here @@ -206,6 +207,7 @@ class ApiUnblock extends ApiBase { 'user' => [ ParamValidator::PARAM_TYPE => 'user', UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id' ], + UserDef::PARAM_RETURN_OBJECT => true, ], 'userid' => [ ParamValidator::PARAM_TYPE => 'integer', diff --git a/includes/api/Hook/ApiOptionsHook.php b/includes/api/Hook/ApiOptionsHook.php index 2294a85165f3..7215bbd3bbe7 100644 --- a/includes/api/Hook/ApiOptionsHook.php +++ b/includes/api/Hook/ApiOptionsHook.php @@ -23,7 +23,7 @@ interface ApiOptionsHook { * @param User $user User object whose preferences are being changed * @param array $changes Associative array of preference name => value * @param string[] $resetKinds Array of strings specifying which options kinds to reset - * See User::resetOptions() and User::getOptionKinds() for possible values. + * See PreferencesFactory::listResetKinds() for possible values. * @return bool|void True or no return value to continue or false to abort */ public function onApiOptions( $apiModule, $user, $changes, $resetKinds ); diff --git a/includes/api/i18n/ar.json b/includes/api/i18n/ar.json index 3b146f609cd7..35c59dce08fd 100644 --- a/includes/api/i18n/ar.json +++ b/includes/api/i18n/ar.json @@ -1672,7 +1672,6 @@ "apierror-compare-relative-to-deleted": "لا يمكن استخدام <kbd>torelative=$1</kbd> بالنسبة لمراجعة محذوفة.", "apierror-compare-relative-to-nothing": "لا توجد مراجعة 'من' لـ<var>torelative</var> لتكون نسبة.", "apierror-contentserializationexception": "فشل تسلسل المحتوى: $1", - "apierror-contenttoobig": "المحتوى الذي قمت بتقديمه يتجاوز الحد الأقصى لحجم الصفحة وهو $1 {{PLURAL:$1| kibibyte|kibibytes}}.", "apierror-copyuploadbaddomain": "لا يُسمَح بالمرفوعات بواسطة مسار من هذا النطاق.", "apierror-copyuploadbadurl": "لا يُسمَح بالرفع من هذا المسار.", "apierror-create-titleexists": "لا يمكن حماية العناوين الموجودة باستخدام <kbd>create</kbd>.", diff --git a/includes/api/i18n/be-tarask.json b/includes/api/i18n/be-tarask.json index af0285f289d0..c036f025ca00 100644 --- a/includes/api/i18n/be-tarask.json +++ b/includes/api/i18n/be-tarask.json @@ -20,11 +20,13 @@ "apihelp-main-param-curtimestamp": "Уключае ў вынік пазнаку актуальнага часу.", "apihelp-main-param-responselanginfo": "Уключыць мовы, выкарыстаныя для <var>uselang</var> і <var>errorlang</var>, у вынік.", "apihelp-main-param-origin": "Пры звароце да API з дапамогай міждамэннага AJAX-запыту (CORS), выстаўце парамэтру значэньне зыходнага дамэну. Ён мусіць быць уключаны ў кожны папярэдні запыт і такім чынам мусіць быць часткай URI-запыту (ня цела POST).\n\nДля аўтэнтыфікаваных запытаў ён мусіць супадаць з адной з крыніц у загалоўку <code>Origin</code>, павінна быць зададзена нешта кшталту <kbd>https://en.wikipedia.org</kbd> або <kbd>https://meta.wikimedia.org</kbd>. Калі парамэтар не супадае з загалоўкам <code>Origin</code>, будзе вернуты адказ з кодам памылкі 403. Калі парамэтар супадае з загалоўкам <code>Origin</code> і крыніца дазволеная, будуць выстаўленыя загалоўкі <code>Access-Control-Allow-Origin</code> і <code>Access-Control-Allow-Credentials</code>.\n\nДля неаўтэнтыфікаваных запытаў выстаўце значэньне <kbd>*</kbd>. Гэта прывядзе да выстаўленьня загалоўку <code>Access-Control-Allow-Origin</code>, але <code>Access-Control-Allow-Credentials</code> будзе мець значэньне <code>false</code> і ўсе зьвесткі пра карыстальніка будуць абмежаваныя.", + "apihelp-main-param-crossorigin": "Пры доступе да API з дапамогай кросдамэнавых запытаў AJAX request (CORS) і сэсійнага пастаўніка, абароненага ад кросбачынавых атакаў падробкі запытаў (CSRF, прыкладам, OAuth), дзеля аўтэнтыфікацыі запыту (т. б. бяз выхаду з сыстэмы) карыстайцеся гэтым замест <code>origin=*</code>. Гэта трэба ўключаць у любыя перадпалётныя запыты, а значыць, у склад запыту URI (ня ў цела POST).\n\nЗьвярніце ўвагу, што большасьць сэсійных пастаўнікоў, у тым ліку стандартныя сэсіі на базе маркёраў, не падтрымліваюць аўтэнтыфікаваныя CORS і ня могуць выкарыстоўвацца з гэтым парамэтрам.", "apihelp-main-param-uselang": "Мова для выкарыстаньня ў перакладах паведамленьняў. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo&siprop=languages]]</kbd> з <kbd>siprop=languages</kbd> вяртае сьпіс кодаў мовы. Вы можаце пазначыць <kbd>user</kbd>, каб ужываць налады мовы цяперашняга карыстальніка, або пазначыць <kbd>content</kbd>, каб ужываць мову зьместу гэтай вікі.", "apihelp-main-param-variant": "Варыянт мовы. Працуе толькі ў выпадку, калі базавая мова падтрымлівае пераўтварэньне варыянтаў.", "apihelp-main-param-errorformat": "Фармат для вываду тэксту папярэджаньняў і памылак", "apihelp-main-paramvalue-errorformat-plaintext": "Вікітэкст з выдаленымі HTML-цэтлікамі і замененымі існасьцямі.", "apihelp-main-paramvalue-errorformat-wikitext": "Неразабраны вікітэкст.", + "apihelp-main-paramvalue-errorformat-raw": "Ключ паведамленьня і парамэтры.", "apihelp-main-param-errorlang": "Мова для выкарыстаньня ў папярэджаньнях і памылках. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo&siprop=languages]]</kbd> з <kbd>siprop=languages</kbd> вяртае сьпіс кодаў моваў. Пазначце <kbd>content</kbd> для выкарыстаньня мовы зьместу гэтай вікі, ці пазначце <kbd>uselang</kbd> для выкарыстаньня таго ж значэньня, што і ў парамэтры <var>uselang</var>.", "apihelp-main-param-errorsuselocal": "Калі зададзена, тэксты памылак будуць выкарыстоўваць лякальна-наладжаныя паведамленьні з прасторы назваў {{ns:MediaWiki}}.", "apihelp-block-summary": "Блякаваньне ўдзельніка.", @@ -38,7 +40,7 @@ "apihelp-block-param-noemail": "Забараняе ўдзельніку дасылаць лісты электроннай пошты празь вікі (трэба мець права <code>blockemail</code>).", "apihelp-block-param-hidename": "Схаваць імя ўдзельніка з журналу блякаваньняў (патрабуе права <code>hideuser</code>).", "apihelp-block-param-allowusertalk": "Дазволіць удзельніку рэдагаваць уласную старонку гутарак (залежыць ад <var>[[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]</var>).", - "apihelp-block-param-reblock": "Калі ўдзельнік ужо заблякаваны, перапісаць дзейнае блякаваньне.", + "apihelp-block-param-reblock": "Калі ўдзельнік ужо заблякаваны, перапісаць дзейнае блякаваньне. Калі ўдзельнік заблякаваны больш аднаго разу, гэта не спрацуе — тады скарыстайцеся парамэтрам <var>id</var>, каб задачыць блякаваньне, якое трэба перапісаць.", "apihelp-block-param-watchuser": "Назіраць за старонкай удзельніка або старонкай IP-адрасу, а таксама старонкай гутарак.", "apihelp-block-param-tags": "Зьмяніць меткі запісу ў журнале блякаваньняў.", "apihelp-block-param-partial": "Блякуе ўдзельніку магчымасьць рэдагаваць асобныя старонкі ці прасторы назваў замест усяго сайту.", diff --git a/includes/api/i18n/br.json b/includes/api/i18n/br.json index 629e91943ce7..d743b377e19c 100644 --- a/includes/api/i18n/br.json +++ b/includes/api/i18n/br.json @@ -17,7 +17,7 @@ "apihelp-block-param-reason": "Abeg evit stankañ.", "apihelp-block-param-nocreate": "Mirout a grouiñ kontoù.", "apihelp-block-param-hidename": "Kuzhat a ra anv implijer e marilh ar stankadennoù. (Rekis eo kaout ar gwir <code>hideuser</code> right).", - "apihelp-block-param-reblock": "Mard eo stanket an implijer c'hoazh, frikañ ar stankadenn zo.", + "apihelp-block-param-reblock": "Mard eo stanket an implijer ur wech hepken, frikañ ar stankadenn zo. Ne vo ket posubl mard eo stanket an implijer meur a wech — grit neuze gant an arventenn <var>id</var> da spisaat pe stankadenn a vo friket.", "apihelp-block-param-watchuser": "Evezhiañ pajennoù implijer ha kaozeal an den pe e chomlec'h IP.", "apihelp-block-param-tags": "Kemmañ an tikedennoù da lakaat e talvoud en enmont marilh ar stankadennoù.", "apihelp-block-example-ip-simple": "Stankañ ar chomlec'h IP <kbd>192.0.2.5</kbd> tri devezh-pad gant un abeg.", @@ -101,7 +101,7 @@ "apihelp-parse-example-text": "Dielfennañ ar wikitestenn.", "apihelp-protect-param-reason": "Abeg evit gwareziñ/diwareziñ.", "apihelp-protect-example-protect": "Gwareziñ ur bajenn.", - "apihelp-purge-param-forcelinkupdate": "Hizivaat taolennoù al liammoù.", + "apihelp-purge-param-forcelinkupdate": "Hizivaat taolennoù al liammoù hag ober hivizadurioù roadennoù eilrenk all.", "apihelp-query+filearchive-paramvalue-prop-dimensions": "Alias evit ar vent.", "apihelp-query+imageinfo-paramvalue-prop-dimensions": "Alias evit ar vent.", "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "Roll an aliasoù evit an esaouennoù anv enrollet.", @@ -109,8 +109,8 @@ "apihelp-query+stashimageinfo-param-sessionkey": "Alias evit $1filekey, evit ar c'henglotañ war-gil.", "apihelp-rollback-param-tags": "Tikedennoù da lakaat e talvoud war an distroioù.", "apihelp-watch-param-expiry": "Eurdeiziañ termen da vezañ lakaet ouzh an holl bajennoù pourchaset. Na ober gan an arventenn-mañ evit lezel an holl dermenoù evel m'emaint bremañ.", - "apihelp-watch-example-watch-expiry": "Evezhiañ ar pajennoù <kbd>Main Page</kbd>, <kbd>Foo</kbd>, ha <kbd>Bar</kbd> e-pad miz.", - "api-help-datatype-expiry": "Talvoudoù termen relativel (da sk. <kbd>5 months</kbd> pe <kbd>2 weeks</kbd>) pe absolut (da sk. <kbd>2014-09-18T12:34:56Z</kbd>). Kuit da gaout termen ebet, ober gant <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd> pe <kbd>never</kbd>.", + "apihelp-watch-example-watch-expiry": "Evezhiañ ar pajennoù [[{{MediaWiki:Mainpage}}]], <kbd>Foo</kbd>, ha <kbd>Bar</kbd> e-pad miz.", + "api-help-datatype-expiry": "Talvoudoù termen a c’hall bezañ relativel (da sk. <kbd>5 months</kbd> pe <kbd>2 weeks</kbd>) pe absolut (da sk. <kbd>2014-09-18T12:34:56Z</kbd>). Kuit da gaout termen ebet, ober gant <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd> pe <kbd>never</kbd>.", "api-help-param-type-expiry": "Seurt : {{PLURAL:$1|1=aet d'e dermen|2=roll aet d'e dermen}} ([[Special:ApiHelp/main#main/datatype/expiry|munudoù]])", "apierror-concurrency-limit": "Tizhet ez eus bet ur vevenn kensturiegezh. Gortozit ma vo distro pep reked a-raok kas an hini war-lerc'h." } diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index 6cd22bc817f0..4e811a569fe3 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -1486,7 +1486,6 @@ "apierror-compare-notext": "Der Parameter <var>$1</var> kann nicht ohne <var>$2</var> verwendet werden.", "apierror-compare-notorevision": "Keine Version „to“. <var>torev</var>, <var>totitle</var> oder <var>toid</var> angeben.", "apierror-compare-relative-to-deleted": "<kbd>torelative=$1</kbd> kann nicht relativ zu einer gelöschten Version verwendet werden.", - "apierror-contenttoobig": "Der gelieferte Inhalt überschreitet die Seitengrößenbegrenzung von $1 {{PLURAL:$1|Kibibyte}}.", "apierror-contentmodel-mismatch": "Der von dir angegebene Inhalt hat das Inhaltsmodell <kbd>$1</kbd>, das sich vom aktuellen Inhaltsmodell der Seite <kbd>$2</kbd> unterscheidet.", "apierror-emptypage": "Das Erstellen neuer leerer Seiten ist nicht erlaubt.", "apierror-filedoesnotexist": "Die Datei ist nicht vorhanden.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 8c14eab90bb2..6136df868f07 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1846,7 +1846,6 @@ "apierror-compare-relative-to-deleted": "Cannot use <kbd>torelative=$1</kbd> relative to a deleted revision.", "apierror-compare-relative-to-nothing": "No 'from' revision for <var>torelative</var> to be relative to.", "apierror-contentserializationexception": "Content serialization failed: $1", - "apierror-contenttoobig": "The content you supplied exceeds the page size limit of $1 {{PLURAL:$1|kibibyte|kibibytes}}.", "apierror-contentmodel-mismatch": "The content you supplied has <kbd>$1</kbd> content model, which differs from the current content model of the page <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Uploads by URL are not allowed from this domain.", "apierror-copyuploadbadurl": "Upload not allowed from this URL.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index 531ea15591b0..a04a0e18fc97 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -47,6 +47,7 @@ "Ncontinanza", "No se", "Pierpao", + "PoLuX124", "Poco a poco", "Pompilos", "Rodney Araujo", @@ -200,6 +201,7 @@ "apihelp-edit-param-redirect": "Resolver redirecciones automáticamente.", "apihelp-edit-param-contentformat": "Formato de serialización de contenido utilizado para el texto de entrada.", "apihelp-edit-param-contentmodel": "Modelo de contenido del nuevo contenido.", + "apihelp-edit-param-returnto": "Título de la página. Si al guardar la edición se creó una cuenta temporal, la API puede responder con una URL que el cliente debe visitar para completar el inicio de sesión. Si se proporciona este parámetro, la URL redirigirá a la página indicada, en lugar de a la página que se editó.", "apihelp-edit-param-token": "La clave debe enviarse siempre como el último parámetro o, al menos, después del parámetro $1text.", "apihelp-edit-example-edit": "Editar una página", "apihelp-edit-example-prepend": "Anteponer <kbd>__NOTOC__</kbd> a una página.", @@ -1574,7 +1576,6 @@ "apierror-compare-nosuchtosection": "No existe una sección $1 en el contenido 'to'.", "apierror-compare-notext": "No se puede usar el parámetro <var>$1</var> sin <var>$2</var>.", "apierror-contentserializationexception": "La serialización de contenido falló: $1", - "apierror-contenttoobig": "El contenido proporcionado supera el límite de tamaño del artículo de $1 {{PLURAL:$1|kibibyte|kibibytes}}.", "apierror-copyuploadbaddomain": "No se permite realizar cargas a partir de este dominio.", "apierror-copyuploadbadurl": "No se permite realizar cargas a partir de este URL.", "apierror-create-titleexists": "Los títulos existentes no se pueden proteger con <kbd>create</kbd>.", @@ -1686,6 +1687,7 @@ "apierror-stashwrongowner": "Propietario incorrecto: $1", "apierror-stashzerolength": "El archivo mide cero bytes y no puede guardarse en el almacén provisional: $1.", "apierror-systemblocked": "Has sido bloqueado automáticamente por el software MediaWiki.", + "apierror-tempuseracquirefailed": "No se puede adquirir un nombre de usuario de cuenta temporal.", "apierror-templateexpansion-notwikitext": "La expansión de plantillas solo es compatible con el contenido en wikitexto. $1 usa el modelo de contenido $2.", "apierror-toomanyvalues": "Se proporcionaron demasiados valores al parámetro <var>$1</var>. El límite es de $2.", "apierror-unknownaction": "La acción especificada, <kbd>$1</kbd>, no está reconocida.", diff --git a/includes/api/i18n/fa.json b/includes/api/i18n/fa.json index 19884535b471..f350a6de8526 100644 --- a/includes/api/i18n/fa.json +++ b/includes/api/i18n/fa.json @@ -498,7 +498,6 @@ "apierror-changecontentmodel-nodirectediting": "مدل محتوای $1 از ویرایش مستقیم پشتیبانی نمیکند", "apierror-changecontentmodel-cannotbeused": "مدل محتوای $1 در $2 قابل استفاده نیست", "apierror-changecontentmodel-cannot-convert": "ناتوان در تبدیل مدل محتوای $1 به $2", - "apierror-contenttoobig": "حجم محتوای ارائهشده از سوی شما از محدودیت اندازهٔ مقاله برابر با $1 {{PLURAL:$1|کیبیبایت}} گذر کرده است.", "apierror-contentmodel-mismatch": "محتوایی که وارد کردهاید دارای مدل محتوایی <kbd>$1</kbd> است که با مدل محتوای کنونی صفحه <kbd>$2</kbd> متفاوت است.", "apierror-nosuchrcid": "تغییر اخیری با شناسهٔ $1 موجود نیست.", "api-credits-header": "اعتبار" diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index 13dba3268e88..66bbe93302fd 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -1768,7 +1768,6 @@ "apierror-compare-relative-to-deleted": "Impossible d’utiliser <kbd>torelative=$1</kbd> par rapport à une révision supprimée.", "apierror-compare-relative-to-nothing": "Pas de révision 'depuis' pour <var>torelative</var> à laquelle se rapporter.", "apierror-contentserializationexception": "Échec de sérialisation du contenu : $1", - "apierror-contenttoobig": "Le contenu que vous avez fourni dépasse la taille maximale autorisée pour une page ($1 {{PLURAL:$1|kibioctet|kibioctets}}).", "apierror-contentmodel-mismatch": "Le contenu que vous avez fourni utilise le modèle de contenu <kbd>$1</kbd>, alors que le modèle de contenu actuel de la page est <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Les téléversements par URL ne sont pas autorisés depuis ce domaine.", "apierror-copyuploadbadurl": "Les téléversements ne sont pas autorisés depuis cette URL.", diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index 6f7494ba851d..3707b55e3c92 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -1741,7 +1741,6 @@ "apierror-compare-relative-to-deleted": "Non se pode usar <kbd>torelative=$1</kbd> en relación a unha revisión eliminada.", "apierror-compare-relative-to-nothing": "Non hai ningunha revisión \"from\" en relación á cal <var>torelative</var> sexa relativa.", "apierror-contentserializationexception": "Erro de serialización do contidoː $1", - "apierror-contenttoobig": "O contido que achegaches excede o límite de tamaño dunha páxina, que é de $1 {{PLURAL:$1|kibibyte|kibibytes}}.", "apierror-contentmodel-mismatch": "O contido que achegaches ten o modelo de contido <kbd>$1</kbd>, que difire do modelo de contido actual da páxina: <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "As subas por URL non están permitidas para este dominio.", "apierror-copyuploadbadurl": "As subas non están permitidas para este enderezo URL.", diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index 5061308b70d8..5f7574af1868 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -249,7 +249,7 @@ "apihelp-feedrecentchanges-param-hidecategorization": "להסתיר שינויים בחברות בקטגוריה.", "apihelp-feedrecentchanges-param-tagfilter": "סינון לפי תג.", "apihelp-feedrecentchanges-param-inverttags": "כל העריכות מלבד אלה שתויגו עם אלה שנבחרו.", - "apihelp-feedrecentchanges-param-target": "הצגת שינויים שנעשו בדפים המקושרים מהדף זה בלבד.", + "apihelp-feedrecentchanges-param-target": "הצגת שינויים שנעשו בדפים המקושרים מהדף הזה בלבד.", "apihelp-feedrecentchanges-param-showlinkedto": "להציג את השינויים בדפים שמקושרים לדף שנבחר במקום זה.", "apihelp-feedrecentchanges-example-simple": "הצגת שינויים אחרונים.", "apihelp-feedrecentchanges-example-30days": "הצגת שינויים אחרונים עבור 30 ימים.", @@ -1745,7 +1745,6 @@ "apierror-compare-relative-to-deleted": "לא ניתן להשתמש ב־<kbd>torelative=$1</kbd> יחסית לגרסה מחוקה.", "apierror-compare-relative-to-nothing": "אין גרסת \"from\" עבור <var>torelative</var> שתהיה יחסית.", "apierror-contentserializationexception": "הסדרת התוכן נכשלה: $1", - "apierror-contenttoobig": "התוכן שסיפקת חורג ממגבלת גודל הדף של {{PLURAL:$1|קיביבייט אחד|$1 קיביבייטים}}.", "apierror-contentmodel-mismatch": "התוכן שסיפקת משתייך למודל התוכן <kbd>$1</kbd>, ששונה ממודל התוכן הנוכחי של הדף שהוא <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "העלאות לפי URL אינם מורשות מהתחום הזה.", "apierror-copyuploadbadurl": "העלאה אינה מותרת מה־URL הזה.", diff --git a/includes/api/i18n/ia.json b/includes/api/i18n/ia.json index 97955e679725..3ee25236bc32 100644 --- a/includes/api/i18n/ia.json +++ b/includes/api/i18n/ia.json @@ -1724,7 +1724,6 @@ "apierror-compare-relative-to-deleted": "Non pote usar <kbd>torelative=$1</kbd> relative a un version delite.", "apierror-compare-relative-to-nothing": "Necun version 'from' pro <var>torelative</var> al qual esser relative.", "apierror-contentserializationexception": "Serialisation de contento fallite: $1", - "apierror-contenttoobig": "Le contento fornite excede le limite de grandor de paginas de $1 kibibyte{{PLURAL:$1||s}}.", "apierror-contentmodel-mismatch": "Le contento que tu forniva ha le modello de contento <kbd>$1</kbd>, que es differente del modello de contento actual del pagina, <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Le incargamentos per URL ab iste dominio non es autorisate.", "apierror-copyuploadbadurl": "Le incargamentos ab iste URL non es autorisate.", diff --git a/includes/api/i18n/ja.json b/includes/api/i18n/ja.json index c6e2e1d232bf..7a7b7cca92f3 100644 --- a/includes/api/i18n/ja.json +++ b/includes/api/i18n/ja.json @@ -23,6 +23,7 @@ "Sujiniku", "Suyama", "Whym", + "Yaakiyu.jp", "Yamagata Yusuke", "Yusuke1109", "Yuukin0248", @@ -351,6 +352,7 @@ "apihelp-parse-param-disablepp": "<var>$1disablelimitreport</var> を代わりに使用してください。", "apihelp-parse-param-disableeditsection": "構文解析の出力で節リンクを省略する。", "apihelp-parse-param-preview": "プレビューモードでのパース", + "apihelp-parse-param-useskin": "選択したスキンをパーサー出力に適用します。次のプロパティに影響する可能性があります: <kbd>text</kbd> 、 <kbd>langlinks</kbd> 、 <kbd>headitems</kbd> 、 <kbd>modules</kbd> 、 <kbd>jsconfigvars</kbd> 、 <kbd>indicators</kbd> 。", "apihelp-parse-example-page": "ページを構文解析する。", "apihelp-parse-example-text": "ウィキテキストを構文解析", "apihelp-parse-example-summary": "要約を構文解析します。", diff --git a/includes/api/i18n/lij.json b/includes/api/i18n/lij.json index a68ae91b4219..0902326759fc 100644 --- a/includes/api/i18n/lij.json +++ b/includes/api/i18n/lij.json @@ -1726,7 +1726,6 @@ "apierror-compare-relative-to-deleted": "O no peu dêuviâ <kbd>torelative=$1</kbd> relatîvo a 'na versción scancelâ.", "apierror-compare-relative-to-nothing": "Nisciùnn-a versción 'from' pe <var>torelative</var> relatîvo a lê.", "apierror-contentserializationexception": "Serializaçión do contegnûo falîaː $1", - "apierror-contenttoobig": "O contegnûo fornîo o sùpera o lìmite de dimensción da pàgina de $1 {{PLURAL:$1|kibibyte}}.", "apierror-contentmodel-mismatch": "O contegnûo fornîo o l'à o modèllo de contegnûo <kbd>$1</kbd>, ch'o diferìsce da-o modèllo de contegnûo corénte da pàgina <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "I caregaménti da URL no són consentîi da sto domìnnio chi.", "apierror-copyuploadbadurl": "I caregaménti no són consentîi da sto URL chi.", diff --git a/includes/api/i18n/nb.json b/includes/api/i18n/nb.json index 286d1a0c9e22..80434a8ac360 100644 --- a/includes/api/i18n/nb.json +++ b/includes/api/i18n/nb.json @@ -1723,7 +1723,6 @@ "apierror-compare-relative-to-deleted": "Kan ikke bruke <kbd>torelative=$1</kbd> relativt til en slettet sideversjon.", "apierror-compare-relative-to-nothing": "Ingen «from»-sideversjon som <var>torelative</var> kan være relativ til.", "apierror-contentserializationexception": "Innholdsserialisering feliet: $1", - "apierror-contenttoobig": "Innholdet du oppga overskrider sidestørrelsesgrensen på $1 {{PLURAL:$1|kibibyte}}.", "apierror-contentmodel-mismatch": "Innholdet du oppga har innholdsmodellen <kbd>$1</kbd>, som er forskjellig fra sidens gjeldende innholdsmodell, <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Opplasting via URL tillates ikke fra dette domenet.", "apierror-copyuploadbadurl": "Opplasting tillates ikke fra denne URL-en.", diff --git a/includes/api/i18n/nl.json b/includes/api/i18n/nl.json index 04e605afe5d2..6e1da13d1f40 100644 --- a/includes/api/i18n/nl.json +++ b/includes/api/i18n/nl.json @@ -1750,7 +1750,6 @@ "apierror-compare-relative-to-deleted": "<kbd>torelative=$1</kbd> kan niet ten opzichte van een verwijderde versie worden gebruikt.", "apierror-compare-relative-to-nothing": "Er is geen ‘from’-versie waaraan <var>torelative</var> relatief kan zijn.", "apierror-contentserializationexception": "Inhoudsserialisatie mislukt: $1", - "apierror-contenttoobig": "De opgegeven inhoud overschrijdt de limiet op de paginagrootte van $1 {{PLURAL:$1|kibibyte|kibibytes}}.", "apierror-contentmodel-mismatch": "De door u aangeleverde inhoud is van het inhoudsmodel <kbd>$1</kbd>, wat verschilt van het huidige inhoudsmodel van de pagina, <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Uploads via URL zijn niet toegestaan vanaf dit domein.", "apierror-copyuploadbadurl": "Uploaden vanaf deze URL is niet toegestaan.", diff --git a/includes/api/i18n/pa.json b/includes/api/i18n/pa.json index ee081d8276f8..151f6f0c2332 100644 --- a/includes/api/i18n/pa.json +++ b/includes/api/i18n/pa.json @@ -48,7 +48,7 @@ "apihelp-feedrecentchanges-example-30days": "30 ਦਿਨਾਂ ਲਈ ਹਾਲੀਆ ਤਬਦੀਲੀਆਂ ਵਿਖਾਓ।", "apihelp-feedwatchlist-example-all6hrs": "ਪਿਛਲੇ 6 ਘੰਟਿਆਂ ਵਿੱਚ ਵੇਖੇ ਗਏ ਸਫ਼ਿਆਂ ਵਿੱਚ ਸਾਰੀਆਂ ਤਬਦੀਲੀਆਂ ਵਿਖਾਓ।", "apihelp-help-example-recursive": "ਇੱਕੋ ਸਫ਼ੇ 'ਤੇ ਸਾਰੀ ਮਦਦ", - "apihelp-import-param-interwikipage": "ਅੰਤਰ-ਵਿਕੀ ਆਯਾਤ ਵਾਸਤੇ: ਆਯਾਤ ਕਰਨ ਲਈ ਸਫ਼ਾ।", + "apihelp-import-param-interwikipage": "ਅੰਤਰ-ਵਿਕੀ ਦਰਾਮਦ ਲਈ: ਦਰਾਮਦ ਕਰਨ ਲਈ ਸਫ਼ਾ।", "apihelp-login-param-name": "ਵਰਤੋਂਕਾਰ ਨਾਂ।", "apihelp-login-param-password": "ਪਾਰਸ਼ਬਦ।", "apihelp-login-param-domain": "ਡੋਮੇਨ (ਇਖਤਿਆਰੀ)", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index 2132a5b1094d..b6249054fb5c 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -876,7 +876,6 @@ "apierror-cantview-deleted-description": "Nie masz uprawnień do podglądu opisów usuniętych plików.", "apierror-cantview-deleted-metadata": "Nie masz uprawnień do podglądu metadanych usuniętych plików.", "apierror-cantview-deleted-revision-content": "Nie masz uprawnień do podglądu treści usuniętych wersji.", - "apierror-contenttoobig": "Podana treść przekracza limit rozmiaru strony wynoszący $1 {{PLURAL:$1|kibibajt|kibibajty|kibibajtów}}.", "apierror-contentmodel-mismatch": "Dostarczona treść posiada model zawartości <kbd>$1</kbd>, który różni się od obecnego modelu zawartości strony – <kbd>$2</kbd>.", "apierror-exceptioncaught": "[$1] Stwierdzono wyjątek: $2", "apierror-filedoesnotexist": "Plik nie istnieje.", diff --git a/includes/api/i18n/ps.json b/includes/api/i18n/ps.json index e3f67879851b..e8183f05efbf 100644 --- a/includes/api/i18n/ps.json +++ b/includes/api/i18n/ps.json @@ -3,7 +3,8 @@ "authors": [ "1233qwer1234qwer4", "Ahmed-Najib-Biabani-Ibrahimkhel", - "Macofe" + "Macofe", + "شاه زمان پټان" ] }, "apihelp-main-param-action": "کومه کړنه ترسره کړم.", diff --git a/includes/api/i18n/pt-br.json b/includes/api/i18n/pt-br.json index 381c7388b3cb..9dba97e178e7 100644 --- a/includes/api/i18n/pt-br.json +++ b/includes/api/i18n/pt-br.json @@ -1660,7 +1660,6 @@ "apierror-compare-relative-to-deleted": "Não pode usar <kbd>torelative=$1</kbd> com relação a uma revisão eliminada.", "apierror-compare-relative-to-nothing": "Nenhuma revisão 'from' para <var>torelative</var> para ser relativa à.", "apierror-contentserializationexception": "Falha na serialização de conteúdo: $1", - "apierror-contenttoobig": "O conteúdo fornecido excede o limite de tamanho do artigo de $1 {{PLURAL: $1|kibibyte|kibibytes}}.", "apierror-contentmodel-mismatch": "O conteúdo que forneceu tem o modelo de conteúdo <kbd>$1</kbd>, que é diferente do modelo de conteúdo atual da página, <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Os uploads por URL não são permitidos deste domínio.", "apierror-copyuploadbadurl": "Envio não permitido a partir deste URL.", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 459be75d98af..017ef9a42921 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -1729,7 +1729,6 @@ "apierror-compare-relative-to-deleted": "Não pode usar <kbd>torelative=$1</kbd> com relação a uma revisão eliminada.", "apierror-compare-relative-to-nothing": "Não existe uma revisão 'from' em relação à qual <var>torelative</var> possa ser relativo.", "apierror-contentserializationexception": "A seriação do conteúdo falhou: $1", - "apierror-contenttoobig": "O conteúdo fornecido excede o limite de tamanho de página de $1 {{PLURAL:$1| kibibyte|kibibytes}}.", "apierror-contentmodel-mismatch": "O conteúdo que forneceu tem o modelo de conteúdo <kbd>$1</kbd>, que é diferente do modelo de conteúdo atual da página, <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Não são permitidos carregamentos por URL a partir deste domínio.", "apierror-copyuploadbadurl": "Não são permitidos carregamentos a partir deste URL.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 99bcd16295e7..c433ab7db557 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1750,7 +1750,6 @@ "apierror-compare-relative-to-deleted": "{{doc-apierror}}", "apierror-compare-relative-to-nothing": "{{doc-apierror}}", "apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.", - "apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kibibytes.", "apierror-contentmodel-mismatch": "{{doc-apierror}}\n\nParameters:\n* $1 content model of the old revision\n* $2 - content model of the current revision.", "apierror-copyuploadbaddomain": "{{doc-apierror}}", "apierror-copyuploadbadurl": "{{doc-apierror}}", diff --git a/includes/api/i18n/ru.json b/includes/api/i18n/ru.json index 5c6b23bf709a..9367c39d039a 100644 --- a/includes/api/i18n/ru.json +++ b/includes/api/i18n/ru.json @@ -1695,7 +1695,6 @@ "apierror-compare-notext": "Параметр <var>$1</var> нельзя использовать без <var>$2</var>.", "apierror-compare-relative-to-nothing": "Нет версии 'from', к которой относится <var>torelative</var>.", "apierror-contentserializationexception": "Сериализация содержимого провалилась: $1", - "apierror-contenttoobig": "Предоставленное вами содержимое превышает максимальный размер страницы в $1 {{PLURAL:$1|килобайт|килобайта|килобайтов}}.", "apierror-copyuploadbaddomain": "Загрузка по ссылке недоступна с этого домена.", "apierror-copyuploadbadurl": "Загрузка по этой ссылке недоступна.", "apierror-create-titleexists": "Существующие названия не могут быть защищены с помощью <kbd>create</kbd>.", diff --git a/includes/api/i18n/sl.json b/includes/api/i18n/sl.json index cf515766161a..61238d93e2af 100644 --- a/includes/api/i18n/sl.json +++ b/includes/api/i18n/sl.json @@ -65,7 +65,9 @@ "apihelp-expandtemplates-param-generatexml": "Ustvari drevo razčlembe XML (zamenjano z $1prop=parsetree).", "apihelp-expandtemplates-param-showstrategykeys": "Ali naj bodo v jsconfigvars vključene informacije o interni strategiji združevanja.", "apihelp-expandtemplates-example-simple": "Razširi vikibesedilo <kbd><nowiki>{{Project:Sandbox}}</nowiki></kbd>.", + "apihelp-feedcontributions-summary": "Vrnitev vira prispevkov uporabnika.", "apihelp-feedcontributions-param-feedformat": "Format vira.", + "apihelp-feedcontributions-param-deletedonly": "Prikaži samo izbrisane prispevke.", "apihelp-feedcontributions-param-showsizediff": "Prikaže razliko v velikosti med redakcijami.", "apihelp-feedrecentchanges-summary": "Vrne vir zadnjih sprememb", "apihelp-feedrecentchanges-param-feedformat": "Format vira.", @@ -340,6 +342,7 @@ "apihelp-setpagelanguage-example-language": "Sprememba jezika strani [[{{MediaWiki:Mainpage}}]] v baskovščino.", "apihelp-stashedit-param-title": "Naslov urejane strani.", "apihelp-tag-param-remove": "Oznake za odstranitev. Odstraniti je mogoče samo oznake, ki so opredeljene ročno ali popolnoma neopredeljene.", + "apihelp-unblock-param-tags": "Sprememba oznak za uporabo v vnosu v dnevniku blokiranja.", "apihelp-unblock-param-watchuser": "Opazovanje uporabniške in pogovorne strani uporabnika ali IP-naslova.", "apihelp-unblock-param-watchlistexpiry": "Časovni žig preteka spiska nadzorov. Da ostane trenutni pretek nespremenjen, ta parameter v celoti izpustite.", "apihelp-undelete-summary": "Odizbris redakcij izbrisane strani.", @@ -393,11 +396,11 @@ "apierror-botsnotsupported": "Ta uporabniški vmesnik ni podprt za bote.", "apierror-cannotreauthenticate": "To dejanje ni na voljo, ker ni mogoče preveriti vaše identitete.", "apierror-cantchangecontentmodel": "Za spreminjanje vsebinskega modela strani nimate dovoljenja.", + "apierror-canthide": "Nimate dovoljenja za skrivanje uporabniških imen v dnevniku blokiranja.", "apierror-cantimport": "Za uvažanje strani nimate dovoljenja.", "apierror-cantundelete": "Ni bilo mogoče odizbrisati: Zahtevane redakcije morda ne obstajajo ali pa so že odizbrisane.", "apierror-compare-maintextrequired": "Če <var>$1slots</var> vsebuje <kbd>main</kbd>, je potreben parameter <var>$1text-main</var> (glavne reže ni mogoče izbrisati).", "apierror-contentserializationexception": "Serializacija vsebine je spodletela: $1", - "apierror-contenttoobig": "Vsebina, ki ste jo posredovali, presega omejitev velikosti strani $1 {{PLURAL:$1|one=kibizlog|two=kibizloga|few=kibizlige|kibizlogov}}.", "apierror-copyuploadbaddomain": "Nalaganje po URL-ju s te domene ni dovoljeno.", "apierror-copyuploadbadurl": "Nalaganje s tega URL-ja ni dovoljeno.", "apierror-emptynewsection": "Ustvarjanje praznih novih razdelkov ni mogoče.", diff --git a/includes/api/i18n/tr.json b/includes/api/i18n/tr.json index d6f2adfde7d4..aa40079afbdb 100644 --- a/includes/api/i18n/tr.json +++ b/includes/api/i18n/tr.json @@ -1674,7 +1674,6 @@ "apierror-compare-relative-to-deleted": "Silinmiş bir düzeltmeye göre <kbd>torelative=$1</kbd> kullanılamaz.", "apierror-compare-relative-to-nothing": "<var>torelative</var> göreli olması için 'from' revizyonu yok.", "apierror-contentserializationexception": "İçerik serileştirme başarısız oldu: $1", - "apierror-contenttoobig": "Sağladığınız içerik, $1 {{PLURAL:$1|kibibit}} ürün boyutu sınırını aşıyor.", "apierror-contentmodel-mismatch": "Sağladığınız içerik, <kbd>$2</kbd> sayfasının mevcut içerik modelinden farklı olan <kbd>$1</kbd> içerik modeline sahip.", "apierror-copyuploadbaddomain": "URL ile yüklemelere bu alan adından izin verilmiyor.", "apierror-copyuploadbadurl": "Bu URL'den yüklemeye izin verilmiyor.", diff --git a/includes/api/i18n/udm.json b/includes/api/i18n/udm.json index 2bd16217aa20..52e6ad5aa5ba 100644 --- a/includes/api/i18n/udm.json +++ b/includes/api/i18n/udm.json @@ -8,6 +8,5 @@ }, "apihelp-block-summary": "Блокировка пыриськисьёс.", "apihelp-edit-example-edit": "Бамез тупатъяно.", - "apihelp-login-example-login": "Пырыны.", - "apierror-contenttoobig": "Сётэм пуштросты ортче статья быдӟалалэсь $1 {{PLURAL:$1|кибибайтъем}} висгожзэ." + "apihelp-login-example-login": "Пырыны." } diff --git a/includes/api/i18n/uk.json b/includes/api/i18n/uk.json index d543fa359829..de5de89da87b 100644 --- a/includes/api/i18n/uk.json +++ b/includes/api/i18n/uk.json @@ -1700,7 +1700,6 @@ "apierror-compare-relative-to-deleted": "Неможливо використати <kbd>torelative=$1</kbd> стосовно вилученої версії.", "apierror-compare-relative-to-nothing": "Відсутня версія 'from', якої б стосувалося <var>torelative</var>.", "apierror-contentserializationexception": "Невдача серіалізації вмісту: $1", - "apierror-contenttoobig": "Наданий вами вміст перевищує ліміт у $1 {{PLURAL:$1|кібібайт|кібібайти|кібібайтів}} розміру сторінки.", "apierror-contentmodel-mismatch": "Наданий вами вміст має <kbd>$1</kbd> модель вмісту, яка відрізняється від поточної моделі вмісту сторінки <kbd>$2</kbd>.", "apierror-copyuploadbaddomain": "Завантаження за URL-адресою недозволені з цього домену.", "apierror-copyuploadbadurl": "Завантаження з цієї URL-адреси недозволені.", diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index 5806fa23d36d..99088bcb15a9 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -1783,7 +1783,6 @@ "apierror-compare-relative-to-deleted": "不能相对于已删除的修订版本使用<kbd>torelative=$1</kbd>。", "apierror-compare-relative-to-nothing": "没有与<var>torelative</var>的“from”修订版本相对的版本。", "apierror-contentserializationexception": "内容序列化失败:$1", - "apierror-contenttoobig": "您提供的内容超过了$1{{PLURAL:$1|千字节}}的页面大小限制。", "apierror-contentmodel-mismatch": "您提供的内容具有<kbd>$1</kbd>内容模型,它不同于页面<kbd>$2</kbd>的当前内容模型。", "apierror-copyuploadbaddomain": "不允许从此域名通过URL上传。", "apierror-copyuploadbadurl": "不允许从此URL上传。", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index 09e6103d3870..37b4ae10d6d9 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -1748,7 +1748,6 @@ "apierror-compare-relative-to-deleted": "相關已刪除修訂時不能使用 <kbd>torelative=$1</kbd>。", "apierror-compare-relative-to-nothing": "沒有相關 <var>torelative</var> 的 'from' 修訂。", "apierror-contentserializationexception": "內容序列化失敗:$1", - "apierror-contenttoobig": "您提供的內容超過了$1{{PLURAL:$1|千位元組}}的頁面大小限制。", "apierror-contentmodel-mismatch": "您提供的內容具有 <kbd>$1</kbd> 內容模組,該模組與頁面 <kbd>$2</kbd> 目前的內容模組不同。", "apierror-copyuploadbaddomain": "不允許從此網域來透過 URL 上傳。", "apierror-copyuploadbadurl": "不允許從此 URL 來上傳。", diff --git a/includes/block/AbstractBlock.php b/includes/block/AbstractBlock.php index 5204d2c56c40..667a5bd586c2 100644 --- a/includes/block/AbstractBlock.php +++ b/includes/block/AbstractBlock.php @@ -21,6 +21,7 @@ namespace MediaWiki\Block; use InvalidArgumentException; +use LogicException; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\DAO\WikiAwareEntityTrait; use MediaWiki\MainConfigNames; @@ -61,15 +62,9 @@ abstract class AbstractBlock implements Block { /** @var bool */ protected $isHardblock; - /** @var UserIdentity|string|null */ + /** @var BlockTarget|null */ protected $target; - /** - * @var int|null AbstractBlock::TYPE_ constant. After the block has been loaded - * from the database, this can only be USER, IP or RANGE. - */ - protected $type; - /** @var bool */ protected $isSitewide = true; @@ -80,8 +75,9 @@ abstract class AbstractBlock implements Block { * Create a new block with specified parameters on a user, IP or IP range. * * @param array $options Parameters of the block, with supported options: + * - target: (BlockTarget) The target object (since 1.44) * - address: (string|UserIdentity) Target user name, user identity object, - * IP address or IP range + * IP address or IP range. * - wiki: (string|false) The wiki the block has been issued in, * self::LOCAL for the local wiki (since 1.38) * - reason: (string|Message|CommentStoreComment) Reason for the block @@ -95,7 +91,6 @@ abstract class AbstractBlock implements Block { */ public function __construct( array $options = [] ) { $defaults = [ - 'address' => '', 'wiki' => self::LOCAL, 'reason' => '', 'timestamp' => '', @@ -106,7 +101,16 @@ abstract class AbstractBlock implements Block { $options += $defaults; $this->wikiId = $options['wiki']; - $this->setTarget( $options['address'] ); + if ( isset( $options['target'] ) ) { + if ( !( $options['target'] instanceof BlockTarget ) ) { + throw new InvalidArgumentException( 'Invalid block target' ); + } + $this->setTarget( $options['target'] ); + } elseif ( isset( $options['address'] ) ) { + $this->setTarget( $options['address'] ); + } else { + $this->setTarget( null ); + } $this->setReason( $options['reason'] ); if ( isset( $options['decodedTimestamp'] ) ) { $this->setTimestamp( $options['decodedTimestamp'] ); @@ -297,12 +301,30 @@ abstract class AbstractBlock implements Block { return $res; } + public function getTarget(): ?BlockTarget { + return $this->target; + } + + public function getRedactedTarget(): ?BlockTarget { + $target = $this->getTarget(); + if ( $this->getType() === Block::TYPE_AUTO + && !( $target instanceof AutoBlockTarget ) + ) { + $id = $this->getId( $this->wikiId ); + if ( $id === null ) { + throw new LogicException( 'no ID available for autoblock redaction' ); + } + $target = new AutoBlockTarget( $id, $this->wikiId ); + } + return $target; + } + /** * Get the type of target for this particular block. - * @return int|null AbstractBlock::TYPE_ constant, will never be TYPE_ID + * @return int|null AbstractBlock::TYPE_ constant */ public function getType(): ?int { - return $this->type; + return $this->target ? $this->target->getType() : null; } /** @@ -310,7 +332,8 @@ abstract class AbstractBlock implements Block { * @return ?UserIdentity */ public function getTargetUserIdentity(): ?UserIdentity { - return $this->target instanceof UserIdentity ? $this->target : null; + return $this->target instanceof BlockTargetWithUserIdentity + ? $this->target->getUserIdentity() : null; } /** @@ -318,13 +341,11 @@ abstract class AbstractBlock implements Block { * @return string */ public function getTargetName(): string { - return $this->target instanceof UserIdentity - ? $this->target->getName() - : (string)$this->target; + return (string)$this->target; } /** - * @param UserIdentity|string $target + * @param BlockTarget|UserIdentity|string $target * * @return bool * @since 1.37 @@ -385,22 +406,22 @@ abstract class AbstractBlock implements Block { } /** - * Set the target for this block, and update $this->type accordingly - * @param string|UserIdentity|null $target + * Set the target for this block + * @param BlockTarget|string|UserIdentity|null $target */ public function setTarget( $target ) { // Small optimization to make this code testable, this is what would happen anyway - if ( $target === '' ) { + if ( $target === '' || $target === null ) { $this->target = null; - $this->type = null; + } elseif ( $target instanceof BlockTarget ) { + $this->assertWiki( $target->getWikiId() ); + $this->target = $target; } else { - [ $parsedTarget, $this->type ] = MediaWikiServices::getInstance() - ->getBlockUtilsFactory() - ->getBlockUtils( $this->wikiId ) - ->parseBlockTarget( $target ); - if ( $parsedTarget !== null ) { - $this->assertWiki( is_string( $parsedTarget ) ? self::LOCAL : $parsedTarget->getWikiId() ); - } + $parsedTarget = MediaWikiServices::getInstance() + ->getCrossWikiBlockTargetFactory() + ->getFactory( $this->wikiId ) + ->newFromLegacyUnion( $target ); + $this->assertWiki( $parsedTarget->getWikiId() ); $this->target = $parsedTarget; } } @@ -442,10 +463,10 @@ abstract class AbstractBlock implements Block { */ public function appliesToUsertalk( ?Title $usertalk = null ) { if ( !$usertalk ) { - if ( $this->target instanceof UserIdentity ) { + if ( $this->target instanceof BlockTargetWithUserPage ) { $usertalk = Title::makeTitle( NS_USER_TALK, - $this->target->getName() + $this->target->getUserPage()->getDBkey() ); } else { throw new InvalidArgumentException( diff --git a/includes/block/AnonIpBlockTarget.php b/includes/block/AnonIpBlockTarget.php new file mode 100644 index 000000000000..e3acfedbf933 --- /dev/null +++ b/includes/block/AnonIpBlockTarget.php @@ -0,0 +1,79 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityValue; +use StatusValue; +use Wikimedia\IPUtils; + +/** + * A block target for a single IP address with an associated user page + * + * @since 1.44 + */ +class AnonIpBlockTarget extends BlockTarget implements BlockTargetWithUserPage, BlockTargetWithIp { + private string $addr; + + /** + * @param string $addr + * @param string|false $wikiId + */ + public function __construct( string $addr, $wikiId ) { + parent::__construct( $wikiId ); + $this->addr = $addr; + } + + public function toString(): string { + return $this->addr; + } + + public function getType(): int { + return Block::TYPE_IP; + } + + public function getSpecificity() { + return 2; + } + + public function getLogPage(): PageReference { + return $this->getUserPage(); + } + + public function getUserPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->addr, $this->wikiId ); + } + + public function getUserIdentity(): UserIdentity { + return new UserIdentityValue( 0, $this->addr, $this->wikiId ); + } + + public function validateForCreation(): StatusValue { + return StatusValue::newGood(); + } + + /** + * Get the IP address in hexadecimal form + * + * @return string + */ + public function toHex(): string { + return IPUtils::toHex( $this->addr ); + } + + /** + * Get the IP address as a hex "range" tuple, with the start and end equal + * + * @return string[] + */ + public function toHexRange(): array { + $hex = $this->toHex(); + return [ $hex, $hex ]; + } + + protected function getLegacyUnion() { + return $this->getUserIdentity(); + } +} diff --git a/includes/block/AutoBlockTarget.php b/includes/block/AutoBlockTarget.php new file mode 100644 index 000000000000..373c694aa31a --- /dev/null +++ b/includes/block/AutoBlockTarget.php @@ -0,0 +1,60 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use StatusValue; + +/** + * A block target of the form #1234 where the number is the block ID. For user + * input or display when the IP address needs to be hidden. + * + * @since 1.44 + */ +class AutoBlockTarget extends BlockTarget { + private int $id; + + /** + * @param int $id The block ID + * @param string|false $wikiId + */ + public function __construct( int $id, $wikiId ) { + parent::__construct( $wikiId ); + $this->id = $id; + } + + public function toString(): string { + return '#' . $this->id; + } + + public function getType(): int { + return Block::TYPE_AUTO; + } + + public function getLogPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->toString(), $this->wikiId ); + } + + public function getSpecificity() { + return 2; + } + + public function validateForCreation(): StatusValue { + // Autoblocks are never valid for creation + return StatusValue::newFatal( 'badipaddress' ); + } + + /** + * Get the block ID + * + * @return int + */ + public function getId(): int { + return $this->id; + } + + protected function getLegacyUnion() { + return (string)$this->id; + } +} diff --git a/includes/block/Block.php b/includes/block/Block.php index 244aab5fc4c6..a1f5b9ed0f43 100644 --- a/includes/block/Block.php +++ b/includes/block/Block.php @@ -50,7 +50,6 @@ interface Block extends WikiAwareEntity { public const TYPE_IP = 2; public const TYPE_RANGE = 3; public const TYPE_AUTO = 4; - public const TYPE_ID = 5; /** * Map block types to strings, to allow convenient logging. @@ -60,7 +59,6 @@ interface Block extends WikiAwareEntity { self::TYPE_IP => 'ip', self::TYPE_RANGE => 'range', self::TYPE_AUTO => 'autoblock', - self::TYPE_ID => 'id', ]; /** @@ -97,6 +95,27 @@ interface Block extends WikiAwareEntity { public function getReasonComment(): CommentStoreComment; /** + * Get the target as an object. + * + * For autoblocks this can be either the IP address or the autoblock ID + * depending on how the block was loaded. Use getRedactedTarget() to safely + * get a target for display. + * + * @since 1.44 + * @return BlockTarget|null + */ + public function getTarget(): ?BlockTarget; + + /** + * Get the target, with the IP address hidden behind an AutoBlockTarget + * if the block is an autoblock. + * + * @since 1.44 + * @return BlockTarget|null + */ + public function getRedactedTarget(): ?BlockTarget; + + /** * Get the UserIdentity identifying the blocked user, * if the target is indeed a user (that is, if getType() returns TYPE_USER). * @@ -139,7 +158,7 @@ interface Block extends WikiAwareEntity { /** * Get the type of target for this particular block. - * @return int|null Block::TYPE_ constant, will never be TYPE_ID + * @return int|null Block::TYPE_ constant */ public function getType(): ?int; diff --git a/includes/block/BlockErrorFormatter.php b/includes/block/BlockErrorFormatter.php index 77348d456fbd..06adf7c85f15 100644 --- a/includes/block/BlockErrorFormatter.php +++ b/includes/block/BlockErrorFormatter.php @@ -20,6 +20,8 @@ namespace MediaWiki\Block; +use MediaWiki\Api\ApiBlockInfoHelper; +use MediaWiki\Api\ApiMessage; use MediaWiki\CommentStore\CommentStoreComment; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; @@ -68,7 +70,7 @@ class BlockErrorFormatter { /** * Get a block error message. Different message keys are chosen depending on the * block features. Message parameters are formatted for the specified user and - * language. + * language. The message includes machine-readable data for API error responses. * * If passed a CompositeBlock, will get a generic message stating that there are * multiple blocks. To get all the block messages, use getMessages instead. @@ -77,7 +79,7 @@ class BlockErrorFormatter { * @param UserIdentity $user * @param mixed $language Unused since 1.42 * @param string $ip - * @return Message + * @return ApiMessage */ public function getMessage( Block $block, @@ -87,7 +89,14 @@ class BlockErrorFormatter { ): Message { $key = $this->getBlockErrorMessageKey( $block, $user ); $params = $this->getBlockErrorMessageParams( $block, $user, $ip ); - return $this->uiContext->msg( $key, $params ); + $apiHelper = new ApiBlockInfoHelper; + + // @phan-suppress-next-line PhanTypeMismatchReturnSuperType + return ApiMessage::create( + $this->uiContext->msg( $key, $params ), + $apiHelper->getBlockCode( $block ), + [ 'blockinfo' => $apiHelper->getBlockDetails( $block, $this->getLanguage(), $user ) ] + ); } /** diff --git a/includes/block/BlockPermissionChecker.php b/includes/block/BlockPermissionChecker.php index edce233144c1..10eafbbdef6c 100644 --- a/includes/block/BlockPermissionChecker.php +++ b/includes/block/BlockPermissionChecker.php @@ -40,14 +40,14 @@ use Wikimedia\Rdbms\IDBAccessObject; class BlockPermissionChecker { /** * Legacy target state - * @var UserIdentity|string|null Block target or null when unknown + * @var BlockTarget|null Block target or null when unknown */ private $target; /** - * @var BlockUtils + * @var BlockTargetFactory */ - private $blockUtils; + private $blockTargetFactory; /** * @var Authority Block performer @@ -65,17 +65,17 @@ class BlockPermissionChecker { /** * @param ServiceOptions $options - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory For legacy branches only * @param Authority $performer */ public function __construct( ServiceOptions $options, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, Authority $performer ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->performer = $performer; } @@ -85,7 +85,7 @@ class BlockPermissionChecker { * @return void */ public function setTarget( $target ) { - [ $this->target, ] = $this->blockUtils->parseBlockTarget( $target ); + $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target ); } /** @@ -119,9 +119,11 @@ class BlockPermissionChecker { * * T208965: Partially blocked admins can block and unblock others as normal. * - * @param UserIdentity|string|null $target Passing null for this parameter - * is deprecated. This parameter will soon be required. It is the target - * of the proposed block. + * @param BlockTarget|UserIdentity|string|null $target The target of the + * proposed block or unblock operation. Passing null for this parameter + * is deprecated. This parameter will soon be required. Passing a + * UserIdentity or string for this parameter is deprecated. Pass a + * BlockTarget in new code. * @param int $freshness Indicates whether slightly stale data is acceptable * in exchange for a fast response. * @return bool|string True when checks passed, message code for failures @@ -139,9 +141,13 @@ class BlockPermissionChecker { } else { throw new InvalidArgumentException( 'A target is required' ); } - } else { - [ $target, ] = $this->blockUtils->parseBlockTarget( $target ); + } elseif ( !( $target instanceof BlockTarget ) ) { + $target = $this->blockTargetFactory->newFromLegacyUnion( $target ); + if ( !$target ) { + throw new InvalidArgumentException( 'Invalid block target' ); + } } + $block = $this->performer->getBlock( $freshness ); if ( !$block ) { // User is not blocked, process as normal @@ -156,8 +162,8 @@ class BlockPermissionChecker { $performerIdentity = $this->performer->getUser(); if ( - $target instanceof UserIdentity && - $target->getId() === $performerIdentity->getId() + $target instanceof UserBlockTarget && + $target->getUserIdentity()->getId() === $performerIdentity->getId() ) { // Blocked admin is trying to alter their own block @@ -175,9 +181,9 @@ class BlockPermissionChecker { } if ( - $target instanceof UserIdentity && + $target instanceof UserBlockTarget && $block->getBlocker() && - $target->equals( $block->getBlocker() ) + $target->getUserIdentity()->equals( $block->getBlocker() ) ) { // T150826: Blocked admins can always block the admin who blocked them return true; diff --git a/includes/block/BlockPermissionCheckerFactory.php b/includes/block/BlockPermissionCheckerFactory.php index 8e43f17394d4..da9c21648836 100644 --- a/includes/block/BlockPermissionCheckerFactory.php +++ b/includes/block/BlockPermissionCheckerFactory.php @@ -37,15 +37,15 @@ class BlockPermissionCheckerFactory { public const CONSTRUCTOR_OPTIONS = BlockPermissionChecker::CONSTRUCTOR_OPTIONS; private ServiceOptions $options; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; public function __construct( ServiceOptions $options, - BlockUtils $blockUtils + BlockTargetFactory $blockTargetFactory ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; } /** @@ -73,7 +73,7 @@ class BlockPermissionCheckerFactory { public function newChecker( Authority $performer ) { return new BlockPermissionChecker( $this->options, - $this->blockUtils, + $this->blockTargetFactory, $performer ); } diff --git a/includes/block/BlockTarget.php b/includes/block/BlockTarget.php new file mode 100644 index 000000000000..4b963d2cc53b --- /dev/null +++ b/includes/block/BlockTarget.php @@ -0,0 +1,117 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\DAO\WikiAwareEntityTrait; +use MediaWiki\Page\PageReference; +use MediaWiki\User\UserIdentity; +use StatusValue; +use Stringable; + +/** + * Base class for block targets + * + * @since 1.44 + */ +abstract class BlockTarget implements WikiAwareEntity, Stringable { + use WikiAwareEntityTrait; + + /** @var string|false */ + protected $wikiId; + + /** + * @param string|false $wikiId UserIdentity and Block extend WikiAwareEntity + * and so we must ask for a wiki ID as well, to forward it through, even + * though we don't use it. + */ + protected function __construct( $wikiId ) { + $this->wikiId = $wikiId; + } + + public function getWikiId() { + return $this->wikiId; + } + + public function __toString() { + return $this->toString(); + } + + /** + * Compare this object with another one + * + * @param BlockTarget|null $other + * @return bool + */ + public function equals( ?BlockTarget $other ) { + return $other !== null + && static::class === get_class( $other ) + && $this->toString() === $other->toString(); + } + + /** + * Get the username, the IP address, range, or autoblock ID prefixed with + * a "#". Such a string will round-trip through BlockTarget::newFromString(), + * giving back the same target. + * + * @return string + */ + abstract public function toString(): string; + + /** + * Get one of the Block::TYPE_xxx constants associated with this target + * @return int + */ + abstract public function getType(): int; + + /** + * Get the title to be used when logging an action on this block. For an + * autoblock, the title is technically invalid, with a hash character in + * the DB key. For a range block, the title is valid but is not a user + * page for a specific user. + * + * See also getUserPage(), which exists only for subclasses which relate to + * a specific user with a talk page. + * + * @return PageReference + */ + abstract public function getLogPage(): PageReference; + + /** + * Get the score of this block for purposes of choosing a more specific + * block, where lower is more specific. + * + * - 1: user block + * - 2: single IP block + * - 2-3: range block scaled according to the size of the range + * + * @return float|int + */ + abstract public function getSpecificity(); + + /** + * Get the target and type tuple conventionally returned by + * BlockUtils::parseBlockTarget() + * + * @return array + */ + public function getLegacyTuple(): array { + return [ $this->getLegacyUnion(), $this->getType() ]; + } + + /** + * Check the target data against more stringent requirements imposed when + * a block is created from user input. This is in addition to the loose + * validation done by BlockTargetFactory::newFromString(). + * + * @return StatusValue + */ + abstract public function validateForCreation(): StatusValue; + + /** + * Get the first part of the legacy tuple. + * + * @return UserIdentity|string + */ + abstract protected function getLegacyUnion(); +} diff --git a/includes/block/BlockTargetFactory.php b/includes/block/BlockTargetFactory.php new file mode 100644 index 000000000000..8b09be8eaa07 --- /dev/null +++ b/includes/block/BlockTargetFactory.php @@ -0,0 +1,280 @@ +<?php + +namespace MediaWiki\Block; + +use InvalidArgumentException; +use MediaWiki\Config\ServiceOptions; +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\DAO\WikiAwareEntityTrait; +use MediaWiki\MainConfigNames; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserIdentityLookup; +use MediaWiki\User\UserIdentityValue; +use MediaWiki\User\UserNameUtils; +use RuntimeException; +use stdClass; +use Wikimedia\IPUtils; +use Wikimedia\Rdbms\IDBAccessObject; + +/** + * Factory for BlockTarget objects + * + * @since 1.44 + */ +class BlockTargetFactory implements WikiAwareEntity { + use WikiAwareEntityTrait; + + private UserIdentityLookup $userIdentityLookup; + private UserNameUtils $userNameUtils; + + /** @var string|false */ + private $wikiId; + + /** + * @var array The range block minimum prefix lengths indexed by protocol (IPv4 or IPv6) + */ + private $rangePrefixLimits; + + /** + * @internal Only for use by ServiceWiring + */ + public const CONSTRUCTOR_OPTIONS = [ + MainConfigNames::BlockCIDRLimit, + ]; + + /** + * @param ServiceOptions $options + * @param UserIdentityLookup $userIdentityLookup + * @param UserNameUtils $userNameUtils + * @param string|false $wikiId + */ + public function __construct( + ServiceOptions $options, + UserIdentityLookup $userIdentityLookup, + UserNameUtils $userNameUtils, + /* string|false */ $wikiId = Block::LOCAL + ) { + $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); + $this->rangePrefixLimits = $options->get( MainConfigNames::BlockCIDRLimit ); + $this->userIdentityLookup = $userIdentityLookup; + $this->userNameUtils = $userNameUtils; + $this->wikiId = $wikiId; + } + + public function getWikiId() { + return $this->wikiId; + } + + /** + * Try to create a block target from a user input string. + * + * @param string|null $str + * @return BlockTarget|null + */ + public function newFromString( ?string $str ): ?BlockTarget { + if ( $str === null ) { + return null; + } + + $str = trim( $str ); + + if ( IPUtils::isValid( $str ) ) { + return new AnonIpBlockTarget( IPUtils::sanitizeIP( $str ), $this->wikiId ); + } elseif ( IPUtils::isValidRange( $str ) ) { + return new RangeBlockTarget( + IPUtils::sanitizeRange( $str ), + $this->rangePrefixLimits, + $this->wikiId + ); + } + + if ( preg_match( '/^#\d+$/', $str ) ) { + // Autoblock reference in the form "#12345" + return new AutoBlockTarget( + (int)substr( $str, 1 ), + $this->wikiId + ); + } + + $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $str ); + if ( $userFromDB instanceof UserIdentity ) { + return new UserBlockTarget( $userFromDB ); + } + + // Wrap the invalid user in a UserIdentityValue. + // This allows validateTarget() to return a "nosuchusershort" message, + // which is needed for Special:Block. + $canonicalName = $this->userNameUtils->getCanonical( $str ); + if ( $canonicalName !== false ) { + return new UserBlockTarget( new UserIdentityValue( 0, $canonicalName ) ); + } + + return null; + } + + /** + * Create a BlockTarget from a UserIdentity, which may refer to a + * registered user, an IP address or range. + * + * @param UserIdentity $user + * @return BlockTarget + */ + public function newFromUser( UserIdentity $user ): BlockTarget { + $this->assertWiki( $user->getWikiId() ); + $name = $user->getName(); + if ( $user->getId( $this->wikiId ) !== 0 ) { + // We'll trust the caller and skip IP validity checks + return new UserBlockTarget( $user ); + } elseif ( IPUtils::isValidRange( $name ) ) { + return $this->newRangeBlockTarget( $name ); + } elseif ( IPUtils::isValid( $name ) ) { + return $this->newAnonIpBlockTarget( $name ); + } else { + return new UserBlockTarget( $user ); + } + } + + /** + * Try to create a BlockTarget from a UserIdentity|string|null, a union type + * previously used as a target by various methods. + * + * @param UserIdentity|string|null $union + * @return BlockTarget|null + */ + public function newFromLegacyUnion( $union ): ?BlockTarget { + if ( $union instanceof UserIdentity ) { + if ( IPUtils::isValid( $union->getName() ) ) { + return new AnonIpBlockTarget( $union->getName(), $this->wikiId ); + } else { + return new UserBlockTarget( $union ); + } + } elseif ( is_string( $union ) ) { + return $this->newFromString( $union ); + } else { + return null; + } + } + + /** + * Try to create a BlockTarget from a row which must contain bt_user, + * bt_address and optionally bt_user_text. + * + * bt_auto is ignored, so this is suitable for permissions and for block + * creation but not for display. + * + * @param stdClass $row + * @return BlockTarget|null + */ + public function newFromRowRaw( $row ): ?BlockTarget { + return $this->newFromRowInternal( $row, false ); + } + + /** + * Try to create a BlockTarget from a row which must contain bt_auto, + * bt_user, bt_address and bl_id, and optionally bt_user_text. + * + * If bt_auto is set, the address will be redacted to avoid disclosing it. + * The ID will be wrapped in an AutoblockTarget. + * + * @param stdClass $row + * @return BlockTarget|null + */ + public function newFromRowRedacted( $row ): ?BlockTarget { + return $this->newFromRowInternal( $row, true ); + } + + /** + * @param stdClass $row + * @param bool $redact + * @return BlockTarget|null + */ + private function newFromRowInternal( $row, $redact ): ?BlockTarget { + if ( $redact && $row->bt_auto ) { + return $this->newAutoBlockTarget( $row->bl_id ); + } elseif ( isset( $row->bt_user ) ) { + if ( isset( $row->bt_user_text ) ) { + $user = new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId ); + } else { + $user = $this->userIdentityLookup->getUserIdentityByUserId( $row->bt_user ); + if ( !$user ) { + $user = $this->userIdentityLookup->getUserIdentityByUserId( + $row->bt_user, IDBAccessObject::READ_LATEST ); + if ( !$user ) { + throw new RuntimeException( + "Unable to find name for user ID {$row->bt_user}" ); + } + } + } + return new UserBlockTarget( $user ); + } elseif ( $row->bt_address === null ) { + return null; + } elseif ( IPUtils::isValid( $row->bt_address ) ) { + return $this->newAnonIpBlockTarget( IPUtils::sanitizeIP( $row->bt_address ) ); + } elseif ( IPUtils::isValidRange( $row->bt_address ) ) { + return $this->newRangeBlockTarget( IPUtils::sanitizeRange( $row->bt_address ) ); + } else { + return null; + } + } + + /** + * Create an AutoBlockTarget for the given ID + * + * A simple constructor proxy for pre-validated input. + * + * @param int $id + * @return AutoBlockTarget + */ + public function newAutoBlockTarget( int $id ): AutoBlockTarget { + return new AutoBlockTarget( $id, $this->wikiId ); + } + + /** + * Create a UserBlockTarget for the given user. + * + * A simple constructor proxy for pre-validated input. + * + * The user must be a real registered user. Use newFromUser() to create a + * block target from a UserIdentity which may represent an IP address. + * + * @param UserIdentity $user + * @return UserBlockTarget + */ + public function newUserBlockTarget( UserIdentity $user ): UserBlockTarget { + $this->assertWiki( $user->getWikiId() ); + if ( IPUtils::isValid( $user->getName() ) ) { + throw new InvalidArgumentException( 'IP address passed to newUserBlockTarget' ); + } + return new UserBlockTarget( $user ); + } + + /** + * Create an IP block target + * + * A simple constructor proxy for pre-validated input. + * + * @param string $ip + * @return AnonIpBlockTarget + */ + public function newAnonIpBlockTarget( string $ip ): AnonIpBlockTarget { + if ( !IPUtils::isValid( $ip ) ) { + throw new InvalidArgumentException( 'Invalid IP address for block target' ); + } + return new AnonIpBlockTarget( $ip, $this->wikiId ); + } + + /** + * Create a range block target. + * + * A simple constructor proxy for pre-validated input. + * + * @param string $cidr + * @return RangeBlockTarget + */ + public function newRangeBlockTarget( string $cidr ): RangeBlockTarget { + if ( !IPUtils::isValidRange( $cidr ) ) { + throw new InvalidArgumentException( 'Invalid IP range for block target' ); + } + return new RangeBlockTarget( $cidr, $this->rangePrefixLimits, $this->wikiId ); + } +} diff --git a/includes/block/BlockTargetWithIp.php b/includes/block/BlockTargetWithIp.php new file mode 100644 index 000000000000..edce20544327 --- /dev/null +++ b/includes/block/BlockTargetWithIp.php @@ -0,0 +1,17 @@ +<?php + +namespace MediaWiki\Block; + +/** + * Shared interface for IP or range blocks + * + * @since 1.44 + */ +interface BlockTargetWithIp { + /** + * Get the range as a hexadecimal tuple. + * + * @return string[] + */ + public function toHexRange(); +} diff --git a/includes/block/BlockTargetWithUserIdentity.php b/includes/block/BlockTargetWithUserIdentity.php new file mode 100644 index 000000000000..ecfb0b752505 --- /dev/null +++ b/includes/block/BlockTargetWithUserIdentity.php @@ -0,0 +1,19 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\User\UserIdentity; + +/** + * Interface for block targets which can be converted to a UserIdentity. + * + * @since 1.44 + */ +interface BlockTargetWithUserIdentity { + /** + * Get a UserIdentity associated with this target. + * + * @return UserIdentity + */ + public function getUserIdentity(): UserIdentity; +} diff --git a/includes/block/BlockTargetWithUserPage.php b/includes/block/BlockTargetWithUserPage.php new file mode 100644 index 000000000000..717123566bcc --- /dev/null +++ b/includes/block/BlockTargetWithUserPage.php @@ -0,0 +1,22 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; + +/** + * Shared interface for user and single IP targets, that is, for targets with a + * meaningful user page link. Single IP addresses are traditionally treated as + * the names of anonymous users. + * + * @since 1.44 + */ +interface BlockTargetWithUserPage extends BlockTargetWithUserIdentity { + /** + * Get the target's user page. The page has an associated talk page which + * can be used to talk to the target. + * + * @return PageReference + */ + public function getUserPage(): PageReference; +} diff --git a/includes/block/BlockUser.php b/includes/block/BlockUser.php index 178722d2cb14..bd24a795d9f5 100644 --- a/includes/block/BlockUser.php +++ b/includes/block/BlockUser.php @@ -60,24 +60,13 @@ class BlockUser { public const CONFLICT_REBLOCK = true; /** - * @var UserIdentity|string|null + * @var BlockTarget|null * - * Target of the block - * - * This is null in case BlockUtils::parseBlockTarget failed to parse the target. - * Such case is detected in placeBlockUnsafe, by calling validateTarget from SpecialBlock. + * Target of the block. This is null in case BlockTargetFactory failed to + * parse the target. */ private $target; - /** - * @var int - * - * One of AbstractBlock::TYPE_* constants - * - * This will be -1 if BlockUtils::parseBlockTarget failed to parse the target. - */ - private $targetType; - /** @var DatabaseBlock|null */ private $blockToUpdate; @@ -90,7 +79,7 @@ class BlockUser { private ServiceOptions $options; private BlockRestrictionStore $blockRestrictionStore; private BlockPermissionChecker $blockPermissionChecker; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private BlockActionInfo $blockActionInfo; private HookRunner $hookRunner; private DatabaseBlockStore $blockStore; @@ -173,7 +162,7 @@ class BlockUser { * @param ServiceOptions $options * @param BlockRestrictionStore $blockRestrictionStore * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory * @param BlockActionInfo $blockActionInfo * @param HookContainer $hookContainer * @param DatabaseBlockStore $databaseBlockStore @@ -182,7 +171,7 @@ class BlockUser { * @param LoggerInterface $logger * @param TitleFactory $titleFactory * @param DatabaseBlock|null $blockToUpdate - * @param string|UserIdentity|null $target Target of the block + * @param BlockTarget|string|UserIdentity|null $target Target of the block * @param Authority $performer Performer of the block * @param string $expiry Expiry of the block (timestamp or 'infinity') * @param string $reason Reason of the block @@ -204,7 +193,7 @@ class BlockUser { ServiceOptions $options, BlockRestrictionStore $blockRestrictionStore, BlockPermissionCheckerFactory $blockPermissionCheckerFactory, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, BlockActionInfo $blockActionInfo, HookContainer $hookContainer, DatabaseBlockStore $databaseBlockStore, @@ -225,7 +214,7 @@ class BlockUser { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->blockRestrictionStore = $blockRestrictionStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->hookRunner = new HookRunner( $hookContainer ); $this->blockStore = $databaseBlockStore; $this->userFactory = $userFactory; @@ -237,16 +226,15 @@ class BlockUser { // Process block target if ( $blockToUpdate !== null ) { $this->blockToUpdate = $blockToUpdate; - $this->target = $blockToUpdate->getTargetUserIdentity() - ?? $blockToUpdate->getTargetName(); - $this->targetType = $blockToUpdate->getType() ?? -1; + $this->target = $blockToUpdate->getTarget(); + } elseif ( $target instanceof BlockTarget ) { + $this->target = $target; + } elseif ( $target === null ) { + throw new \InvalidArgumentException( + 'Either $target or $blockToUpdate must be specified' ); } else { - [ $this->target, $rawTargetType ] = $this->blockUtils->parseBlockTarget( $target ); - if ( $rawTargetType !== null ) { // Guard against invalid targets - $this->targetType = $rawTargetType; - } else { - $this->targetType = -1; - } + // TODO: deprecate + $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target ); } $this->blockPermissionChecker = $blockPermissionCheckerFactory @@ -302,10 +290,7 @@ class BlockUser { } - if ( - isset( $blockOptions['isHideUser'] ) && - $this->targetType === AbstractBlock::TYPE_USER - ) { + if ( isset( $blockOptions['isHideUser'] ) && $this->target instanceof UserBlockTarget ) { $this->isHideUser = $blockOptions['isHideUser']; } } @@ -403,7 +388,7 @@ class BlockUser { foreach ( $priorBlocks as $i => $block ) { // If we're blocking an IP, ignore any matching autoblocks (T287798) // TODO: put this in the query conditions - if ( $this->targetType !== Block::TYPE_AUTO + if ( $this->target->getType() !== Block::TYPE_AUTO && $block->getType() === Block::TYPE_AUTO ) { unset( $priorBlocks[$i] ); @@ -419,7 +404,7 @@ class BlockUser { * @return bool */ private function wasTargetHidden() { - if ( $this->targetType !== AbstractBlock::TYPE_USER ) { + if ( $this->target->getType() !== AbstractBlock::TYPE_USER ) { return false; } foreach ( $this->getPriorBlocksForTarget() as $block ) { @@ -515,7 +500,7 @@ class BlockUser { * Status is an instance of a newly placed block. */ public function placeBlockUnsafe( $conflictMode = self::CONFLICT_FAIL ): Status { - $status = $this->blockUtils->validateTarget( $this->target ); + $status = Status::wrap( $this->target->validateForCreation() ); if ( !$status->isOK() ) { $this->logger->debug( 'placeBlockUnsafe: invalid target' ); @@ -544,7 +529,8 @@ class BlockUser { return Status::newFatal( 'ipb_expiry_old' ); } - if ( $this->isHideUser ) { + $hideUserTarget = $this->getHideUserTarget(); + if ( $hideUserTarget ) { if ( $this->isPartial() ) { $this->logger->debug( 'placeBlockUnsafe: partial block cannot hide user' ); return Status::newFatal( 'ipb_hide_partial' ); @@ -558,7 +544,7 @@ class BlockUser { $hideUserContribLimit = $this->options->get( MainConfigNames::HideUserContribLimit ); if ( $hideUserContribLimit !== false && - $this->userEditTracker->getUserEditCount( $this->target ) > $hideUserContribLimit + $this->userEditTracker->getUserEditCount( $hideUserTarget ) > $hideUserContribLimit ) { $this->logger->debug( 'placeBlockUnsafe: hide user with too many contribs' ); return Status::newFatal( 'ipb_hide_invalid', Message::numParam( $hideUserContribLimit ) ); @@ -677,10 +663,9 @@ class BlockUser { $logEntry->addParameter( 'blockId', $block->getId() ); // Set *_deleted fields if requested - if ( $this->isHideUser ) { - // This should only be the case of $this->target is a user, so we can - // safely call ->getId() - RevisionDeleteUser::suppressUserName( $this->target->getName(), $this->target->getId() ); + $hideUserTarget = $this->getHideUserTarget(); + if ( $hideUserTarget ) { + RevisionDeleteUser::suppressUserName( $hideUserTarget->getName(), $hideUserTarget->getId() ); } DeferredUpdates::addCallableUpdate( function () use ( $block, $legacyUser, $priorBlock ) { @@ -698,6 +683,22 @@ class BlockUser { } /** + * If the operation is hiding a user, get the user being hidden + * + * @return UserIdentity|null + */ + private function getHideUserTarget(): ?UserIdentity { + if ( !$this->isHideUser ) { + return null; + } + if ( !( $this->target instanceof UserBlockTarget ) ) { + // Should be unreachable -- constructor checks this + throw new \LogicException( 'Wrong target type used with hide user option' ); + } + return $this->target->getUserIdentity(); + } + + /** * Build namespace restrictions array from $this->blockRestrictions * * Returns an array of namespace IDs. @@ -820,7 +821,7 @@ class BlockUser { private function blockLogFlags(): string { $flags = []; - if ( $this->targetType != AbstractBlock::TYPE_USER && !$this->isHardBlock ) { + if ( $this->target->getType() != AbstractBlock::TYPE_USER && !$this->isHardBlock ) { // For grepping: message block-log-flags-anononly $flags[] = 'anononly'; } @@ -830,7 +831,7 @@ class BlockUser { $flags[] = 'nocreate'; } - if ( $this->targetType == AbstractBlock::TYPE_USER && !$this->isAutoblocking ) { + if ( $this->target->getType() == AbstractBlock::TYPE_USER && !$this->isAutoblocking ) { // For grepping: message block-log-flags-noautoblock $flags[] = 'noautoblock'; } diff --git a/includes/block/BlockUserFactory.php b/includes/block/BlockUserFactory.php index 23e0422bd69c..5323d53e0982 100644 --- a/includes/block/BlockUserFactory.php +++ b/includes/block/BlockUserFactory.php @@ -31,7 +31,7 @@ interface BlockUserFactory { /** * Create BlockUser * - * @param string|UserIdentity $target Target of the block + * @param BlockTarget|string|UserIdentity $target Target of the block * @param Authority $performer Performer of the block * @param string $expiry Expiry of the block (timestamp or 'infinity') * @param string $reason Reason of the block diff --git a/includes/block/BlockUtils.php b/includes/block/BlockUtils.php index fd833becf380..a701c75df2dc 100644 --- a/includes/block/BlockUtils.php +++ b/includes/block/BlockUtils.php @@ -21,14 +21,8 @@ namespace MediaWiki\Block; -use MediaWiki\Config\ServiceOptions; -use MediaWiki\MainConfigNames; use MediaWiki\Status\Status; use MediaWiki\User\UserIdentity; -use MediaWiki\User\UserIdentityLookup; -use MediaWiki\User\UserIdentityValue; -use MediaWiki\User\UserNameUtils; -use Wikimedia\IPUtils; /** * Backend class for blocking utils @@ -42,34 +36,14 @@ use Wikimedia\IPUtils; * - block target validation * - parsing the target and type of a block in the database * + * @deprecated since 1.44 use BlockTargetFactory * @since 1.36 */ class BlockUtils { - private ServiceOptions $options; - private UserIdentityLookup $userIdentityLookup; - private UserNameUtils $userNameUtils; + private BlockTargetFactory $blockTargetFactory; - /** @var string|false */ - private $wikiId; - - /** - * @internal Only for use by ServiceWiring - */ - public const CONSTRUCTOR_OPTIONS = [ - MainConfigNames::BlockCIDRLimit, - ]; - - public function __construct( - ServiceOptions $options, - UserIdentityLookup $userIdentityLookup, - UserNameUtils $userNameUtils, - /* string|false */ $wikiId = Block::LOCAL - ) { - $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); - $this->options = $options; - $this->userIdentityLookup = $userIdentityLookup; - $this->userNameUtils = $userNameUtils; - $this->wikiId = $wikiId; + public function __construct( BlockTargetFactory $blockTargetFactory ) { + $this->blockTargetFactory = $blockTargetFactory; } /** @@ -91,55 +65,12 @@ class BlockUtils { * @return array [ UserIdentity|String|null, int|null ] */ public function parseBlockTarget( $target ): array { - // We may have been through this before - if ( $target instanceof UserIdentity ) { - if ( IPUtils::isValid( $target->getName() ) ) { - return [ $target, AbstractBlock::TYPE_IP ]; - } else { - return [ $target, AbstractBlock::TYPE_USER ]; - } - } elseif ( $target === null ) { + $targetObj = $this->blockTargetFactory->newFromLegacyUnion( $target ); + if ( $targetObj ) { + return $targetObj->getLegacyTuple(); + } else { return [ null, null ]; } - - $target = trim( $target ); - - if ( IPUtils::isValid( $target ) ) { - return [ - UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $target ), $this->wikiId ), - AbstractBlock::TYPE_IP - ]; - - } elseif ( IPUtils::isValidRange( $target ) ) { - // Can't create a UserIdentity from an IP range - return [ IPUtils::sanitizeRange( $target ), AbstractBlock::TYPE_RANGE ]; - } - - if ( preg_match( '/^#\d+$/', $target ) ) { - // Autoblock reference in the form "#12345" - return [ substr( $target, 1 ), AbstractBlock::TYPE_AUTO ]; - } - - $userFromDB = $this->userIdentityLookup->getUserIdentityByName( $target ); - if ( $userFromDB instanceof UserIdentity ) { - // Note that since numbers are valid usernames, a $target of "12345" will be - // considered a UserIdentity. If you want to pass a block ID, prepend a hash "#12345", - // since hash characters are not valid in usernames or titles generally. - return [ $userFromDB, AbstractBlock::TYPE_USER ]; - } - - // Wrap the invalid user in a UserIdentityValue. - // This allows validateTarget() to return a "nosuchusershort" message, - // which is needed for Special:Block. - $canonicalName = $this->userNameUtils->getCanonical( $target ); - if ( $canonicalName !== false ) { - return [ - new UserIdentityValue( 0, $canonicalName ), - AbstractBlock::TYPE_USER - ]; - } - - return [ null, null ]; } /** @@ -151,25 +82,9 @@ class BlockUtils { * @return array [ UserIdentity|String|null, int|null ] */ public function parseBlockTargetRow( $row ) { - if ( $row->bt_auto ) { - return [ $row->bl_id, AbstractBlock::TYPE_AUTO ]; - } elseif ( isset( $row->bt_user ) ) { - if ( isset( $row->bt_user_text ) ) { - $user = new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId ); - } else { - $user = $this->userIdentityLookup->getUserIdentityByUserId( $row->bt_user ); - } - return [ $user, AbstractBlock::TYPE_USER ]; - } elseif ( $row->bt_address === null ) { - return [ null, null ]; - } elseif ( IPUtils::isValid( $row->bt_address ) ) { - return [ - UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $row->bt_address ), $this->wikiId ), - AbstractBlock::TYPE_IP - ]; - } elseif ( IPUtils::isValidRange( $row->bt_address ) ) { - // Can't create a UserIdentity from an IP range - return [ IPUtils::sanitizeRange( $row->bt_address ), AbstractBlock::TYPE_RANGE ]; + $target = $this->blockTargetFactory->newFromRowRedacted( $row ); + if ( $target ) { + return $target->getLegacyTuple(); } else { return [ null, null ]; } @@ -183,90 +98,12 @@ class BlockUtils { * @return Status */ public function validateTarget( $value ): Status { - [ $target, $type ] = $this->parseBlockTarget( $value ); - - $status = Status::newGood( $target ); - - switch ( $type ) { - case AbstractBlock::TYPE_USER: - if ( !$target->isRegistered() ) { - $status->fatal( - 'nosuchusershort', - wfEscapeWikiText( $target->getName() ) - ); - } - break; - - case AbstractBlock::TYPE_RANGE: - [ $ip, $range ] = explode( '/', $target, 2 ); - - if ( IPUtils::isIPv4( $ip ) ) { - $status->merge( $this->validateIPv4Range( (int)$range ) ); - } elseif ( IPUtils::isIPv6( $ip ) ) { - $status->merge( $this->validateIPv6Range( (int)$range ) ); - } else { - // Something is FUBAR - $status->fatal( 'badipaddress' ); - } - break; - - case AbstractBlock::TYPE_IP: - // All is well - break; - - default: - $status->fatal( 'badipaddress' ); - break; - } - - return $status; - } - - /** - * Validate an IPv4 range - * - * @param int $range - * - * @return Status - */ - private function validateIPv4Range( int $range ): Status { - $status = Status::newGood(); - $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit ); - - if ( $blockCIDRLimit['IPv4'] == 32 ) { - // Range block effectively disabled - $status->fatal( 'range_block_disabled' ); - } elseif ( $range > 32 ) { - // Such a range cannot exist - $status->fatal( 'ip_range_invalid' ); - } elseif ( $range < $blockCIDRLimit['IPv4'] ) { - $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv4'] ); + $target = $this->blockTargetFactory->newFromLegacyUnion( $value ); + if ( $target ) { + return Status::wrap( $target->validateForCreation() ); + } else { + return Status::newFatal( 'badipaddress' ); } - - return $status; } - /** - * Validate an IPv6 range - * - * @param int $range - * - * @return Status - */ - private function validateIPv6Range( int $range ): Status { - $status = Status::newGood(); - $blockCIDRLimit = $this->options->get( MainConfigNames::BlockCIDRLimit ); - - if ( $blockCIDRLimit['IPv6'] == 128 ) { - // Range block effectively disabled - $status->fatal( 'range_block_disabled' ); - } elseif ( $range > 128 ) { - // Dodgy range - such a range cannot exist - $status->fatal( 'ip_range_invalid' ); - } elseif ( $range < $blockCIDRLimit['IPv6'] ) { - $status->fatal( 'ip_range_toolarge', $blockCIDRLimit['IPv6'] ); - } - - return $status; - } } diff --git a/includes/block/BlockUtilsFactory.php b/includes/block/BlockUtilsFactory.php index 2691b8041c07..76607f3c4fce 100644 --- a/includes/block/BlockUtilsFactory.php +++ b/includes/block/BlockUtilsFactory.php @@ -21,40 +21,22 @@ namespace MediaWiki\Block; -use MediaWiki\Config\ServiceOptions; -use MediaWiki\User\ActorStoreFactory; -use MediaWiki\User\UserNameUtils; -use Wikimedia\Rdbms\LBFactory; +use MediaWiki\WikiMap\WikiMap; /** + * @deprecated since 1.44 use CrossWikiBlockTargetFactory * @since 1.42 */ class BlockUtilsFactory { - /** - * @internal For use by ServiceWiring - */ - public const CONSTRUCTOR_OPTIONS = BlockUtils::CONSTRUCTOR_OPTIONS; - - private ServiceOptions $options; - private ActorStoreFactory $actorStoreFactory; - private UserNameUtils $userNameUtils; - private LBFactory $loadBalancerFactory; + private CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory; /** @var BlockUtils[] */ private array $storeCache = []; public function __construct( - ServiceOptions $options, - ActorStoreFactory $actorStoreFactory, - UserNameUtils $userNameUtils, - LBFactory $loadBalancerFactory + CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory ) { - $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); - - $this->options = $options; - $this->actorStoreFactory = $actorStoreFactory; - $this->userNameUtils = $userNameUtils; - $this->loadBalancerFactory = $loadBalancerFactory; + $this->crossWikiBlockTargetFactory = $crossWikiBlockTargetFactory; } /** @@ -62,17 +44,14 @@ class BlockUtilsFactory { * @return BlockUtils */ public function getBlockUtils( $wikiId = Block::LOCAL ): BlockUtils { - if ( is_string( $wikiId ) && $this->loadBalancerFactory->getLocalDomainID() === $wikiId ) { + if ( is_string( $wikiId ) && WikiMap::getCurrentWikiId() === $wikiId ) { $wikiId = Block::LOCAL; } $storeCacheKey = $wikiId === Block::LOCAL ? 'LOCAL' : 'crosswikistore-' . $wikiId; if ( !isset( $this->storeCache[$storeCacheKey] ) ) { $this->storeCache[$storeCacheKey] = new BlockUtils( - $this->options, - $this->actorStoreFactory->getUserIdentityLookup( $wikiId ), - $this->userNameUtils, - $wikiId + $this->crossWikiBlockTargetFactory->getFactory( $wikiId ) ); } return $this->storeCache[$storeCacheKey]; diff --git a/includes/block/CompositeBlock.php b/includes/block/CompositeBlock.php index c74481b98fbe..76707a46cdee 100644 --- a/includes/block/CompositeBlock.php +++ b/includes/block/CompositeBlock.php @@ -57,7 +57,7 @@ class CompositeBlock extends AbstractBlock { throw new InvalidArgumentException( 'No blocks given' ); } return new self( [ - 'address' => $originalBlocks[0]->target, + 'target' => $originalBlocks[0]->target, 'reason' => new Message( 'blockedtext-composite-reason' ), 'originalBlocks' => $originalBlocks, ] ); diff --git a/includes/block/CrossWikiBlockTargetFactory.php b/includes/block/CrossWikiBlockTargetFactory.php new file mode 100644 index 000000000000..f2560ed0a0e7 --- /dev/null +++ b/includes/block/CrossWikiBlockTargetFactory.php @@ -0,0 +1,60 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Config\ServiceOptions; +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\User\ActorStoreFactory; +use MediaWiki\User\UserNameUtils; +use MediaWiki\WikiMap\WikiMap; + +/** + * Factory for BlockTargetFactory objects. This is needed for cross-wiki block + * operations, since BlockTargetFactory needs a wiki ID passed to its constructor. + * + * @since 1.44 + */ +class CrossWikiBlockTargetFactory { + private ActorStoreFactory $actorStoreFactory; + private UserNameUtils $userNameUtils; + private ServiceOptions $options; + + /** @var BlockTargetFactory[] */ + private array $storeCache = []; + + /** + * @internal Only for use by ServiceWiring + */ + public const CONSTRUCTOR_OPTIONS = BlockTargetFactory::CONSTRUCTOR_OPTIONS; + + public function __construct( + ServiceOptions $options, + ActorStoreFactory $actorStoreFactory, + UserNameUtils $userNameUtils + ) { + $this->options = $options; + $this->actorStoreFactory = $actorStoreFactory; + $this->userNameUtils = $userNameUtils; + } + + /** + * @param string|false $wikiId + * @return BlockTargetFactory + */ + public function getFactory( $wikiId = WikiAwareEntity::LOCAL ) { + if ( is_string( $wikiId ) && WikiMap::getCurrentWikiId() === $wikiId ) { + $wikiId = WikiAwareEntity::LOCAL; + } + + $storeCacheKey = $wikiId === WikiAwareEntity::LOCAL ? 'LOCAL' : 'crosswikistore-' . $wikiId; + if ( !isset( $this->storeCache[$storeCacheKey] ) ) { + $this->storeCache[$storeCacheKey] = new BlockTargetFactory( + $this->options, + $this->actorStoreFactory->getUserIdentityLookup( $wikiId ), + $this->userNameUtils, + $wikiId + ); + } + return $this->storeCache[$storeCacheKey]; + } +} diff --git a/includes/block/DatabaseBlock.php b/includes/block/DatabaseBlock.php index c316ff360bb9..c478ed49bd5e 100644 --- a/includes/block/DatabaseBlock.php +++ b/includes/block/DatabaseBlock.php @@ -33,8 +33,6 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Title\Title; use MediaWiki\User\UserIdentity; use stdClass; -use UnexpectedValueException; -use Wikimedia\IPUtils; use Wikimedia\Rdbms\IDatabase; /** @@ -151,8 +149,7 @@ class DatabaseBlock extends AbstractBlock { */ public function equals( DatabaseBlock $block ) { return ( - (string)$this->target == (string)$block->target - && $this->type == $block->type + ( $this->target ? $this->target->equals( $block->target ) : $block->target === null ) && $this->auto == $block->auto && $this->isHardblock() == $block->isHardblock() && $this->isCreateAccountBlocked() == $block->isCreateAccountBlocked() @@ -290,38 +287,41 @@ class DatabaseBlock extends AbstractBlock { } /** - * Get the IP address at the start of the range in Hex form - * @return string IP in Hex form + * Get the IP address at the start of the range in hex form, or null if + * the target is not a range. + * + * @return string|null */ public function getRangeStart() { - switch ( $this->type ) { - case self::TYPE_USER: - return ''; - case self::TYPE_IP: - return IPUtils::toHex( $this->target ); - case self::TYPE_RANGE: - [ $start, /*...*/ ] = IPUtils::parseRange( $this->target ); - return $start; - default: - throw new UnexpectedValueException( "Block with invalid type" ); - } + return $this->target instanceof RangeBlockTarget + ? $this->target->getHexRangeStart() : null; } /** - * Get the IP address at the end of the range in Hex form - * @return string IP in Hex form + * Get the IP address at the end of the range in hex form, or null if + * the target is not a range. + * + * @return string|null */ public function getRangeEnd() { - switch ( $this->type ) { - case self::TYPE_USER: - return ''; - case self::TYPE_IP: - return IPUtils::toHex( $this->target ); - case self::TYPE_RANGE: - [ /*...*/, $end ] = IPUtils::parseRange( $this->target ); - return $end; - default: - throw new UnexpectedValueException( "Block with invalid type" ); + return $this->target instanceof RangeBlockTarget + ? $this->target->getHexRangeEnd() : null; + } + + /** + * Get the IP address of the single-IP block or the start of the range in + * hexadecimal form, or null if the target is not an IP. + * + * @since 1.44 + * @return string|null + */ + public function getIpHex() { + if ( $this->target instanceof RangeBlockTarget ) { + return $this->target->getHexRangeStart(); + } elseif ( $this->target instanceof AnonIpBlockTarget ) { + return $this->target->toHex(); + } else { + return null; } } @@ -501,7 +501,7 @@ class DatabaseBlock extends AbstractBlock { * @inheritDoc * * Autoblocks have whichever type corresponds to their target, so to detect if a block is an - * autoblock, we have to check the mAuto property instead. + * autoblock, we have to check the auto property instead. */ public function getType(): ?int { return $this->auto diff --git a/includes/block/DatabaseBlockStore.php b/includes/block/DatabaseBlockStore.php index bf53d04e166a..e3bcadcdf60b 100644 --- a/includes/block/DatabaseBlockStore.php +++ b/includes/block/DatabaseBlockStore.php @@ -35,7 +35,6 @@ use MediaWiki\User\ActorStoreFactory; use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; -use MediaWiki\User\UserIdentityValue; use Psr\Log\LoggerInterface; use RuntimeException; use stdClass; @@ -82,7 +81,7 @@ class DatabaseBlockStore { private ReadOnlyMode $readOnlyMode; private UserFactory $userFactory; private TempUserConfig $tempUserConfig; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private AutoblockExemptionList $autoblockExemptionList; public function __construct( @@ -96,7 +95,7 @@ class DatabaseBlockStore { ReadOnlyMode $readOnlyMode, UserFactory $userFactory, TempUserConfig $tempUserConfig, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, AutoblockExemptionList $autoblockExemptionList, /* string|false */ $wikiId = DatabaseBlock::LOCAL ) { @@ -114,7 +113,7 @@ class DatabaseBlockStore { $this->readOnlyMode = $readOnlyMode; $this->userFactory = $userFactory; $this->tempUserConfig = $tempUserConfig; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->autoblockExemptionList = $autoblockExemptionList; } @@ -191,16 +190,15 @@ class DatabaseBlockStore { * Load blocks from the database which target the specific target exactly, or which cover the * vague target. * - * @param UserIdentity|string|null $specificTarget - * @param int|null $specificType + * @param BlockTarget|null $specificTarget * @param bool $fromPrimary - * @param UserIdentity|string|null $vagueTarget Also search for blocks affecting this target. - * Doesn't make any sense to use TYPE_AUTO / TYPE_ID here. Leave blank to skip IP lookups. + * @param BlockTarget|null $vagueTarget Also search for blocks affecting + * this target. Doesn't make any sense to use TYPE_AUTO here. Leave blank to + * skip IP lookups. * @return DatabaseBlock[] Any relevant blocks */ private function newLoad( $specificTarget, - $specificType, $fromPrimary, $vagueTarget = null ) { @@ -214,49 +212,37 @@ class DatabaseBlockStore { $userNames = []; $addresses = []; $ranges = []; - if ( $specificType === Block::TYPE_USER ) { - if ( $specificTarget instanceof UserIdentity ) { - $userId = $specificTarget->getId( $this->wikiId ); - if ( $userId ) { - $userIds[] = $specificTarget->getId( $this->wikiId ); - } else { - // A nonexistent user can have no blocks. - // This case is hit in testing, possibly production too. - // Ignoring the user is optimal for production performance. - } + if ( $specificTarget instanceof UserBlockTarget ) { + $userId = $specificTarget->getUserIdentity()->getId( $this->wikiId ); + if ( $userId ) { + $userIds[] = $userId; } else { - $userNames[] = (string)$specificTarget; + // A nonexistent user can have no blocks. + // This case is hit in testing, possibly production too. + // Ignoring the user is optimal for production performance. } - } elseif ( in_array( $specificType, [ Block::TYPE_IP, Block::TYPE_RANGE ], true ) ) { + } elseif ( $specificTarget instanceof AnonIpBlockTarget + || $specificTarget instanceof RangeBlockTarget + ) { $addresses[] = (string)$specificTarget; } // Be aware that the != '' check is explicit, since empty values will be // passed by some callers (T31116) - if ( $vagueTarget != '' ) { - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $vagueTarget ); - switch ( $type ) { - case Block::TYPE_USER: - // Slightly weird, but who are we to argue? - /** @var UserIdentity $vagueUser */ - $vagueUser = $target; - if ( $vagueUser->getId( $this->wikiId ) ) { - $userIds[] = $vagueUser->getId( $this->wikiId ); - } else { - $userNames[] = $vagueUser->getName(); - } - break; - - case Block::TYPE_IP: - $ranges[] = [ IPUtils::toHex( $target ), null ]; - break; - - case Block::TYPE_RANGE: - $ranges[] = IPUtils::parseRange( $target ); - break; - - default: - $this->logger->debug( "Ignoring invalid vague target" ); + if ( $vagueTarget !== null ) { + if ( $vagueTarget instanceof UserBlockTarget ) { + // Slightly weird, but who are we to argue? + $vagueUser = $vagueTarget->getUserIdentity(); + $userId = $vagueUser->getId( $this->wikiId ); + if ( $userId ) { + $userIds[] = $userId; + } else { + $userNames[] = $vagueUser->getName(); + } + } elseif ( $vagueTarget instanceof BlockTargetWithIp ) { + $ranges[] = $vagueTarget->toHexRange(); + } else { + $this->logger->debug( "Ignoring invalid vague target" ); } } @@ -302,9 +288,9 @@ class DatabaseBlockStore { // Don't use anon only blocks on users if ( - $specificType == Block::TYPE_USER && $specificTarget && + $specificTarget instanceof UserBlockTarget && !$block->isHardblock() && - !$this->tempUserConfig->isTempName( $specificTarget ) + !$this->tempUserConfig->isTempName( $specificTarget->toString() ) ) { continue; } @@ -348,21 +334,7 @@ class DatabaseBlockStore { // Lower will be better $bestBlockScore = 100; foreach ( $blocks as $block ) { - if ( $block->getType() == Block::TYPE_RANGE ) { - // This is the number of bits that are allowed to vary in the block, give - // or take some floating point errors - $target = $block->getTargetName(); - $max = IPUtils::isIPv6( $target ) ? 128 : 32; - [ , $bits ] = IPUtils::parseCIDR( $target ); - $size = $max - $bits; - - // Rank a range block covering a single IP equally with a single-IP block - $score = Block::TYPE_RANGE - 1 + ( $size / $max ); - - } else { - $score = $block->getType(); - } - + $score = $block->getTarget()->getSpecificity(); if ( $score < $bestBlockScore ) { $bestBlockScore = $score; $bestBlock = $block; @@ -434,10 +406,8 @@ class DatabaseBlockStore { * @return DatabaseBlock */ public function newFromRow( IReadableDatabase $db, $row ) { - $address = $row->bt_address - ?? new UserIdentityValue( $row->bt_user, $row->bt_user_text, $this->wikiId ); return new DatabaseBlock( [ - 'address' => $address, + 'target' => $this->blockTargetFactory->newFromRowRaw( $row ), 'wiki' => $this->wikiId, 'timestamp' => $row->bl_timestamp, 'auto' => (bool)$row->bt_auto, @@ -465,7 +435,7 @@ class DatabaseBlockStore { * Given a target and the target's type, get an existing block object if possible. * * @since 1.42 - * @param string|UserIdentity|int|null $specificTarget A block target, which may be one of + * @param BlockTarget|string|UserIdentity|int|null $specificTarget A block target, which may be one of * several types: * * A user to block, in which case $target will be a User * * An IP to block, in which case $target will be a User generated by using @@ -476,7 +446,7 @@ class DatabaseBlockStore { * Calling this with a user, IP address or range will not select autoblocks, and will * only select a block where the targets match exactly (so looking for blocks on * 1.2.3.4 will not select 1.2.0.0/16 or even 1.2.3.4/32) - * @param string|UserIdentity|int|null $vagueTarget As above, but we will search for *any* + * @param BlockTarget|string|UserIdentity|int|null $vagueTarget As above, but we will search for *any* * block which affects that target (so for an IP address, get ranges containing that IP; * and also get any relevant autoblocks). Leave empty or blank to skip IP-based lookups. * @param bool $fromPrimary Whether to use the DB_PRIMARY database @@ -497,8 +467,8 @@ class DatabaseBlockStore { * This is similar to DatabaseBlockStore::newFromTarget, but it returns all the relevant blocks. * * @since 1.42 - * @param string|UserIdentity|int|null $specificTarget - * @param string|UserIdentity|int|null $vagueTarget + * @param BlockTarget|string|UserIdentity|int|null $specificTarget + * @param BlockTarget|string|UserIdentity|int|null $vagueTarget * @param bool $fromPrimary * @return DatabaseBlock[] Any relevant blocks */ @@ -507,22 +477,21 @@ class DatabaseBlockStore { $vagueTarget = null, $fromPrimary = false ) { - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $specificTarget ); - if ( $type == Block::TYPE_ID || $type == Block::TYPE_AUTO ) { - $block = $this->newFromID( $target ); + if ( !( $specificTarget instanceof BlockTarget ) ) { + $specificTarget = $this->blockTargetFactory->newFromLegacyUnion( $specificTarget ); + } + if ( $vagueTarget !== null && !( $vagueTarget instanceof BlockTarget ) ) { + $vagueTarget = $this->blockTargetFactory->newFromLegacyUnion( $vagueTarget ); + } + if ( $specificTarget instanceof AutoBlockTarget ) { + $block = $this->newFromID( $specificTarget->getId() ); return $block ? [ $block ] : []; - } elseif ( $target === null && $vagueTarget == '' ) { + } elseif ( $specificTarget === null && $vagueTarget === null ) { // We're not going to find anything useful here - // Be aware that the == '' check is explicit, since empty values will be - // passed by some callers (T31116) return []; - } elseif ( in_array( - $type, - [ Block::TYPE_USER, Block::TYPE_IP, Block::TYPE_RANGE, null ] ) - ) { - return $this->newLoad( $target, $type, $fromPrimary, $vagueTarget ); + } else { + return $this->newLoad( $specificTarget, $fromPrimary, $vagueTarget ); } - return []; } /** @@ -612,6 +581,32 @@ class DatabaseBlockStore { /** @name Database write methods */ /** + * Create a DatabaseBlock representing an unsaved block. Pass the returned + * object to insertBlock(). + * + * @since 1.44 + * + * @param array $options Options as documented in DatabaseBlock and + * AbstractBlock, and additionally: + * - targetUser: (UserIdentity) The UserIdentity to block + * - targetString (string) A string specifying the block target + * @return DatabaseBlock + */ + public function newUnsaved( array $options ): DatabaseBlock { + if ( isset( $options['targetUser'] ) ) { + $options['target'] = $this->blockTargetFactory + ->newUserBlockTarget( $options['targetUser'] ); + unset( $options['targetUser'] ); + } + if ( isset( $options['targetString'] ) ) { + $options['target'] = $this->blockTargetFactory + ->newFromString( $options['targetString'] ); + unset( $options['targetString'] ); + } + return new DatabaseBlock( $options ); + } + + /** * Delete expired blocks from the block table * * @internal only public for use in DatabaseBlock @@ -953,18 +948,19 @@ class DatabaseBlockStore { } /** - * Get conditions matching the block's block_target row + * Get conditions matching an existing block's block_target row * * @param DatabaseBlock $block * @return array */ private function getTargetConds( DatabaseBlock $block ) { - if ( $block->getType() === Block::TYPE_USER ) { + $target = $block->getTarget(); + if ( $target instanceof UserBlockTarget ) { return [ - 'bt_user' => $block->getTargetUserIdentity()->getId( $this->wikiId ) + 'bt_user' => $target->getUserIdentity()->getId( $this->wikiId ) ]; } else { - return [ 'bt_address' => $block->getTargetName() ]; + return [ 'bt_address' => $target->toString() ]; } } @@ -987,24 +983,24 @@ class DatabaseBlockStore { IDatabase $dbw, $expectedTargetCount ) { - $isUser = $block->getType() === Block::TYPE_USER; - $isRange = $block->getType() === Block::TYPE_RANGE; + $target = $block->getTarget(); + // Note: for new autoblocks, the target is an IpBlockTarget $isAuto = $block->getType() === Block::TYPE_AUTO; - $isSingle = !$isUser && !$isRange; - $targetAddress = $isUser ? null : $block->getTargetName(); - $targetUserName = $isUser ? $block->getTargetName() : null; - $targetUserId = $isUser - ? $block->getTargetUserIdentity()->getId( $this->wikiId ) : null; - - // Update bt_count field in existing target, if there is one - if ( $isUser ) { + if ( $target instanceof UserBlockTarget ) { + $targetAddress = null; + $targetUserName = (string)$target; + $targetUserId = $target->getUserIdentity()->getId( $this->wikiId ); $targetConds = [ 'bt_user' => $targetUserId ]; } else { + $targetAddress = (string)$target; + $targetUserName = null; + $targetUserId = null; $targetConds = [ 'bt_address' => $targetAddress, 'bt_auto' => $isAuto, ]; } + $condsWithCount = $targetConds; if ( $expectedTargetCount !== null ) { $condsWithCount['bt_count'] = $expectedTargetCount; @@ -1062,9 +1058,9 @@ class DatabaseBlockStore { 'bt_user' => $targetUserId, 'bt_user_text' => $targetUserName, 'bt_auto' => $isAuto, - 'bt_range_start' => $isRange ? $block->getRangeStart() : null, - 'bt_range_end' => $isRange ? $block->getRangeEnd() : null, - 'bt_ip_hex' => $isSingle || $isRange ? $block->getRangeStart() : null, + 'bt_range_start' => $block->getRangeStart(), + 'bt_range_end' => $block->getRangeEnd(), + 'bt_ip_hex' => $block->getIpHex(), 'bt_count' => 1 ]; $dbw->newInsertQueryBuilder() @@ -1167,7 +1163,7 @@ class DatabaseBlockStore { * * @since 1.42 * @param DatabaseBlock $block - * @param UserIdentity|string $newTarget + * @param BlockTarget|UserIdentity|string $newTarget * @return bool True if the update was successful, false if there was no * match for the block ID. */ @@ -1179,6 +1175,9 @@ class DatabaseBlockStore { __METHOD__ . " requires that a block id be set\n" ); } + if ( !( $newTarget instanceof BlockTarget ) ) { + $newTarget = $this->blockTargetFactory->newFromLegacyUnion( $newTarget ); + } $oldTargetConds = $this->getTargetConds( $block ); $block->setTarget( $newTarget ); @@ -1383,18 +1382,17 @@ class DatabaseBlockStore { return []; } - $type = $block->getType(); - if ( $type !== AbstractBlock::TYPE_USER ) { + $target = $block->getTarget(); + if ( !( $target instanceof UserBlockTarget ) ) { // Autoblocks only apply to users return []; } $dbr = $this->getReplicaDB(); - $targetUser = $block->getTargetUserIdentity(); - $actor = $targetUser ? $this->actorStoreFactory + $actor = $this->actorStoreFactory ->getActorNormalization( $this->wikiId ) - ->findActorId( $targetUser, $dbr ) : null; + ->findActorId( $target->getUserIdentity(), $dbr ); if ( !$actor ) { $this->logger->debug( 'No actor found to retroactively autoblock' ); @@ -1435,20 +1433,19 @@ class DatabaseBlockStore { } $parentBlock->assertWiki( $this->wikiId ); - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $autoblockIP ); - if ( $type != Block::TYPE_IP ) { - $this->logger->debug( "Autoblock not supported for ip ranges." ); + if ( !IPUtils::isValid( $autoblockIP ) ) { + $this->logger->debug( "Invalid autoblock IP" ); return false; } - $target = (string)$target; + $target = $this->blockTargetFactory->newAnonIpBlockTarget( $autoblockIP ); // Check if autoblock exempt. - if ( $this->autoblockExemptionList->isExempt( $target ) ) { + if ( $this->autoblockExemptionList->isExempt( $autoblockIP ) ) { return false; } // Allow hooks to cancel the autoblock. - if ( !$this->hookRunner->onAbortAutoblock( $target, $parentBlock ) ) { + if ( !$this->hookRunner->onAbortAutoblock( $autoblockIP, $parentBlock ) ) { $this->logger->debug( "Autoblock aborted by hook." ); return false; } @@ -1456,7 +1453,7 @@ class DatabaseBlockStore { // It's okay to autoblock. Go ahead and insert/update the block... // Do not add a *new* block if the IP is already blocked. - $blocks = $this->newLoad( $target, Block::TYPE_IP, false ); + $blocks = $this->newLoad( $target, false ); if ( $blocks ) { foreach ( $blocks as $ipblock ) { // Check if the block is an autoblock and would exceed the user block @@ -1480,7 +1477,7 @@ class DatabaseBlockStore { $expiry = $this->getAutoblockExpiry( $timestamp, $parentBlock->getExpiry() ); $autoblock = new DatabaseBlock( [ 'wiki' => $this->wikiId, - 'address' => UserIdentityValue::newAnonymous( $target, $this->wikiId ), + 'target' => $target, 'by' => $blocker, 'reason' => $this->getAutoblockReason( $parentBlock ), 'decodedTimestamp' => $timestamp, diff --git a/includes/block/DatabaseBlockStoreFactory.php b/includes/block/DatabaseBlockStoreFactory.php index bf77cda3d21e..915574c8355a 100644 --- a/includes/block/DatabaseBlockStoreFactory.php +++ b/includes/block/DatabaseBlockStoreFactory.php @@ -51,7 +51,7 @@ class DatabaseBlockStoreFactory { private ReadOnlyMode $readOnlyMode; private UserFactory $userFactory; private TempUserConfig $tempUserConfig; - private BlockUtilsFactory $blockUtilsFactory; + private CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory; private AutoblockExemptionList $autoblockExemptionList; /** @var DatabaseBlockStore[] */ @@ -68,7 +68,7 @@ class DatabaseBlockStoreFactory { ReadOnlyMode $readOnlyMode, UserFactory $userFactory, TempUserConfig $tempUserConfig, - BlockUtilsFactory $blockUtilsFactory, + CrossWikiBlockTargetFactory $crossWikiBlockTargetFactory, AutoblockExemptionList $autoblockExemptionList ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); @@ -83,7 +83,7 @@ class DatabaseBlockStoreFactory { $this->readOnlyMode = $readOnlyMode; $this->userFactory = $userFactory; $this->tempUserConfig = $tempUserConfig; - $this->blockUtilsFactory = $blockUtilsFactory; + $this->crossWikiBlockTargetFactory = $crossWikiBlockTargetFactory; $this->autoblockExemptionList = $autoblockExemptionList; } @@ -109,7 +109,7 @@ class DatabaseBlockStoreFactory { $this->readOnlyMode, $this->userFactory, $this->tempUserConfig, - $this->blockUtilsFactory->getBlockUtils( $wikiId ), + $this->crossWikiBlockTargetFactory->getFactory( $wikiId ), $this->autoblockExemptionList, $wikiId ); diff --git a/includes/block/RangeBlockTarget.php b/includes/block/RangeBlockTarget.php new file mode 100644 index 000000000000..db9da098c7fe --- /dev/null +++ b/includes/block/RangeBlockTarget.php @@ -0,0 +1,116 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use StatusValue; +use Wikimedia\IPUtils; + +/** + * A block target for an IP address range + * + * @since 1.44 + */ +class RangeBlockTarget extends BlockTarget implements BlockTargetWithIp { + private string $cidr; + + /** + * @var array The minimum prefix lengths indexed by protocol (IPv4 or IPv6) + */ + private array $limits; + + /** + * @param string $cidr The range specification in CIDR notation + * @param array $limits The minimum prefix lengths indexed by protocol (IPv4 or IPv6) + * @param string|false $wikiId The wiki ID + */ + public function __construct( string $cidr, array $limits, $wikiId ) { + parent::__construct( $wikiId ); + $this->cidr = $cidr; + $this->limits = $limits; + } + + public function toString(): string { + return $this->cidr; + } + + public function getType(): int { + return Block::TYPE_RANGE; + } + + public function getLogPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->cidr, $this->wikiId ); + } + + public function getSpecificity() { + // This is the number of bits that are allowed to vary in the block, give + // or take some floating point errors + [ $ip, $bits ] = explode( '/', $this->cidr, 2 ); + $max = IPUtils::isIPv6( $ip ) ? 128 : 32; + $size = $max - (int)$bits; + + // Rank a range block covering a single IP equally with a single-IP block + return 2 + ( $size / $max ); + } + + public function validateForCreation(): StatusValue { + $status = StatusValue::newGood(); + [ $ip, $prefixLength ] = explode( '/', $this->cidr, 2 ); + + if ( IPUtils::isIPv4( $ip ) ) { + $minLength = $this->limits['IPv4']; + $totalLength = 32; + } elseif ( IPUtils::isIPv6( $ip ) ) { + $minLength = $this->limits['IPv6']; + $totalLength = 128; + } else { + // The factory should not have called the constructor with an invalid range + throw new \RuntimeException( 'invalid IP range' ); + } + + if ( $minLength == $totalLength ) { + // Range block effectively disabled + $status->fatal( 'range_block_disabled' ); + } elseif ( $prefixLength > $totalLength ) { + // Such a range cannot exist + $status->fatal( 'ip_range_invalid' ); + } elseif ( $prefixLength < $minLength ) { + $status->fatal( 'ip_range_toolarge', $minLength ); + } + + return $status; + } + + public function toHexRange() { + $range = IPUtils::parseRange( $this->cidr ); + if ( count( $range ) !== 2 || !is_string( $range[0] ) || !is_string( $range[1] ) ) { + throw new \RuntimeException( + 'Failed to parse range: constructor caller should have validated it' ); + } + return $range; + } + + /** + * Get the start of the range in hexadecimal form. + * + * @return string + */ + public function getHexRangeStart(): string { + return $this->toHexRange()[0]; + } + + /** + * Get the end of the range in hexadecimal form. + * + * @return string + */ + public function getHexRangeEnd(): string { + return $this->toHexRange()[1]; + } + + protected function getLegacyUnion() { + return $this->cidr; + } + +} diff --git a/includes/block/UnblockUser.php b/includes/block/UnblockUser.php index 82d2a6561270..5cb275c7cc60 100644 --- a/includes/block/UnblockUser.php +++ b/includes/block/UnblockUser.php @@ -28,7 +28,6 @@ use MediaWiki\HookContainer\HookRunner; use MediaWiki\Message\Message; use MediaWiki\Permissions\Authority; use MediaWiki\Status\Status; -use MediaWiki\Title\TitleValue; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use RevisionDeleteUser; @@ -41,16 +40,13 @@ use RevisionDeleteUser; class UnblockUser { private BlockPermissionChecker $blockPermissionChecker; private DatabaseBlockStore $blockStore; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private UserFactory $userFactory; private HookRunner $hookRunner; - /** @var UserIdentity|string */ + /** @var BlockTarget|null */ private $target; - /** @var int */ - private $targetType; - private ?DatabaseBlock $block; private ?DatabaseBlock $blockToRemove = null; @@ -67,7 +63,7 @@ class UnblockUser { /** * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory * @param DatabaseBlockStore $blockStore - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory * @param UserFactory $userFactory * @param HookContainer $hookContainer * @param DatabaseBlock|null $blockToRemove @@ -79,7 +75,7 @@ class UnblockUser { public function __construct( BlockPermissionCheckerFactory $blockPermissionCheckerFactory, DatabaseBlockStore $blockStore, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, UserFactory $userFactory, HookContainer $hookContainer, ?DatabaseBlock $blockToRemove, @@ -90,25 +86,19 @@ class UnblockUser { ) { // Process dependencies $this->blockStore = $blockStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->userFactory = $userFactory; $this->hookRunner = new HookRunner( $hookContainer ); // Process params if ( $blockToRemove !== null ) { $this->blockToRemove = $blockToRemove; - $this->target = $blockToRemove->getTargetUserIdentity() - ?? $blockToRemove->getTargetName(); - $this->targetType = $blockToRemove->getType() ?? -1; + $this->target = $blockToRemove->getTarget(); + } elseif ( $target instanceof BlockTarget ) { + $this->target = $target; } else { - [ $this->target, $this->targetType ] = $this->blockUtils->parseBlockTarget( $target ); - if ( - $this->targetType === AbstractBlock::TYPE_AUTO && - is_numeric( $this->target ) - ) { - // Needed, because BlockUtils::parseBlockTarget will strip the # from autoblocks. - $this->target = '#' . $this->target; - } + // TODO: deprecate (T382106) + $this->target = $this->blockTargetFactory->newFromLegacyUnion( $target ); } $this->blockPermissionChecker = $blockPermissionCheckerFactory @@ -181,7 +171,7 @@ class UnblockUser { if ( $this->block->getType() === AbstractBlock::TYPE_RANGE && - $this->targetType === AbstractBlock::TYPE_IP + $this->target->getType() === AbstractBlock::TYPE_IP ) { return $status->fatal( 'ipb_blocked_as_range', $this->target, $this->block->getTargetName() ); } @@ -219,18 +209,10 @@ class UnblockUser { * Log the unblock to Special:Log/block */ private function log() { - // Redact IP for autoblocks - if ( $this->block->getType() === DatabaseBlock::TYPE_AUTO ) { - $page = TitleValue::tryNew( NS_USER, '#' . $this->block->getId() ); - } else { - $page = TitleValue::tryNew( NS_USER, $this->block->getTargetName() ); - } - + $page = $this->block->getRedactedTarget()->getLogPage(); $logEntry = new ManualLogEntry( 'block', 'unblock' ); - if ( $page !== null ) { - $logEntry->setTarget( $page ); - } + $logEntry->setTarget( $page ); $logEntry->setComment( $this->reason ); $logEntry->setPerformer( $this->performer->getUser() ); $logEntry->addTags( $this->tags ); diff --git a/includes/block/UnblockUserFactory.php b/includes/block/UnblockUserFactory.php index 520146adbd47..9c09c00baa28 100644 --- a/includes/block/UnblockUserFactory.php +++ b/includes/block/UnblockUserFactory.php @@ -29,7 +29,7 @@ use MediaWiki\User\UserIdentity; */ interface UnblockUserFactory { /** - * @param UserIdentity|string $target + * @param BlockTarget|UserIdentity|string $target * @param Authority $performer * @param string $reason * @param string[] $tags diff --git a/includes/block/UserBlockCommandFactory.php b/includes/block/UserBlockCommandFactory.php index 9981ee661300..64e4d80009be 100644 --- a/includes/block/UserBlockCommandFactory.php +++ b/includes/block/UserBlockCommandFactory.php @@ -32,7 +32,7 @@ use Psr\Log\LoggerInterface; class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { private BlockPermissionCheckerFactory $blockPermissionCheckerFactory; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private HookContainer $hookContainer; private BlockRestrictionStore $blockRestrictionStore; private ServiceOptions $options; @@ -52,7 +52,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { ServiceOptions $options, HookContainer $hookContainer, BlockPermissionCheckerFactory $blockPermissionCheckerFactory, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, DatabaseBlockStore $blockStore, BlockRestrictionStore $blockRestrictionStore, UserFactory $userFactory, @@ -66,7 +66,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { $this->options = $options; $this->hookContainer = $hookContainer; $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->blockStore = $blockStore; $this->blockRestrictionStore = $blockRestrictionStore; $this->userFactory = $userFactory; @@ -79,7 +79,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { /** * Create BlockUser * - * @param string|UserIdentity $target Target of the block + * @param BlockTarget|string|UserIdentity $target Target of the block * @param Authority $performer Performer of the block * @param string $expiry Expiry of the block (timestamp or 'infinity') * @param string $reason Reason of the block @@ -102,7 +102,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { $this->options, $this->blockRestrictionStore, $this->blockPermissionCheckerFactory, - $this->blockUtils, + $this->blockTargetFactory, $this->blockActionInfo, $this->hookContainer, $this->blockStore, @@ -148,7 +148,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { $this->options, $this->blockRestrictionStore, $this->blockPermissionCheckerFactory, - $this->blockUtils, + $this->blockTargetFactory, $this->blockActionInfo, $this->hookContainer, $this->blockStore, @@ -172,7 +172,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { * * @since 1.44 * - * @param UserIdentity|string $target + * @param BlockTarget|UserIdentity|string $target * @param Authority $performer * @param string $reason * @param string[] $tags @@ -188,7 +188,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { return new UnblockUser( $this->blockPermissionCheckerFactory, $this->blockStore, - $this->blockUtils, + $this->blockTargetFactory, $this->userFactory, $this->hookContainer, null, @@ -218,7 +218,7 @@ class UserBlockCommandFactory implements BlockUserFactory, UnblockUserFactory { return new UnblockUser( $this->blockPermissionCheckerFactory, $this->blockStore, - $this->blockUtils, + $this->blockTargetFactory, $this->userFactory, $this->hookContainer, $block, diff --git a/includes/block/UserBlockTarget.php b/includes/block/UserBlockTarget.php new file mode 100644 index 000000000000..9fb78503b4ad --- /dev/null +++ b/includes/block/UserBlockTarget.php @@ -0,0 +1,60 @@ +<?php + +namespace MediaWiki\Block; + +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use MediaWiki\User\UserIdentity; +use StatusValue; + +/** + * A block target for a registered user + * + * @since 1.44 + */ +class UserBlockTarget extends BlockTarget implements BlockTargetWithUserPage { + private UserIdentity $userIdentity; + + public function __construct( UserIdentity $userIdentity ) { + parent::__construct( $userIdentity->getWikiId() ); + $this->userIdentity = $userIdentity; + } + + public function toString(): string { + return $this->userIdentity->getName(); + } + + public function getType(): int { + return Block::TYPE_USER; + } + + public function getSpecificity() { + return 1; + } + + public function getLogPage(): PageReference { + return $this->getUserPage(); + } + + public function getUserPage(): PageReference { + return new PageReferenceValue( NS_USER, $this->userIdentity->getName(), $this->wikiId ); + } + + public function getUserIdentity(): UserIdentity { + return $this->userIdentity; + } + + public function validateForCreation(): StatusValue { + if ( !$this->userIdentity->isRegistered() ) { + return StatusValue::newFatal( + 'nosuchusershort', + wfEscapeWikiText( $this->userIdentity->getName() ) + ); + } + return StatusValue::newGood(); + } + + protected function getLegacyUnion() { + return $this->getUserIdentity(); + } +} diff --git a/includes/config-schema.php b/includes/config-schema.php index cad9703297f9..076e345547e0 100644 --- a/includes/config-schema.php +++ b/includes/config-schema.php @@ -647,6 +647,7 @@ return [ 'src' => null, 'url' => 'https://www.mediawiki.org/', 'alt' => 'Powered by MediaWiki', + 'lang' => 'en', ], ], ], @@ -844,6 +845,7 @@ return [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'UserFactory', + 'ConnectionProvider', ], ], ], diff --git a/includes/editpage/Constraint/BrokenRedirectConstraint.php b/includes/editpage/Constraint/BrokenRedirectConstraint.php index 9c8db7c498b6..643d35d121df 100644 --- a/includes/editpage/Constraint/BrokenRedirectConstraint.php +++ b/includes/editpage/Constraint/BrokenRedirectConstraint.php @@ -23,6 +23,7 @@ namespace MediaWiki\EditPage\Constraint; use MediaWiki\Content\Content; use MediaWiki\Linker\LinkTarget; use StatusValue; +use Wikimedia\Message\MessageValue; /** * Verify the page does not redirect to an unknown page unless @@ -38,6 +39,7 @@ class BrokenRedirectConstraint implements IEditConstraint { private Content $newContent; private Content $originalContent; private LinkTarget $title; + private string $submitButtonLabel; private string $result; /** @@ -50,12 +52,14 @@ class BrokenRedirectConstraint implements IEditConstraint { bool $allowBrokenRedirects, Content $newContent, Content $originalContent, - LinkTarget $title + LinkTarget $title, + string $submitButtonLabel ) { $this->allowBrokenRedirects = $allowBrokenRedirects; $this->newContent = $newContent; $this->originalContent = $originalContent; $this->title = $title; + $this->submitButtonLabel = $submitButtonLabel; } public function checkConstraint(): string { @@ -83,8 +87,10 @@ class BrokenRedirectConstraint implements IEditConstraint { public function getLegacyStatus(): StatusValue { $statusValue = StatusValue::newGood(); + if ( $this->result === self::CONSTRAINT_FAILED ) { - $statusValue->fatal( 'edit-constraint-brokenredirect' ); + $statusValue->fatal( MessageValue::new( 'edit-constraint-brokenredirect', + [ MessageValue::new( $this->submitButtonLabel ) ] ) ); $statusValue->value = self::AS_BROKEN_REDIRECT; } diff --git a/includes/editpage/Constraint/PageSizeConstraint.php b/includes/editpage/Constraint/PageSizeConstraint.php index 0dbedfd0ef5e..b2aa4b7c92ec 100644 --- a/includes/editpage/Constraint/PageSizeConstraint.php +++ b/includes/editpage/Constraint/PageSizeConstraint.php @@ -22,6 +22,7 @@ namespace MediaWiki\EditPage\Constraint; use InvalidArgumentException; use StatusValue; +use Wikimedia\Message\MessageValue; /** * Verify the page isn't larger than the maximum @@ -48,7 +49,7 @@ class PageSizeConstraint implements IEditConstraint { private string $type; /** - * @param int $maxSize In kibibytes, from $wgMaxArticleSize + * @param int $maxSize In kilobytes, from $wgMaxArticleSize * @param int $contentSize * @param string $type */ @@ -57,7 +58,7 @@ class PageSizeConstraint implements IEditConstraint { int $contentSize, string $type ) { - $this->maxSize = $maxSize * 1024; // Convert from kibibytes + $this->maxSize = $maxSize * 1024; // Convert from kilobytes $this->contentSize = $contentSize; if ( $type === self::BEFORE_MERGE ) { @@ -83,6 +84,9 @@ class PageSizeConstraint implements IEditConstraint { // Either self::AS_CONTENT_TOO_BIG, if it was too big before merging, // or self::AS_MAX_ARTICLE_SIZE_EXCEEDED, if it was too big after merging $statusValue->setResult( false, $this->errorCode ); + $statusValue->fatal( MessageValue::new( 'longpageerror' ) + ->numParams( round( $this->contentSize / 1024, 3 ), $this->maxSize / 1024 ) + ); } return $statusValue; } diff --git a/includes/editpage/EditPage.php b/includes/editpage/EditPage.php index 4241952d0220..df89d88be439 100644 --- a/includes/editpage/EditPage.php +++ b/includes/editpage/EditPage.php @@ -241,9 +241,6 @@ class EditPage implements IEditObject { private $incompleteForm = false; /** @var bool */ - private $tooBig = false; - - /** @var bool */ private $missingComment = false; /** @var bool */ @@ -1866,17 +1863,16 @@ class EditPage implements IEditObject { $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' ); switch ( $statusValue ) { + // Status codes for which the error/warning message is generated somewhere else in this class. + // They should be refactored to provide their own messages and handled below (T384399). case self::AS_HOOK_ERROR_EXPECTED: - case self::AS_CONTENT_TOO_BIG: case self::AS_ARTICLE_WAS_DELETED: case self::AS_CONFLICT_DETECTED: case self::AS_SUMMARY_NEEDED: case self::AS_TEXTBOX_EMPTY: - case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: case self::AS_END: case self::AS_BLANK_ARTICLE: case self::AS_SELF_REDIRECT: - case self::AS_BROKEN_REDIRECT: case self::AS_DOUBLE_REDIRECT: case self::AS_REVISION_WAS_DELETED: return true; @@ -1884,9 +1880,14 @@ class EditPage implements IEditObject { case self::AS_HOOK_ERROR: return false; + // Status codes that provide their own error/warning messages. Most error scenarios that don't + // need custom user interface (e.g. edit conflicts) should be handled here, one day (T384399). + case self::AS_BROKEN_REDIRECT: + case self::AS_CONTENT_TOO_BIG: + case self::AS_MAX_ARTICLE_SIZE_EXCEEDED: case self::AS_PARSE_ERROR: - case self::AS_UNICODE_NOT_SUPPORTED: case self::AS_UNABLE_TO_ACQUIRE_TEMP_ACCOUNT: + case self::AS_UNICODE_NOT_SUPPORTED: foreach ( $status->getMessages() as $msg ) { $out->addHTML( Html::errorBox( $this->context->msg( $msg )->parse() @@ -2496,6 +2497,9 @@ class EditPage implements IEditObject { // Check for length errors again now that the section is merged in $this->contentLength = strlen( $this->toEditText( $content ) ); + // Message key of the label of the submit button - used by some constraint error messages + $submitButtonLabel = $this->getSubmitButtonLabel(); + // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658) // Create a new runner to avoid rechecking the prior constraints, use the same factory $constraintRunner = new EditConstraintRunner(); @@ -2512,7 +2516,8 @@ class EditPage implements IEditObject { $this->allowBrokenRedirects, $content, $this->getCurrentContent(), - $this->getTitle() + $this->getTitle(), + $submitButtonLabel ) ); $constraintRunner->addConstraint( @@ -2618,10 +2623,7 @@ class EditPage implements IEditObject { * result from the backend. */ private function handleFailedConstraint( IEditConstraint $failed ): void { - if ( $failed instanceof PageSizeConstraint ) { - // Error will be displayed by showEditForm() - $this->tooBig = true; - } elseif ( $failed instanceof UserBlockConstraint ) { + if ( $failed instanceof UserBlockConstraint ) { // Auto-block user's IP if the account was "hard" blocked if ( !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { $this->context->getUser()->spreadAnyEditBlock(); @@ -3403,13 +3405,6 @@ class EditPage implements IEditObject { ); } - if ( $this->brokenRedirect ) { - $out->wrapWikiMsg( - "<div id='mw-brokenredirect'>\n$1\n</div>", - [ 'edit-constraint-brokenredirect', $buttonLabel ] - ); - } - if ( $this->doubleRedirect ) { $editContent = $this->toEditContent( $this->textbox1 ); $redirectTarget = $editContent->getRedirectTarget(); @@ -4664,22 +4659,13 @@ class EditPage implements IEditObject { } $out = $this->context->getOutput(); - $maxArticleSize = $this->context->getConfig()->get( MainConfigNames::MaxArticleSize ); - if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) { - $out->addHTML( "<div id='mw-edit-longpageerror'>" . Html::errorBox( - $this->context->msg( 'longpageerror' ) - ->numParams( round( $this->contentLength / 1024, 3 ), $maxArticleSize ) - ->parse() - ) . "</div>" ); - } else { - $longPageHint = $this->context->msg( 'longpage-hint' ); - if ( !$longPageHint->isDisabled() ) { - $msgText = trim( $longPageHint->sizeParams( $this->contentLength ) - ->params( $this->contentLength ) // Keep this unformatted for math inside message - ->parse() ); - if ( $msgText !== '' && $msgText !== '-' ) { - $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" ); - } + $longPageHint = $this->context->msg( 'longpage-hint' ); + if ( !$longPageHint->isDisabled() ) { + $msgText = trim( $longPageHint->sizeParams( $this->contentLength ) + ->params( $this->contentLength ) // Keep this unformatted for math inside message + ->parse() ); + if ( $msgText !== '' && $msgText !== '-' ) { + $out->addHTML( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" ); } } } diff --git a/includes/editpage/IntroMessageBuilder.php b/includes/editpage/IntroMessageBuilder.php index 37c5ef054c0f..e28fbdbae700 100644 --- a/includes/editpage/IntroMessageBuilder.php +++ b/includes/editpage/IntroMessageBuilder.php @@ -323,7 +323,6 @@ class IntroMessageBuilder { $validation = UserRigorOptions::RIGOR_NONE; $user = $this->userFactory->newFromName( $username, $validation ); $ip = $this->userNameUtils->isIP( $username ); - $block = $this->blockStore->newFromTarget( $user, $user ); $userExists = ( $user && $user->isRegistered() ); if ( $userExists && $user->isHidden() && !$performer->isAllowed( 'hideuser' ) ) { @@ -341,33 +340,42 @@ class IntroMessageBuilder { 'mw-userpage-userdoesnotexist' ) ); - } elseif ( - $block !== null && - $block->getType() !== Block::TYPE_AUTO && - ( - $block->isSitewide() || - $this->permManager->isBlockedFrom( - // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive - $user, - $title, - true - ) - ) - ) { - // Show log extract if the user is sitewide blocked or is partially - // blocked and not allowed to edit their user page or user talk page + return; + } + + // TODO: factor out nearly identical code in Article::showMissingArticle + $numBlocks = 0; + $appliesToTitle = false; + $logTargetPage = ''; + foreach ( $this->blockStore->newListFromTarget( $user, $user ) as $block ) { + if ( $block->getType() !== Block::TYPE_AUTO ) { + $numBlocks++; + if ( $block->appliesToTitle( $title ) ) { + $appliesToTitle = true; + } + $logTargetPage = $this->namespaceInfo->getCanonicalName( NS_USER ) . + ':' . $block->getTargetName(); + } + } + + // Show log extract if the user is sitewide blocked or is partially + // blocked and not allowed to edit their user page or user talk page + if ( $numBlocks && $appliesToTitle ) { + $msgKey = $numBlocks === 1 + ? 'blocked-notice-logextract' : 'blocked-notice-logextract-multi'; $messages->addWithKey( 'blocked-notice-logextract', $this->getLogExtract( 'block', - $this->namespaceInfo->getCanonicalName( NS_USER ) . ':' . $block->getTargetName(), + $logTargetPage, '', [ 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => [ - 'blocked-notice-logextract', - $user->getName() # Support GENDER in notice + $msgKey, + $user->getName(), # Support GENDER in notice + $numBlocks ], ] ) diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 53de993b3a8b..2934e89376be 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -2108,7 +2108,7 @@ class LocalFile extends File { $nullRevRecord = $wikiPage->newPageUpdater( $performer->getUser() ) ->setCause( PageUpdater::CAUSE_UPLOAD ) - ->saveDummyRevision( $editSummary ); + ->saveDummyRevision( $editSummary, EDIT_SILENT ); // Associate null revision id $logEntry->setAssociatedRevId( $nullRevRecord->getId() ); diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index f404e4709702..366457b8bb2f 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -47,6 +47,7 @@ use MediaWiki\HTMLForm\Field\HTMLInfoField; use MediaWiki\HTMLForm\Field\HTMLIntField; use MediaWiki\HTMLForm\Field\HTMLMultiSelectField; use MediaWiki\HTMLForm\Field\HTMLNamespacesMultiselectField; +use MediaWiki\HTMLForm\Field\HTMLOrderedMultiselectField; use MediaWiki\HTMLForm\Field\HTMLRadioField; use MediaWiki\HTMLForm\Field\HTMLSelectAndOtherField; use MediaWiki\HTMLForm\Field\HTMLSelectField; @@ -253,6 +254,7 @@ class HTMLForm extends ContextSource { 'title' => HTMLTitleTextField::class, 'user' => HTMLUserTextField::class, 'tagmultiselect' => HTMLTagMultiselectField::class, + 'orderedmultiselect' => HTMLOrderedMultiselectField::class, 'usersmultiselect' => HTMLUsersMultiselectField::class, 'titlesmultiselect' => HTMLTitlesMultiselectField::class, 'namespacesmultiselect' => HTMLNamespacesMultiselectField::class, diff --git a/includes/htmlform/fields/HTMLOrderedMultiselectField.php b/includes/htmlform/fields/HTMLOrderedMultiselectField.php new file mode 100644 index 000000000000..13348f1c8180 --- /dev/null +++ b/includes/htmlform/fields/HTMLOrderedMultiselectField.php @@ -0,0 +1,59 @@ +<?php + +namespace MediaWiki\HTMLForm\Field; + +use MediaWiki\Html\Html; +use MediaWiki\Widget\OrderedMultiselectWidget; + +/** + * Implements a tag multiselect input field with a searchable dropdown containing valid tags. + * + * Besides the parameters recognized by HTMLTagMultiselectField, additional recognized + * parameters are: + * options - array, the list of allowed values. + * + * The result is a newline-delimited string of selected tags. + * + * @note This widget is not likely to remain functional in non-OOUI forms. + */ +class HTMLOrderedMultiselectField extends HTMLTagMultiselectField { + + protected function getInputWidget( $params ) { + $widget = new OrderedMultiselectWidget( $params + [ + 'options' => $this->getOptionsOOUI(), + ] ); + $widget->setAttributes( [ 'data-mw-modules' => implode( ',', $this->getOOUIModules() ) ] ); + return $widget; + } + + public function validate( $value, $alldata ) { + $this->mParams['allowedValues'] = self::flattenOptions( $this->getOptions() ); + return parent::validate( $value, $alldata ); + } + + public function getOptionsOOUI() { + $optionsOouiSections = []; + $options = $this->getOptions(); + + foreach ( $options as $label => $section ) { + if ( is_array( $section ) ) { + $optionsOouiSections[ $label ] = Html::listDropdownOptionsOoui( $section ); + unset( $options[$label] ); + } + } + + // If anything remains in the array, they are sectionless options. Put them at the beginning. + if ( $options ) { + $optionsOouiSections = array_merge( + [ '' => Html::listDropdownOptionsOoui( $options ) ], + $optionsOouiSections + ); + } + + return $optionsOouiSections; + } + + public function getOOUIModules() { + return [ 'mediawiki.widgets.OrderedMultiselectWidget' ]; + } +} diff --git a/includes/import/ImportableOldRevisionImporter.php b/includes/import/ImportableOldRevisionImporter.php index e78c331da46f..2905aa576565 100644 --- a/includes/import/ImportableOldRevisionImporter.php +++ b/includes/import/ImportableOldRevisionImporter.php @@ -197,7 +197,7 @@ class ImportableOldRevisionImporter implements OldRevisionImporter { $options = [ PageUpdatedEvent::FLAG_SILENT => true, - PageUpdatedEvent::FLAG_AUTOMATED => true, + PageUpdatedEvent::FLAG_IMPLICIT => true, 'created' => $mustCreatePage, 'oldcountable' => 'no-change', ]; diff --git a/includes/installer/i18n/br.json b/includes/installer/i18n/br.json index 8efda3569f13..11294d4071cb 100644 --- a/includes/installer/i18n/br.json +++ b/includes/installer/i18n/br.json @@ -46,9 +46,9 @@ "config-page-existingwiki": "Wiki zo anezhañ dija", "config-help-restart": "Ha c'hoant hoc'h eus da ziverkañ an holl roadennoù hoc'h eus ebarzhet ha da adlañsañ an argerzh staliañ ?", "config-restart": "Ya, adloc'hañ anezhañ", - "config-welcome": "=== Gwiriadennoù a denn d'an endro ===\nRekis eo un nebeud gwiriadennoù diazez da welet hag azas eo an endro evit gallout staliañ MediaWiki.\nHo pet soñj merkañ disoc'hoù ar gwiriadennoù-se m'ho pez ezhomm skoazell e-pad ar staliadenn.", + "config-welcome": "MediaWiki zo ur meziant wiki digoust ha digor skrivet e PHP. Gantañ e ra savenn Wikipedia hag ar raktresoù Wikimedia all, implijet eo gant kantadoù a vilionoù a dud bep miz. Troet eo MediaWiki e ouzhpenn 350 yezh hag a-drugarez d’e arc'hwelioù solut ez eus dezhañ ur gumuniezh vras ha bev a implijerien ha diorroerien.\n=== Gwiriadennoù a denn d'an endro ===\nRekis eo un nebeud gwiriadennoù diazez da welet hag azas eo an endro evit gallout staliañ MediaWiki.\nHo pet soñj merkañ disoc'hoù ar gwiriadennoù-se m'ho pez ezhomm skoazell e-pad ar staliadenn.", "config-welcome-section-copyright": "=== Gwiriañ aozer ha Termenoù implijout ===\n\n$1\n\nUr meziant frank eo ar programm-mañ; gallout a rit skignañ anezhañ ha/pe kemmañ anezhañ dindan termenoù ar GNU Aotre-implijout Foran Hollek evel m'emañ embannet gant Diazezadur ar Meziantoù Frank; pe diouzh stumm 2 an aotre-implijout, pe (evel mar karit) diouzh ne vern pe stumm nevesoc'h.\n\nIngalet eo ar programm gant ar spi e vo talvoudus met n'eus '''tamm gwarant ebet'''; hep zoken gwarant empleg ar '''varc'hadusted''' pe an '''azaster ouzh ur pal bennak'''. Gwelet ar GNU Aotre-Implijout Foran Hollek evit muioc'h a ditouroù.\n\nSañset oc'h bezañ resevet [$2 un eilskrid eus ar GNU Aotre-implijout Foran Hollek] a-gevret gant ar programm-mañ; ma n'hoc'h eus ket, skrivit da Diazezadur ar Meziantoù Frank/Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, SUA pe [https://www.gnu.org/licenses/old-licenses/gpl-2.0.html lennit anezhañ enlinenn].", - "config-sidebar": "* [https://www.mediawiki.org MediaWiki degemer]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Sturlevr an implijerien]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Sturlevr ar verourien]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAG]\n----\n* <doclink href=Readme>Lennit-me</doclink>\n* <doclink href=ReleaseNotes>Notennoù embann</doclink>\n* <doclink href=Copying>Oc'h eilañ</doclink>\n* <doclink href=UpgradeDoc>O hizivaat</doclink>", + "config-sidebar": "* [https://www.mediawiki.org Degemer MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Sturlevr an implijerien]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Sturlevr ar verourien]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAG]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Communication Goulenn sikour]\n* [https://phabricator.wikimedia.org/ Roudenner drein]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/How_to_contribute Kemer perzh]", "config-env-good": "Gwiriet eo bet an endro.\nGallout a rit staliañ MediaWiki.", "config-env-bad": "Gwiriet eo bet an endro.\nNe c'hallit ket staliañ MediaWiki.", "config-env-php": "Staliet eo PHP $1.", @@ -59,11 +59,11 @@ "config-memory-bad": "'''Diwallit :''' Da $1 emañ arventenn <code>memory_limit</code> PHP.\nRe izel eo moarvat.\nMarteze e c'hwito ar staliadenn !", "config-apc": "Staliet eo [https://www.php.net/apc APC]", "config-apcu": "Staliet eo [https://www.php.net/apcu APCu]", - "config-no-cache-apcu": "<strong>Taolit pled :</strong> N'eus ket bet gallet kavout [https://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] pe [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nN'eo ket gweredekaet ar c'hrubuilhañ traezoù.", + "config-no-cache-apcu": "<strong>Taolit pled :</strong>[https://www.php.net/apcu APCu] n'eo ket bet kavet.\nN'eo ket gweredekaet ar c'hrubuilhañ traezoù.", "config-mod-security": "<strong>Taolit pled :</strong> Gweredekaet eo [https://modsecurity.org/ mod_security]/mod_security2 gant ho servijer web. Ma n'eo ket kfluniet mat e c'hall tegas trubuilhoù da MediaWiki ha meziantoù all a aotre implijerien da ouzhpennañ danvez evel ma karont.\nE kement ha m'eo posupl e tlefe bezañ diweredekaet. A-hend-all, sellit ouzh [https://modsecurity.org/documentation/ mod_security an teuliadur] pe kit e darempred gant skoazell ho herberc'hier m'en em gavit gant fazioù dargouezhek.", - "config-diff3-bad": "N'eo ket bet kavet GNU diff3.", + "config-diff3-bad": "N'eo ket bet kavet ar benveg keñveriañ testennoù GNU diff3. Gallout a rit lezel an dra-se a-gostez evit ar mare, met gallout a rafec’h en em gavout gant tabutoù kemmañ aliesoc'h.", "config-git": "Kavet eo bet ar meziant kontrolliñ adstummoù Git : <code>$1</code>.", - "config-git-bad": "N'eo ket bet kavet ar meziant kontrolliñ stummoù Git.", + "config-git-bad": "N'eo ket bet kavet ar meziant kontrolliñ stummoù Git. Gallout a rit lezel an dra-se a-gostez evit ar mare. Notit Special:Version ne ziskouezo ket hachoù ar c'hemmoù.", "config-imagemagick": "ImageMagick kavet : <code>$1</code>.\nGweredekaet e vo ar bihanaat skeudennoù ma vez gweredekaet ganeoc'h ar pellgargañ restroù.", "config-gd": "Kavet eo bet al levraoueg c'hrafek GD enframmet.\nGweredekaet e vo ar bihanaat skeudennoù ma vez gweredekaet an enporzhiañ restroù.", "config-no-scaling": "N'eus ket bet gallet kavout al levraoueg GD pe ImageMagick.\nDiweredekaet e vo ar bihanaat skeudennoù.", @@ -75,9 +75,9 @@ "config-no-cli-uploads-check": "<strong>Diwallit :</strong> N'eo ket bet gwiriet ho kavlec'h enporzhiañ dre ziouer (<code>$1</code>) e-keñver breskted erounezadur skriptoù tidek e-pad staliadur CLI.", "config-db-type": "Doare an diaz roadennoù :", "config-db-host": "Anv implijer an diaz roadennoù :", - "config-db-host-help": "M'emañ ho servijer roadennoù war ur servijer disheñvel, merkit amañ anv an ostiz pe ar chomlec'h IP.\n\nMa rit gant un herberc'hiañ kenrannet, e tlefe ho herberc'hier bezañ pourchaset deoc'h an anv ostiz reizh en teulioù titouriñ.\n\nM'emaoc'h o staliañ ur servijer Windows ha ma rit gant MySQL, marteze ne'z aio ket en-dro \"localhost\" evel anv servijer. Ma ne dro ket, klaskit ober gant \"127.0.0.1\" da chomlec'h IP lechel.", + "config-db-host-help": "M'emañ ho servijer roadennoù war ur servijer all, merkit amañ anv an ostiz pe ar chomlec'h IP.\n\nMa rit gant un herberc'hiañ kenrannet, e tlefe ho herberc'hier bezañ pourchaset deoc'h an anv ostiz reizh en teulioù titouriñ.\n\nMa rit gant MySQL, marteze ne'z aio ket en-dro \"localhost\" evel anv servijer. Ma ne dro ket, klaskit ober gant \"127.0.0.1\" da chomlec'h IP lechel.\n\nMa rit gant PostgreSQL, lezit ar vaezienn-mañ goullo evit kennaskañ dre ul lugell Unix.", "config-db-wiki-settings": "Anavezout ar wiki-mañ", - "config-db-name": "Anv an diaz roadennoù :", + "config-db-name": "Anv an diaz roadennoù (tired ebet) :", "config-db-name-help": "Dibabit un anv evit ho wiki.\nNa lakait ket a esaouennoù ennañ.\n\nMa ri gant un herberc'hiañ kenrannet e vo pourchaset deoc'h un anv diaz roadennoù dibar da vezañ graet gantañ gant ho herberc'hier pe e lezo ac'hanoc'h da grouiñ diazoù roadennoù dre ur banell gontrolliñ.", "config-db-install-account": "Kont implijer evit ar staliadur", "config-db-username": "Anv implijer an diaz roadennoù :", @@ -88,11 +88,11 @@ "config-db-account-lock": "Implijout ar memes anv implijer ha ger-tremen e-kerzh oberiadurioù boutin", "config-db-wiki-account": "Kont implijer evit oberiadurioù boutin", "config-db-wiki-help": "Merkañ an anv-implijer hag ar ger-tremen a vo implijet evit kevreañ ouzh an diaz roadennoù e-pad oberiadurioù normal ar wiki.\nMa n'eus ket eus ar gont ha ma'z eus gwirioù a-walc'h gant ar gont staliañ, e vo krouet ar gont implijer-mañ gant al live gwirioù rekis izelañ evit gallout lakaat ar wiki da vont en-dro.", - "config-db-prefix": "Rakrann taolennoù an diaz roadennoù :", + "config-db-prefix": "Rakrann taolennoù an diaz roadennoù (tired ebet) :", "config-db-prefix-help": "Mard eo ret deoc'h rannañ un diaz roadennoù gant meur a wiki, pe etre MediaWiki hag un arload benak all e c'hallit dibab ouzhpennañ ur rakger da holl anvioù an taolennoù kuit na vije tabutoù.\nArabat ober gant esaouennoù.\n\nPeurliesañ e vez laosket goullo ar vaezienn-mañ.", "config-mysql-old": "Rekis eo MySQL $1 pe ur stumm nevesoc'h; ober a rit gant $2.", "config-db-port": "Porzh an diaz roadennoù :", - "config-db-schema": "Brastres evit MediaWiki", + "config-db-schema": "Brastres evit MediaWiki (tired ebet)ː", "config-db-schema-help": "Peurliesañ e vo digudenn ar chema-mañ.\nArabat cheñch anezho ma n'hoc'h eus ket ezhomm d'en ober.", "config-pg-test-error": "N'haller ket kevreañ ouzh an diaz-titouroù '''$1''' : $2", "config-sqlite-dir": "Kavlec'h roadennoù SQLite :", @@ -199,7 +199,7 @@ "config-instantcommons": "Gweredekaat ''InstantCommons''", "config-advanced-settings": "Kefluniadur araokaet", "config-cache-options": "Arventennoù evit krubuilhañ traezoù :", - "config-cache-accel": "Krubuilhañ traezoù PHP (APC, APCu, XCache pe WinCache)", + "config-cache-accel": "Krubuilhañ traezoù PHP (APC pe APCu)", "config-cache-memcached": "Implijout Memcached (en deus ezhomm bezañ staliet ha kefluniet)", "config-memcached-servers": "Servijerioù Memcached :", "config-memcached-help": "Roll ar chomlec'hioù IP da implijout evit Memcached.\nRet eo spisaat unan dre linenn ha spisaat ar porzh da vezañ implijet. Da skouer :\n127.0.0.1:11211\n192.168.1.25:1234", @@ -233,7 +233,7 @@ "config-install-user-grant-failed": "N'eus ket bet gallet reiñ an aotre d'an implijer \"$1\" : $2", "config-install-user-missing": "N'eus ket eus an implijer \"$1\"", "config-install-user-missing-create": "N'eus ket eus an implijer \"$1\".\nMa fell deoc'h krouiñ anezhañ, klikit war ar voest \"krouiñ ur gont\" amañ dindan.", - "config-install-tables": "O krouiñ taolennoù, pazenn gentañ", + "config-install-tables": "O krouiñ taolennoù", "config-install-tables-exist": "<strong>Diwallit :</strong> An taolennoù MediaWiki zo anezho dija war a seblant.\nN'int ket bet adkrouet.", "config-install-tables-failed": "'''Fazi :''' c'hwitet eo krouidigezh an daolenn gant ar fazi-mañ : $1", "config-install-interwiki": "O leuniañ dre ziouer an daolenn etrewiki", diff --git a/includes/installer/i18n/fi.json b/includes/installer/i18n/fi.json index 6eeb90b4e8b9..f473a6cb39b4 100644 --- a/includes/installer/i18n/fi.json +++ b/includes/installer/i18n/fi.json @@ -276,7 +276,7 @@ "config-install-user-missing-create": "Määriteltyä käyttäjää \"$1\" ei ole olemassa.\nValitse alapuolelta \"lisää tili\" jos haluat että se luodaan.", "config-install-logo-blank": "Anna kelvollinen logon URL.", "config-install-restore-services": "Palautetaan mediawiki-palveluja", - "config-install-tables": "Luodaan tauluja, vaihe yksi", + "config-install-tables": "Luodaan tauluja", "config-install-tables-exist": "<strong>Varoitus:</strong> MediaWiki taulut ovat jo olemassa.\nOhitetaan taulujen luonti.", "config-install-tables-failed": "<strong>Virhe:</strong> Taulujen luominen epäonnistui seuraavaan virheen takia: $1", "config-install-interwiki": "Luodaan oletustaulua interwikille", diff --git a/includes/installer/i18n/ie.json b/includes/installer/i18n/ie.json new file mode 100644 index 000000000000..bf870f729a2e --- /dev/null +++ b/includes/installer/i18n/ie.json @@ -0,0 +1,53 @@ +{ + "@metadata": { + "authors": [ + "Lisyeng", + "Renan" + ] + }, + "config-desc": "Li installator del MediaWiki", + "config-title": "Installation $1", + "config-information": "Information", + "config-your-language": "Tui lingue:", + "config-wiki-language": "Lingue del wiki:", + "config-back": "← Retornar", + "config-continue": "Continuar →", + "config-page-language": "Lingue", + "config-page-welcome": "Benevenit al MediaWiki!", + "config-page-name": "Nómine", + "config-page-options": "Optiones", + "config-page-install": "Installar", + "config-page-existingwiki": "Existent wiki", + "config-sidebar-upgrade": "Actualisant", + "config-env-php": "PHP $1 es installat.", + "config-apc": "[https://www.php.net/apc APC] es installat", + "config-apcu": "[https://www.php.net/apcu APCu] es installat", + "config-db-wiki-settings": "Identificar ti ci wiki", + "config-regenerate": "Regenerar LocalSettings.php →", + "config-site-name": "Nómine del wiki:", + "config-ns-generic": "Projecte", + "config-ns-site-name": "Identic ao nómine del wiki: $1", + "config-ns-other": "Altri (specificar)", + "config-ns-other-default": "MiWiki", + "config-admin-box": "Conto de administrator", + "config-admin-name": "Tui nómine de usator:", + "config-admin-password": "Parol-passe:", + "config-admin-password-confirm": "Parol-passe denov:", + "config-admin-email": "Adresse postal electronic:", + "config-profile-private": "Privat wiki", + "config-license": "Jure editorial e autorisation", + "config-logo-preview-main": "Principal págine", + "config-extensions": "Extensiones", + "config-install-database": "Fixant base de data", + "config-install-schema": "Creant schema", + "config-install-user": "Creant usator del base de data", + "config-install-user-alreadyexists": "Usator \"$1\" ja existe", + "config-install-tables": "Creant tabelles, passu un", + "config-install-interwiki": "Populant li tabelle uniform de interwiki", + "config-install-stats": "Inicialant statisticas", + "config-install-sysop": "Creant conto de usator administrator", + "config-install-mainpage": "Creant principal págine che contenete uniform", + "config-install-mainpage-failed": "Ne posset inserter principal págine: $1", + "config-help": "auxilie", + "mainpagetext": "<strong>MediaWiki ha esset installat.</strong>" +} diff --git a/includes/installer/i18n/ja.json b/includes/installer/i18n/ja.json index b97126e107f0..149ec2b891f6 100644 --- a/includes/installer/i18n/ja.json +++ b/includes/installer/i18n/ja.json @@ -29,6 +29,7 @@ "W.CC", "Waki285", "Whym", + "Yaakiyu.jp", "Yanajin66", "Yusuke1109", "もなー(偽物)", @@ -271,11 +272,11 @@ "config-memcache-badport": "Memcached のポート番号は $1 から $2 の範囲にしてください。", "config-extensions": "拡張機能", "config-extensions-help": "<code>./extensions</code> ディレクトリ内で、上に列挙した拡張機能を検出しました。\n\nこれらの拡張機能には追加の設定が必要な場合がありますが、今すぐ有効化できます。", - "config-skins": "外装", + "config-skins": "スキン", "config-skins-help": "上に列挙した外装は<code>./skins</code>で検出されたものです。少なくともひとつを有効にして、既定値を選択してください。", "config-skins-use-as-default": "この外装を既定値として使用", - "config-skins-missing": "外装が見つかりませんでした。適切なものをいくつかインストールするまで、MediaWikiはフォールバック外装を使用します。", - "config-skins-must-enable-some": "少なくとも1つの有効化する外装を選択する必要があります。", + "config-skins-missing": "スキンが見つかりませんでした。適切なものをいくつかインストールするまで、MediaWikiはフォールバックスキンを使用します。", + "config-skins-must-enable-some": "少なくとも1つの有効化するスキンを選択する必要があります。", "config-skins-must-enable-default": "既定値として選択された外装は有効である必要があります。", "config-install-alreadydone": "<strong>警告:</strong> 既にMediaWikiがインストール済みで、再びインストールし直そうとしています。\n次のページへ進んでください。", "config-install-begin": "「{{int:config-continue}}」を押すと、MediaWiki のインストールを開始できます。\n変更したい設定がある場合は、「{{int:config-back}}」を押してください。", diff --git a/includes/installer/i18n/ps.json b/includes/installer/i18n/ps.json index 1140ca6f31af..6070ab00ff06 100644 --- a/includes/installer/i18n/ps.json +++ b/includes/installer/i18n/ps.json @@ -1,7 +1,8 @@ { "@metadata": { "authors": [ - "Ahmed-Najib-Biabani-Ibrahimkhel" + "Ahmed-Najib-Biabani-Ibrahimkhel", + "شاه زمان پټان" ] }, "config-desc": "د مېډياويکي نصبونکی", @@ -68,7 +69,7 @@ "config-email-user": "کارن تر کارن برېښليک چارنول", "config-upload-enable": "دوتنې پورته کېدنې چارنول", "config-logo-preview-main": "لومړیمخ", - "config-logo-icon": "لوگو (نښه):", + "config-logo-icon": "نښان (نښه):", "config-extensions": "شاتاړي", "config-skins": "پوښۍ", "config-skins-use-as-default": "همدا پوښۍ په تلواليزه توگه کارول", diff --git a/includes/language/FormatterFactory.php b/includes/language/FormatterFactory.php index 6a038713ca41..bf92fffb0e7b 100644 --- a/includes/language/FormatterFactory.php +++ b/includes/language/FormatterFactory.php @@ -8,7 +8,6 @@ use MediaWiki\Languages\LanguageFactory; use MediaWiki\Status\StatusFormatter; use MediaWiki\Title\TitleFormatter; use MediaWiki\User\UserIdentityUtils; -use MessageCache; use MessageLocalizer; use Psr\Log\LoggerInterface; @@ -19,7 +18,7 @@ use Psr\Log\LoggerInterface; */ class FormatterFactory { - private MessageCache $messageCache; + private MessageParser $messageParser; private TitleFormatter $titleFormatter; private HookContainer $hookContainer; private UserIdentityUtils $userIdentityUtils; @@ -27,14 +26,14 @@ class FormatterFactory { private LoggerInterface $logger; public function __construct( - MessageCache $messageCache, + MessageParser $messageParser, TitleFormatter $titleFormatter, HookContainer $hookContainer, UserIdentityUtils $userIdentityUtils, LanguageFactory $languageFactory, LoggerInterface $logger ) { - $this->messageCache = $messageCache; + $this->messageParser = $messageParser; $this->titleFormatter = $titleFormatter; $this->hookContainer = $hookContainer; $this->userIdentityUtils = $userIdentityUtils; @@ -43,7 +42,7 @@ class FormatterFactory { } public function getStatusFormatter( MessageLocalizer $messageLocalizer ): StatusFormatter { - return new StatusFormatter( $messageLocalizer, $this->messageCache, $this->logger ); + return new StatusFormatter( $messageLocalizer, $this->messageParser, $this->logger ); } public function getBlockErrorFormatter( LocalizationContext $context ): BlockErrorFormatter { diff --git a/includes/language/Language.php b/includes/language/Language.php index 1c9b047550d4..34ba77884d19 100644 --- a/includes/language/Language.php +++ b/includes/language/Language.php @@ -3143,19 +3143,18 @@ class Language implements Bcp47Code { * @return string */ public function formatNum( $number ) { - return $this->formatNumInternal( (string)$number, false, false ); + return $this->formatNumInternal( (string)$number, false ); } /** * Internal implementation function, shared between formatNum and formatNumNoSeparators. * * @param string $number The stringification of a valid PHP number - * @param bool $noTranslate Whether to translate digits and separators * @param bool $noSeparators Whether to add separators * @return string */ private function formatNumInternal( - string $number, bool $noTranslate, bool $noSeparators + string $number, bool $noSeparators ): string { $translateNumerals = $this->config->get( MainConfigNames::TranslateNumerals ); @@ -3184,8 +3183,8 @@ class Language implements Bcp47Code { // numbers in the string. Don't split on NAN/INF in this legacy // case as they are likely to be found embedded inside non-numeric // text. - return preg_replace_callback( "/{$validNumberRe}/", function ( $m ) use ( $noTranslate, $noSeparators ) { - return $this->formatNumInternal( $m[0], $noTranslate, $noSeparators ); + return preg_replace_callback( "/{$validNumberRe}/", function ( $m ) use ( $noSeparators ) { + return $this->formatNumInternal( $m[0], $noSeparators ); }, $number ); } @@ -3211,15 +3210,11 @@ class Language implements Bcp47Code { // enough. We still need to do decimal separator // transformation, though. For example, 1234.56 becomes 1234,56 // in pl with $minimumGroupingDigits = 2. - if ( !$noTranslate ) { - $number = strtr( $number, $separatorTransformTable ?: [] ); - } + $number = strtr( $number, $separatorTransformTable ?: [] ); } elseif ( $number === '-0' ) { // Special case to ensure we don't lose the minus sign by // converting to an int. - if ( !$noTranslate ) { - $number = strtr( $number, $separatorTransformTable ?: [] ); - } + $number = strtr( $number, $separatorTransformTable ?: [] ); } else { // NumberFormatter supports separator transformation, // but it does not know all languages MW @@ -3227,16 +3222,7 @@ class Language implements Bcp47Code { // customisation. So manually set it. $fmt = clone $fmt; - if ( $noTranslate ) { - $fmt->setSymbol( - NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, - '.' - ); - $fmt->setSymbol( - NumberFormatter::GROUPING_SEPARATOR_SYMBOL, - ',' - ); - } elseif ( $separatorTransformTable ) { + if ( $separatorTransformTable ) { $fmt->setSymbol( NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $separatorTransformTable[ '.' ] ?? '.' @@ -3260,19 +3246,17 @@ class Language implements Bcp47Code { } } - if ( !$noTranslate ) { - if ( $translateNumerals ) { - // This is often unnecessary: PHP's NumberFormatter will often - // do the digit transform itself (T267614) - $s = $this->digitTransformTable(); - if ( $s ) { - $number = strtr( $number, $s ); - } + if ( $translateNumerals ) { + // This is often unnecessary: PHP's NumberFormatter will often + // do the digit transform itself (T267614) + $s = $this->digitTransformTable(); + if ( $s ) { + $number = strtr( $number, $s ); } - # T10327: Make our formatted numbers prettier by using a - # proper Unicode 'minus' character. - $number = strtr( $number, [ '-' => "\u{2212}" ] ); } + # T10327: Make our formatted numbers prettier by using a + # proper Unicode 'minus' character. + $number = strtr( $number, [ '-' => "\u{2212}" ] ); // Remove any LRM or RLM characters generated from NumberFormatter, // since directionality is handled outside of this context. @@ -3293,7 +3277,7 @@ class Language implements Bcp47Code { * @return string */ public function formatNumNoSeparators( $number ) { - return $this->formatNumInternal( (string)$number, false, true ); + return $this->formatNumInternal( (string)$number, true ); } /** diff --git a/includes/language/Message/Message.php b/includes/language/Message/Message.php index afec3409d43a..04c52a133244 100644 --- a/includes/language/Message/Message.php +++ b/includes/language/Message/Message.php @@ -1450,7 +1450,7 @@ class Message implements Stringable, MessageSpecifier, Serializable { * @return ParserOutput Wikitext parsed into HTML. */ protected function parseText( string $string ): ParserOutput { - $out = MediaWikiServices::getInstance()->getMessageCache()->parseWithPostprocessing( + $out = MediaWikiServices::getInstance()->getMessageParser()->parse( $string, $this->contextPage ?? PageReferenceValue::localReference( NS_SPECIAL, 'Badtitle/Message' ), /*linestart*/ true, @@ -1472,7 +1472,7 @@ class Message implements Stringable, MessageSpecifier, Serializable { * @return string Wikitext with {{-constructs substituted with its parsed result. */ protected function transformText( $string ) { - return MediaWikiServices::getInstance()->getMessageCache()->transform( + return MediaWikiServices::getInstance()->getMessageParser()->transform( $string, $this->isInterface, $this->getLanguage(), diff --git a/includes/language/MessageCache.php b/includes/language/MessageCache.php index 45eff8cf547b..490f9f0fc4bc 100644 --- a/includes/language/MessageCache.php +++ b/includes/language/MessageCache.php @@ -20,13 +20,13 @@ use MediaWiki\Config\ServiceOptions; use MediaWiki\Content\Content; -use MediaWiki\Context\RequestContext; use MediaWiki\Deferred\DeferredUpdates; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Language\ILanguageConverter; use MediaWiki\Language\Language; use MediaWiki\Language\MessageCacheUpdate; +use MediaWiki\Language\MessageParser; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\Languages\LanguageFactory; use MediaWiki\Languages\LanguageFallback; @@ -37,9 +37,6 @@ use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageIdentity; use MediaWiki\Page\PageReference; use MediaWiki\Page\PageReferenceValue; -use MediaWiki\Parser\Parser; -use MediaWiki\Parser\ParserFactory; -use MediaWiki\Parser\ParserOptions; use MediaWiki\Parser\ParserOutput; use MediaWiki\Revision\SlotRecord; use MediaWiki\StubObject\StubObject; @@ -139,23 +136,6 @@ class MessageCache implements LoggerAwareInterface { /** @var string[] */ private $rawHtmlMessages; - /** - * Message cache has its own parser which it uses to transform messages - * @var ParserOptions - */ - private $parserOptions; - - /** @var Parser[] Lazy-created via self::getParser() */ - private array $parsers = []; - private int $curParser = -1; - - /** - * Parsing some messages may require parsing another message first, due to special page - * transclusion and some hooks (T372891). This constant is the limit of nesting depth where - * we'll display an error instead of the other message. - */ - private const MAX_PARSER_DEPTH = 5; - /** @var WANObjectCache */ private $wanCache; /** @var BagOStuff */ @@ -178,8 +158,8 @@ class MessageCache implements LoggerAwareInterface { private $languageFallback; /** @var HookRunner */ private $hookRunner; - /** @var ParserFactory */ - private $parserFactory; + /** @var MessageParser */ + private $messageParser; /** @var (string|callable)[]|null */ private $messageKeyOverrides; @@ -220,7 +200,7 @@ class MessageCache implements LoggerAwareInterface { * @param LanguageNameUtils $languageNameUtils * @param LanguageFallback $languageFallback * @param HookContainer $hookContainer - * @param ParserFactory $parserFactory + * @param MessageParser $messageParser */ public function __construct( WANObjectCache $wanCache, @@ -235,7 +215,7 @@ class MessageCache implements LoggerAwareInterface { LanguageNameUtils $languageNameUtils, LanguageFallback $languageFallback, HookContainer $hookContainer, - ParserFactory $parserFactory + MessageParser $messageParser ) { $this->wanCache = $wanCache; $this->clusterCache = $clusterCache; @@ -249,7 +229,7 @@ class MessageCache implements LoggerAwareInterface { $this->languageNameUtils = $languageNameUtils; $this->languageFallback = $languageFallback; $this->hookRunner = new HookRunner( $hookContainer ); - $this->parserFactory = $parserFactory; + $this->messageParser = $messageParser; // limit size $this->cache = new MapCacheLRU( self::MAX_REQUEST_LANGUAGES ); @@ -267,34 +247,6 @@ class MessageCache implements LoggerAwareInterface { } /** - * ParserOptions is lazily initialised. - * - * @return ParserOptions - */ - private function getParserOptions() { - if ( !$this->parserOptions ) { - $context = RequestContext::getMain(); - $user = $context->getUser(); - if ( !$user->isSafeToLoad() ) { - // It isn't safe to use the context user yet, so don't try to get a - // ParserOptions for it. And don't cache this ParserOptions - // either. - $po = ParserOptions::newFromAnon(); - $po->setAllowUnsafeRawHtml( false ); - return $po; - } - - $this->parserOptions = ParserOptions::newFromContext( $context ); - // Messages may take parameters that could come - // from malicious sources. As a precaution, disable - // the <html> parser tag when parsing messages. - $this->parserOptions->setAllowUnsafeRawHtml( false ); - } - - return $this->parserOptions; - } - - /** * Try to load the cache from APC. * * @param string $code Optional language code, see documentation of load(). @@ -1456,6 +1408,8 @@ class MessageCache implements LoggerAwareInterface { } /** + * @deprecated since 1.44 use MessageParser::transform() + * * @param string $message * @param bool $interface * @param Language|null $language @@ -1463,48 +1417,14 @@ class MessageCache implements LoggerAwareInterface { * @return string */ public function transform( $message, $interface = false, $language = null, ?PageReference $page = null ) { - // Avoid creating parser if nothing to transform - if ( !str_contains( $message, '{{' ) ) { - return $message; - } - - $popts = $this->getParserOptions(); - $popts->setInterfaceMessage( $interface ); - $popts->setTargetLanguage( $language ); - - $userlang = $popts->setUserLang( $language ); - try { - $this->curParser++; - $parser = $this->getParser(); - if ( !$parser ) { - return '<span class="error">Message transform depth limit exceeded</span>'; - } - $message = $parser->transformMsg( $message, $popts, $page ); - } finally { - $this->curParser--; - } - $popts->setUserLang( $userlang ); - - return $message; - } - - /** - * You should increment $this->curParser before calling this method and decrement it after - * to support recursive calls to message parsing. - */ - private function getParser(): ?Parser { - if ( $this->curParser >= self::MAX_PARSER_DEPTH ) { - $this->logger->debug( __METHOD__ . ": Refusing to create a new parser with index {$this->curParser}" ); - return null; - } - if ( !isset( $this->parsers[ $this->curParser ] ) ) { - $this->logger->debug( __METHOD__ . ": Creating a new parser with index {$this->curParser}" ); - $this->parsers[ $this->curParser ] = $this->parserFactory->create(); - } - return $this->parsers[ $this->curParser ]; + return $this->messageParser->transform( + $message, $interface, $language, $page ); } /** + * @deprecated since 1.44 use MessageParser::parse() + * @internal + * * @param string $text * @param PageReference $contextPage * @param bool $linestart Whether this should be parsed in start-of-line @@ -1513,7 +1433,6 @@ class MessageCache implements LoggerAwareInterface { * (defaults to false) * @param Language|StubUserLang|string|null $language Language code * @return ParserOutput - * @internal */ public function parseWithPostprocessing( string $text, PageReference $contextPage, @@ -1521,48 +1440,25 @@ class MessageCache implements LoggerAwareInterface { bool $interface = false, $language = null ): ParserOutput { - $options = [ - 'allowTOC' => false, - 'enableSectionEditLinks' => false, - // Wrapping messages in an extra <div> is probably not expected. If - // they're outside the content area they probably shouldn't be - // targeted by CSS that's targeting the parser output, and if - // they're inside they already are from the outer div. - 'unwrap' => true, - 'userLang' => $language, - ]; - // Parse $text to yield a ParserOutput - $po = $this->parse( $text, $contextPage, $linestart, $interface, $language ); - if ( is_string( $po ) ) { - $po = new ParserOutput( $po ); - } - // Run the post-processing pipeline - return MediaWikiServices::getInstance()->getDefaultOutputPipeline() - ->run( $po, $this->getParserOptions(), $options ); + return $this->messageParser->parse( + $text, $contextPage, $linestart, $interface, $language ); } /** + * @deprecated since 1.44 use MessageParser::parseWithoutPostprocessing() + * * @param string $text * @param PageReference|null $page * @param bool $linestart Whether this is at the start of a line * @param bool $interface Whether this is an interface message * @param Language|StubUserLang|string|null $language Language code - * @return ParserOutput|string + * @return ParserOutput */ - public function parse( $text, ?PageReference $page = null, $linestart = true, - $interface = false, $language = null + public function parse( $text, ?PageReference $page = null, + $linestart = true, $interface = false, $language = null ) { // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle global $wgTitle; - - $popts = $this->getParserOptions(); - $popts->setInterfaceMessage( $interface ); - - if ( is_string( $language ) ) { - $language = $this->langFactory->getLanguage( $language ); - } - $popts->setTargetLanguage( $language ); - if ( !$page ) { $logger = LoggerFactory::getInstance( 'GlobalTitleFail' ); $logger->info( @@ -1581,16 +1477,8 @@ class MessageCache implements LoggerAwareInterface { ); } - try { - $this->curParser++; - $parser = $this->getParser(); - if ( !$parser ) { - return '<span class="error">Message parse depth limit exceeded</span>'; - } - return $parser->parse( $text, $page, $popts, $linestart ); - } finally { - $this->curParser--; - } + return $this->messageParser->parseWithoutPostprocessing( + $text, $page, $linestart, $interface, $language ); } public function disable() { diff --git a/includes/language/MessageParser.php b/includes/language/MessageParser.php new file mode 100644 index 000000000000..8f595fbe8f23 --- /dev/null +++ b/includes/language/MessageParser.php @@ -0,0 +1,243 @@ +<?php + +namespace MediaWiki\Language; + +use MediaWiki\Context\RequestContext; +use MediaWiki\DAO\WikiAwareEntity; +use MediaWiki\Languages\LanguageFactory; +use MediaWiki\OutputTransform\OutputTransformPipeline; +use MediaWiki\Page\PageReference; +use MediaWiki\Page\PageReferenceValue; +use MediaWiki\Parser\Parser; +use MediaWiki\Parser\ParserFactory; +use MediaWiki\Parser\ParserOptions; +use MediaWiki\Parser\ParserOutput; +use MediaWiki\StubObject\StubUserLang; +use Psr\Log\LoggerInterface; + +/** + * Service for transformation of interface message text. + * + * @since 1.44 + */ +class MessageParser { + private const DEPTH_EXCEEDED_MESSAGE = + '<span class="error">Message parse depth limit exceeded</span>'; + + private ParserFactory $parserFactory; + private OutputTransformPipeline $outputPipeline; + private LanguageFactory $langFactory; + private LoggerInterface $logger; + + /** @var ParserOptions|null Lazy-initialised */ + private ?ParserOptions $parserOptions = null; + + /** @var Parser[] Cached Parser objects */ + private array $parsers = []; + /** @var int Index into $this->parsers for the active Parser */ + private int $curParser = -1; + + /** + * Parsing some messages may require parsing another message first, due to special page + * transclusion and some hooks (T372891). This constant is the limit of nesting depth where + * we'll display an error instead of the other message. + */ + private const MAX_PARSER_DEPTH = 5; + + public function __construct( + ParserFactory $parserFactory, + OutputTransformPipeline $outputPipeline, + LanguageFactory $languageFactory, + LoggerInterface $logger + ) { + $this->parserFactory = $parserFactory; + $this->outputPipeline = $outputPipeline; + $this->langFactory = $languageFactory; + $this->logger = $logger; + } + + private function getParserOptions(): ParserOptions { + if ( !$this->parserOptions ) { + $context = RequestContext::getMain(); + $user = $context->getUser(); + if ( !$user->isSafeToLoad() ) { + // It isn't safe to use the context user yet, so don't try to get a + // ParserOptions for it. And don't cache this ParserOptions + // either. + $po = ParserOptions::newFromAnon(); + $po->setAllowUnsafeRawHtml( false ); + return $po; + } + + $this->parserOptions = ParserOptions::newFromContext( $context ); + // Messages may take parameters that could come + // from malicious sources. As a precaution, disable + // the <html> parser tag when parsing messages. + $this->parserOptions->setAllowUnsafeRawHtml( false ); + } + + return $this->parserOptions; + } + + /** + * Run message text through the preprocessor, expanding parser functions + * + * @param string $message + * @param bool $interface + * @param Language|string|null $language + * @param PageReference|null $page + * @return string + */ + public function transform( + $message, + $interface = false, + $language = null, + ?PageReference $page = null + ) { + // Avoid creating parser if nothing to transform + if ( !str_contains( $message, '{{' ) ) { + return $message; + } + if ( is_string( $language ) ) { + $language = $this->langFactory->getLanguage( $language ); + } + + $popts = $this->getParserOptions(); + $popts->setInterfaceMessage( $interface ); + $popts->setTargetLanguage( $language ); + + if ( $language ) { + $oldUserLang = $popts->setUserLang( $language ); + } else { + $oldUserLang = null; + } + $page ??= $this->getPlaceholderTitle(); + + $parser = $this->acquireParser(); + if ( !$parser ) { + return self::DEPTH_EXCEEDED_MESSAGE; + } + try { + return $parser->transformMsg( $message, $popts, $page ); + } finally { + $this->releaseParser( $parser ); + if ( $oldUserLang ) { + $popts->setUserLang( $oldUserLang ); + } + } + } + + /** + * @param string $text + * @param ?PageReference $contextPage The context page, or null to use a placeholder + * @param bool $lineStart Whether this should be parsed in start-of-line context + * @param bool $interface Whether this is an interface message + * @param Language|StubUserLang|string|null $language Language code + * @return ParserOutput + */ + public function parse( + string $text, + ?PageReference $contextPage = null, + bool $lineStart = true, + bool $interface = false, + $language = null + ): ParserOutput { + $options = [ + 'allowTOC' => false, + 'enableSectionEditLinks' => false, + // Wrapping messages in an extra <div> is probably not expected. If + // they're outside the content area they probably shouldn't be + // targeted by CSS that's targeting the parser output, and if + // they're inside they already are from the outer div. + 'unwrap' => true, + 'userLang' => $language, + ]; + // Parse $text to yield a ParserOutput + $po = $this->parseWithoutPostprocessing( $text, $contextPage, $lineStart, $interface, $language ); + // Run the post-processing pipeline + return $this->outputPipeline->run( $po, $this->getParserOptions(), $options ); + } + + /** + * @param string $text + * @param ?PageReference $page The context title, or null to use a placeholder + * @param bool $lineStart Whether this is at the start of a line + * @param bool $interface Whether this is an interface message + * @param Language|StubUserLang|string|null $language Language code + * @return ParserOutput + */ + public function parseWithoutPostprocessing( + $text, + ?PageReference $page = null, + $lineStart = true, + $interface = false, + $language = null + ): ParserOutput { + $popts = $this->getParserOptions(); + $popts->setInterfaceMessage( $interface ); + + if ( is_string( $language ) ) { + $language = $this->langFactory->getLanguage( $language ); + } + $popts->setTargetLanguage( $language ); + + $page ??= $this->getPlaceholderTitle(); + + $parser = $this->acquireParser(); + if ( !$parser ) { + return new ParserOutput( self::DEPTH_EXCEEDED_MESSAGE ); + } + try { + return $parser->parse( $text, $page, $popts, $lineStart ); + } finally { + $this->releaseParser( $parser ); + } + } + + private function getPlaceholderTitle(): PageReference { + return new PageReferenceValue( + NS_SPECIAL, + 'Badtitle/MessageParser', + WikiAwareEntity::LOCAL + ); + } + + /** + * Attempt to get a free parser from the cache. If none exists, create one, + * up to a limit of MAX_PARSER_DEPTH. If the limit is exceeded, return null. + * + * If a parser is returned, it must be released with releaseParser(). + * + * @return Parser|null + */ + private function acquireParser(): ?Parser { + $index = $this->curParser + 1; + if ( $index >= self::MAX_PARSER_DEPTH ) { + $this->logger->debug( __METHOD__ . ": Refusing to create a new parser with index {$index}" ); + return null; + } + $parser = $this->parsers[ $index ] ?? null; + if ( !$parser ) { + $this->logger->debug( __METHOD__ . ": Creating a new parser with index {$index}" ); + $parser = $this->parserFactory->create(); + } + $this->parsers[ $index ] = $parser; + $this->curParser = $index; + return $parser; + } + + /** + * Release a parser previously acquired by acquireParser(). + * + * @param Parser $parser + */ + private function releaseParser( Parser $parser ) { + if ( $this->parsers[$this->curParser] !== $parser ) { + throw new \LogicException( 'releaseParser called with the wrong ' . + "parser instance: #{$this->curParser} = " . + gettype( $this->parsers[$this->curParser] ) ); + } + $this->curParser--; + } + +} diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index f703adc5f4f4..a1c3f56a8770 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -1086,7 +1086,7 @@ class WANObjectCache implements // mitigation systems. $now = $this->getCurrentTime(); // Set the key to the purge value in all datacenters - $purge = $this->makeTombstonePurgeValue( $now ); + $purge = self::PURGE_VAL_PREFIX . ':' . (int)$now; $ok = $this->relayVolatilePurge( $valueSisterKey, $purge, $ttl ); } @@ -1740,7 +1740,14 @@ class WANObjectCache implements $curState[self::RES_CHECK_AS_OF] ); $safeMinAsOf = max( $minAsOf, $lastPurgeTime + self::TINY_POSITIVE ); - if ( $this->isExtremelyNewValue( $volState, $safeMinAsOf, $startTime ) ) { + + if ( $volState[self::RES_VALUE] === false || $volState[self::RES_AS_OF] < $safeMinAsOf ) { + $isExtremelyNewValue = false; + } else { + $age = $startTime - $volState[self::RES_AS_OF]; + $isExtremelyNewValue = ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 ); + } + if ( $isExtremelyNewValue ) { $this->logger->debug( "fetchOrRegenerate($key): volatile hit" ); $this->stats->getTiming( 'wanobjectcache_getwithset_seconds' ) @@ -1782,7 +1789,9 @@ class WANObjectCache implements // If a regeneration lock is required, threads that do not get the lock will try to use // the stale value, the interim value, or the $busyValue placeholder, in that order. If // none of those are set then all threads will bypass the lock and regenerate the value. - $hasLock = $useRegenerationLock && $this->claimStampedeLock( $key ); + $mutexKey = $this->makeSisterKey( $key, self::TYPE_MUTEX ); + // Note that locking is not bypassed due to I/O errors; this avoids stampedes + $hasLock = $useRegenerationLock && $this->cache->add( $mutexKey, 1, self::LOCK_TTL ); if ( $useRegenerationLock && !$hasLock ) { // Determine if there is stale or volatile cached value that is still usable // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive @@ -1808,7 +1817,7 @@ class WANObjectCache implements ->copyToStatsdAt( "wanobjectcache.$keygroup.$miss.busy" ) ->observe( 1e3 * ( $this->getCurrentTime() - $startTime ) ); - $placeholderValue = $this->resolveBusyValue( $busyValue ); + $placeholderValue = ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue; return [ $placeholderValue, $version, $curState[self::RES_AS_OF] ]; } @@ -1878,7 +1887,9 @@ class WANObjectCache implements } } - $this->yieldStampedeLock( $key, $hasLock ); + if ( $hasLock ) { + $this->cache->delete( $mutexKey, $this->cache::WRITE_BACKGROUND ); + } $miss = is_infinite( $minAsOf ) ? 'renew' : 'miss'; $this->logger->debug( "fetchOrRegenerate($key): $miss, new value computed" ); @@ -1894,46 +1905,6 @@ class WANObjectCache implements } /** - * @param string $key Cache key made with makeKey()/makeGlobalKey() - * @return bool Success - */ - private function claimStampedeLock( $key ) { - $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX ); - // Note that locking is not bypassed due to I/O errors; this avoids stampedes - return $this->cache->add( $checkSisterKey, 1, self::LOCK_TTL ); - } - - /** - * @param string $key Cache key made with makeKey()/makeGlobalKey() - * @param bool $hasLock - */ - private function yieldStampedeLock( $key, $hasLock ) { - if ( $hasLock ) { - $checkSisterKey = $this->makeSisterKey( $key, self::TYPE_MUTEX ); - $this->cache->delete( $checkSisterKey, $this->cache::WRITE_BACKGROUND ); - } - } - - /** - * Get sister keys that should be collocated with their corresponding base cache keys - * - * The key will bear the WANCache prefix and use the configured coalescing scheme - * - * @param string[] $baseKeys Cache keys made with makeKey()/makeGlobalKey() - * @param string $type Consistent hashing agnostic suffix character matching [a-zA-Z] - * @param string|null $route Routing prefix (optional) - * @return string[] Order-corresponding list of sister keys - */ - private function makeSisterKeys( array $baseKeys, string $type, ?string $route = null ) { - $sisterKeys = []; - foreach ( $baseKeys as $baseKey ) { - $sisterKeys[] = $this->makeSisterKey( $baseKey, $type, $route ); - } - - return $sisterKeys; - } - - /** * Get a sister key that should be collocated with a base cache key * * The keys will bear the WANCache prefix and use the configured coalescing scheme @@ -1960,28 +1931,6 @@ class WANObjectCache implements } /** - * Check if a key value is non-false, new enough, and has an "as of" time almost equal to now - * - * If the value was just written to cache, and it did not take an unusually long time to - * generate, then it is probably not worth regenerating yet. For example, replica databases - * might still return lagged pre-purge values anyway. - * - * @param array $res Current value WANObjectCache::RES_* data map - * @param float $minAsOf Minimum acceptable value "as of" UNIX timestamp - * @param float $now Current UNIX timestamp - * @return bool Whether the age of a volatile value is negligible - */ - private function isExtremelyNewValue( $res, $minAsOf, $now ) { - if ( $res[self::RES_VALUE] === false || $res[self::RES_AS_OF] < $minAsOf ) { - return false; - } - - $age = $now - $res[self::RES_AS_OF]; - - return ( $age < mt_rand( self::RECENT_SET_LOW_MS, self::RECENT_SET_HIGH_MS ) / 1e3 ); - } - - /** * @param string $key Cache key made with makeKey()/makeGlobalKey() * @param float $minAsOf Minimum acceptable value "as of" UNIX timestamp * @param float $now Fetch time to determine "age" metadata @@ -2047,14 +1996,6 @@ class WANObjectCache implements } /** - * @param mixed $busyValue - * @return mixed - */ - private function resolveBusyValue( $busyValue ) { - return ( $busyValue instanceof Closure ) ? $busyValue() : $busyValue; - } - - /** * Method to fetch multiple cache keys at once with regeneration * * This works the same as getWithSetCallback() except: @@ -3031,14 +2972,6 @@ class WANObjectCache implements /** * @param float $timestamp UNIX timestamp - * @return string Wrapped purge value; format is "PURGED:<timestamp>" - */ - private function makeTombstonePurgeValue( float $timestamp ) { - return self::PURGE_VAL_PREFIX . ':' . (int)$timestamp; - } - - /** - * @param float $timestamp UNIX timestamp * @param int $holdoff In seconds * @param array|null &$purge Unwrapped purge value array [returned] * @return string Wrapped purge value; format is "PURGED:<timestamp>:<holdoff>" @@ -3100,7 +3033,10 @@ class WANObjectCache implements } // Get all the value keys to fetch... - $sisterKeys = $this->makeSisterKeys( $keys, self::TYPE_VALUE ); + $sisterKeys = []; + foreach ( $keys as $baseKey ) { + $sisterKeys[] = $this->makeSisterKey( $baseKey, self::TYPE_VALUE ); + } // Get all the "check" keys to fetch... foreach ( $checkKeys as $i => $checkKeyOrKeyGroup ) { // Note: avoid array_merge() inside loop in case there are many keys diff --git a/includes/mail/EmailNotification.php b/includes/mail/EmailNotification.php index 6cfa1361d990..4001d7f2905a 100644 --- a/includes/mail/EmailNotification.php +++ b/includes/mail/EmailNotification.php @@ -232,9 +232,7 @@ class EmailNotification { $pageStatus = 'changed' ) { # we use $wgPasswordSender as sender's address - $mwServices = MediaWikiServices::getInstance(); - $messageCache = $mwServices->getMessageCache(); $config = $mwServices->getMainConfig(); # The following code is only run, if several conditions are met: @@ -269,7 +267,7 @@ class EmailNotification { && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit ) ) { $targetUser = User::newFromName( $title->getText() ); - $this->compose( $targetUser, self::USER_TALK, $messageCache ); + $this->compose( $targetUser, self::USER_TALK ); $userTalkId = $targetUser->getId(); } @@ -291,7 +289,7 @@ class EmailNotification { $watchingUser->getBlock() ) && $hookRunner->onSendWatchlistEmailNotification( $watchingUser, $title, $this ) ) { - $this->compose( $watchingUser, self::WATCHLIST, $messageCache ); + $this->compose( $watchingUser, self::WATCHLIST ); } } } @@ -304,7 +302,7 @@ class EmailNotification { } $user = User::newFromName( $name ); if ( $user instanceof User ) { - $this->compose( $user, self::ALL_CHANGES, $messageCache ); + $this->compose( $user, self::ALL_CHANGES ); } } $this->sendMails(); @@ -361,11 +359,12 @@ class EmailNotification { /** * Generate the generic "this page has been changed" e-mail text. */ - private function composeCommonMailtext( MessageCache $messageCache ) { + private function composeCommonMailtext() { $services = MediaWikiServices::getInstance(); $config = $services->getMainConfig(); $userOptionsLookup = $services->getUserOptionsLookup(); $urlUtils = $services->getUrlUtils(); + $messageParser = $services->getMessageParser(); $this->composed_common = true; @@ -457,7 +456,7 @@ class EmailNotification { $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain(); $body = strtr( $body, $keys ); - $body = $messageCache->transform( $body, false, null, $this->title ); + $body = $messageParser->transform( $body, false, null, $this->title ); $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 ); # Reveal the page editor's address as REPLY-TO address only if @@ -493,11 +492,10 @@ class EmailNotification { * Call sendMails() to send any mails that were queued. * @param UserEmailContact $user * @param string $source - * @param MessageCache $messageCache */ - private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) { + private function compose( UserEmailContact $user, $source ) { if ( !$this->composed_common ) { - $this->composeCommonMailtext( $messageCache ); + $this->composeCommonMailtext(); } if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) { diff --git a/includes/page/Article.php b/includes/page/Article.php index 93d5f228c395..b33315ba3397 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -18,6 +18,7 @@ * @file */ +use MediaWiki\Block\Block; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\CommentFormatter\CommentFormatter; @@ -1519,8 +1520,6 @@ class Article implements Page { $userFactory = $services->getUserFactory(); $user = $userFactory->newFromNameOrIp( $rootPart ); - $block = $this->blockStore->newFromTarget( $user, $user ); - if ( $user && $user->isRegistered() && $user->isHidden() && !$context->getAuthority()->isAllowed( 'hideuser' ) ) { @@ -1549,34 +1548,46 @@ class Article implements Page { 'msgKey' => [ 'renameuser-renamed-notice', $title->getBaseText() ] ] ); - } elseif ( - $user && $block !== null && - $block->getType() != DatabaseBlock::TYPE_AUTO && - ( - $block->isSitewide() || - $services->getPermissionManager()->isBlockedFrom( $user, $title, true ) - ) - ) { + } else { + $validUserPage = !$title->isSubpage(); + + // TODO: factor out nearly identical code in IntroMessageBuilder::addUserWarnings + $blocks = $this->blockStore->newListFromTarget( $user, $user ); + $numBlocks = 0; + $appliesToTitle = false; + $logTargetPage = ''; + foreach ( $blocks as $block ) { + if ( $block->getType() !== Block::TYPE_AUTO ) { + $numBlocks++; + if ( $block->appliesToTitle( $title ) ) { + $appliesToTitle = true; + } + $logTargetPage = $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . + ':' . $block->getTargetName(); + } + } + // Show log extract if the user is sitewide blocked or is partially // blocked and not allowed to edit their user page or user talk page - LogEventsList::showLogExtract( - $outputPage, - 'block', - $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' . - $block->getTargetName(), - '', - [ - 'lim' => 1, - 'showIfEmpty' => false, - 'msgKey' => [ - 'blocked-notice-logextract', - $user->getName() # Support GENDER in notice + if ( $numBlocks && $appliesToTitle ) { + $msgKey = $numBlocks === 1 + ? 'blocked-notice-logextract' : 'blocked-notice-logextract-multi'; + LogEventsList::showLogExtract( + $outputPage, + 'block', + $logTargetPage, + '', + [ + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => [ + $msgKey, + $user->getName(), # Support GENDER in notice + $numBlocks + ], ] - ] - ); - $validUserPage = !$title->isSubpage(); - } else { - $validUserPage = !$title->isSubpage(); + ); + } } } diff --git a/includes/page/Event/PageEvent.php b/includes/page/Event/PageEvent.php index 50ad6341c171..899519426236 100644 --- a/includes/page/Event/PageEvent.php +++ b/includes/page/Event/PageEvent.php @@ -39,6 +39,14 @@ abstract class PageEvent extends DomainEvent { public const TYPE = 'Page'; + /** + * @var string This is a reconciliation event, triggered in order to give + * listeners an opportunity to catch up on missed events or recreate + * corrupted data. Can be triggered by a user action such as a null + * edit, or by a maintenance script. + */ + public const FLAG_RECONCILIATION_REQUEST = 'reconciliation'; + private string $cause; private ProperPageIdentity $page; private UserIdentity $performer; @@ -65,7 +73,11 @@ abstract class PageEvent extends DomainEvent { array $flags = [], $timestamp = false ) { - parent::__construct( $timestamp ); + parent::__construct( + $timestamp, + $flags[ self::FLAG_RECONCILIATION_REQUEST ] ?? false + ); + $this->declareEventType( self::TYPE ); Assert::parameterElementType( 'string', $tags, '$tags' ); @@ -104,7 +116,7 @@ abstract class PageEvent extends DomainEvent { * Checks flags describing the page update. * Use with FLAG_XXX constants declared by subclasses. */ - public function hasFlag( string $name ): bool { + protected function hasFlag( string $name ): bool { return $this->flags[$name] ?? false; } diff --git a/includes/page/Event/PageUpdatedEvent.php b/includes/page/Event/PageUpdatedEvent.php index 9e55d00af73b..3ed5197344d6 100644 --- a/includes/page/Event/PageUpdatedEvent.php +++ b/includes/page/Event/PageUpdatedEvent.php @@ -29,17 +29,27 @@ use MediaWiki\User\UserIdentity; use Wikimedia\Assert\Assert; /** - * Domain event representing a page edit. + * Domain event representing a page updated. A PageUpdatedEvent is triggered + * when a page's current revision changes, even if the content did not change + * (for a dummy revision). A reconciliation version of this event may be + * triggered even when the page's current version did not change (on null edits), + * to provide an opportunity to listeners to recover from data loss and + * corruption by re-generating any derived data. * - * This event is emitted by PageUpdater. It can be used by core components and - * extensions to follow up on page edits. - * - * Extensions that want to subscribe to this event should list "PageUpdated" - * as a subscribed event type. + * PageUpdatedEvent is emitted by DerivedPageDataUpdater, typically triggered by + * PageUpdater. User activities that trigger a PageUpdated event include: + * - editing, including page creation and null-edits + * - moving pages + * - undeleting pages + * - importing revisions + * - Any activity that creates a dummy revision, such as changing the page's + * protection level. * + * Extensions that want to subscribe to this event should list + * "PageUpdated" as a subscribed event type. * Subscribers based on EventSubscriberBase should implement the * handlePageUpdatedEventAfterCommit() listener method to be informed when - * a page edit has been committed to the database. + * a page update has been committed to the database. * * See the documentation of EventSubscriberBase and DomainEventSource for * more options and details. @@ -54,31 +64,32 @@ class PageUpdatedEvent extends PageEvent implements PageUpdateCauses { public const TYPE = 'PageUpdated'; /** - * @var string The update reverted to an earlier revision. - * Best effort, false negatives are possible. + * @var string Do not notify other users (e.g. via RecentChanges or + * watchlist). + * See EDIT_SILENT. */ - public const FLAG_REVERTED = 'revert'; - - /** @var string The update should not be reported to users in feeds. */ public const FLAG_SILENT = 'silent'; - /** @var string The update should be attributed to a bot. */ + /** + * @var string The update was performed by a bot. + * See EDIT_FORCE_BOT. + */ public const FLAG_BOT = 'bot'; /** - * @var string The update was automated and should not be counted as - * user activity. + * @var string The page update is a side effect and does not represent an + * active user contribution. + * See EDIT_IMPLICIT. */ - public const FLAG_AUTOMATED = 'automated'; + public const FLAG_IMPLICIT = 'implicit'; /** * All available flags and their default values. */ public const DEFAULT_FLAGS = [ - self::FLAG_REVERTED => false, self::FLAG_SILENT => false, self::FLAG_BOT => false, - self::FLAG_AUTOMATED => false, + self::FLAG_IMPLICIT => false, ]; private RevisionSlotsUpdate $slotsUpdate; @@ -86,15 +97,17 @@ class PageUpdatedEvent extends PageEvent implements PageUpdateCauses { private ?RevisionRecord $oldRevision; private ?EditResult $editResult; + private int $patrolStatus; + /** * @param string $cause See the self::CAUSE_XXX constants. * @param ProperPageIdentity $page The page affected by the update. * @param UserIdentity $performer The user performing the update. - * @param RevisionSlotsUpdate $slotsUpdate Page content changed by the edit. + * @param RevisionSlotsUpdate $slotsUpdate Page content changed by the update. * @param RevisionRecord $newRevision The revision object resulting from the - * edit. + * update. * @param RevisionRecord|null $oldRevision The revision that used to be - * current before the edit. + * current before the updated. * @param EditResult|null $editResult An EditResult representing the effects * of an edit. * @param array<string> $tags Applicable tags, see ChangeTags. @@ -138,12 +151,21 @@ class PageUpdatedEvent extends PageEvent implements PageUpdateCauses { $this->patrolStatus = $patrolStatus; } - private int $patrolStatus; - /** - * Whether the edit created the page. + * @deprecated since 1.44, use isCreation() instead. + * @note Unreleased but used in GrowthExperiments */ public function isNew(): bool { + return $this->isCreation(); + } + + /** + * Whether the updated created the page. + * A deleted/archived page is not considered to "exist". + * When undeleting a page, the page will be restored using its old page ID, + * so the "created" page may have an ID that was seen previously. + */ + public function isCreation(): bool { return $this->oldRevision === null; } @@ -152,9 +174,6 @@ class PageUpdatedEvent extends PageEvent implements PageUpdateCauses { * or purge. * This returns false if no new revision was created and the event was * generated in order to trigger re-generation of derived data. - * This will also return true for derived data updates, since they do not - * create a new revision. Such updates may however still be relevant to - * listeners. */ public function isRevisionChange(): bool { return $this->oldRevision === null @@ -186,54 +205,60 @@ class PageUpdatedEvent extends PageEvent implements PageUpdateCauses { } /** - * Page content changed by the edit. Can be used to determine which slots - * were changed, and whether slots were added or removed. + * Returns which slots were changed, added, or removed by the update. */ public function getSlotsUpdate(): RevisionSlotsUpdate { return $this->slotsUpdate; } /** - * Whether the given slot was modified by the edit. + * Whether the given slot was modified by the page update. * Slots that were removed do not count as modified. + * This is a convenience method for + * $this->getSlotsUpdate()->isModifiedSlot( $slotRole ). */ public function isModifiedSlot( string $slotRole ): bool { return $this->getSlotsUpdate()->isModifiedSlot( $slotRole ); } /** - * An EditResult representing the effects of an edit. + * An EditResult representing the effects of the update. * Can be used to determine whether the edit was a revert * and which edits were reverted. - * Will be null for page creations. + * + * This may return null for updates that do not result from edits, + * such as imports or undeletions. */ public function getEditResult(): ?EditResult { return $this->editResult; } /** - * Returned the revision that used to be current before the edit. + * Returned the revision that used to be current before the update. * Will be null if the edit created the page. - * Will be the same as $newRevision if the edit was a "null-edit". - * Note that this is not necessarily the revision the edit was based - * on: in the case of edit conflicts, manual reverts, or imports, - * the base revision at the beginning of the edit process may be - * different from the parent revision after the conclusion of the edit - * process. + * Will be the same as getNewRevision() if the edit was a "null-edit". + * + * Note that this is not necessarily the new revision's parent revision. + * For instance, when undeleting a page, the old revision will be null + * because the page didn't exist before, even if the undeleted page has + * many revisions and the new current revision indeed has a parent revision. + * + * The parent revision can be determined by calling + * getNewRevision()->getParentId(). */ public function getOldRevision(): ?RevisionRecord { return $this->oldRevision; } /** - * The revision that became the current one because of the edit. + * The revision that became the current one because of the update. */ public function getNewRevision(): RevisionRecord { return $this->newRevision; } /** - * Returns the edit's initial patrol status. + * Returns the page update's initial patrol status. * @see PageUpdater::setRcPatrolStatus() * @see RecentChange::PRC_XXX */ @@ -253,16 +278,15 @@ class PageUpdatedEvent extends PageEvent implements PageUpdateCauses { * Whether the update was performed automatically without the user's * initiative. */ - public function isAutomated(): bool { - return $this->hasFlag( self::FLAG_AUTOMATED ); + public function isImplicit(): bool { + return $this->hasFlag( self::FLAG_IMPLICIT ); } /** * Whether the update is a revert to a previous state of the page. */ public function isRevert(): bool { - return ( $this->editResult && $this->editResult->isRevert() ) - || $this->hasFlag( self::FLAG_REVERTED ); + return $this->editResult && $this->editResult->isRevert(); } /** diff --git a/includes/page/MovePage.php b/includes/page/MovePage.php index 4313fff45994..d6d9a76b6b10 100644 --- a/includes/page/MovePage.php +++ b/includes/page/MovePage.php @@ -928,7 +928,7 @@ class MovePage { 'oldtitle' => $this->oldTitle, 'oldcountable' => $oldcountable, ] ) - ->saveDummyRevision( $comment ); + ->saveDummyRevision( $comment, EDIT_SILENT | EDIT_MINOR ); $logEntry->setAssociatedRevId( $nullRevision->getId() ); @@ -944,8 +944,7 @@ class MovePage { ->addTags( $changeTags ) ->addSoftwareTag( 'mw-new-redirect' ) ->setUsePageCreationLog( false ) - ->setAutomated( true ) - ->setFlags( EDIT_SUPPRESS_RC | EDIT_INTERNAL ) + ->setFlags( EDIT_SILENT | EDIT_INTERNAL | EDIT_IMPLICIT ) ->saveRevision( $comment ); } diff --git a/includes/page/UndeletePage.php b/includes/page/UndeletePage.php index 8e37ba0220d2..020049c6329b 100644 --- a/includes/page/UndeletePage.php +++ b/includes/page/UndeletePage.php @@ -641,7 +641,7 @@ class UndeletePage { // Update site stats, link tables, etc $options = [ PageUpdatedEvent::FLAG_SILENT => true, - PageUpdatedEvent::FLAG_AUTOMATED => true, + PageUpdatedEvent::FLAG_IMPLICIT => true, 'created' => $created, 'oldcountable' => $oldcountable, ]; diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index f9eebd8ea506..558dd58d4974 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -1533,66 +1533,14 @@ class WikiPage implements Stringable, Page, PageRecord { } /** - * Returns a PageUpdater for creating new revisions on this page (or creating the page). - * - * The PageUpdater can also be used to detect the need for edit conflict resolution, - * and to protected such conflict resolution from concurrent edits using a check-and-set - * mechanism. - * - * @since 1.32 - * - * @note Once extensions no longer rely on WikiPage to get access to the state of an ongoing - * edit via prepareContentForEdit() and WikiPage::getCurrentUpdate(), - * this method should be deprecated and callers should be migrated to using - * PageUpdaterFactory::newPageUpdater() instead. - * - * @param Authority|UserIdentity $performer - * @param RevisionSlotsUpdate|null $forUpdate If given, allows any cached ParserOutput - * that may already have been returned via getDerivedDataUpdater to be re-used. - * - * @return PageUpdater - */ - public function newPageUpdater( $performer, ?RevisionSlotsUpdate $forUpdate = null ) { - if ( $performer instanceof Authority ) { - // TODO: Deprecate this. But better get rid of this method entirely. - $performer = $performer->getUser(); - } - - $pageUpdater = $this->getPageUpdaterFactory()->newPageUpdaterForDerivedPageDataUpdater( - $this, - $performer, - $this->getDerivedDataUpdater( $performer, null, $forUpdate, true ) - ); - - return $pageUpdater; - } - - /** * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * - * @deprecated since 1.36, use PageUpdater::saveRevision instead. Note that the new method - * expects callers to take care of checking EDIT_MINOR against the minoredit right, and to - * apply the autopatrol right as appropriate. - * * @param Content $content New content * @param Authority $performer doing the edit * @param string|CommentStoreComment $summary Edit summary - * @param int $flags Bitfield: - * EDIT_NEW - * Article is known or assumed to be non-existent, create a new one - * EDIT_UPDATE - * Article is known or assumed to be pre-existing, update it - * EDIT_MINOR - * Mark this edit minor, if the user is allowed to do so - * EDIT_SUPPRESS_RC - * Do not log the change in recentchanges - * EDIT_FORCE_BOT - * Mark the edit a "bot" edit regardless of user rights - * EDIT_AUTOSUMMARY - * Fill in blank summaries with generated text where possible - * EDIT_INTERNAL - * Signal that the page retrieve/save cycle happened entirely in this request. + * @param int $flags Bitfield, see the EDIT_XXX constants such as EDIT_NEW + * or EDIT_FORCE_BOT. * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the * article will be detected. If EDIT_UPDATE is specified and the article @@ -1625,6 +1573,10 @@ class WikiPage implements Stringable, Page, PageRecord { * new: Boolean indicating if the function attempted to create a new article. * revision-record: The revision record object for the inserted revision, or null. * + * @deprecated since 1.36, use PageUpdater::saveRevision instead. Note that the new method + * expects callers to take care of checking EDIT_MINOR against the minoredit right, and to + * apply the autopatrol right as appropriate. + * * @since 1.36 */ public function doUserEditContent( @@ -1658,7 +1610,7 @@ class WikiPage implements Stringable, Page, PageRecord { // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also // used by this PageUpdater. However, there is no guarantee for this. $updater = $this->newPageUpdater( $performer, $slotsUpdate ) - ->setContent( SlotRecord::MAIN, $content ) + ->setContent( SlotRecord::MAIN, $content ) ->setOriginalRevisionId( $originalRevId ); if ( $undidRevId ) { $updater->markAsRevert( @@ -1698,6 +1650,41 @@ class WikiPage implements Stringable, Page, PageRecord { } /** + * Returns a PageUpdater for creating new revisions on this page (or creating the page). + * + * The PageUpdater can also be used to detect the need for edit conflict resolution, + * and to protected such conflict resolution from concurrent edits using a check-and-set + * mechanism. + * + * @since 1.32 + * + * @note Once extensions no longer rely on WikiPage to get access to the state of an ongoing + * edit via prepareContentForEdit() and WikiPage::getCurrentUpdate(), + * this method should be deprecated and callers should be migrated to using + * PageUpdaterFactory::newPageUpdater() instead. + * + * @param Authority|UserIdentity $performer + * @param RevisionSlotsUpdate|null $forUpdate If given, allows any cached ParserOutput + * that may already have been returned via getDerivedDataUpdater to be re-used. + * + * @return PageUpdater + */ + public function newPageUpdater( $performer, ?RevisionSlotsUpdate $forUpdate = null ) { + if ( $performer instanceof Authority ) { + // TODO: Deprecate this. But better get rid of this method entirely. + $performer = $performer->getUser(); + } + + $pageUpdater = $this->getPageUpdaterFactory()->newPageUpdaterForDerivedPageDataUpdater( + $this, + $performer, + $this->getDerivedDataUpdater( $performer, null, $forUpdate, true ) + ); + + return $pageUpdater; + } + + /** * Get parser options suitable for rendering the primary article wikitext * * @see ParserOptions::newCanonical diff --git a/includes/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index 0a6d78b0bae2..9d0559a42223 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -885,6 +885,7 @@ class DefaultPreferencesFactory implements PreferencesFactory { 'type' => 'toggle', 'section' => 'personal/email', 'label-message' => 'email-allow-new-users-label', + 'help-message' => 'prefs-help-email-allow-new-users', 'disabled' => $disableEmailPrefs, 'disable-if' => [ '!==', 'disablemail', '1' ], ]; diff --git a/includes/recentchanges/ChangeTrackingEventIngress.php b/includes/recentchanges/ChangeTrackingEventIngress.php index 43bfc14e974a..2394bfce5896 100644 --- a/includes/recentchanges/ChangeTrackingEventIngress.php +++ b/includes/recentchanges/ChangeTrackingEventIngress.php @@ -132,7 +132,7 @@ class ChangeTrackingEventIngress extends EventSubscriberBase { } if ( $event->isContentChange() - && !$event->isAutomated() + && !$event->isImplicit() ) { $this->updateUserEditTrackerAfterPageUpdated( $event->getPerformer() diff --git a/includes/recentchanges/ChangesList.php b/includes/recentchanges/ChangesList.php index c89397ede429..2fc79012d28a 100644 --- a/includes/recentchanges/ChangesList.php +++ b/includes/recentchanges/ChangesList.php @@ -109,7 +109,7 @@ class ChangesList extends ContextSource { private LogFormatterFactory $logFormatterFactory; - private UserLinkRenderer $userLinkRenderer; + protected UserLinkRenderer $userLinkRenderer; /** * @param IContextSource $context diff --git a/includes/recentchanges/EnhancedChangesList.php b/includes/recentchanges/EnhancedChangesList.php index 249619f29adf..84cebe0d20e9 100644 --- a/includes/recentchanges/EnhancedChangesList.php +++ b/includes/recentchanges/EnhancedChangesList.php @@ -22,7 +22,6 @@ use MediaWiki\Context\IContextSource; use MediaWiki\Html\Html; use MediaWiki\Html\TemplateParser; use MediaWiki\MainConfigNames; -use MediaWiki\MediaWikiServices; use MediaWiki\Parser\Sanitizer; use MediaWiki\Revision\RevisionRecord; use MediaWiki\SpecialPage\SpecialPage; @@ -62,7 +61,7 @@ class EnhancedChangesList extends ChangesList { $context, $this->message, $this->linkRenderer, - MediaWikiServices::getInstance()->getUserLinkRenderer() + $this->userLinkRenderer ); $this->templateParser = new TemplateParser(); } diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index ce0783c393d8..ae54073741fd 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -629,11 +629,17 @@ abstract class SearchEngine { ->autoConvertToAllVariants( $search ); $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] ); + $origLimit = $this->limit; + $origOffset = $this->offset; foreach ( $fallbackSearches as $fbs ) { - $this->setLimitOffset( $fallbackLimit ); - $fallbackSearchResult = $this->completionSearch( $fbs ); - $results->appendAll( $fallbackSearchResult ); - $fallbackLimit -= $fallbackSearchResult->getSize(); + try { + $this->setLimitOffset( $fallbackLimit ); + $fallbackSearchResult = $this->completionSearch( $fbs ); + $results->appendAll( $fallbackSearchResult ); + $fallbackLimit -= $fallbackSearchResult->getSize(); + } finally { + $this->setLimitOffset( $origLimit, $origOffset ); + } if ( $fallbackLimit <= 0 ) { break; } diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index 5b8c79b965df..4d3c54ff0857 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -1746,7 +1746,7 @@ abstract class Skin extends ContextSource { $messageTitle = $config->get( MainConfigNames::EnableSidebarCache ) ? Title::newMainPage() : $this->getTitle(); $services = MediaWikiServices::getInstance(); - $messageCache = $services->getMessageCache(); + $messageParser = $services->getMessageParser(); $urlUtils = $services->getUrlUtils(); foreach ( $lines as $line ) { @@ -1764,7 +1764,7 @@ abstract class Skin extends ContextSource { $line = trim( $line, '* ' ); if ( strpos( $line, '|' ) !== false ) { - $line = $messageCache->transform( $line, false, null, $messageTitle ); + $line = $messageParser->transform( $line, false, null, $messageTitle ); $line = array_map( 'trim', explode( '|', $line, 2 ) ); if ( count( $line ) !== 2 ) { // Second check, could be hit by people doing diff --git a/includes/skins/SkinTemplate.php b/includes/skins/SkinTemplate.php index b8db2da8601c..3ed9a43b1f5c 100644 --- a/includes/skins/SkinTemplate.php +++ b/includes/skins/SkinTemplate.php @@ -1053,6 +1053,8 @@ class SkinTemplate extends Skin { 'user-menu' => $this->buildPersonalUrls( false ), 'notifications' => [], 'associated-pages' => [], + // Added in 1.44: a fixed position menu at bottom of page + 'dock-bottom' => [], // Legacy keys 'namespaces' => [], 'views' => [], diff --git a/includes/specialpage/ContributionsSpecialPage.php b/includes/specialpage/ContributionsSpecialPage.php index 8fd15fc3011e..313bfe225b32 100644 --- a/includes/specialpage/ContributionsSpecialPage.php +++ b/includes/specialpage/ContributionsSpecialPage.php @@ -412,7 +412,6 @@ class ContributionsSpecialPage extends IncludableSpecialPage { protected function contributionsSub( $userObj, $targetName ) { $out = $this->getOutput(); $user = $this->getUserLink( $userObj ); - $nt = $userObj->getUserPage(); $links = ''; if ( $this->shouldDisplayActionLinks( $userObj ) ) { @@ -434,42 +433,60 @@ class ContributionsSpecialPage extends IncludableSpecialPage { // For IP ranges you must give DatabaseBlock::newFromTarget the CIDR string // and not a user object. if ( IPUtils::isValidRange( $userObj->getName() ) ) { - $block = $this->blockStore - ->newFromTarget( $userObj->getName(), $userObj->getName() ); + $blocks = $this->blockStore + ->newListFromTarget( $userObj->getName(), $userObj->getName() ); } else { - $block = $this->blockStore->newFromTarget( $userObj, $userObj ); + $blocks = $this->blockStore->newListFromTarget( $userObj, $userObj ); } - if ( $block !== null && $block->getType() != Block::TYPE_AUTO ) { - if ( $block->getType() == Block::TYPE_RANGE ) { - $nt = $this->namespaceInfo->getCanonicalName( NS_USER ) - . ':' . $block->getTargetName(); + $numBlocks = 0; + $sitewide = false; + $logTargetPage = ''; + foreach ( $blocks as $block ) { + if ( $block->getType() !== Block::TYPE_AUTO ) { + $numBlocks++; + if ( $block->isSitewide() ) { + $sitewide = true; + } + $logTargetPage = $this->namespaceInfo->getCanonicalName( NS_USER ) . + ':' . $block->getTargetName(); } + } - if ( $userObj->isAnon() ) { - $msgKey = $block->isSitewide() ? - 'sp-contributions-blocked-notice-anon' : - 'sp-contributions-blocked-notice-anon-partial'; + if ( $numBlocks ) { + if ( $numBlocks === 1 ) { + if ( $userObj->isAnon() ) { + $msgKey = $sitewide ? + 'sp-contributions-blocked-notice-anon' : + 'sp-contributions-blocked-notice-anon-partial'; + } else { + $msgKey = $sitewide ? + 'sp-contributions-blocked-notice' : + 'sp-contributions-blocked-notice-partial'; + } } else { - $msgKey = $block->isSitewide() ? - 'sp-contributions-blocked-notice' : - 'sp-contributions-blocked-notice-partial'; + if ( $userObj->isAnon() ) { + $msgKey = 'sp-contributions-blocked-notice-anon-multi'; + } else { + $msgKey = 'sp-contributions-blocked-notice-multi'; + } } // Allow local styling overrides for different types of block - $class = $block->isSitewide() ? + $class = $sitewide ? 'mw-contributions-blocked-notice' : 'mw-contributions-blocked-notice-partial'; LogEventsList::showLogExtract( $out, 'block', - $nt, + $logTargetPage, '', [ 'lim' => 1, 'showIfEmpty' => false, 'msgKey' => [ $msgKey, - $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' + $userObj->getName(), # Support GENDER in 'sp-contributions-blocked-notice' + $numBlocks ], 'offset' => '', # don't use WebRequest parameter offset 'wrap' => Html::rawElement( diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index b2ba12ce523c..dca17b3e81c1 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -513,12 +513,13 @@ class SpecialPageFactory { 'UserGroupManager', 'UserIdentityLookup', 'HideUserUtils', + 'TempUserConfig', ] ], 'Block' => [ 'class' => SpecialBlock::class, 'services' => [ - 'BlockUtils', + 'BlockTargetFactory', 'BlockPermissionCheckerFactory', 'BlockUserFactory', 'DatabaseBlockStore', @@ -533,7 +534,7 @@ class SpecialPageFactory { 'class' => SpecialUnblock::class, 'services' => [ 'UnblockUserFactory', - 'BlockUtils', + 'BlockTargetFactory', 'DatabaseBlockStore', 'UserNameUtils', 'UserNamePrefixSearch', @@ -548,7 +549,7 @@ class SpecialPageFactory { 'BlockRestrictionStore', 'ConnectionProvider', 'CommentStore', - 'BlockUtils', + 'BlockTargetFactory', 'HideUserUtils', 'BlockActionInfo', 'RowCommentFormatter', @@ -562,7 +563,7 @@ class SpecialPageFactory { 'BlockRestrictionStore', 'ConnectionProvider', 'CommentStore', - 'BlockUtils', + 'BlockTargetFactory', 'HideUserUtils', 'BlockActionInfo', 'RowCommentFormatter', @@ -656,6 +657,7 @@ class SpecialPageFactory { 'UserGroupManager', 'UserIdentityLookup', 'HideUserUtils', + 'TempUserConfig', ] ], 'Listadmins' => [ @@ -745,7 +747,7 @@ class SpecialPageFactory { 'class' => SpecialRecentChanges::class, 'services' => [ 'WatchedItemStore', - 'MessageCache', + 'MessageParser', 'UserOptionsLookup', 'ChangeTagsStore', 'UserIdentityUtils', @@ -756,7 +758,7 @@ class SpecialPageFactory { 'class' => SpecialRecentChangesLinked::class, 'services' => [ 'WatchedItemStore', - 'MessageCache', + 'MessageParser', 'UserOptionsLookup', 'SearchEngineFactory', 'ChangeTagsStore', @@ -1058,6 +1060,7 @@ class SpecialPageFactory { 'ConnectionProvider', 'RevisionStore', 'CommentFormatter', + 'ChangeTagsStore', ] ], 'ExpandTemplates' => [ diff --git a/includes/specials/SpecialActiveUsers.php b/includes/specials/SpecialActiveUsers.php index 7ae24663efcd..35a96c2fca61 100644 --- a/includes/specials/SpecialActiveUsers.php +++ b/includes/specials/SpecialActiveUsers.php @@ -28,6 +28,7 @@ use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\MainConfigNames; use MediaWiki\Pager\ActiveUsersPager; use MediaWiki\SpecialPage\SpecialPage; +use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserGroupManager; use MediaWiki\User\UserIdentityLookup; use Wikimedia\Rdbms\IConnectionProvider; @@ -44,20 +45,15 @@ class SpecialActiveUsers extends SpecialPage { private UserGroupManager $userGroupManager; private UserIdentityLookup $userIdentityLookup; private HideUserUtils $hideUserUtils; + private TempUserConfig $tempUserConfig; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param UserGroupManager $userGroupManager - * @param UserIdentityLookup $userIdentityLookup - * @param HideUserUtils $hideUserUtils - */ public function __construct( LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, UserGroupManager $userGroupManager, UserIdentityLookup $userIdentityLookup, - HideUserUtils $hideUserUtils + HideUserUtils $hideUserUtils, + TempUserConfig $tempUserConfig ) { parent::__construct( 'Activeusers' ); $this->linkBatchFactory = $linkBatchFactory; @@ -65,6 +61,7 @@ class SpecialActiveUsers extends SpecialPage { $this->userGroupManager = $userGroupManager; $this->userIdentityLookup = $userIdentityLookup; $this->hideUserUtils = $hideUserUtils; + $this->tempUserConfig = $tempUserConfig; } /** @@ -99,6 +96,7 @@ class SpecialActiveUsers extends SpecialPage { $this->userGroupManager, $this->userIdentityLookup, $this->hideUserUtils, + $this->tempUserConfig, $opts ); $usersBody = $pager->getBody(); diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 9645663b38e7..c5e2b4b86208 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -43,12 +43,6 @@ class SpecialAllMessages extends SpecialPage { private IConnectionProvider $dbProvider; private LocalisationCache $localisationCache; - /** - * @param LanguageFactory $languageFactory - * @param LanguageNameUtils $languageNameUtils - * @param LocalisationCache $localisationCache - * @param IConnectionProvider $dbProvider - */ public function __construct( LanguageFactory $languageFactory, LanguageNameUtils $languageNameUtils, diff --git a/includes/specials/SpecialAncientPages.php b/includes/specials/SpecialAncientPages.php index 5e660a697d64..f6f2538a93f1 100644 --- a/includes/specials/SpecialAncientPages.php +++ b/includes/specials/SpecialAncientPages.php @@ -40,12 +40,6 @@ class SpecialAncientPages extends QueryPage { private NamespaceInfo $namespaceInfo; private ILanguageConverter $languageConverter; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialAutoblockList.php b/includes/specials/SpecialAutoblockList.php index 389428cefa24..c28673335636 100644 --- a/includes/specials/SpecialAutoblockList.php +++ b/includes/specials/SpecialAutoblockList.php @@ -22,7 +22,7 @@ namespace MediaWiki\Specials; use MediaWiki\Block\BlockActionInfo; use MediaWiki\Block\BlockRestrictionStore; -use MediaWiki\Block\BlockUtils; +use MediaWiki\Block\BlockTargetFactory; use MediaWiki\Block\HideUserUtils; use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\CommentFormatter\RowCommentFormatter; @@ -45,27 +45,17 @@ class SpecialAutoblockList extends SpecialPage { private BlockRestrictionStore $blockRestrictionStore; private IConnectionProvider $dbProvider; private CommentStore $commentStore; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private HideUserUtils $hideUserUtils; private BlockActionInfo $blockActionInfo; private RowCommentFormatter $rowCommentFormatter; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param BlockRestrictionStore $blockRestrictionStore - * @param IConnectionProvider $dbProvider - * @param CommentStore $commentStore - * @param BlockUtils $blockUtils - * @param HideUserUtils $hideUserUtils - * @param BlockActionInfo $blockActionInfo - * @param RowCommentFormatter $rowCommentFormatter - */ public function __construct( LinkBatchFactory $linkBatchFactory, BlockRestrictionStore $blockRestrictionStore, IConnectionProvider $dbProvider, CommentStore $commentStore, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, HideUserUtils $hideUserUtils, BlockActionInfo $blockActionInfo, RowCommentFormatter $rowCommentFormatter @@ -76,7 +66,7 @@ class SpecialAutoblockList extends SpecialPage { $this->blockRestrictionStore = $blockRestrictionStore; $this->dbProvider = $dbProvider; $this->commentStore = $commentStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->hideUserUtils = $hideUserUtils; $this->blockActionInfo = $blockActionInfo; $this->rowCommentFormatter = $rowCommentFormatter; @@ -136,7 +126,7 @@ class SpecialAutoblockList extends SpecialPage { $this->getContext(), $this->blockActionInfo, $this->blockRestrictionStore, - $this->blockUtils, + $this->blockTargetFactory, $this->hideUserUtils, $this->commentStore, $this->linkBatchFactory, diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index b3b55945dcbc..3f6ae79420b1 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -23,16 +23,22 @@ namespace MediaWiki\Specials; use ErrorPageError; use HtmlArmor; use LogEventsList; +use MediaWiki\Block\AnonIpBlockTarget; use MediaWiki\Block\BlockActionInfo; use MediaWiki\Block\BlockPermissionCheckerFactory; +use MediaWiki\Block\BlockTarget; +use MediaWiki\Block\BlockTargetFactory; +use MediaWiki\Block\BlockTargetWithIp; +use MediaWiki\Block\BlockTargetWithUserPage; use MediaWiki\Block\BlockUser; use MediaWiki\Block\BlockUserFactory; -use MediaWiki\Block\BlockUtils; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Block\DatabaseBlockStore; +use MediaWiki\Block\RangeBlockTarget; use MediaWiki\Block\Restriction\ActionRestriction; use MediaWiki\Block\Restriction\NamespaceRestriction; use MediaWiki\Block\Restriction\PageRestriction; +use MediaWiki\Block\UserBlockTarget; use MediaWiki\CommentStore\CommentStore; use MediaWiki\Context\IContextSource; use MediaWiki\Html\Html; @@ -41,8 +47,6 @@ use MediaWiki\Language\Language; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Message\Message; -use MediaWiki\Page\PageReference; -use MediaWiki\Page\PageReferenceValue; use MediaWiki\Permissions\Authority; use MediaWiki\Request\WebRequest; use MediaWiki\SpecialPage\FormSpecialPage; @@ -59,7 +63,6 @@ use OOUI\FieldLayout; use OOUI\HtmlSnippet; use OOUI\LabelWidget; use OOUI\Widget; -use Wikimedia\IPUtils; use Wikimedia\Message\MessageSpecifier; /** @@ -70,7 +73,7 @@ use Wikimedia\Message\MessageSpecifier; */ class SpecialBlock extends FormSpecialPage { - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private BlockPermissionCheckerFactory $blockPermissionCheckerFactory; private BlockUserFactory $blockUserFactory; private DatabaseBlockStore $blockStore; @@ -79,15 +82,12 @@ class SpecialBlock extends FormSpecialPage { private BlockActionInfo $blockActionInfo; private TitleFormatter $titleFormatter; - /** @var UserIdentity|string|null User to be blocked, as passed either by parameter + /** @var BlockTarget|null User to be blocked, as passed either by parameter * (url?wpTarget=Foo) or as subpage (Special:Block/Foo) */ protected $target; - /** @var int DatabaseBlock::TYPE_ constant */ - protected $type; - - /** @var User|string The previous block target */ + /** @var BlockTarget|null The previous block target */ protected $previousTarget; /** @var bool Whether the previous submission of the form asked for HideUser */ @@ -111,19 +111,8 @@ class SpecialBlock extends FormSpecialPage { private NamespaceInfo $namespaceInfo; - /** - * @param BlockUtils $blockUtils - * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory - * @param BlockUserFactory $blockUserFactory - * @param DatabaseBlockStore $blockStore - * @param UserNameUtils $userNameUtils - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param BlockActionInfo $blockActionInfo - * @param TitleFormatter $titleFormatter - * @param NamespaceInfo $namespaceInfo - */ public function __construct( - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, BlockPermissionCheckerFactory $blockPermissionCheckerFactory, BlockUserFactory $blockUserFactory, DatabaseBlockStore $blockStore, @@ -135,7 +124,7 @@ class SpecialBlock extends FormSpecialPage { ) { parent::__construct( 'Block', 'block' ); - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory; $this->blockUserFactory = $blockUserFactory; $this->blockStore = $blockStore; @@ -164,9 +153,8 @@ class SpecialBlock extends FormSpecialPage { // Ensure wgUseCodexSpecialBlock is set when ?usecodex=1 is used. $this->codexFormData[ 'wgUseCodexSpecialBlock' ] = true; $this->codexFormData[ 'blockEnableMultiblocks' ] = $this->useMultiblocks; - $this->codexFormData[ 'blockTargetUser' ] = $this->target instanceof UserIdentity ? - $this->target->getName() : - $this->target ?? null; + $this->codexFormData[ 'blockTargetUser' ] = + $this->target ? $this->target->toString() : null; $authority = $this->getAuthority(); $this->codexFormData[ 'blockShowSuppressLog' ] = $authority->isAllowed( 'suppressionlog' ); $this->codexFormData[ 'canDeleteLogEntry' ] = $authority->isAllowed( 'deletelogentry' ); @@ -219,15 +207,15 @@ class SpecialBlock extends FormSpecialPage { // need to extract *every* variable from the form just for processing here, but // there are legitimate uses for some variables $request = $this->getRequest(); - [ $this->target, $this->type ] = $this->getTargetAndTypeInternal( $par, $request ); - if ( $this->target instanceof UserIdentity ) { + $this->target = $this->getTargetInternal( $par, $request ); + if ( $this->target instanceof UserBlockTarget ) { // Set the 'relevant user' in the skin, so it displays links like Contributions, // User logs, UserRights, etc. - $this->getSkin()->setRelevantUser( $this->target ); + $this->getSkin()->setRelevantUser( $this->target->getUserIdentity() ); } - [ $this->previousTarget, /*...*/ ] = $this->blockUtils - ->parseBlockTarget( $request->getVal( 'wpPreviousTarget' ) ); + $this->previousTarget = $this->blockTargetFactory + ->newFromString( $request->getVal( 'wpPreviousTarget' ) ); $this->requestedHideUser = $request->getBool( 'wpHideUser' ); if ( $this->useCodex ) { @@ -375,7 +363,7 @@ class SpecialBlock extends FormSpecialPage { 'required' => true, 'placeholder' => $this->msg( 'block-target-placeholder' )->text(), 'validation-callback' => function ( $value, $alldata, $form ) { - $status = $this->blockUtils->validateTarget( $value ); + $status = $this->blockTargetFactory->newFromString( $value )->validateForCreation(); if ( !$status->isOK() ) { $errors = $status->getMessages(); return $form->msg( $errors[0] ); @@ -502,7 +490,7 @@ class SpecialBlock extends FormSpecialPage { } $defaultExpiry = $this->msg( 'ipb-default-expiry' )->inContentLanguage(); - if ( $this->type === DatabaseBlock::TYPE_RANGE || $this->type === DatabaseBlock::TYPE_IP ) { + if ( $this->target instanceof BlockTargetWithIp ) { $defaultExpiryIP = $this->msg( 'ipb-default-expiry-ip' )->inContentLanguage(); if ( !$defaultExpiryIP->isDisabled() ) { $defaultExpiry = $defaultExpiryIP; @@ -595,6 +583,8 @@ class SpecialBlock extends FormSpecialPage { 'cssclass' => 'mw-block-confirm', ]; + $this->validateTarget(); + // (T382496) Only load the modified defaults from a previous // block if multiblocks are not enabled if ( !$this->useMultiblocks ) { @@ -619,6 +609,35 @@ class SpecialBlock extends FormSpecialPage { } /** + * Validate the target, setting preErrors if necessary. + */ + private function validateTarget(): void { + if ( !$this->target ) { + return; + } + + $status = $this->target->validateForCreation(); + $this->codexFormData[ 'blockTargetExists' ] = true; + + if ( !$status->isOK() ) { + $errors = $status->getMessages( 'error' ); + $this->preErrors = array_merge( $this->preErrors, $errors ); + + // Remove top-level errors that are later handled per-field in Codex. + if ( $this->useCodex ) { + $this->preErrors = array_filter( $this->preErrors, function ( $error ) { + if ( $error->getKey() === 'nosuchusershort' ) { + // Avoids us having to re-query the API to validate the user. + $this->codexFormData[ 'blockTargetExists' ] = false; + return false; + } + return true; + } ); + } + } + } + + /** * If the user has already been blocked with similar settings, load that block * and change the defaults for the form fields to match the existing settings. * @param array &$fields HTMLForm descriptor array @@ -627,14 +646,6 @@ class SpecialBlock extends FormSpecialPage { // This will be overwritten by request data $fields['Target']['default'] = (string)$this->target; - if ( $this->target ) { - $status = $this->blockUtils->validateTarget( $this->target ); - if ( !$status->isOK() ) { - $errors = $status->getMessages( 'error' ); - $this->preErrors = array_merge( $this->preErrors, $errors ); - } - } - // This won't be $fields['PreviousTarget']['default'] = (string)$this->target; @@ -643,8 +654,8 @@ class SpecialBlock extends FormSpecialPage { // Populate fields if there is a block that is not an autoblock; if it is a range // block, only populate the fields if the range is the same as $this->target if ( $block instanceof DatabaseBlock && $block->getType() !== DatabaseBlock::TYPE_AUTO - && ( $this->type != DatabaseBlock::TYPE_RANGE - || ( $this->target && $block->isBlocking( $this->target ) ) ) + && ( !( $this->target instanceof RangeBlockTarget ) + || $block->isBlocking( $this->target ) ) ) { $fields['HardBlock']['default'] = $block->isHardblock(); $fields['CreateAccount']['default'] = $block->isCreateAccountBlocked(); @@ -791,13 +802,9 @@ class SpecialBlock extends FormSpecialPage { $otherBlockMessages = []; if ( $this->target !== null ) { - $targetName = $this->target; - if ( $this->target instanceof UserIdentity ) { - $targetName = $this->target->getName(); - } // Get other blocks, i.e. from GlobalBlocking or TorBlock extension $this->getHookRunner()->onOtherBlockLogLink( - $otherBlockMessages, $targetName ); + $otherBlockMessages, $this->target->toString() ); if ( count( $otherBlockMessages ) ) { $s = Html::rawElement( @@ -836,21 +843,21 @@ class SpecialBlock extends FormSpecialPage { $linkRenderer = $this->getLinkRenderer(); // Link to the user's contributions, if applicable - if ( $this->target instanceof UserIdentity ) { - $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->target->getName() ); + if ( $this->target instanceof BlockTargetWithUserPage ) { + $contribsPage = SpecialPage::getTitleFor( 'Contributions', (string)$this->target ); $links[] = $linkRenderer->makeLink( $contribsPage, - $this->msg( 'ipb-blocklist-contribs', $this->target->getName() )->text() + $this->msg( 'ipb-blocklist-contribs', (string)$this->target )->text() ); } // Link to unblock the specified user, or to a blank unblock form - if ( $this->target instanceof UserIdentity ) { + if ( $this->target instanceof BlockTargetWithUserPage ) { $message = $this->msg( 'ipb-unblock-addr', - wfEscapeWikiText( $this->target->getName() ) + wfEscapeWikiText( (string)$this->target ) )->parse(); - $list = SpecialPage::getTitleFor( 'Unblock', $this->target->getName() ); + $list = SpecialPage::getTitleFor( 'Unblock', (string)$this->target ); } else { $message = $this->msg( 'ipb-unblock' )->parse(); $list = SpecialPage::getTitleFor( 'Unblock' ); @@ -882,8 +889,8 @@ class SpecialBlock extends FormSpecialPage { $this->getLanguage()->pipeList( $links ) ); - $userPage = self::getTargetUserTitle( $this->target ); - if ( $userPage ) { + if ( $this->target ) { + $userPage = $this->target->getLogPage(); // Get relevant extracts from the block and suppression logs, if possible $out = ''; @@ -929,24 +936,6 @@ class SpecialBlock extends FormSpecialPage { } /** - * Get a user page target for things like logs. - * This handles account and IP range targets. - * @param UserIdentity|string|null $target - * @return PageReference|null - */ - protected static function getTargetUserTitle( $target ): ?PageReference { - if ( $target instanceof UserIdentity ) { - return PageReferenceValue::localReference( NS_USER, $target->getName() ); - } - - if ( is_string( $target ) && IPUtils::isIPAddress( $target ) ) { - return PageReferenceValue::localReference( NS_USER, $target ); - } - - return null; - } - - /** * Get the target and type, given the request and the subpage parameter. * Several parameters are handled for backwards compatability. 'wpTarget' is * prioritized, since it matches the HTML form. @@ -954,10 +943,9 @@ class SpecialBlock extends FormSpecialPage { * @param string|null $par Subpage parameter passed to setup, or data value from * the HTMLForm * @param WebRequest $request Try and get data from a request too - * @return array [ UserIdentity|string|null, DatabaseBlock::TYPE_ constant|null ] - * @phan-return array{0:UserIdentity|string|null,1:int|null} + * @return BlockTarget|null */ - private function getTargetAndTypeInternal( ?string $par, WebRequest $request ) { + private function getTargetInternal( ?string $par, WebRequest $request ) { $possibleTargets = [ $request->getVal( 'wpTarget', null ), $par, @@ -966,14 +954,14 @@ class SpecialBlock extends FormSpecialPage { $request->getVal( 'wpBlockAddress', null ), ]; foreach ( $possibleTargets as $possibleTarget ) { - $targetAndType = $this->blockUtils - ->parseBlockTarget( $possibleTarget ); + $target = $this->blockTargetFactory + ->newFromString( $possibleTarget ); // If type is not null then target is valid - if ( $targetAndType[ 1 ] !== null ) { + if ( $target !== null ) { break; } } - return $targetAndType; + return $target; } /** @@ -992,7 +980,7 @@ class SpecialBlock extends FormSpecialPage { $data, $context->getAuthority(), $services->getBlockUserFactory(), - $services->getBlockUtils() + $services->getBlockTargetFactory() ); } @@ -1003,14 +991,14 @@ class SpecialBlock extends FormSpecialPage { * @param array $data * @param Authority $performer * @param BlockUserFactory $blockUserFactory - * @param BlockUtils $blockUtils + * @param BlockTargetFactory $blockTargetFactory * @return bool|string|array|Status */ private static function processFormInternal( array $data, Authority $performer, BlockUserFactory $blockUserFactory, - BlockUtils $blockUtils + BlockTargetFactory $blockTargetFactory ) { // Temporarily access service container until the feature flag is removed: T280532 $enablePartialActionBlocks = MediaWikiServices::getInstance() @@ -1030,19 +1018,17 @@ class SpecialBlock extends FormSpecialPage { } /** @var User $target */ - [ $target, $type ] = $blockUtils->parseBlockTarget( $data['Target'] ); - if ( $type == DatabaseBlock::TYPE_USER ) { - $target = $target->getName(); - + $target = $blockTargetFactory->newFromString( $data['Target'] ); + if ( $target instanceof UserBlockTarget ) { // Give admins a heads-up before they go and block themselves. Much messier // to do this for IPs, but it's pretty unlikely they'd ever get the 'block' // permission anyway, although the code does allow for it. // Note: Important to use $target instead of $data['Target'] // since both $data['PreviousTarget'] and $target are normalized - // but $data['target'] gets overridden by (non-normalized) request variable + // but $data['Target'] gets overridden by (non-normalized) request variable // from previous request. - if ( $target === $performer->getUser()->getName() && - ( $data['PreviousTarget'] !== $target || !$data['Confirm'] ) + if ( $target->toString() === $performer->getUser()->getName() && + ( $data['PreviousTarget'] !== $target->toString() || !$data['Confirm'] ) ) { return [ 'ipb-blockingself', 'ipb-confirmaction' ]; } @@ -1050,9 +1036,7 @@ class SpecialBlock extends FormSpecialPage { if ( $data['HideUser'] && !$data['Confirm'] ) { return [ 'ipb-confirmhideuser', 'ipb-confirmaction' ]; } - } elseif ( $type == DatabaseBlock::TYPE_IP ) { - $target = $target->getName(); - } elseif ( $type != DatabaseBlock::TYPE_RANGE ) { + } elseif ( !( $target instanceof AnonIpBlockTarget || $target instanceof RangeBlockTarget ) ) { // This should have been caught in the form field validation return [ 'badipaddress' ]; } @@ -1120,7 +1104,7 @@ class SpecialBlock extends FormSpecialPage { // Indicates whether the user is confirming the block and is aware of // the conflict (did not change the block target in the meantime) $blockNotConfirmed = !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data ) - && $data['PreviousTarget'] !== $target ); + && $data['PreviousTarget'] !== $target->toString() ); // Special case for API - T34434 $reblockNotAllowed = ( array_key_exists( 'Reblock', $data ) && !$data['Reblock'] ); @@ -1134,7 +1118,7 @@ class SpecialBlock extends FormSpecialPage { if ( // Can't watch a range block - $type != DatabaseBlock::TYPE_RANGE + $target instanceof BlockTargetWithUserPage // Technically a wiki can be configured to allow anonymous users to place blocks, // in which case the 'Watch' field isn't included in the form shown, and we should @@ -1144,7 +1128,7 @@ class SpecialBlock extends FormSpecialPage { ) { MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights( $performer->getUser(), - Title::makeTitle( NS_USER, $target ) + Title::newFromPageReference( $target->getUserPage() ) ); } @@ -1211,7 +1195,7 @@ class SpecialBlock extends FormSpecialPage { $data, $this->getAuthority(), $this->blockUserFactory, - $this->blockUtils + $this->blockTargetFactory ); } @@ -1222,7 +1206,7 @@ class SpecialBlock extends FormSpecialPage { public function onSuccess() { $out = $this->getOutput(); $out->setPageTitleMsg( $this->msg( 'blockipsuccesssub' ) ); - $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( $this->target ) ); + $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( (string)$this->target ) ); } /** diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 1f6f3e6189e7..9fd57e357d42 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -20,12 +20,16 @@ namespace MediaWiki\Specials; +use InvalidArgumentException; +use MediaWiki\Block\AutoBlockTarget; use MediaWiki\Block\BlockActionInfo; use MediaWiki\Block\BlockRestrictionStore; -use MediaWiki\Block\BlockUtils; -use MediaWiki\Block\DatabaseBlock; +use MediaWiki\Block\BlockTarget; +use MediaWiki\Block\BlockTargetFactory; +use MediaWiki\Block\BlockTargetWithIp; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Block\HideUserUtils; +use MediaWiki\Block\UserBlockTarget; use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\CommentFormatter\RowCommentFormatter; use MediaWiki\CommentStore\CommentStore; @@ -34,7 +38,6 @@ use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Pager\BlockListPager; use MediaWiki\SpecialPage\SpecialPage; use MediaWiki\User\TempUser\TempUserConfig; -use Wikimedia\IPUtils; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IExpression; use Wikimedia\Rdbms\IReadableDatabase; @@ -61,7 +64,7 @@ class SpecialBlockList extends SpecialPage { private BlockRestrictionStore $blockRestrictionStore; private IConnectionProvider $dbProvider; private CommentStore $commentStore; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private HideUserUtils $hideUserUtils; private BlockActionInfo $blockActionInfo; private RowCommentFormatter $rowCommentFormatter; @@ -73,7 +76,7 @@ class SpecialBlockList extends SpecialPage { BlockRestrictionStore $blockRestrictionStore, IConnectionProvider $dbProvider, CommentStore $commentStore, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, HideUserUtils $hideUserUtils, BlockActionInfo $blockActionInfo, RowCommentFormatter $rowCommentFormatter, @@ -86,7 +89,7 @@ class SpecialBlockList extends SpecialPage { $this->blockRestrictionStore = $blockRestrictionStore; $this->dbProvider = $dbProvider; $this->commentStore = $commentStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->hideUserUtils = $hideUserUtils; $this->blockActionInfo = $blockActionInfo; $this->rowCommentFormatter = $rowCommentFormatter; @@ -198,31 +201,11 @@ class SpecialBlockList extends SpecialPage { $conds = []; $db = $this->getDB(); + // Add target conditions if ( $this->target !== '' ) { - [ $target, $type ] = $this->blockUtils->parseBlockTarget( $this->target ); - - switch ( $type ) { - case DatabaseBlock::TYPE_ID: - case DatabaseBlock::TYPE_AUTO: - $conds['bl_id'] = $target; - break; - - case DatabaseBlock::TYPE_IP: - case DatabaseBlock::TYPE_RANGE: - [ $start, $end ] = IPUtils::parseRange( $target ); - $conds[] = $this->blockStore->getRangeCond( $start, $end ); - $conds['bt_auto'] = 0; - break; - - case DatabaseBlock::TYPE_USER: - if ( $target->getId() ) { - $conds['bt_user'] = $target->getId(); - $conds['bt_auto'] = 0; - } else { - // No such user - $conds[] = '1=0'; - } - break; + $target = $this->blockTargetFactory->newFromString( $this->target ); + if ( $target ) { + $conds = $this->getTargetConds( $target ); } } @@ -285,7 +268,7 @@ class SpecialBlockList extends SpecialPage { $this->getContext(), $this->blockActionInfo, $this->blockRestrictionStore, - $this->blockUtils, + $this->blockTargetFactory, $this->hideUserUtils, $this->commentStore, $this->linkBatchFactory, @@ -298,6 +281,45 @@ class SpecialBlockList extends SpecialPage { } /** + * Get conditions matching a parsed block target. + * + * The details are different from other similarly named functions elsewhere: + * - If an IP address or range is requested, autoblocks are not shown. + * - Requests for single IP addresses include range blocks covering the + * address. This is like a "vague target" query in DatabaseBlockStore, + * except that autoblocks are excluded. + * - If a named user doesn't exist, it is assumed that there are no blocks. + * + * @param BlockTarget $target + * @return array + */ + private function getTargetConds( BlockTarget $target ) { + if ( $target instanceof AutoBlockTarget ) { + return [ 'bl_id' => $target->getId() ]; + } + if ( $target instanceof BlockTargetWithIp ) { + $range = $target->toHexRange(); + return [ + $this->blockStore->getRangeCond( $range[0], $range[1] ), + 'bt_auto' => 0 + ]; + } + if ( $target instanceof UserBlockTarget ) { + $user = $target->getUserIdentity(); + if ( $user->getId() ) { + return [ + 'bt_user' => $user->getId(), + 'bt_auto' => 0 + ]; + } else { + // No such user + return [ '1=0' ]; + } + } + throw new InvalidArgumentException( 'Invalid block target type' ); + } + + /** * Show the list of blocked accounts matching the actual filter. * @param BlockListPager $pager The BlockListPager instance for this page */ diff --git a/includes/specials/SpecialBotPasswords.php b/includes/specials/SpecialBotPasswords.php index 96f0ef35ecf3..c7bda313e44f 100644 --- a/includes/specials/SpecialBotPasswords.php +++ b/includes/specials/SpecialBotPasswords.php @@ -64,13 +64,6 @@ class SpecialBotPasswords extends FormSpecialPage { private GrantsInfo $grantsInfo; private GrantsLocalization $grantsLocalization; - /** - * @param PasswordFactory $passwordFactory - * @param AuthManager $authManager - * @param CentralIdLookup $centralIdLookup - * @param GrantsInfo $grantsInfo - * @param GrantsLocalization $grantsLocalization - */ public function __construct( PasswordFactory $passwordFactory, AuthManager $authManager, diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index 21065716cd40..2dee3569238d 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -41,11 +41,6 @@ class SpecialBrokenRedirects extends QueryPage { private IContentHandlerFactory $contentHandlerFactory; - /** - * @param IContentHandlerFactory $contentHandlerFactory - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( IContentHandlerFactory $contentHandlerFactory, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialChangeContentModel.php b/includes/specials/SpecialChangeContentModel.php index d29c602936a7..e2376a5075cd 100644 --- a/includes/specials/SpecialChangeContentModel.php +++ b/includes/specials/SpecialChangeContentModel.php @@ -36,15 +36,6 @@ class SpecialChangeContentModel extends FormSpecialPage { private SearchEngineFactory $searchEngineFactory; private CollationFactory $collationFactory; - /** - * @param IContentHandlerFactory $contentHandlerFactory - * @param ContentModelChangeFactory $contentModelChangeFactory - * @param SpamChecker $spamChecker - * @param RevisionLookup $revisionLookup - * @param WikiPageFactory $wikiPageFactory - * @param SearchEngineFactory $searchEngineFactory - * @param CollationFactory $collationFactory - */ public function __construct( IContentHandlerFactory $contentHandlerFactory, ContentModelChangeFactory $contentModelChangeFactory, diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index a5a5ab926561..96af53e127d1 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -52,21 +52,6 @@ class SpecialContributions extends ContributionsSpecialPage { private TempUserConfig $tempUserConfig; private ?ContribsPager $pager = null; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param PermissionManager $permissionManager - * @param IConnectionProvider $dbProvider - * @param RevisionStore $revisionStore - * @param NamespaceInfo $namespaceInfo - * @param UserNameUtils $userNameUtils - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param UserOptionsLookup $userOptionsLookup - * @param CommentFormatter $commentFormatter - * @param UserFactory $userFactory - * @param UserIdentityLookup $userIdentityLookup - * @param DatabaseBlockStore $blockStore - * @param TempUserConfig $tempUserConfig - */ public function __construct( LinkBatchFactory $linkBatchFactory, PermissionManager $permissionManager, diff --git a/includes/specials/SpecialCreateAccount.php b/includes/specials/SpecialCreateAccount.php index 8db8b9397bbf..68251c68b42b 100644 --- a/includes/specials/SpecialCreateAccount.php +++ b/includes/specials/SpecialCreateAccount.php @@ -54,11 +54,6 @@ class SpecialCreateAccount extends LoginSignupSpecialPage { private UserIdentityUtils $identityUtils; - /** - * @param AuthManager $authManager - * @param FormatterFactory $formatterFactory - * @param UserIdentityUtils $identityUtils - */ public function __construct( AuthManager $authManager, FormatterFactory $formatterFactory, diff --git a/includes/specials/SpecialDeadendPages.php b/includes/specials/SpecialDeadendPages.php index 259763fb8357..8e6aff5af3e1 100644 --- a/includes/specials/SpecialDeadendPages.php +++ b/includes/specials/SpecialDeadendPages.php @@ -35,12 +35,6 @@ class SpecialDeadendPages extends PageQueryPage { private NamespaceInfo $namespaceInfo; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 05981d7acd80..d95efbde2f38 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -53,21 +53,6 @@ class SpecialDeletedContributions extends ContributionsSpecialPage { private LinkBatchFactory $linkBatchFactory; private TempUserConfig $tempUserConfig; - /** - * @param PermissionManager $permissionManager - * @param IConnectionProvider $dbProvider - * @param RevisionStore $revisionStore - * @param NamespaceInfo $namespaceInfo - * @param UserNameUtils $userNameUtils - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param UserOptionsLookup $userOptionsLookup - * @param CommentFormatter $commentFormatter - * @param LinkBatchFactory $linkBatchFactory - * @param UserFactory $userFactory - * @param UserIdentityLookup $userIdentityLookup - * @param DatabaseBlockStore $blockStore - * @param TempUserConfig $tempUserConfig - */ public function __construct( PermissionManager $permissionManager, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index 902fe6c071f1..4aa273f0763b 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -44,11 +44,6 @@ class SpecialDoubleRedirects extends QueryPage { private IContentHandlerFactory $contentHandlerFactory; private LinkBatchFactory $linkBatchFactory; - /** - * @param IContentHandlerFactory $contentHandlerFactory - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - */ public function __construct( IContentHandlerFactory $contentHandlerFactory, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialEditTags.php b/includes/specials/SpecialEditTags.php index c786d220fa20..e5f0012cb2e3 100644 --- a/includes/specials/SpecialEditTags.php +++ b/includes/specials/SpecialEditTags.php @@ -70,11 +70,6 @@ class SpecialEditTags extends UnlistedSpecialPage { private PermissionManager $permissionManager; private ChangeTagsStore $changeTagsStore; - /** - * @inheritDoc - * - * @param PermissionManager $permissionManager - */ public function __construct( PermissionManager $permissionManager, ChangeTagsStore $changeTagsStore ) { parent::__construct( 'EditTags', 'changetags' ); diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index 38401055d368..1238f647c82d 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -90,15 +90,6 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { /** @var int|false where the value is one of the EDIT_ prefixed constants (e.g. EDIT_NORMAL) */ private $currentMode; - /** - * @param WatchedItemStoreInterface|null $watchedItemStore - * @param TitleParser|null $titleParser - * @param GenderCache|null $genderCache - * @param LinkBatchFactory|null $linkBatchFactory - * @param NamespaceInfo|null $nsInfo - * @param WikiPageFactory|null $wikiPageFactory - * @param WatchlistManager|null $watchlistManager - */ public function __construct( ?WatchedItemStoreInterface $watchedItemStore = null, ?TitleParser $titleParser = null, diff --git a/includes/specials/SpecialEmailUser.php b/includes/specials/SpecialEmailUser.php index 2e156a143be0..1dcda4554a92 100644 --- a/includes/specials/SpecialEmailUser.php +++ b/includes/specials/SpecialEmailUser.php @@ -51,13 +51,6 @@ class SpecialEmailUser extends SpecialPage { private EmailUserFactory $emailUserFactory; private UserFactory $userFactory; - /** - * @param UserNameUtils $userNameUtils - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param UserOptionsLookup $userOptionsLookup - * @param EmailUserFactory $emailUserFactory - * @param UserFactory $userFactory - */ public function __construct( UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index dedcc44a1d1f..e7bc4e944776 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -52,11 +52,6 @@ class SpecialExpandTemplates extends SpecialPage { private UserOptionsLookup $userOptionsLookup; private TidyDriverBase $tidy; - /** - * @param ParserFactory $parserFactory - * @param UserOptionsLookup $userOptionsLookup - * @param TidyDriverBase $tidy - */ public function __construct( ParserFactory $parserFactory, UserOptionsLookup $userOptionsLookup, @@ -138,7 +133,6 @@ class SpecialExpandTemplates extends SpecialPage { $rawhtml = MediaWikiServices::getInstance()->getDefaultOutputPipeline() ->run( $pout, $options, [ 'enableSectionEditLinks' => false ] )->getContentHolderText(); if ( $generateRawHtml && $rawhtml !== '' ) { - // @phan-suppress-next-line SecurityCheck-DoubleEscaped Wanted here to display the html $out->addHTML( $this->makeOutput( $rawhtml, 'expand_templates_html_output' ) ); } diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index e0d618012e4e..413fc8cbec21 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -53,12 +53,6 @@ class SpecialExport extends SpecialPage { private TitleFormatter $titleFormatter; private LinksMigration $linksMigration; - /** - * @param IConnectionProvider $dbProvider - * @param WikiExporterFactory $wikiExporterFactory - * @param TitleFormatter $titleFormatter - * @param LinksMigration $linksMigration - */ public function __construct( IConnectionProvider $dbProvider, WikiExporterFactory $wikiExporterFactory, diff --git a/includes/specials/SpecialFewestRevisions.php b/includes/specials/SpecialFewestRevisions.php index b8fb819aa7c3..3d826a40f789 100644 --- a/includes/specials/SpecialFewestRevisions.php +++ b/includes/specials/SpecialFewestRevisions.php @@ -46,12 +46,6 @@ class SpecialFewestRevisions extends QueryPage { private NamespaceInfo $namespaceInfo; private ILanguageConverter $languageConverter; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index 798c41176b8c..76ae5509403c 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -59,12 +59,6 @@ class SpecialFileDuplicateSearch extends SpecialPage { private SearchEngineFactory $searchEngineFactory; private ILanguageConverter $languageConverter; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param RepoGroup $repoGroup - * @param SearchEngineFactory $searchEngineFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( LinkBatchFactory $linkBatchFactory, RepoGroup $repoGroup, diff --git a/includes/specials/SpecialInterwiki.php b/includes/specials/SpecialInterwiki.php index 28985bee1a93..4679bf710f0f 100644 --- a/includes/specials/SpecialInterwiki.php +++ b/includes/specials/SpecialInterwiki.php @@ -35,13 +35,6 @@ class SpecialInterwiki extends SpecialPage { private array $virtualDomainsMapping; private bool $interwikiMagic; - /** - * @param Language $contLang - * @param InterwikiLookup $interwikiLookup - * @param LanguageNameUtils $languageNameUtils - * @param UrlUtils $urlUtils - * @param IConnectionProvider $dbProvider - */ public function __construct( Language $contLang, InterwikiLookup $interwikiLookup, diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index 8da3d814737a..76e5f572df38 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -60,11 +60,6 @@ class SpecialLinkSearch extends QueryPage { $this->mProt = $params['protocol']; } - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param UrlUtils $urlUtils - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialListDuplicatedFiles.php b/includes/specials/SpecialListDuplicatedFiles.php index 64b293a75e7d..d078887771fa 100644 --- a/includes/specials/SpecialListDuplicatedFiles.php +++ b/includes/specials/SpecialListDuplicatedFiles.php @@ -23,6 +23,8 @@ namespace MediaWiki\Specials; use MediaWiki\Cache\LinkBatchFactory; +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; use MediaWiki\SpecialPage\QueryPage; use MediaWiki\SpecialPage\SpecialPage; use MediaWiki\Title\Title; @@ -40,6 +42,7 @@ use Wikimedia\Rdbms\IResultWrapper; * @author Brian Wolff */ class SpecialListDuplicatedFiles extends QueryPage { + private int $migrationStage; public function __construct( IConnectionProvider $dbProvider, @@ -48,6 +51,9 @@ class SpecialListDuplicatedFiles extends QueryPage { parent::__construct( 'ListDuplicatedFiles' ); $this->setDatabaseProvider( $dbProvider ); $this->setLinkBatchFactory( $linkBatchFactory ); + $this->migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( + MainConfigNames::FileSchemaMigrationStage + ); } public function isExpensive() { @@ -63,22 +69,37 @@ class SpecialListDuplicatedFiles extends QueryPage { * * A cheaper (but less useful) version of this * query would be to not care how many duplicates a - * particular file has, and do a self-join on image table. + * particular file has, and do a self-join on image or file table. * However this version should be no more expensive then * Special:MostLinked, which seems to get handled fine * with however we are doing cached special pages. * @return array */ public function getQueryInfo() { + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $tables = [ 'image' ]; + $nameField = 'img_name'; + $hashField = 'img_sha1'; + $conds = []; + $joins = []; + } else { + $tables = [ 'file', 'filerevision' ]; + $nameField = 'file_name'; + $hashField = 'fr_sha1'; + $conds = [ 'file_deleted' => 0 ]; + $joins = [ 'filerevision' => [ 'JOIN', 'file_latest = fr_id' ] ]; + } return [ - 'tables' => [ 'image' ], + 'tables' => $tables, 'fields' => [ 'namespace' => NS_FILE, - 'title' => 'MIN(img_name)', + 'title' => "MIN($nameField)", 'value' => 'count(*)' ], + 'conds' => $conds, + 'join_conds' => $joins, 'options' => [ - 'GROUP BY' => 'img_sha1', + 'GROUP BY' => $hashField, 'HAVING' => 'count(*) > 1', ], ]; diff --git a/includes/specials/SpecialListFiles.php b/includes/specials/SpecialListFiles.php index 33e39baadcf0..074bd1d3a240 100644 --- a/includes/specials/SpecialListFiles.php +++ b/includes/specials/SpecialListFiles.php @@ -46,15 +46,6 @@ class SpecialListFiles extends IncludableSpecialPage { private RowCommentFormatter $rowCommentFormatter; private LinkBatchFactory $linkBatchFactory; - /** - * @param RepoGroup $repoGroup - * @param IConnectionProvider $dbProvider - * @param CommentStore $commentStore - * @param UserNameUtils $userNameUtils - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param RowCommentFormatter $rowCommentFormatter - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( RepoGroup $repoGroup, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialListGroupRights.php b/includes/specials/SpecialListGroupRights.php index 0154df312365..717cb6064120 100644 --- a/includes/specials/SpecialListGroupRights.php +++ b/includes/specials/SpecialListGroupRights.php @@ -48,12 +48,6 @@ class SpecialListGroupRights extends SpecialPage { private ILanguageConverter $languageConverter; private GroupPermissionsLookup $groupPermissionsLookup; - /** - * @param NamespaceInfo $nsInfo - * @param UserGroupManager $userGroupManager - * @param LanguageConverterFactory $languageConverterFactory - * @param GroupPermissionsLookup $groupPermissionsLookup - */ public function __construct( NamespaceInfo $nsInfo, UserGroupManager $userGroupManager, diff --git a/includes/specials/SpecialListRedirects.php b/includes/specials/SpecialListRedirects.php index 6babf6f7a8d0..d2689dc138d7 100644 --- a/includes/specials/SpecialListRedirects.php +++ b/includes/specials/SpecialListRedirects.php @@ -46,12 +46,6 @@ class SpecialListRedirects extends QueryPage { private WikiPageFactory $wikiPageFactory; private RedirectLookup $redirectLookup; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param WikiPageFactory $wikiPageFactory - * @param RedirectLookup $redirectLookup - */ public function __construct( LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialListUsers.php b/includes/specials/SpecialListUsers.php index 87d903c0070a..971ffcdb613a 100644 --- a/includes/specials/SpecialListUsers.php +++ b/includes/specials/SpecialListUsers.php @@ -29,6 +29,7 @@ use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\Html\Html; use MediaWiki\Pager\UsersPager; use MediaWiki\SpecialPage\IncludableSpecialPage; +use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserGroupManager; use MediaWiki\User\UserIdentityLookup; use Wikimedia\Rdbms\IConnectionProvider; @@ -45,20 +46,15 @@ class SpecialListUsers extends IncludableSpecialPage { private UserGroupManager $userGroupManager; private UserIdentityLookup $userIdentityLookup; private HideUserUtils $hideUserUtils; + private TempUserConfig $tempUserConfig; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param UserGroupManager $userGroupManager - * @param UserIdentityLookup $userIdentityLookup - * @param HideUserUtils $hideUserUtils - */ public function __construct( LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, UserGroupManager $userGroupManager, UserIdentityLookup $userIdentityLookup, - HideUserUtils $hideUserUtils + HideUserUtils $hideUserUtils, + TempUserConfig $tempUserConfig ) { parent::__construct( 'Listusers' ); $this->linkBatchFactory = $linkBatchFactory; @@ -66,6 +62,7 @@ class SpecialListUsers extends IncludableSpecialPage { $this->userGroupManager = $userGroupManager; $this->userIdentityLookup = $userIdentityLookup; $this->hideUserUtils = $hideUserUtils; + $this->tempUserConfig = $tempUserConfig; } /** @@ -83,6 +80,7 @@ class SpecialListUsers extends IncludableSpecialPage { $this->userGroupManager, $this->userIdentityLookup, $this->hideUserUtils, + $this->tempUserConfig, $par, $this->including() ); diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 7b179e86b17e..03e01281f9c0 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -62,14 +62,6 @@ class SpecialLog extends SpecialPage { private LogFormatterFactory $logFormatterFactory; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param ActorNormalization $actorNormalization - * @param UserIdentityLookup $userIdentityLookup - * @param UserNameUtils $userNameUtils - * @param LogFormatterFactory $logFormatterFactory - */ public function __construct( LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialLonelyPages.php b/includes/specials/SpecialLonelyPages.php index 9b9c8067e052..4b9619dc5d5b 100644 --- a/includes/specials/SpecialLonelyPages.php +++ b/includes/specials/SpecialLonelyPages.php @@ -38,13 +38,6 @@ class SpecialLonelyPages extends PageQueryPage { private NamespaceInfo $namespaceInfo; private LinksMigration $linksMigration; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - * @param LinksMigration $linksMigration - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialLongPages.php b/includes/specials/SpecialLongPages.php index a47bac71f290..def6c430f69e 100644 --- a/includes/specials/SpecialLongPages.php +++ b/includes/specials/SpecialLongPages.php @@ -31,11 +31,6 @@ use Wikimedia\Rdbms\IConnectionProvider; */ class SpecialLongPages extends SpecialShortPages { - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialMIMESearch.php b/includes/specials/SpecialMIMESearch.php index 61c6d6d97b63..75b4d0951088 100644 --- a/includes/specials/SpecialMIMESearch.php +++ b/includes/specials/SpecialMIMESearch.php @@ -53,11 +53,6 @@ class SpecialMIMESearch extends QueryPage { private ILanguageConverter $languageConverter; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php index 3ad9d8f1e2e8..65e0ca789ce5 100644 --- a/includes/specials/SpecialMediaStatistics.php +++ b/includes/specials/SpecialMediaStatistics.php @@ -22,6 +22,8 @@ namespace MediaWiki\Specials; use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\Html\Html; +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; use MediaWiki\Output\OutputPage; use MediaWiki\SpecialPage\QueryPage; use MediaWiki\SpecialPage\SpecialPage; @@ -61,12 +63,8 @@ class SpecialMediaStatistics extends QueryPage { protected $totalSize = 0; private MimeAnalyzer $mimeAnalyzer; + private int $migrationStage; - /** - * @param MimeAnalyzer $mimeAnalyzer - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( MimeAnalyzer $mimeAnalyzer, IConnectionProvider $dbProvider, @@ -80,6 +78,9 @@ class SpecialMediaStatistics extends QueryPage { $this->mimeAnalyzer = $mimeAnalyzer; $this->setDatabaseProvider( $dbProvider ); $this->setLinkBatchFactory( $linkBatchFactory ); + $this->migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( + MainConfigNames::FileSchemaMigrationStage + ); } public function isExpensive() { @@ -102,32 +103,69 @@ class SpecialMediaStatistics extends QueryPage { */ public function getQueryInfo() { $dbr = $this->getDatabaseProvider()->getReplicaDatabase(); - $fakeTitle = $dbr->buildConcat( [ - 'img_media_type', - $dbr->addQuotes( ';' ), - 'img_major_mime', - $dbr->addQuotes( '/' ), - 'img_minor_mime', - $dbr->addQuotes( ';' ), - $dbr->buildStringCast( 'COUNT(*)' ), - $dbr->addQuotes( ';' ), - $dbr->buildStringCast( 'SUM( img_size )' ) - ] ); - return [ - 'tables' => [ 'image' ], - 'fields' => [ - 'title' => $fakeTitle, - 'namespace' => NS_MEDIA, /* needs to be something */ - 'value' => '1' - ], - 'options' => [ - 'GROUP BY' => [ - 'img_media_type', - 'img_major_mime', - 'img_minor_mime', + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $fakeTitle = $dbr->buildConcat( [ + 'img_media_type', + $dbr->addQuotes( ';' ), + 'img_major_mime', + $dbr->addQuotes( '/' ), + 'img_minor_mime', + $dbr->addQuotes( ';' ), + $dbr->buildStringCast( 'COUNT(*)' ), + $dbr->addQuotes( ';' ), + $dbr->buildStringCast( 'SUM( img_size )' ) + ] ); + return [ + 'tables' => [ 'image' ], + 'fields' => [ + 'title' => $fakeTitle, + 'namespace' => NS_MEDIA, /* needs to be something */ + 'value' => '1' + ], + 'options' => [ + 'GROUP BY' => [ + 'img_media_type', + 'img_major_mime', + 'img_minor_mime', + ] ] - ] - ]; + ]; + } else { + $fakeTitle = $dbr->buildConcat( [ + 'ft_media_type', + $dbr->addQuotes( ';' ), + 'ft_major_mime', + $dbr->addQuotes( '/' ), + 'ft_minor_mime', + $dbr->addQuotes( ';' ), + $dbr->buildStringCast( 'COUNT(*)' ), + $dbr->addQuotes( ';' ), + $dbr->buildStringCast( 'SUM( fr_size )' ) + ] ); + return [ + 'tables' => [ 'file', 'filetypes', 'filerevision' ], + 'fields' => [ + 'title' => $fakeTitle, + 'namespace' => NS_MEDIA, /* needs to be something */ + 'value' => '1' + ], + 'conds' => [ + 'file_deleted' => 0 + ], + 'options' => [ + 'GROUP BY' => [ + 'file_type', + 'ft_media_type', + 'ft_major_mime', + 'ft_minor_mime' + ] + ], + 'join_conds' => [ + 'filetypes' => [ 'JOIN', 'file_type = ft_id' ], + 'filerevision' => [ 'JOIN', 'file_latest = fr_id' ] + ] + ]; + } } /** @@ -138,7 +176,11 @@ class SpecialMediaStatistics extends QueryPage { * @return array Fields to sort by */ protected function getOrderFields() { - return [ 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' ]; + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + return [ 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' ]; + } else { + return [ 'file_type', 'count(*)', 'ft_media_type', 'ft_major_mime', 'ft_minor_mime' ]; + } } /** diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index b21fcbaf163b..19ee2855a1ff 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -23,6 +23,7 @@ namespace MediaWiki\Specials; use LogEventsList; use LogPage; use MediaWiki\Cache\LinkBatchFactory; +use MediaWiki\ChangeTags\ChangeTagsStore; use MediaWiki\CommentFormatter\CommentFormatter; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Page\MergeHistoryFactory; @@ -81,23 +82,18 @@ class SpecialMergeHistory extends SpecialPage { private IConnectionProvider $dbProvider; private RevisionStore $revisionStore; private CommentFormatter $commentFormatter; + private ChangeTagsStore $changeTagsStore; /** @var Status */ private $mStatus; - /** - * @param MergeHistoryFactory $mergeHistoryFactory - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param RevisionStore $revisionStore - * @param CommentFormatter $commentFormatter - */ public function __construct( MergeHistoryFactory $mergeHistoryFactory, LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, RevisionStore $revisionStore, - CommentFormatter $commentFormatter + CommentFormatter $commentFormatter, + ChangeTagsStore $changeTagsStore ) { parent::__construct( 'MergeHistory', 'mergehistory' ); $this->mergeHistoryFactory = $mergeHistoryFactory; @@ -105,6 +101,7 @@ class SpecialMergeHistory extends SpecialPage { $this->dbProvider = $dbProvider; $this->revisionStore = $revisionStore; $this->commentFormatter = $commentFormatter; + $this->changeTagsStore = $changeTagsStore; } public function doesWrites() { @@ -252,6 +249,7 @@ class SpecialMergeHistory extends SpecialPage { $this->dbProvider, $this->revisionStore, $this->commentFormatter, + $this->changeTagsStore, [], $this->mTargetObj, $this->mDestObj, diff --git a/includes/specials/SpecialMostCategories.php b/includes/specials/SpecialMostCategories.php index f0571f99e846..d8d021603267 100644 --- a/includes/specials/SpecialMostCategories.php +++ b/includes/specials/SpecialMostCategories.php @@ -44,11 +44,6 @@ class SpecialMostCategories extends QueryPage { private NamespaceInfo $namespaceInfo; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialMostInterwikis.php b/includes/specials/SpecialMostInterwikis.php index d960b776bd82..64f8fa5b596d 100644 --- a/includes/specials/SpecialMostInterwikis.php +++ b/includes/specials/SpecialMostInterwikis.php @@ -42,11 +42,6 @@ class SpecialMostInterwikis extends QueryPage { private NamespaceInfo $namespaceInfo; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialMostLinked.php b/includes/specials/SpecialMostLinked.php index 1ade003594ab..d01f2d151e08 100644 --- a/includes/specials/SpecialMostLinked.php +++ b/includes/specials/SpecialMostLinked.php @@ -46,11 +46,6 @@ class SpecialMostLinked extends QueryPage { private LinksMigration $linksMigration; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LinksMigration $linksMigration - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialMostLinkedCategories.php b/includes/specials/SpecialMostLinkedCategories.php index 43e4dd61ff45..fe774fe2534b 100644 --- a/includes/specials/SpecialMostLinkedCategories.php +++ b/includes/specials/SpecialMostLinkedCategories.php @@ -46,11 +46,6 @@ class SpecialMostLinkedCategories extends QueryPage { private ILanguageConverter $languageConverter; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialMostLinkedTemplates.php b/includes/specials/SpecialMostLinkedTemplates.php index 766f30d661e9..27359f9baadf 100644 --- a/includes/specials/SpecialMostLinkedTemplates.php +++ b/includes/specials/SpecialMostLinkedTemplates.php @@ -44,11 +44,6 @@ class SpecialMostLinkedTemplates extends QueryPage { private LinksMigration $linksMigration; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LinksMigration $linksMigration - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialMostRevisions.php b/includes/specials/SpecialMostRevisions.php index db2c61c7be51..f8b865e68583 100644 --- a/includes/specials/SpecialMostRevisions.php +++ b/includes/specials/SpecialMostRevisions.php @@ -35,12 +35,6 @@ use Wikimedia\Rdbms\IConnectionProvider; */ class SpecialMostRevisions extends SpecialFewestRevisions { - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialMovePage.php b/includes/specials/SpecialMovePage.php index af75ea2d86f7..1bf65240cd4d 100644 --- a/includes/specials/SpecialMovePage.php +++ b/includes/specials/SpecialMovePage.php @@ -116,22 +116,6 @@ class SpecialMovePage extends UnlistedSpecialPage { private TitleFactory $titleFactory; private DeletePageFactory $deletePageFactory; - /** - * @param MovePageFactory $movePageFactory - * @param PermissionManager $permManager - * @param UserOptionsLookup $userOptionsLookup - * @param IConnectionProvider $dbProvider - * @param IContentHandlerFactory $contentHandlerFactory - * @param NamespaceInfo $nsInfo - * @param LinkBatchFactory $linkBatchFactory - * @param RepoGroup $repoGroup - * @param WikiPageFactory $wikiPageFactory - * @param SearchEngineFactory $searchEngineFactory - * @param WatchlistManager $watchlistManager - * @param RestrictionStore $restrictionStore - * @param TitleFactory $titleFactory - * @param DeletePageFactory $deletePageFactory - */ public function __construct( MovePageFactory $movePageFactory, PermissionManager $permManager, diff --git a/includes/specials/SpecialMute.php b/includes/specials/SpecialMute.php index 943b97ca7614..3c43fe62aa2c 100644 --- a/includes/specials/SpecialMute.php +++ b/includes/specials/SpecialMute.php @@ -52,12 +52,6 @@ class SpecialMute extends FormSpecialPage { private UserIdentityLookup $userIdentityLookup; private UserIdentityUtils $userIdentityUtils; - /** - * @param CentralIdLookup $centralIdLookup - * @param UserOptionsManager $userOptionsManager - * @param UserIdentityLookup $userIdentityLookup - * @param UserIdentityUtils $userIdentityUtils - */ public function __construct( CentralIdLookup $centralIdLookup, UserOptionsManager $userOptionsManager, diff --git a/includes/specials/SpecialNewFiles.php b/includes/specials/SpecialNewFiles.php index a1dab12c37a4..d6aeda015308 100644 --- a/includes/specials/SpecialNewFiles.php +++ b/includes/specials/SpecialNewFiles.php @@ -50,12 +50,6 @@ class SpecialNewFiles extends IncludableSpecialPage { private IConnectionProvider $dbProvider; private LinkBatchFactory $linkBatchFactory; - /** - * @param MimeAnalyzer $mimeAnalyzer - * @param GroupPermissionsLookup $groupPermissionsLookup - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( MimeAnalyzer $mimeAnalyzer, GroupPermissionsLookup $groupPermissionsLookup, diff --git a/includes/specials/SpecialNewPages.php b/includes/specials/SpecialNewPages.php index d341acfe9788..0569d9752df7 100644 --- a/includes/specials/SpecialNewPages.php +++ b/includes/specials/SpecialNewPages.php @@ -68,17 +68,6 @@ class SpecialNewPages extends IncludableSpecialPage { private ChangeTagsStore $changeTagsStore; private TempUserConfig $tempUserConfig; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IContentHandlerFactory $contentHandlerFactory - * @param GroupPermissionsLookup $groupPermissionsLookup - * @param RevisionLookup $revisionLookup - * @param NamespaceInfo $namespaceInfo - * @param UserOptionsLookup $userOptionsLookup - * @param RowCommentFormatter $rowCommentFormatter - * @param ChangeTagsStore $changeTagsStore - * @param TempUserConfig $tempUserConfig - */ public function __construct( LinkBatchFactory $linkBatchFactory, IContentHandlerFactory $contentHandlerFactory, diff --git a/includes/specials/SpecialPageLanguage.php b/includes/specials/SpecialPageLanguage.php index ec9df198655c..10145b4f8324 100644 --- a/includes/specials/SpecialPageLanguage.php +++ b/includes/specials/SpecialPageLanguage.php @@ -59,12 +59,6 @@ class SpecialPageLanguage extends FormSpecialPage { private IConnectionProvider $dbProvider; private SearchEngineFactory $searchEngineFactory; - /** - * @param IContentHandlerFactory $contentHandlerFactory - * @param LanguageNameUtils $languageNameUtils - * @param IConnectionProvider $dbProvider - * @param SearchEngineFactory $searchEngineFactory - */ public function __construct( IContentHandlerFactory $contentHandlerFactory, LanguageNameUtils $languageNameUtils, diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 84ea9bdfa362..627a711cd775 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -43,10 +43,6 @@ class SpecialPreferences extends SpecialPage { private PreferencesFactory $preferencesFactory; private UserOptionsManager $userOptionsManager; - /** - * @param PreferencesFactory|null $preferencesFactory - * @param UserOptionsManager|null $userOptionsManager - */ public function __construct( ?PreferencesFactory $preferencesFactory = null, ?UserOptionsManager $userOptionsManager = null diff --git a/includes/specials/SpecialProtectedPages.php b/includes/specials/SpecialProtectedPages.php index d8f4ba142928..12aae1c6c73d 100644 --- a/includes/specials/SpecialProtectedPages.php +++ b/includes/specials/SpecialProtectedPages.php @@ -45,13 +45,6 @@ class SpecialProtectedPages extends SpecialPage { private RowCommentFormatter $rowCommentFormatter; private RestrictionStore $restrictionStore; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param CommentStore $commentStore - * @param RowCommentFormatter $rowCommentFormatter - * @param RestrictionStore $restrictionStore - */ public function __construct( LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialRecentChanges.php b/includes/specials/SpecialRecentChanges.php index a063a206ade2..e3796c1d849e 100644 --- a/includes/specials/SpecialRecentChanges.php +++ b/includes/specials/SpecialRecentChanges.php @@ -29,6 +29,7 @@ use MediaWiki\ChangeTags\ChangeTagsStore; use MediaWiki\Context\IContextSource; use MediaWiki\Html\FormOptions; use MediaWiki\Html\Html; +use MediaWiki\Language\MessageParser; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\SpecialPage\ChangesListSpecialPage; @@ -39,7 +40,6 @@ use MediaWiki\User\UserIdentityUtils; use MediaWiki\Utils\MWTimestamp; use MediaWiki\Watchlist\WatchedItemStoreInterface; use MediaWiki\Xml\Xml; -use MessageCache; use OOUI\ButtonWidget; use OOUI\HtmlSnippet; use RecentChange; @@ -59,24 +59,16 @@ class SpecialRecentChanges extends ChangesListSpecialPage { private $watchlistFilterGroupDefinition; private WatchedItemStoreInterface $watchedItemStore; - private MessageCache $messageCache; + private MessageParser $messageParser; private UserOptionsLookup $userOptionsLookup; /** @var int */ public $denseRcSizeThreshold = 10000; private ChangeTagsStore $changeTagsStore; - /** - * @param WatchedItemStoreInterface|null $watchedItemStore - * @param MessageCache|null $messageCache - * @param UserOptionsLookup|null $userOptionsLookup - * @param ChangeTagsStore|null $changeTagsStore - * @param UserIdentityUtils|null $userIdentityUtils - * @param TempUserConfig|null $tempUserConfig - */ public function __construct( ?WatchedItemStoreInterface $watchedItemStore = null, - ?MessageCache $messageCache = null, + ?MessageParser $messageParser = null, ?UserOptionsLookup $userOptionsLookup = null, ?ChangeTagsStore $changeTagsStore = null, ?UserIdentityUtils $userIdentityUtils = null, @@ -92,7 +84,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $tempUserConfig ?? $services->getTempUserConfig() ); $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore(); - $this->messageCache = $messageCache ?? $services->getMessageCache(); + $this->messageParser = $messageParser ?? $services->getMessageParser(); $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup(); $this->changeTagsStore = $changeTagsStore ?? $services->getChangeTagsStore(); @@ -732,7 +724,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { // Parse the message in this weird ugly way to preserve the ability to include interlanguage // links in it (T172461). In the future when T66969 is resolved, perhaps we can just use // $message->parse() instead. This code is copied from Message::parseText(). - $parserOutput = $this->messageCache->parseWithPostprocessing( + $parserOutput = $this->messageParser->parse( $message->plain(), $this->getPageTitle(), /*linestart*/ true, diff --git a/includes/specials/SpecialRecentChangesLinked.php b/includes/specials/SpecialRecentChangesLinked.php index 3eb8f2a780d2..693b738ec92c 100644 --- a/includes/specials/SpecialRecentChangesLinked.php +++ b/includes/specials/SpecialRecentChangesLinked.php @@ -23,6 +23,7 @@ namespace MediaWiki\Specials; use MediaWiki\ChangeTags\ChangeTagsStore; use MediaWiki\Html\FormOptions; use MediaWiki\Html\Html; +use MediaWiki\Language\MessageParser; use MediaWiki\MainConfigNames; use MediaWiki\Title\Title; use MediaWiki\User\Options\UserOptionsLookup; @@ -30,7 +31,6 @@ use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserIdentityUtils; use MediaWiki\Watchlist\WatchedItemStoreInterface; use MediaWiki\Xml\Xml; -use MessageCache; use RecentChange; use SearchEngineFactory; use Wikimedia\Rdbms\SelectQueryBuilder; @@ -49,15 +49,9 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { private SearchEngineFactory $searchEngineFactory; private ChangeTagsStore $changeTagsStore; - /** - * @param WatchedItemStoreInterface $watchedItemStore - * @param MessageCache $messageCache - * @param UserOptionsLookup $userOptionsLookup - * @param SearchEngineFactory $searchEngineFactory - */ public function __construct( WatchedItemStoreInterface $watchedItemStore, - MessageCache $messageCache, + MessageParser $messageParser, UserOptionsLookup $userOptionsLookup, SearchEngineFactory $searchEngineFactory, ChangeTagsStore $changeTagsStore, @@ -66,7 +60,7 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { ) { parent::__construct( $watchedItemStore, - $messageCache, + $messageParser, $userOptionsLookup, $changeTagsStore, $userIdentityUtils, diff --git a/includes/specials/SpecialRenameUser.php b/includes/specials/SpecialRenameUser.php index b0bde4056c0f..2c89faffcaf2 100644 --- a/includes/specials/SpecialRenameUser.php +++ b/includes/specials/SpecialRenameUser.php @@ -31,14 +31,6 @@ class SpecialRenameUser extends SpecialPage { private UserNamePrefixSearch $userNamePrefixSearch; private RenameUserFactory $renameUserFactory; - /** - * @param IConnectionProvider $dbConns - * @param PermissionManager $permissionManager - * @param TitleFactory $titleFactory - * @param UserFactory $userFactory - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param RenameUserFactory $renameUserFactory - */ public function __construct( IConnectionProvider $dbConns, PermissionManager $permissionManager, diff --git a/includes/specials/SpecialRevisionDelete.php b/includes/specials/SpecialRevisionDelete.php index e961669746b8..fc20656b576e 100644 --- a/includes/specials/SpecialRevisionDelete.php +++ b/includes/specials/SpecialRevisionDelete.php @@ -127,12 +127,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { ], ]; - /** - * @inheritDoc - * - * @param PermissionManager $permissionManager - * @param RepoGroup $repoGroup - */ public function __construct( PermissionManager $permissionManager, RepoGroup $repoGroup ) { parent::__construct( 'Revisiondelete' ); diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 5f35dea8a604..20a0c0dd7cc8 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -133,19 +133,6 @@ class SpecialSearch extends SpecialPage { private const NAMESPACES_CURRENT = 'sense'; - /** - * @param SearchEngineConfig $searchConfig - * @param SearchEngineFactory $searchEngineFactory - * @param NamespaceInfo $nsInfo - * @param IContentHandlerFactory $contentHandlerFactory - * @param InterwikiLookup $interwikiLookup - * @param ReadOnlyMode $readOnlyMode - * @param UserOptionsManager $userOptionsManager - * @param LanguageConverterFactory $languageConverterFactory - * @param RepoGroup $repoGroup - * @param SearchResultThumbnailProvider $thumbnailProvider - * @param TitleMatcher $titleMatcher - */ public function __construct( SearchEngineConfig $searchConfig, SearchEngineFactory $searchEngineFactory, diff --git a/includes/specials/SpecialShortPages.php b/includes/specials/SpecialShortPages.php index 32438f68c05d..05dafcc28676 100644 --- a/includes/specials/SpecialShortPages.php +++ b/includes/specials/SpecialShortPages.php @@ -42,11 +42,6 @@ class SpecialShortPages extends QueryPage { private NamespaceInfo $namespaceInfo; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index 04a68265fca2..c9b9daf07b60 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -22,7 +22,9 @@ namespace MediaWiki\Specials; use LogEventsList; use MediaWiki\Block\Block; -use MediaWiki\Block\BlockUtils; +use MediaWiki\Block\BlockTarget; +use MediaWiki\Block\BlockTargetFactory; +use MediaWiki\Block\BlockTargetWithUserPage; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Block\DatabaseBlockStore; use MediaWiki\Block\UnblockUserFactory; @@ -32,11 +34,9 @@ use MediaWiki\Request\WebRequest; use MediaWiki\SpecialPage\SpecialPage; use MediaWiki\Title\Title; use MediaWiki\Title\TitleValue; -use MediaWiki\User\UserIdentity; use MediaWiki\User\UserNamePrefixSearch; use MediaWiki\User\UserNameUtils; use MediaWiki\Watchlist\WatchlistManager; -use Wikimedia\IPUtils; /** * A special page for unblocking users @@ -45,17 +45,14 @@ use Wikimedia\IPUtils; */ class SpecialUnblock extends SpecialPage { - /** @var UserIdentity|string|null */ + /** @var BlockTarget|null */ protected $target; - /** @var int|null Block::TYPE_ constant */ - protected $type; - /** @var DatabaseBlock|null */ protected $block; private UnblockUserFactory $unblockUserFactory; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private DatabaseBlockStore $blockStore; private UserNameUtils $userNameUtils; private UserNamePrefixSearch $userNamePrefixSearch; @@ -63,17 +60,9 @@ class SpecialUnblock extends SpecialPage { protected bool $useCodex = false; - /** - * @param UnblockUserFactory $unblockUserFactory - * @param BlockUtils $blockUtils - * @param DatabaseBlockStore $blockStore - * @param UserNameUtils $userNameUtils - * @param UserNamePrefixSearch $userNamePrefixSearch - * @param WatchlistManager $watchlistManager - */ public function __construct( UnblockUserFactory $unblockUserFactory, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, DatabaseBlockStore $blockStore, UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, @@ -81,7 +70,7 @@ class SpecialUnblock extends SpecialPage { ) { parent::__construct( 'Unblock', 'block' ); $this->unblockUserFactory = $unblockUserFactory; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->blockStore = $blockStore; $this->userNameUtils = $userNameUtils; $this->userNamePrefixSearch = $userNamePrefixSearch; @@ -98,7 +87,7 @@ class SpecialUnblock extends SpecialPage { $this->checkPermissions(); $this->checkReadOnly(); - [ $this->target, $this->type ] = $this->getTargetAndType( $par, $this->getRequest() ); + $this->target = $this->getTargetFromRequest( $par, $this->getRequest() ); // T382539 if ( $this->useCodex ) { @@ -114,10 +103,10 @@ class SpecialUnblock extends SpecialPage { } $this->block = $this->blockStore->newFromTarget( $this->target ); - if ( $this->target instanceof UserIdentity ) { + if ( $this->target instanceof BlockTargetWithUserPage ) { // Set the 'relevant user' in the skin, so it displays links like Contributions, // User logs, UserRights, etc. - $this->getSkin()->setRelevantUser( $this->target ); + $this->getSkin()->setRelevantUser( $this->target->getUserIdentity() ); } $this->setHeaders(); @@ -131,17 +120,14 @@ class SpecialUnblock extends SpecialPage { $form = HTMLForm::factory( 'ooui', $this->getFields(), $this->getContext() ) ->setWrapperLegendMsg( 'unblock-target' ) ->setSubmitCallback( function ( array $data, HTMLForm $form ) { - if ( $this->type != Block::TYPE_RANGE - && $this->type != Block::TYPE_AUTO - && $data['Watch'] - ) { + if ( $this->target instanceof BlockTargetWithUserPage && $data['Watch'] ) { $this->watchlistManager->addWatchIgnoringRights( $form->getUser(), - Title::makeTitle( NS_USER, $this->target ) + Title::newFromPageReference( $this->target->getUserPage() ) ); } return $this->unblockUserFactory->newUnblockUser( - $data['Target'], + $this->target, $form->getContext()->getAuthority(), $data['Reason'], $data['Tags'] ?? [] @@ -150,8 +136,9 @@ class SpecialUnblock extends SpecialPage { ->setSubmitTextMsg( 'ipusubmit' ) ->addPreHtml( $this->msg( 'unblockiptext' )->parseAsBlock() ); - $userPage = $this->getTargetUserTitle( $this->target ); - if ( $userPage ) { + if ( $this->target ) { + $userPage = $this->target->getLogPage(); + $targetName = (string)$this->target; // Get relevant extracts from the block and suppression logs, if possible $logExtract = ''; LogEventsList::showLogExtract( @@ -163,7 +150,7 @@ class SpecialUnblock extends SpecialPage { 'lim' => 10, 'msgKey' => [ 'unblocklog-showlog', - $userPage->getText(), + $targetName, ], 'showIfEmpty' => false ] @@ -185,7 +172,7 @@ class SpecialUnblock extends SpecialPage { 'conds' => [ 'log_action' => [ 'block', 'reblock', 'unblock' ] ], 'msgKey' => [ 'unblocklog-showsuppresslog', - $userPage->getText(), + $targetName, ], 'showIfEmpty' => false ] @@ -197,21 +184,16 @@ class SpecialUnblock extends SpecialPage { } if ( $form->show() ) { - switch ( $this->type ) { - case Block::TYPE_IP: - $out->addWikiMsg( 'unblocked-ip', wfEscapeWikiText( $this->target ) ); - break; - case Block::TYPE_USER: - $out->addWikiMsg( 'unblocked', wfEscapeWikiText( $this->target ) ); - break; - case Block::TYPE_RANGE: - $out->addWikiMsg( 'unblocked-range', wfEscapeWikiText( $this->target ) ); - break; - case Block::TYPE_ID: - case Block::TYPE_AUTO: - $out->addWikiMsg( 'unblocked-id', wfEscapeWikiText( $this->target ) ); - break; - } + $msgsByType = [ + Block::TYPE_IP => 'unblocked-ip', + Block::TYPE_USER => 'unblocked', + Block::TYPE_RANGE => 'unblocked-range', + Block::TYPE_AUTO => 'unblocked-id' + ]; + $out->addWikiMsg( + $msgsByType[$this->target->getType()], + wfEscapeWikiText( (string)$this->target ) + ); } } @@ -222,10 +204,9 @@ class SpecialUnblock extends SpecialPage { * * @param string|null $par Subpage parameter * @param WebRequest $request - * @return array [ UserIdentity|string|null, DatabaseBlock::TYPE_ constant|null ] - * @phan-return array{0:UserIdentity|string|null,1:int|null} + * @return BlockTarget|null */ - private function getTargetAndType( ?string $par, WebRequest $request ) { + private function getTargetFromRequest( ?string $par, WebRequest $request ) { $possibleTargets = [ $request->getVal( 'wpTarget', null ), $par, @@ -234,31 +215,13 @@ class SpecialUnblock extends SpecialPage { $request->getVal( 'wpBlockAddress', null ), ]; foreach ( $possibleTargets as $possibleTarget ) { - $targetAndType = $this->blockUtils->parseBlockTarget( $possibleTarget ); + $target = $this->blockTargetFactory->newFromString( $possibleTarget ); // If type is not null then target is valid - if ( $targetAndType[ 1 ] !== null ) { + if ( $target ) { break; } } - return $targetAndType; - } - - /** - * Get a user page target for things like logs. - * This handles account and IP range targets. - * @param UserIdentity|string|null $target - * @return Title|null - */ - private function getTargetUserTitle( $target ): ?Title { - if ( $target instanceof UserIdentity ) { - return Title::makeTitle( NS_USER, $target->getName() ); - } - - if ( is_string( $target ) && IPUtils::isIPAddress( $target ) ) { - return Title::makeTitle( NS_USER, $target ); - } - - return null; + return $target; } protected function getFields() { @@ -289,8 +252,8 @@ class SpecialUnblock extends SpecialPage { // User:Foo, and we've just got any block, auto or not, that applies to a target // the user has specified. Someone could be fishing to connect IPs to autoblocks, // so don't show any distinction between unblocked IPs and autoblocked IPs - if ( $type == Block::TYPE_AUTO && $this->type == Block::TYPE_IP ) { - $fields['Target']['default'] = $this->target; + if ( $type == Block::TYPE_AUTO && $this->target->getType() == Block::TYPE_IP ) { + $fields['Target']['default'] = (string)$this->target; unset( $fields['Name'] ); } else { $fields['Target']['default'] = $targetName; @@ -316,10 +279,10 @@ class SpecialUnblock extends SpecialPage { break; case Block::TYPE_AUTO: + // Don't expose the real target of the autoblock $fields['Name']['default'] = $this->block->getRedactedName(); $fields['Name']['raw'] = true; - // Don't expose the real target of the autoblock - $fields['Target']['default'] = "#{$this->target}"; + $fields['Target']['default'] = $this->block->getRedactedTarget()->toString(); break; } // Target is hidden, so the reason is the first element diff --git a/includes/specials/SpecialUncategorizedCategories.php b/includes/specials/SpecialUncategorizedCategories.php index 8ab911b6d5b1..b7f6f4f71e82 100644 --- a/includes/specials/SpecialUncategorizedCategories.php +++ b/includes/specials/SpecialUncategorizedCategories.php @@ -41,12 +41,6 @@ class SpecialUncategorizedCategories extends SpecialUncategorizedPages { */ private $exceptionList = null; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialUncategorizedPages.php b/includes/specials/SpecialUncategorizedPages.php index 8709886931b5..05d052942134 100644 --- a/includes/specials/SpecialUncategorizedPages.php +++ b/includes/specials/SpecialUncategorizedPages.php @@ -39,12 +39,6 @@ class SpecialUncategorizedPages extends PageQueryPage { private NamespaceInfo $namespaceInfo; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialUncategorizedTemplates.php b/includes/specials/SpecialUncategorizedTemplates.php index 230ea186db47..08a416678e3e 100644 --- a/includes/specials/SpecialUncategorizedTemplates.php +++ b/includes/specials/SpecialUncategorizedTemplates.php @@ -33,12 +33,6 @@ use Wikimedia\Rdbms\IConnectionProvider; */ class SpecialUncategorizedTemplates extends SpecialUncategorizedPages { - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index d2d839ea7965..2b694d21fb21 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -157,23 +157,6 @@ class SpecialUndelete extends SpecialPage { private CommentFormatter $commentFormatter; private WatchlistManager $watchlistManager; - /** - * @param PermissionManager $permissionManager - * @param RevisionStore $revisionStore - * @param RevisionRenderer $revisionRenderer - * @param IContentHandlerFactory $contentHandlerFactory - * @param NameTableStore $changeTagDefStore - * @param LinkBatchFactory $linkBatchFactory - * @param RepoGroup $repoGroup - * @param IConnectionProvider $dbProvider - * @param UserOptionsLookup $userOptionsLookup - * @param WikiPageFactory $wikiPageFactory - * @param SearchEngineFactory $searchEngineFactory - * @param UndeletePageFactory $undeletePageFactory - * @param ArchivedRevisionLookup $archivedRevisionLookup - * @param CommentFormatter $commentFormatter - * @param WatchlistManager $watchlistManager - */ public function __construct( PermissionManager $permissionManager, RevisionStore $revisionStore, diff --git a/includes/specials/SpecialUnusedImages.php b/includes/specials/SpecialUnusedImages.php index e14adde018be..9fe8e1e79161 100644 --- a/includes/specials/SpecialUnusedImages.php +++ b/includes/specials/SpecialUnusedImages.php @@ -21,6 +21,7 @@ namespace MediaWiki\Specials; use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; use MediaWiki\SpecialPage\ImageQueryPage; use Wikimedia\Rdbms\IConnectionProvider; @@ -30,10 +31,14 @@ use Wikimedia\Rdbms\IConnectionProvider; * @ingroup SpecialPage */ class SpecialUnusedImages extends ImageQueryPage { + private int $migrationStage; public function __construct( IConnectionProvider $dbProvider ) { parent::__construct( 'Unusedimages' ); $this->setDatabaseProvider( $dbProvider ); + $this->migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( + MainConfigNames::FileSchemaMigrationStage + ); } public function isExpensive() { @@ -49,15 +54,32 @@ class SpecialUnusedImages extends ImageQueryPage { } public function getQueryInfo() { + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $tables = [ 'image' ]; + $nameField = 'img_name'; + $timestampField = 'img_timestamp'; + $extraConds = []; + $extraJoins = []; + } else { + $tables = [ 'file', 'filerevision' ]; + $nameField = 'file_name'; + $timestampField = 'fr_timestamp'; + $extraConds = [ 'file_deleted' => 0 ]; + $extraJoins = [ 'filerevision' => [ 'JOIN', 'file_latest = fr_id' ] ]; + } + $retval = [ - 'tables' => [ 'image', 'imagelinks' ], + 'tables' => array_merge( $tables, [ 'imagelinks' ] ), 'fields' => [ 'namespace' => NS_FILE, - 'title' => 'img_name', - 'value' => 'img_timestamp', + 'title' => $nameField, + 'value' => $timestampField, ], - 'conds' => [ 'il_to' => null ], - 'join_conds' => [ 'imagelinks' => [ 'LEFT JOIN', 'il_to = img_name' ] ] + 'conds' => array_merge( [ 'il_to' => null ], $extraConds ), + 'join_conds' => array_merge( + [ 'imagelinks' => [ 'LEFT JOIN', 'il_to = ' . $nameField ] ], + $extraJoins + ), ]; if ( $this->getConfig()->get( MainConfigNames::CountCategorizedImagesAsUsed ) ) { @@ -66,7 +88,7 @@ class SpecialUnusedImages extends ImageQueryPage { 'imagelinks' ]; $retval['conds']['page_namespace'] = NS_FILE; $retval['conds']['cl_from'] = null; - $retval['conds'][] = 'img_name = page_title'; + $retval['conds'][] = $nameField . ' = page_title'; $retval['join_conds']['categorylinks'] = [ 'LEFT JOIN', 'cl_from = page_id' ]; $retval['join_conds']['imagelinks'] = [ diff --git a/includes/specials/SpecialUnwatchedPages.php b/includes/specials/SpecialUnwatchedPages.php index b1ce246273bd..1c5f5a2efeca 100644 --- a/includes/specials/SpecialUnwatchedPages.php +++ b/includes/specials/SpecialUnwatchedPages.php @@ -47,11 +47,6 @@ class SpecialUnwatchedPages extends QueryPage { private LinkBatchFactory $linkBatchFactory; private ILanguageConverter $languageConverter; - /** - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index e19b96acd04e..beaa54f6268c 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -72,12 +72,6 @@ class SpecialUpload extends SpecialPage { private JobQueueGroup $jobQueueGroup; private LoggerInterface $log; - /** - * @param RepoGroup|null $repoGroup - * @param UserOptionsLookup|null $userOptionsLookup - * @param NamespaceInfo|null $nsInfo - * @param WatchlistManager|null $watchlistManager - */ public function __construct( ?RepoGroup $repoGroup = null, ?UserOptionsLookup $userOptionsLookup = null, diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index 1b15b09ef5df..6832810081b2 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -74,12 +74,6 @@ class SpecialUploadStash extends UnlistedSpecialPage { */ private const MAX_SERVE_BYTES = 1_048_576; // 1 MiB - /** - * @param RepoGroup $repoGroup - * @param HttpRequestFactory $httpRequestFactory - * @param UrlUtils $urlUtils - * @param IConnectionProvider $dbProvider - */ public function __construct( RepoGroup $repoGroup, HttpRequestFactory $httpRequestFactory, diff --git a/includes/specials/SpecialUserLogin.php b/includes/specials/SpecialUserLogin.php index e4a83345f145..481deb389171 100644 --- a/includes/specials/SpecialUserLogin.php +++ b/includes/specials/SpecialUserLogin.php @@ -52,9 +52,6 @@ class SpecialUserLogin extends LoginSignupSpecialPage { private UserIdentityUtils $identityUtils; - /** - * @param AuthManager $authManager - */ public function __construct( AuthManager $authManager, UserIdentityUtils $identityUtils ) { parent::__construct( 'Userlogin' ); $this->setAuthManager( $authManager ); diff --git a/includes/specials/SpecialUserRights.php b/includes/specials/SpecialUserRights.php index 2993d2d33539..684d73e18054 100644 --- a/includes/specials/SpecialUserRights.php +++ b/includes/specials/SpecialUserRights.php @@ -83,15 +83,6 @@ class SpecialUserRights extends SpecialPage { private WatchlistManager $watchlistManager; private TempUserConfig $tempUserConfig; - /** - * @param UserGroupManagerFactory|null $userGroupManagerFactory - * @param UserNameUtils|null $userNameUtils - * @param UserNamePrefixSearch|null $userNamePrefixSearch - * @param UserFactory|null $userFactory - * @param ActorStoreFactory|null $actorStoreFactory - * @param WatchlistManager|null $watchlistManager - * @param TempUserConfig|null $tempUserConfig - */ public function __construct( ?UserGroupManagerFactory $userGroupManagerFactory = null, ?UserNameUtils $userNameUtils = null, diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index cb460cd59fb4..5124d7aeb467 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -82,11 +82,6 @@ class SpecialVersion extends SpecialPage { private UrlUtils $urlUtils; private IConnectionProvider $dbProvider; - /** - * @param ParserFactory $parserFactory - * @param UrlUtils $urlUtils - * @param IConnectionProvider $dbProvider - */ public function __construct( ParserFactory $parserFactory, UrlUtils $urlUtils, diff --git a/includes/specials/SpecialWantedCategories.php b/includes/specials/SpecialWantedCategories.php index 6dccb102b996..1ff0b3cbca6d 100644 --- a/includes/specials/SpecialWantedCategories.php +++ b/includes/specials/SpecialWantedCategories.php @@ -43,11 +43,6 @@ class SpecialWantedCategories extends WantedQueryPage { private ILanguageConverter $languageConverter; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialWantedFiles.php b/includes/specials/SpecialWantedFiles.php index 42d202658198..bdeac8a13949 100644 --- a/includes/specials/SpecialWantedFiles.php +++ b/includes/specials/SpecialWantedFiles.php @@ -23,6 +23,8 @@ namespace MediaWiki\Specials; use MediaWiki\Cache\LinkBatchFactory; +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageReferenceValue; use MediaWiki\SpecialPage\WantedQueryPage; use MediaWiki\Title\Title; @@ -38,12 +40,8 @@ use Wikimedia\Rdbms\IConnectionProvider; class SpecialWantedFiles extends WantedQueryPage { private RepoGroup $repoGroup; + private int $migrationStage; - /** - * @param RepoGroup $repoGroup - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( RepoGroup $repoGroup, IConnectionProvider $dbProvider, @@ -53,6 +51,9 @@ class SpecialWantedFiles extends WantedQueryPage { $this->repoGroup = $repoGroup; $this->setDatabaseProvider( $dbProvider ); $this->setLinkBatchFactory( $linkBatchFactory ); + $this->migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( + MainConfigNames::FileSchemaMigrationStage + ); } protected function getPageHeader() { @@ -128,13 +129,24 @@ class SpecialWantedFiles extends WantedQueryPage { } public function getQueryInfo() { + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $fileTable = 'image'; + $nameField = 'img_name'; + $extraConds1 = []; + $extraConds2 = []; + } else { + $fileTable = 'file'; + $nameField = 'file_name'; + $extraConds1 = [ 'img1.file_deleted' => 0 ]; + $extraConds2 = [ 'img2.file_deleted' => 0 ]; + } return [ 'tables' => [ 'imagelinks', 'page', 'redirect', - 'img1' => 'image', - 'img2' => 'image', + 'img1' => $fileTable, + 'img2' => $fileTable, ], 'fields' => [ 'namespace' => NS_FILE, @@ -142,14 +154,14 @@ class SpecialWantedFiles extends WantedQueryPage { 'value' => 'COUNT(*)' ], 'conds' => [ - 'img1.img_name' => null, + 'img1.' . $nameField => null, // We also need to exclude file redirects - 'img2.img_name' => null, + 'img2.' . $nameField => null, ], 'options' => [ 'GROUP BY' => 'il_to' ], 'join_conds' => [ 'img1' => [ 'LEFT JOIN', - 'il_to = img1.img_name' + array_merge( [ 'il_to = img1.' . $nameField ], $extraConds1 ), ], 'page' => [ 'LEFT JOIN', [ 'il_to = page_title', @@ -161,7 +173,7 @@ class SpecialWantedFiles extends WantedQueryPage { 'rd_interwiki' => '' ] ], 'img2' => [ 'LEFT JOIN', - 'rd_title = img2.img_name' + array_merge( [ 'rd_title = img2.' . $nameField ], $extraConds2 ), ] ] ]; diff --git a/includes/specials/SpecialWantedPages.php b/includes/specials/SpecialWantedPages.php index 99eff43a5e76..b6c8ea915ed0 100644 --- a/includes/specials/SpecialWantedPages.php +++ b/includes/specials/SpecialWantedPages.php @@ -35,10 +35,6 @@ class SpecialWantedPages extends WantedQueryPage { private LinksMigration $linksMigration; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialWantedTemplates.php b/includes/specials/SpecialWantedTemplates.php index c9a9584078df..f24920aa49da 100644 --- a/includes/specials/SpecialWantedTemplates.php +++ b/includes/specials/SpecialWantedTemplates.php @@ -39,11 +39,6 @@ class SpecialWantedTemplates extends WantedQueryPage { private LinksMigration $linksMigration; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LinksMigration $linksMigration - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 49ce9811f3ba..a5e25cca68bd 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -83,11 +83,6 @@ class SpecialWatchlist extends ChangesListSpecialPage { private $currentMode; private ChangeTagsStore $changeTagsStore; - /** - * @param WatchedItemStoreInterface $watchedItemStore - * @param WatchlistManager $watchlistManager - * @param UserOptionsLookup $userOptionsLookup - */ public function __construct( WatchedItemStoreInterface $watchedItemStore, WatchlistManager $watchlistManager, diff --git a/includes/specials/SpecialWhatLinksHere.php b/includes/specials/SpecialWhatLinksHere.php index 6d251551b7a2..81c95775248b 100644 --- a/includes/specials/SpecialWhatLinksHere.php +++ b/includes/specials/SpecialWhatLinksHere.php @@ -67,15 +67,6 @@ class SpecialWhatLinksHere extends FormSpecialPage { private const LIMITS = [ 20, 50, 100, 250, 500 ]; - /** - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param IContentHandlerFactory $contentHandlerFactory - * @param SearchEngineFactory $searchEngineFactory - * @param NamespaceInfo $namespaceInfo - * @param TitleFactory $titleFactory - * @param LinksMigration $linksMigration - */ public function __construct( IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, diff --git a/includes/specials/SpecialWithoutInterwiki.php b/includes/specials/SpecialWithoutInterwiki.php index 4421f01c4b5c..82368bfb1bf9 100644 --- a/includes/specials/SpecialWithoutInterwiki.php +++ b/includes/specials/SpecialWithoutInterwiki.php @@ -42,12 +42,6 @@ class SpecialWithoutInterwiki extends PageQueryPage { private NamespaceInfo $namespaceInfo; - /** - * @param NamespaceInfo $namespaceInfo - * @param IConnectionProvider $dbProvider - * @param LinkBatchFactory $linkBatchFactory - * @param LanguageConverterFactory $languageConverterFactory - */ public function __construct( NamespaceInfo $namespaceInfo, IConnectionProvider $dbProvider, diff --git a/includes/specials/forms/UploadForm.php b/includes/specials/forms/UploadForm.php index 06b023cfafeb..34eba6c0ce61 100644 --- a/includes/specials/forms/UploadForm.php +++ b/includes/specials/forms/UploadForm.php @@ -67,15 +67,6 @@ class UploadForm extends HTMLForm { private NamespaceInfo $nsInfo; private HookRunner $hookRunner; - /** - * @param array $options - * @param IContextSource|null $context - * @param LinkRenderer|null $linkRenderer - * @param LocalRepo|null $localRepo - * @param Language|null $contentLanguage - * @param NamespaceInfo|null $nsInfo - * @param HookContainer|null $hookContainer - */ public function __construct( array $options = [], ?IContextSource $context = null, diff --git a/includes/specials/helpers/ImportReporter.php b/includes/specials/helpers/ImportReporter.php index 8de11e90d5f3..a798ce2c1389 100644 --- a/includes/specials/helpers/ImportReporter.php +++ b/includes/specials/helpers/ImportReporter.php @@ -160,8 +160,7 @@ class ImportReporter extends ContextSource { $wikiPage = $services->getWikiPageFactory()->newFromTitle( $pageIdentity ); $dummyRevRecord = $wikiPage->newPageUpdater( $this->getUser() ) ->setCause( PageUpdater::CAUSE_IMPORT ) - ->setAutomated( true ) - ->saveDummyRevision( $detail ); + ->saveDummyRevision( $detail, EDIT_SILENT | EDIT_MINOR ); // Create the import log entry $logEntry = new ManualLogEntry( 'import', $action ); diff --git a/includes/specials/helpers/License.php b/includes/specials/helpers/License.php index c537f62d5de8..bf12aec344f8 100644 --- a/includes/specials/helpers/License.php +++ b/includes/specials/helpers/License.php @@ -31,10 +31,7 @@ class License { public string $template; public string $text; - /** - * @param string $str - */ - public function __construct( $str ) { + public function __construct( string $str ) { $str = $this->parse( $str ); [ $this->template, $this->text ] = $this->split( $str ); } diff --git a/includes/specials/pagers/ActiveUsersPager.php b/includes/specials/pagers/ActiveUsersPager.php index dfd0e5a29d13..5dd8cd1e3f64 100644 --- a/includes/specials/pagers/ActiveUsersPager.php +++ b/includes/specials/pagers/ActiveUsersPager.php @@ -30,6 +30,7 @@ use MediaWiki\Html\Html; use MediaWiki\Linker\Linker; use MediaWiki\MainConfigNames; use MediaWiki\Title\Title; +use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserGroupManager; use MediaWiki\User\UserIdentityLookup; use MediaWiki\User\UserIdentityValue; @@ -65,16 +66,6 @@ class ActiveUsersPager extends UsersPager { /** @var string[] */ private $excludegroups; - /** - * @param IContextSource $context - * @param HookContainer $hookContainer - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param UserGroupManager $userGroupManager - * @param UserIdentityLookup $userIdentityLookup - * @param HideUserUtils $hideUserUtils - * @param FormOptions $opts - */ public function __construct( IContextSource $context, HookContainer $hookContainer, @@ -83,6 +74,7 @@ class ActiveUsersPager extends UsersPager { UserGroupManager $userGroupManager, UserIdentityLookup $userIdentityLookup, HideUserUtils $hideUserUtils, + TempUserConfig $tempUserConfig, FormOptions $opts ) { parent::__construct( @@ -93,6 +85,7 @@ class ActiveUsersPager extends UsersPager { $userGroupManager, $userIdentityLookup, $hideUserUtils, + $tempUserConfig, null, null ); diff --git a/includes/specials/pagers/AllMessagesTablePager.php b/includes/specials/pagers/AllMessagesTablePager.php index 3ff5d4220f46..c06fdf99443e 100644 --- a/includes/specials/pagers/AllMessagesTablePager.php +++ b/includes/specials/pagers/AllMessagesTablePager.php @@ -74,15 +74,6 @@ class AllMessagesTablePager extends TablePager { private LocalisationCache $localisationCache; - /** - * @param IContextSource $context - * @param Language $contentLanguage - * @param LanguageFactory $languageFactory - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param LocalisationCache $localisationCache - * @param FormOptions $opts - */ public function __construct( IContextSource $context, Language $contentLanguage, diff --git a/includes/specials/pagers/BlockListPager.php b/includes/specials/pagers/BlockListPager.php index 8eb9b291f1ec..3977e48a925b 100644 --- a/includes/specials/pagers/BlockListPager.php +++ b/includes/specials/pagers/BlockListPager.php @@ -21,11 +21,12 @@ namespace MediaWiki\Pager; -use MediaWiki\Block\Block; use MediaWiki\Block\BlockActionInfo; use MediaWiki\Block\BlockRestrictionStore; -use MediaWiki\Block\BlockUtils; +use MediaWiki\Block\BlockTargetFactory; +use MediaWiki\Block\BlockTargetWithUserPage; use MediaWiki\Block\HideUserUtils; +use MediaWiki\Block\RangeBlockTarget; use MediaWiki\Block\Restriction\ActionRestriction; use MediaWiki\Block\Restriction\NamespaceRestriction; use MediaWiki\Block\Restriction\PageRestriction; @@ -39,7 +40,6 @@ use MediaWiki\Linker\Linker; use MediaWiki\Linker\LinkRenderer; use MediaWiki\MainConfigNames; use MediaWiki\SpecialPage\SpecialPageFactory; -use MediaWiki\User\UserIdentity; use MediaWiki\Utils\MWTimestamp; use stdClass; use Wikimedia\Rdbms\IConnectionProvider; @@ -50,19 +50,18 @@ use Wikimedia\Rdbms\IResultWrapper; */ class BlockListPager extends TablePager { - /** @var array */ - protected $conds; + protected array $conds; /** * Array of restrictions. * * @var Restriction[] */ - protected $restrictions = []; + protected array $restrictions = []; private BlockActionInfo $blockActionInfo; private BlockRestrictionStore $blockRestrictionStore; - private BlockUtils $blockUtils; + private BlockTargetFactory $blockTargetFactory; private HideUserUtils $hideUserUtils; private CommentStore $commentStore; private LinkBatchFactory $linkBatchFactory; @@ -70,30 +69,16 @@ class BlockListPager extends TablePager { private SpecialPageFactory $specialPageFactory; /** @var string[] */ - private $formattedComments = []; + private array $formattedComments = []; /** @var string[] Cache of messages to avoid them being recreated for every row of the pager. */ - private $messages = []; + private array $messages = []; - /** - * @param IContextSource $context - * @param BlockActionInfo $blockActionInfo - * @param BlockRestrictionStore $blockRestrictionStore - * @param BlockUtils $blockUtils - * @param HideUserUtils $hideUserUtils - * @param CommentStore $commentStore - * @param LinkBatchFactory $linkBatchFactory - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param RowCommentFormatter $rowCommentFormatter - * @param SpecialPageFactory $specialPageFactory - * @param array $conds - */ public function __construct( IContextSource $context, BlockActionInfo $blockActionInfo, BlockRestrictionStore $blockRestrictionStore, - BlockUtils $blockUtils, + BlockTargetFactory $blockTargetFactory, HideUserUtils $hideUserUtils, CommentStore $commentStore, LinkBatchFactory $linkBatchFactory, @@ -101,7 +86,7 @@ class BlockListPager extends TablePager { IConnectionProvider $dbProvider, RowCommentFormatter $rowCommentFormatter, SpecialPageFactory $specialPageFactory, - $conds + array $conds ) { // Set database before parent constructor to avoid setting it there $this->mDb = $dbProvider->getReplicaDatabase(); @@ -110,7 +95,7 @@ class BlockListPager extends TablePager { $this->blockActionInfo = $blockActionInfo; $this->blockRestrictionStore = $blockRestrictionStore; - $this->blockUtils = $blockUtils; + $this->blockTargetFactory = $blockTargetFactory; $this->hideUserUtils = $hideUserUtils; $this->commentStore = $commentStore; $this->linkBatchFactory = $linkBatchFactory; @@ -128,7 +113,7 @@ class BlockListPager extends TablePager { 'bl_timestamp' => 'blocklist-timestamp', 'target' => 'blocklist-target', 'bl_expiry' => 'blocklist-expiry', - 'by' => 'blocklist-by', + 'bl_by' => 'blocklist-by', 'params' => 'blocklist-params', 'bl_reason' => 'blocklist-reason', ]; @@ -220,7 +205,7 @@ class BlockListPager extends TablePager { } break; - case 'by': + case 'bl_by': $formatted = Linker::userLink( (int)$value, $row->bl_by_text ); $formatted .= Linker::userToolLinks( (int)$value, $row->bl_by_text ); break; @@ -295,11 +280,11 @@ class BlockListPager extends TablePager { return $this->msg( 'autoblockid', $row->bl_id )->parse(); } - [ $target, $type ] = $this->blockUtils->parseBlockTargetRow( $row ); + $target = $this->blockTargetFactory->newFromRowRedacted( $row ); - if ( $type === Block::TYPE_RANGE ) { + if ( $target instanceof RangeBlockTarget ) { $userId = 0; - $userName = $target; + $userName = $target->toString(); } elseif ( ( $row->hu_deleted ?? null ) && !$this->getAuthority()->isAllowed( 'hideuser' ) ) { @@ -308,11 +293,10 @@ class BlockListPager extends TablePager { [ 'class' => 'mw-blocklist-hidden' ], $this->messages['blocklist-hidden-placeholder'] ); - } elseif ( $target instanceof UserIdentity ) { - $userId = $target->getId(); - $userName = $target->getName(); - } elseif ( is_string( $target ) ) { - return htmlspecialchars( $target ); + } elseif ( $target instanceof BlockTargetWithUserPage ) { + $user = $target->getUserIdentity(); + $userId = $user->getId(); + $userName = $user->getName(); } else { return $this->msg( 'empty-username' )->escaped(); } @@ -334,22 +318,15 @@ class BlockListPager extends TablePager { private function getBlockChangeLinks( $row ): array { $linkRenderer = $this->getLinkRenderer(); $links = []; - if ( $row->bt_auto ) { - $target = "#{$row->bl_id}"; - } else { - $target = $row->bt_address ?? $row->bt_user_text; - } + $target = $this->blockTargetFactory->newFromRowRedacted( $row )->toString(); if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) ) { $query = [ 'id' => $row->bl_id ]; if ( $row->bt_auto ) { $links[] = $linkRenderer->makeKnownLink( - $this->specialPageFactory->getTitleForAlias( 'Block' ), + $this->specialPageFactory->getTitleForAlias( 'Unblock' ), $this->messages['remove-blocklink'], [], - $query + [ - 'wpTarget' => $target, - 'remove' => '1' - ] + [ 'wpTarget' => "#{$row->bl_id}" ] ); } else { $specialBlock = $this->specialPageFactory->getTitleForAlias( "Block/$target" ); @@ -498,7 +475,7 @@ class BlockListPager extends TablePager { $commentQuery['tables'] ), 'fields' => [ - // The target fields should be those accepted by BlockUtils::parseBlockTargetRow() + // The target fields should be those accepted by BlockTargetFactory::newFromRowRedacted() 'bt_address', 'bt_user_text', 'bt_user', diff --git a/includes/specials/pagers/CategoryPager.php b/includes/specials/pagers/CategoryPager.php index 5ae40cb1b772..abff23c7d42e 100644 --- a/includes/specials/pagers/CategoryPager.php +++ b/includes/specials/pagers/CategoryPager.php @@ -37,19 +37,12 @@ class CategoryPager extends AlphabeticPager { private LinkBatchFactory $linkBatchFactory; - /** - * @param IContextSource $context - * @param LinkBatchFactory $linkBatchFactory - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param string $from - */ public function __construct( IContextSource $context, LinkBatchFactory $linkBatchFactory, LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, - $from + string $from ) { // Set database before parent constructor to avoid setting it there $this->mDb = $dbProvider->getReplicaDatabase(); diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index 15c1cb6f9393..37e3baea01fd 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -52,16 +52,6 @@ class ContribsPager extends ContributionsPager { /** * FIXME List services first T266484 / T290405 - * @param IContextSource $context - * @param array $options - * @param LinkRenderer|null $linkRenderer - * @param LinkBatchFactory|null $linkBatchFactory - * @param HookContainer|null $hookContainer - * @param IConnectionProvider|null $dbProvider - * @param RevisionStore|null $revisionStore - * @param NamespaceInfo|null $namespaceInfo - * @param UserIdentity|null $targetUser - * @param CommentFormatter|null $commentFormatter */ public function __construct( IContextSource $context, diff --git a/includes/specials/pagers/DeletedContribsPager.php b/includes/specials/pagers/DeletedContribsPager.php index fb4350a38017..f816f07a013e 100644 --- a/includes/specials/pagers/DeletedContribsPager.php +++ b/includes/specials/pagers/DeletedContribsPager.php @@ -36,19 +36,6 @@ use Wikimedia\Rdbms\IConnectionProvider; * @ingroup Pager */ class DeletedContribsPager extends ContributionsPager { - /** - * @param HookContainer $hookContainer - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param RevisionStore $revisionStore - * @param NamespaceInfo $namespaceInfo - * @param CommentFormatter $commentFormatter - * @param LinkBatchFactory $linkBatchFactory - * @param UserFactory $userFactory - * @param IContextSource $context - * @param array $options - * @param UserIdentity $target - */ public function __construct( HookContainer $hookContainer, LinkRenderer $linkRenderer, @@ -60,7 +47,7 @@ class DeletedContribsPager extends ContributionsPager { UserFactory $userFactory, IContextSource $context, array $options, - $target + UserIdentity $target ) { $options['isArchive'] = true; diff --git a/includes/specials/pagers/ImageListPager.php b/includes/specials/pagers/ImageListPager.php index 5526abe21315..0cffcda073aa 100644 --- a/includes/specials/pagers/ImageListPager.php +++ b/includes/specials/pagers/ImageListPager.php @@ -49,22 +49,18 @@ use Wikimedia\Rdbms\Subquery; class ImageListPager extends TablePager { /** @var string[]|null */ - protected $mFieldNames = null; + protected ?array $mFieldNames = null; /** * @deprecated Subclasses should override {@see buildQueryConds} instead * @var array */ protected $mQueryConds = []; - /** @var string|null */ - protected $mUserName = null; - /** @var User|null The relevant user */ - protected $mUser = null; - /** @var bool */ - protected $mIncluding = false; - /** @var bool */ - protected $mShowAll = false; - /** @var string */ - protected $mTableName = 'image'; + protected ?string $mUserName = null; + /** The relevant user */ + protected ?User $mUser = null; + protected ?bool $mIncluding = false; + protected bool $mShowAll = false; + protected string $mTableName = 'image'; private CommentStore $commentStore; private LocalRepo $localRepo; @@ -72,7 +68,9 @@ class ImageListPager extends TablePager { private LinkBatchFactory $linkBatchFactory; /** @var string[] */ - private $formattedComments = []; + private array $formattedComments = []; + + private int $migrationStage; /** * The unique sort fields for the sort options for unique paginate @@ -83,20 +81,6 @@ class ImageListPager extends TablePager { 'img_size' => [ 'img_size', 'img_name' ], ]; - /** - * @param IContextSource $context - * @param CommentStore $commentStore - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param RepoGroup $repoGroup - * @param UserNameUtils $userNameUtils - * @param RowCommentFormatter $rowCommentFormatter - * @param LinkBatchFactory $linkBatchFactory - * @param string $userName - * @param string $search - * @param bool $including - * @param bool $showAll - */ public function __construct( IContextSource $context, CommentStore $commentStore, @@ -106,10 +90,10 @@ class ImageListPager extends TablePager { UserNameUtils $userNameUtils, RowCommentFormatter $rowCommentFormatter, LinkBatchFactory $linkBatchFactory, - $userName, - $search, - $including, - $showAll + ?string $userName, + string $search, + ?bool $including, + bool $showAll ) { $this->setContext( $context ); @@ -147,6 +131,9 @@ class ImageListPager extends TablePager { $this->localRepo = $repoGroup->getLocalRepo(); $this->rowCommentFormatter = $rowCommentFormatter; $this->linkBatchFactory = $linkBatchFactory; + $this->migrationStage = $this->getConfig()->get( + MainConfigNames::FileSchemaMigrationStage + ); } /** @@ -177,7 +164,7 @@ class ImageListPager extends TablePager { * @param string $table Either "image" or "oldimage" * @return array The query conditions. */ - protected function buildQueryConds( $table ) { + protected function buildQueryCondsOld( $table ) { $conds = []; if ( $this->mUserName !== null ) { @@ -196,6 +183,23 @@ class ImageListPager extends TablePager { return $conds + $this->mQueryConds; } + private function buildQueryConds() { + $conds = [ + 'file_deleted' => 0, + 'fr_deleted' => 0, + ]; + + if ( $this->mUserName !== null ) { + // getQueryInfoReal() should have handled the tables and joins. + $conds['actor_name'] = $this->mUserName; + } + + if ( !$this->mShowAll ) { + $conds[] = 'file_latest = fr_id'; + } + return $conds; + } + protected function getFieldNames() { if ( !$this->mFieldNames ) { $this->mFieldNames = [ @@ -246,10 +250,55 @@ class ImageListPager extends TablePager { } public function getQueryInfo() { - // Hacky Hacky Hacky - I want to get query info - // for two different tables, without reimplementing - // the pager class. - return $this->getQueryInfoReal( $this->mTableName ); + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + // Hacky Hacky Hacky - I want to get query info + // for two different tables, without reimplementing + // the pager class. + return $this->getQueryInfoReal( $this->mTableName ); + } + $dbr = $this->getDatabase(); + $tables = [ 'filerevision', 'file', 'actor' ]; + $fields = [ + 'img_timestamp' => 'fr_timestamp', + 'img_name' => 'file_name', + 'img_size' => 'fr_size', + 'top' => 'CASE WHEN file_latest = fr_id THEN \'yes\' ELSE \'no\' END', + ]; + $join_conds = [ + 'filerevision' => [ 'JOIN', 'fr_file=file_id' ], + 'actor' => [ 'JOIN', 'actor_id=fr_actor' ] + ]; + + # Description field + $commentQuery = $this->commentStore->getJoin( 'fr_description' ); + $tables += $commentQuery['tables']; + $fields += $commentQuery['fields']; + $join_conds += $commentQuery['joins']; + $fields['description_field'] = $dbr->addQuotes( "fr_description" ); + + # Actor fields + $fields[] = 'actor_user'; + $fields[] = 'actor_name'; + + # Depends on $wgMiserMode + # Will also not happen if mShowAll is true. + if ( array_key_exists( 'count', $this->getFieldNames() ) ) { + $fields['count'] = new Subquery( $dbr->newSelectQueryBuilder() + ->select( 'COUNT(fr_archive_name)' ) + ->from( 'filerevision' ) + ->where( 'fr_file = file_id' ) + ->caller( __METHOD__ ) + ->getSQL() + ); + } + + return [ + 'tables' => $tables, + 'fields' => $fields, + 'conds' => $this->buildQueryConds(), + 'options' => [], + 'join_conds' => $join_conds + ]; } /** @@ -313,12 +362,20 @@ class ImageListPager extends TablePager { return [ 'tables' => $tables, 'fields' => $fields, - 'conds' => $this->buildQueryConds( $table ), + 'conds' => $this->buildQueryCondsOld( $table ), 'options' => [], 'join_conds' => $join_conds ]; } + public function reallyDoQuery( $offset, $limit, $order ) { + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + return $this->reallyDoQueryOld( $offset, $limit, $order ); + } else { + return parent::reallyDoQuery( $offset, $limit, $order ); + } + } + /** * Override reallyDoQuery to mix together two queries. * @@ -327,7 +384,7 @@ class ImageListPager extends TablePager { * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING * @return IResultWrapper */ - public function reallyDoQuery( $offset, $limit, $order ) { + public function reallyDoQueryOld( $offset, $limit, $order ) { $dbr = $this->getDatabase(); $prevTableName = $this->mTableName; $this->mTableName = 'image'; @@ -443,7 +500,7 @@ class ImageListPager extends TablePager { protected function doBatchLookups() { $this->mResult->seek( 0 ); $batch = $this->linkBatchFactory->newLinkBatch(); - $rowsWithComments = [ 'img_description' => [], 'oi_description' => [] ]; + $rowsWithComments = [ 'img_description' => [], 'oi_description' => [], 'fr_description' => [] ]; foreach ( $this->mResult as $i => $row ) { $batch->add( NS_USER, $row->actor_name ); $batch->add( NS_USER_TALK, $row->actor_name ); @@ -465,6 +522,12 @@ class ImageListPager extends TablePager { 'oi_description' ); } + if ( $rowsWithComments['fr_description'] ) { + $this->formattedComments += $this->rowCommentFormatter->formatRows( + $rowsWithComments['fr_description'], + 'fr_description' + ); + } } /** @@ -538,7 +601,11 @@ class ImageListPager extends TablePager { case 'img_description': return $this->formattedComments[$this->getResultOffset()]; case 'count': - return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) ); + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) ); + } else { + return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) ) ); + } case 'top': // Messages: listfiles-latestversion-yes, listfiles-latestversion-no return $this->msg( 'listfiles-latestversion-' . $value )->escaped(); diff --git a/includes/specials/pagers/MergeHistoryPager.php b/includes/specials/pagers/MergeHistoryPager.php index d7c0af0635e7..ecc81ed2400a 100644 --- a/includes/specials/pagers/MergeHistoryPager.php +++ b/includes/specials/pagers/MergeHistoryPager.php @@ -23,12 +23,12 @@ namespace MediaWiki\Pager; use ChangeTags; use MediaWiki\Cache\LinkBatchFactory; +use MediaWiki\ChangeTags\ChangeTagsStore; use MediaWiki\CommentFormatter\CommentFormatter; use MediaWiki\Context\IContextSource; use MediaWiki\Html\Html; use MediaWiki\Linker\Linker; use MediaWiki\Linker\LinkRenderer; -use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageIdentity; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStore; @@ -43,40 +43,20 @@ class MergeHistoryPager extends ReverseChronologicalPager { /** @inheritDoc */ public $mGroupByDate = true; - /** @var array */ - public $mConds; - - /** @var int */ - private $articleID; - - /** @var string */ - private $maxTimestamp; - - /** @var string */ - private $maxRevId; - - /** @var string */ - private $mergePointTimestamp; + public array $mConds; + private int $articleID; + private string $maxTimestamp; + private int $maxRevId; + private string $mergePointTimestamp; /** @var int[] */ - public $prevId; + public array $prevId; private LinkBatchFactory $linkBatchFactory; private RevisionStore $revisionStore; private CommentFormatter $commentFormatter; + private ChangeTagsStore $changeTagsStore; - /** - * @param IContextSource $context - * @param LinkRenderer $linkRenderer - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param RevisionStore $revisionStore - * @param CommentFormatter $commentFormatter - * @param array $conds - * @param PageIdentity $source - * @param PageIdentity $dest - * @param string $mergePointTimestamp - */ public function __construct( IContextSource $context, LinkRenderer $linkRenderer, @@ -84,10 +64,11 @@ class MergeHistoryPager extends ReverseChronologicalPager { IConnectionProvider $dbProvider, RevisionStore $revisionStore, CommentFormatter $commentFormatter, + ChangeTagsStore $changeTagsStore, $conds, PageIdentity $source, PageIdentity $dest, - $mergePointTimestamp + string $mergePointTimestamp ) { $this->mConds = $conds; $this->articleID = $source->getId(); @@ -105,7 +86,7 @@ class MergeHistoryPager extends ReverseChronologicalPager { ->where( [ 'rev_timestamp' => $maxtimestamp ] ) ->caller( __METHOD__ )->fetchField(); $this->maxTimestamp = $maxtimestamp; - $this->maxRevId = $maxRevId; + $this->maxRevId = (int)$maxRevId; $this->mergePointTimestamp = $mergePointTimestamp; // Set database before parent constructor to avoid setting it there @@ -114,6 +95,7 @@ class MergeHistoryPager extends ReverseChronologicalPager { $this->linkBatchFactory = $linkBatchFactory; $this->revisionStore = $revisionStore; $this->commentFormatter = $commentFormatter; + $this->changeTagsStore = $changeTagsStore; } protected function doBatchLookups() { @@ -235,7 +217,7 @@ class MergeHistoryPager extends ReverseChronologicalPager { ] ) ] ); - MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQueryBuilder( $queryBuilder, 'revision' ); + $this->changeTagsStore->modifyDisplayQueryBuilder( $queryBuilder, 'revision' ); return $queryBuilder->getQueryInfo( 'join_conds' ); } diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php index 1591a167ff37..fdf2de43970c 100644 --- a/includes/specials/pagers/NewFilesPager.php +++ b/includes/specials/pagers/NewFilesPager.php @@ -27,6 +27,7 @@ use MediaWiki\Cache\LinkBatchFactory; use MediaWiki\Context\IContextSource; use MediaWiki\Html\FormOptions; use MediaWiki\Linker\LinkRenderer; +use MediaWiki\MainConfigNames; use MediaWiki\Permissions\GroupPermissionsLookup; use MediaWiki\Title\Title; use MediaWiki\Title\TitleValue; @@ -39,27 +40,13 @@ use Wikimedia\Rdbms\IConnectionProvider; */ class NewFilesPager extends RangeChronologicalPager { - /** - * @var ImageGalleryBase - */ - protected $gallery; - - /** - * @var FormOptions - */ - protected $opts; + protected ?ImageGalleryBase $gallery = null; + protected FormOptions $opts; private GroupPermissionsLookup $groupPermissionsLookup; private LinkBatchFactory $linkBatchFactory; + private int $migrationStage; - /** - * @param IContextSource $context - * @param GroupPermissionsLookup $groupPermissionsLookup - * @param LinkBatchFactory $linkBatchFactory - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param FormOptions $opts - */ public function __construct( IContextSource $context, GroupPermissionsLookup $groupPermissionsLookup, @@ -87,16 +74,33 @@ class NewFilesPager extends RangeChronologicalPager { $endTimestamp = $opts->getValue( 'end' ) . ' 23:59:59'; } $this->getDateRangeCond( $startTimestamp, $endTimestamp ); + $this->migrationStage = $context->getConfig()->get( + MainConfigNames::FileSchemaMigrationStage + ); } public function getQueryInfo() { $opts = $this->opts; $conds = []; $dbr = $this->getDatabase(); - $tables = [ 'image', 'actor' ]; - $fields = [ 'img_name', 'img_timestamp', 'actor_user', 'actor_name' ]; + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $tables = [ 'image' ]; + $nameField = 'img_name'; + $actorField = 'img_actor'; + $timestampField = 'img_timestamp'; + $jconds = []; + + } else { + $tables = [ 'file', 'filerevision' ]; + $nameField = 'file_name'; + $actorField = 'fr_actor'; + $timestampField = 'fr_timestamp'; + $jconds = [ 'filerevision' => [ 'JOIN', 'file_latest=fr_id' ] ]; + } + $tables[] = 'actor'; + $fields = [ 'img_name' => $nameField, 'img_timestamp' => $timestampField, 'actor_user', 'actor_name' ]; $options = []; - $jconds = [ 'actor' => [ 'JOIN', 'actor_id=img_actor' ] ]; + $jconds['actor'] = [ 'JOIN', 'actor_id=' . $actorField ]; $user = $opts->getValue( 'user' ); if ( $user !== '' ) { @@ -130,15 +134,21 @@ class NewFilesPager extends RangeChronologicalPager { $jconds['recentchanges'] = [ 'JOIN', [ - 'rc_title = img_name', - 'rc_actor = img_actor', - 'rc_timestamp = img_timestamp' + 'rc_title = ' . $nameField, + 'rc_actor = ' . $nameField, + 'rc_timestamp = ' . $timestampField, ] ]; } if ( $opts->getValue( 'mediatype' ) ) { - $conds['img_media_type'] = $opts->getValue( 'mediatype' ); + if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $conds['img_media_type'] = $opts->getValue( 'mediatype' ); + } else { + $tables[] = 'filetypes'; + $jconds['filetypes'] = [ 'JOIN', 'file_type = ft_id' ]; + $conds['ft_media_type'] = $opts->getValue( 'mediatype' ); + } } // We're ordering by img_timestamp, but MariaDB sometimes likes to query other tables first diff --git a/includes/specials/pagers/NewPagesPager.php b/includes/specials/pagers/NewPagesPager.php index b841430fb17a..d7bc461f11c8 100644 --- a/includes/specials/pagers/NewPagesPager.php +++ b/includes/specials/pagers/NewPagesPager.php @@ -54,15 +54,11 @@ use Wikimedia\Rdbms\IExpression; */ class NewPagesPager extends ReverseChronologicalPager { - /** - * @var FormOptions - */ - protected $opts; - + protected FormOptions $opts; protected MapCacheLRU $tagsCache; /** @var string[] */ - private $formattedComments = []; + private array $formattedComments = []; /** @var bool Whether to group items by date by default this is disabled, but eventually the intention * should be to default to true once all pages have been transitioned to support date grouping. */ @@ -77,19 +73,6 @@ class NewPagesPager extends ReverseChronologicalPager { private IContentHandlerFactory $contentHandlerFactory; private TempUserConfig $tempUserConfig; - /** - * @param IContextSource $context - * @param LinkRenderer $linkRenderer - * @param GroupPermissionsLookup $groupPermissionsLookup - * @param HookContainer $hookContainer - * @param LinkBatchFactory $linkBatchFactory - * @param NamespaceInfo $namespaceInfo - * @param ChangeTagsStore $changeTagsStore - * @param RowCommentFormatter $rowCommentFormatter - * @param IContentHandlerFactory $contentHandlerFactory - * @param TempUserConfig $tempUserConfig - * @param FormOptions $opts - */ public function __construct( IContextSource $context, LinkRenderer $linkRenderer, diff --git a/includes/specials/pagers/ProtectedPagesPager.php b/includes/specials/pagers/ProtectedPagesPager.php index 5b5755915320..d595dff3f127 100644 --- a/includes/specials/pagers/ProtectedPagesPager.php +++ b/includes/specials/pagers/ProtectedPagesPager.php @@ -37,46 +37,22 @@ use Wikimedia\Rdbms\IConnectionProvider; class ProtectedPagesPager extends TablePager { - /** @var string */ - private $type; - /** @var string */ - private $level; - /** @var int|null */ - private $namespace; - /** @var string */ - private $sizetype; - /** @var int */ - private $size; - /** @var bool */ - private $indefonly; - /** @var bool */ - private $cascadeonly; - /** @var bool */ - private $noredirect; + private string $type; + private ?string $level; + private ?int $namespace; + private ?string $sizetype; + private int $size; + private bool $indefonly; + private bool $cascadeonly; + private bool $noredirect; private CommentStore $commentStore; private LinkBatchFactory $linkBatchFactory; private RowCommentFormatter $rowCommentFormatter; /** @var string[] */ - private $formattedComments = []; + private array $formattedComments = []; - /** - * @param IContextSource $context - * @param CommentStore $commentStore - * @param LinkBatchFactory $linkBatchFactory - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param RowCommentFormatter $rowCommentFormatter - * @param string $type - * @param string $level - * @param int|null $namespace - * @param string $sizetype - * @param int|null $size - * @param bool $indefonly - * @param bool $cascadeonly - * @param bool $noredirect - */ public function __construct( IContextSource $context, CommentStore $commentStore, @@ -84,14 +60,14 @@ class ProtectedPagesPager extends TablePager { LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, RowCommentFormatter $rowCommentFormatter, - $type, - $level, - $namespace, - $sizetype, - $size, - $indefonly, - $cascadeonly, - $noredirect + ?string $type, + ?string $level, + ?int $namespace, + ?string $sizetype, + ?int $size, + bool $indefonly, + bool $cascadeonly, + bool $noredirect ) { // Set database before parent constructor to avoid setting it there $this->mDb = $dbProvider->getReplicaDatabase(); @@ -99,14 +75,14 @@ class ProtectedPagesPager extends TablePager { $this->commentStore = $commentStore; $this->linkBatchFactory = $linkBatchFactory; $this->rowCommentFormatter = $rowCommentFormatter; - $this->type = $type ?: 'edit'; + $this->type = $type ?? 'edit'; $this->level = $level; $this->namespace = $namespace; $this->sizetype = $sizetype; - $this->size = intval( $size ); - $this->indefonly = (bool)$indefonly; - $this->cascadeonly = (bool)$cascadeonly; - $this->noredirect = (bool)$noredirect; + $this->size = $size ?? 0; + $this->indefonly = $indefonly; + $this->cascadeonly = $cascadeonly; + $this->noredirect = $noredirect; } public function preprocessResults( $result ) { diff --git a/includes/specials/pagers/ProtectedTitlesPager.php b/includes/specials/pagers/ProtectedTitlesPager.php index 247e3c9470c1..52cf5ae90719 100644 --- a/includes/specials/pagers/ProtectedTitlesPager.php +++ b/includes/specials/pagers/ProtectedTitlesPager.php @@ -34,29 +34,18 @@ use Wikimedia\Rdbms\IConnectionProvider; */ class ProtectedTitlesPager extends AlphabeticPager { - /** @var string|null */ - private $level; - - /** @var int|null */ - private $namespace; + private ?string $level; + private ?int $namespace; private LinkBatchFactory $linkBatchFactory; - /** - * @param IContextSource $context - * @param LinkRenderer $linkRenderer - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param string|null $level - * @param int|null $namespace - */ public function __construct( IContextSource $context, LinkRenderer $linkRenderer, LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, - $level, - $namespace + ?string $level, + ?int $namespace ) { // Set database before parent constructor to avoid setting it there $this->mDb = $dbProvider->getReplicaDatabase(); diff --git a/includes/specials/pagers/UploadStashPager.php b/includes/specials/pagers/UploadStashPager.php index 9e79cf2ac2df..68fb9eab61d0 100644 --- a/includes/specials/pagers/UploadStashPager.php +++ b/includes/specials/pagers/UploadStashPager.php @@ -46,13 +46,6 @@ class UploadStashPager extends TablePager { /** @var File[] */ private array $files = []; - /** - * @param IContextSource $context - * @param LinkRenderer $linkRenderer - * @param IConnectionProvider $dbProvider - * @param UploadStash $stash - * @param LocalRepo $localRepo - */ public function __construct( IContextSource $context, LinkRenderer $linkRenderer, diff --git a/includes/specials/pagers/UsersPager.php b/includes/specials/pagers/UsersPager.php index c75390007879..291242634a8f 100644 --- a/includes/specials/pagers/UsersPager.php +++ b/includes/specials/pagers/UsersPager.php @@ -40,7 +40,6 @@ use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Linker\Linker; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; -use MediaWiki\MediaWikiServices; use MediaWiki\Title\Title; use MediaWiki\User\TempUser\TempUserConfig; use MediaWiki\User\UserGroupManager; @@ -66,48 +65,21 @@ class UsersPager extends AlphabeticPager { */ protected $userGroupCache; - /** @var string */ - public $requestedGroup; - - /** @var bool */ - protected $editsOnly; - - /** @var bool */ - protected $temporaryGroupsOnly; - - /** @var bool */ - protected $temporaryAccountsOnly; - - /** @var bool */ - protected $creationSort; - - /** @var bool|null */ - protected $including; - - /** @var string */ - protected $requestedUser; - - /** @var HideUserUtils */ - protected $hideUserUtils; + public ?string $requestedGroup; + protected bool $editsOnly; + protected bool $temporaryGroupsOnly; + protected bool $temporaryAccountsOnly; + protected bool $creationSort; + protected ?bool $including; + protected ?string $requestedUser; + protected HideUserUtils $hideUserUtils; private HookRunner $hookRunner; private LinkBatchFactory $linkBatchFactory; private UserGroupManager $userGroupManager; private UserIdentityLookup $userIdentityLookup; private TempUserConfig $tempUserConfig; - /** - * @param IContextSource $context - * @param HookContainer $hookContainer - * @param LinkBatchFactory $linkBatchFactory - * @param IConnectionProvider $dbProvider - * @param UserGroupManager $userGroupManager - * @param UserIdentityLookup $userIdentityLookup - * @param HideUserUtils $hideUserUtils - * @param string|null $par - * @param bool|null $including Whether this page is being transcluded in - * another page - */ public function __construct( IContextSource $context, HookContainer $hookContainer, @@ -116,8 +88,9 @@ class UsersPager extends AlphabeticPager { UserGroupManager $userGroupManager, UserIdentityLookup $userIdentityLookup, HideUserUtils $hideUserUtils, - $par, - $including + TempUserConfig $tempUserConfig, + ?string $par, + ?bool $including ) { $this->setContext( $context ); @@ -169,7 +142,7 @@ class UsersPager extends AlphabeticPager { $this->linkBatchFactory = $linkBatchFactory; $this->userIdentityLookup = $userIdentityLookup; $this->hideUserUtils = $hideUserUtils; - $this->tempUserConfig = MediaWikiServices::getInstance()->getTempUserConfig(); + $this->tempUserConfig = $tempUserConfig; } /** diff --git a/includes/tidy/RemexCompatMunger.php b/includes/tidy/RemexCompatMunger.php index e100072b3547..a0faf73925df 100644 --- a/includes/tidy/RemexCompatMunger.php +++ b/includes/tidy/RemexCompatMunger.php @@ -21,14 +21,17 @@ class RemexCompatMunger implements TreeHandler { "abbr" => true, "acronym" => true, "applet" => true, + "audio" => true, "b" => true, "basefont" => true, + "bdi" => true, "bdo" => true, "big" => true, "br" => true, "button" => true, "cite" => true, "code" => true, + "data" => true, "del" => true, "dfn" => true, "em" => true, @@ -42,6 +45,7 @@ class RemexCompatMunger implements TreeHandler { "label" => true, "legend" => true, "map" => true, + "mark" => true, "object" => true, "param" => true, "q" => true, @@ -55,22 +59,20 @@ class RemexCompatMunger implements TreeHandler { "samp" => true, "select" => true, "small" => true, + "source" => true, "span" => true, "strike" => true, "strong" => true, "sub" => true, "sup" => true, "textarea" => true, + "time" => true, + "track" => true, "tt" => true, "u" => true, "var" => true, - // Those defined in tidy.conf "video" => true, - "audio" => true, - "bdi" => true, - "data" => true, - "time" => true, - "mark" => true, + "wbr" => true, ]; /** diff --git a/includes/title/MediaWikiTitleCodec.php b/includes/title/MediaWikiTitleCodec.php index 36f66c3e8ce6..46e0393b111f 100644 --- a/includes/title/MediaWikiTitleCodec.php +++ b/includes/title/MediaWikiTitleCodec.php @@ -63,6 +63,27 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser { private $createMalformedTitleException; /** + * Temporary migration helper for DiscussionTools test + * + * @unstable + * @param Language $language + * @param GenderCache $genderCache + * @param array $localInterwikis + * @param InterwikiLookup $interwikiLookup + * @param NamespaceInfo $nsInfo + * @return TitleParser + */ + public static function createParser( + Language $language, + GenderCache $genderCache, + $localInterwikis, + InterwikiLookup $interwikiLookup, + NamespaceInfo $nsInfo + ) { + return new self( $language, $genderCache, $localInterwikis, $interwikiLookup, $nsInfo ); + } + + /** * @param Language $language The language object to use for localizing namespace names, * capitalization, etc. * @param GenderCache $genderCache The gender cache for generating gendered namespace names diff --git a/includes/title/Title.php b/includes/title/Title.php index 2b2e5b04eea2..34e5e03a9d7c 100644 --- a/includes/title/Title.php +++ b/includes/title/Title.php @@ -2953,13 +2953,23 @@ class Title implements Stringable, LinkTarget, PageIdentity { return $data; } - $dbr = $this->getDbProvider()->getReplicaDatabase(); + $migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( + MainConfigNames::CategoryLinksSchemaMigrationStage + ); - $res = $dbr->newSelectQueryBuilder() - ->select( 'cl_to' ) + $dbr = $this->getDbProvider()->getReplicaDatabase(); + $queryBuilder = $dbr->newSelectQueryBuilder() ->from( 'categorylinks' ) - ->where( [ 'cl_from' => $titleKey ] ) - ->caller( __METHOD__ )->fetchResultSet(); + ->where( [ 'cl_from' => $titleKey ] ); + + if ( $migrationStage & SCHEMA_COMPAT_READ_OLD ) { + $queryBuilder->select( 'cl_to' ); + } else { + $queryBuilder->field( 'lt_title', 'cl_to' ) + ->join( 'linktarget', null, 'cl_target_id = lt_id' ) + ->where( [ 'lt_namespace' => NS_CATEGORY ] ); + } + $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); if ( $res->numRows() > 0 ) { $contLang = MediaWikiServices::getInstance()->getContentLanguage(); diff --git a/includes/user/Options/UserOptionsManager.php b/includes/user/Options/UserOptionsManager.php index 7219fcf5959a..b3372374ad7a 100644 --- a/includes/user/Options/UserOptionsManager.php +++ b/includes/user/Options/UserOptionsManager.php @@ -22,14 +22,12 @@ namespace MediaWiki\User\Options; use InvalidArgumentException; use MediaWiki\Config\ServiceOptions; -use MediaWiki\Context\IContextSource; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\Language\LanguageCode; use MediaWiki\Language\LanguageConverter; use MediaWiki\Languages\LanguageConverterFactory; use MediaWiki\MainConfigNames; -use MediaWiki\MediaWikiServices; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use MediaWiki\User\UserNameUtils; @@ -77,6 +75,14 @@ class UserOptionsManager extends UserOptionsLookup { */ public const GLOBAL_UPDATE = 'update'; + /** + * Create a new global preference in the first available global store. + * If there are no global stores, update the local value. If there was + * already a global preference, update it. + * @since 1.44 + */ + public const GLOBAL_CREATE = 'create'; + private const LOCAL_STORE_KEY = 'local'; private ServiceOptions $serviceOptions; @@ -246,12 +252,13 @@ class UserOptionsManager extends UserOptionsLookup { * @param UserIdentity $user * @param string $oname The option to set * @param mixed $val New value to set. - * @param string $global Since 1.43. What to do if the option was set - * globally using the GlobalPreferences extension. One of the - * self::GLOBAL_* constants: - * - GLOBAL_IGNORE: Do nothing. The option remains with its previous value. - * - GLOBAL_OVERRIDE: Add a local override. - * - GLOBAL_UPDATE: Update the option globally. + * @param string $global Since 1.43. The global update behaviour, used if + * GlobalPreferences is installed: + * - GLOBAL_IGNORE: If there is a global preference, do nothing. The option remains with + * its previous value. + * - GLOBAL_OVERRIDE: If there is a global preference, add a local override. + * - GLOBAL_UPDATE: If there is a global preference, update it. + * - GLOBAL_CREATE: Create a new global preference, overriding any local value. * The UI should typically ask for the user's consent before setting a global * option. */ @@ -267,34 +274,6 @@ class UserOptionsManager extends UserOptionsLookup { } /** - * Reset certain (or all) options to the site defaults - * - * The optional parameter determines which kinds of preferences will be reset. - * Supported values are everything that can be reported by getOptionKinds() - * and 'all', which forces a reset of *all* preferences and overrides everything else. - * - * @note You need to call saveOptions() to actually write to the database. - * - * @deprecated since 1.43 use resetOptionsByName() with PreferencesFactory::getOptionNamesForReset() - * - * @param UserIdentity $user - * @param IContextSource $context Context source used when $resetKinds does not contain 'all'. - * @param array|string $resetKinds Which kinds of preferences to reset. - * Defaults to [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ] - */ - public function resetOptions( - UserIdentity $user, - IContextSource $context, - $resetKinds = [ 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ] - ) { - wfDeprecated( __METHOD__, '1.43' ); - $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); - $optionsToReset = $preferencesFactory->getOptionNamesForReset( - $this->userFactory->newFromUserIdentity( $user ), $context, $resetKinds ); - $this->resetOptionsByName( $user, $optionsToReset ); - } - - /** * Reset a list of options to the site defaults * * @note You need to call saveOptions() to actually write to the database. @@ -325,36 +304,6 @@ class UserOptionsManager extends UserOptionsLookup { } /** - * @deprecated since 1.43 use PreferencesFactory::listResetKinds() - * - * @return string[] Option kinds - */ - public function listOptionKinds(): array { - wfDeprecated( __METHOD__, '1.43' ); - $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); - return $preferencesFactory->listResetKinds(); - } - - /** - * @deprecated since 1.43 use PreferencesFactory::getResetKinds - * - * @param UserIdentity $userIdentity - * @param IContextSource $context - * @param array|null $options - * @return string[] - */ - public function getOptionKinds( - UserIdentity $userIdentity, - IContextSource $context, - $options = null - ): array { - wfDeprecated( __METHOD__, '1.43' ); - $user = $this->userFactory->newFromUserIdentity( $userIdentity ); - $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory(); - return $preferencesFactory->getResetKinds( $user, $context, $options ); - } - - /** * Saves the non-default options for this user, as previously set e.g. via * setOption(), in the database's "user_properties" (preferences) table. * @@ -412,11 +361,16 @@ class UserOptionsManager extends UserOptionsLookup { $valOrNull = (string)$value; } $source = $cache->sources[$key] ?? self::LOCAL_STORE_KEY; + $updateAction = $cache->globalUpdateActions[$key] ?? self::GLOBAL_IGNORE; + if ( $source === self::LOCAL_STORE_KEY ) { - $updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull; + if ( $updateAction === self::GLOBAL_CREATE ) { + $updatesByStore[$this->getStoreNameForGlobalCreate()][$key] = $valOrNull; + } else { + $updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull; + } } else { - $updateAction = $cache->globalUpdateActions[$key] ?? self::GLOBAL_IGNORE; - if ( $updateAction === self::GLOBAL_UPDATE ) { + if ( $updateAction === self::GLOBAL_UPDATE || $updateAction === self::GLOBAL_CREATE ) { $updatesByStore[$source][$key] = $valOrNull; } elseif ( $updateAction === self::GLOBAL_OVERRIDE ) { $updatesByStore[self::LOCAL_STORE_KEY][$key] = $valOrNull; @@ -667,6 +621,21 @@ class UserOptionsManager extends UserOptionsLookup { } return $this->stores; } + + /** + * Get the name of the store to be used when setOption() is called with + * GLOBAL_CREATE and there is no existing global preference value. + * + * @return string + */ + private function getStoreNameForGlobalCreate() { + foreach ( $this->getStores() as $name => $store ) { + if ( $name !== self::LOCAL_STORE_KEY ) { + return $name; + } + } + return self::LOCAL_STORE_KEY; + } } /** @deprecated class alias since 1.42 */ diff --git a/includes/user/Registration/IUserRegistrationProvider.php b/includes/user/Registration/IUserRegistrationProvider.php index 307df5a78258..4d9feecbc84f 100644 --- a/includes/user/Registration/IUserRegistrationProvider.php +++ b/includes/user/Registration/IUserRegistrationProvider.php @@ -18,4 +18,14 @@ interface IUserRegistrationProvider { * cannot be fetched (anonymous users, for example). */ public function fetchRegistration( UserIdentity $user ); + + /** + * Get user registration timestamps for a batch of users. + * + * @since 1.44 + * @param iterable<UserIdentity> $users + * @return string[]|null[] Map of registration timestamps in MediaWiki format + * (or `null` if not available) keyed by user ID. + */ + public function fetchRegistrationBatch( iterable $users ): array; } diff --git a/includes/user/Registration/LocalUserRegistrationProvider.php b/includes/user/Registration/LocalUserRegistrationProvider.php index 4cb1b0ec1e5a..f522f0cc9e1c 100644 --- a/includes/user/Registration/LocalUserRegistrationProvider.php +++ b/includes/user/Registration/LocalUserRegistrationProvider.php @@ -4,15 +4,21 @@ namespace MediaWiki\User\Registration; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; +use Wikimedia\Rdbms\IConnectionProvider; class LocalUserRegistrationProvider implements IUserRegistrationProvider { public const TYPE = 'local'; private UserFactory $userFactory; + private IConnectionProvider $connectionProvider; - public function __construct( UserFactory $userFactory ) { + public function __construct( + UserFactory $userFactory, + IConnectionProvider $connectionProvider + ) { $this->userFactory = $userFactory; + $this->connectionProvider = $connectionProvider; } /** @@ -23,4 +29,35 @@ class LocalUserRegistrationProvider implements IUserRegistrationProvider { $user = $this->userFactory->newFromUserIdentity( $user ); return $user->getRegistration(); } + + /** + * @inheritDoc + */ + public function fetchRegistrationBatch( iterable $users ): array { + $timestampsById = []; + + foreach ( $users as $user ) { + // Make the list of user IDs unique. + $timestampsById[$user->getId()] = null; + } + + $batches = array_chunk( array_keys( $timestampsById ), 1_000 ); + + $dbr = $this->connectionProvider->getReplicaDatabase(); + + foreach ( $batches as $userIdBatch ) { + $res = $dbr->newSelectQueryBuilder() + ->select( [ 'user_id', 'user_registration' ] ) + ->from( 'user' ) + ->where( [ 'user_id' => $userIdBatch ] ) + ->caller( __METHOD__ ) + ->fetchResultSet(); + + foreach ( $res as $row ) { + $timestampsById[$row->user_id] = wfTimestampOrNull( TS_MW, $row->user_registration ); + } + } + + return $timestampsById; + } } diff --git a/includes/user/Registration/UserRegistrationLookup.php b/includes/user/Registration/UserRegistrationLookup.php index 2a9fad46aa3b..0eb0e43323a9 100644 --- a/includes/user/Registration/UserRegistrationLookup.php +++ b/includes/user/Registration/UserRegistrationLookup.php @@ -108,4 +108,34 @@ class UserRegistrationLookup { return $firstRegistrationTimestamp; } + + /** + * Get the first registration timestamp for a batch of users. + * This invokes all registered providers. + * + * @param iterable<UserIdentity> $users + * @return string[]|null[] Map of registration timestamps in MediaWiki format keyed by user ID. + * The timestamp may be `null` for users without a stored registration timestamp and for anonymous users. + */ + public function getFirstRegistrationBatch( iterable $users ): array { + $earliestTimestampsById = []; + + foreach ( $users as $user ) { + $earliestTimestampsById[$user->getId()] = null; + } + + foreach ( $this->providersSpecs as $providerKey => $_ ) { + $timestampsById = $this->getProvider( $providerKey )->fetchRegistrationBatch( $users ); + + foreach ( $timestampsById as $userId => $timestamp ) { + $curValue = $earliestTimestampsById[$userId]; + + if ( $timestamp !== null && ( $curValue === null || $timestamp < $curValue ) ) { + $earliestTimestampsById[$userId] = $timestamp; + } + } + } + + return $earliestTimestampsById; + } } diff --git a/includes/user/TempUser/TempUserDetailsLookup.php b/includes/user/TempUser/TempUserDetailsLookup.php new file mode 100644 index 000000000000..a84730d1c3a8 --- /dev/null +++ b/includes/user/TempUser/TempUserDetailsLookup.php @@ -0,0 +1,96 @@ +<?php +declare( strict_types=1 ); +namespace MediaWiki\User\TempUser; + +use ArrayIterator; +use CallbackFilterIterator; +use IteratorIterator; +use MapCacheLRU; +use MediaWiki\User\Registration\UserRegistrationLookup; +use MediaWiki\User\UserIdentity; + +/** + * Caching lookup service for metadata related to temporary accounts, such as expiration. + * @since 1.44 + */ +class TempUserDetailsLookup { + private TempUserConfig $tempUserConfig; + private UserRegistrationLookup $userRegistrationLookup; + + private MapCacheLRU $expiryCache; + + public function __construct( + TempUserConfig $tempUserConfig, + UserRegistrationLookup $userRegistrationLookup + ) { + $this->tempUserConfig = $tempUserConfig; + $this->userRegistrationLookup = $userRegistrationLookup; + + // Use a relatively large cache size to account for pages with a high number of user links, + // such as Special:RecentChanges or history pages. + $this->expiryCache = new MapCacheLRU( 1_000 ); + } + + /** + * Check if a temporary user account is expired. + * @param UserIdentity $user + * @return bool `true` if the account is expired, `false` otherwise. + */ + public function isExpired( UserIdentity $user ): bool { + if ( + !$this->tempUserConfig->isTempName( $user->getName() ) || + !$user->isRegistered() + ) { + return false; + } + + $userId = $user->getId(); + + if ( !$this->expiryCache->has( $userId ) ) { + $registration = $this->userRegistrationLookup->getFirstRegistration( $user ); + + $this->expiryCache->set( $userId, $this->getExpirationState( $registration ) ); + } + + return $this->expiryCache->get( $userId ); + } + + /** + * Preload the expiration status of temporary accounts within a set of users. + * + * @param iterable<UserIdentity> $users The users to preload the expiration status for. + */ + public function preloadExpirationStatus( iterable $users ): void { + $users = is_array( $users ) ? new ArrayIterator( $users ) : new IteratorIterator( $users ); + $timestampsById = $this->userRegistrationLookup->getFirstRegistrationBatch( + new CallbackFilterIterator( + $users, + fn ( UserIdentity $user ): bool => + $user->isRegistered() && $this->tempUserConfig->isTempName( $user->getName() ) + ) + ); + + foreach ( $timestampsById as $userId => $registrationTimestamp ) { + $this->expiryCache->set( + $userId, + $this->getExpirationState( $registrationTimestamp ) + ); + } + } + + /** + * Check whether a temporary account registered at the given timestamp is expired now. + * @param string|null|false $registration DB timestamp of the registration time. + * May be `null` or `false` if not known. + * @return bool `true` if the account is expired, `false` otherwise. + */ + private function getExpirationState( $registration ): bool { + if ( !is_string( $registration ) ) { + return false; + } + + $expireAfterDays = $this->tempUserConfig->getExpireAfterDays(); + $expiresAt = (int)wfTimestamp( TS_UNIX, $registration ) + $expireAfterDays * 86400; + return $expiresAt < wfTimestamp( TS_UNIX ); + } +} diff --git a/includes/user/User.php b/includes/user/User.php index a3a72c667cf2..9621d1c830a6 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -990,8 +990,13 @@ class User implements Stringable, Authority, UserIdentity, UserEmailContact { * @since 1.23 */ public function checkPasswordValidity( $password ) { - $passwordPolicy = MediaWikiServices::getInstance()->getMainConfig() - ->get( MainConfigNames::PasswordPolicy ); + $services = MediaWikiServices::getInstance(); + $userNameUtils = $services->getUserNameUtils(); + if ( $userNameUtils->isTemp( $this->getName() ) ) { + return Status::newFatal( 'error-temporary-accounts-cannot-have-passwords' ); + } + + $passwordPolicy = $services->getMainConfig()->get( MainConfigNames::PasswordPolicy ); $upp = new UserPasswordPolicy( $passwordPolicy['policies'], diff --git a/includes/user/UserArray.php b/includes/user/UserArray.php index 629a72824f53..c2dbef3ebcab 100644 --- a/includes/user/UserArray.php +++ b/includes/user/UserArray.php @@ -38,7 +38,7 @@ abstract class UserArray implements Iterator, Countable { * moving towards deprecation. * * @param IResultWrapper $res - * @return UserArray + * @return self */ public static function newFromResult( $res ): self { $userArray = null; @@ -55,11 +55,11 @@ abstract class UserArray implements Iterator, Countable { * In case you need full User objects, you can keep using this method, but it's * moving towards deprecation. * - * @param array $ids - * @return UserArray + * @param int[] $ids + * @return self */ - public static function newFromIDs( $ids ): self { - $ids = array_map( 'intval', (array)$ids ); // paranoia + public static function newFromIDs( array $ids ): self { + $ids = array_map( 'intval', $ids ); // paranoia if ( !$ids ) { // Database::select() doesn't like empty arrays return new UserArrayFromResult( new FakeResultWrapper( [] ) ); @@ -79,11 +79,11 @@ abstract class UserArray implements Iterator, Countable { * moving towards deprecation. * * @since 1.25 - * @param array $names - * @return UserArray + * @param string[] $names + * @return self */ - public static function newFromNames( $names ): self { - $names = array_map( 'strval', (array)$names ); // paranoia + public static function newFromNames( array $names ): self { + $names = array_map( 'strval', $names ); // paranoia if ( !$names ) { // Database::select() doesn't like empty arrays return new UserArrayFromResult( new FakeResultWrapper( [] ) ); @@ -96,19 +96,10 @@ abstract class UserArray implements Iterator, Countable { return self::newFromResult( $res ); } - /** - * @return int - */ abstract public function count(): int; - /** - * @return User - */ abstract public function current(): User; - /** - * @return int - */ abstract public function key(): int; } diff --git a/includes/user/UserArrayFromResult.php b/includes/user/UserArrayFromResult.php index febb61135b6f..611449152dbf 100644 --- a/includes/user/UserArrayFromResult.php +++ b/includes/user/UserArrayFromResult.php @@ -27,34 +27,23 @@ use Wikimedia\Rdbms\IResultWrapper; * @internal Call and type against UserArray instead. */ class UserArrayFromResult extends UserArray { - /** @var IResultWrapper */ - public $res; - /** @var int */ - public $key; + private IResultWrapper $res; + private int $key = 0; + /** FIXME not private because CentralAuth is extending this class when it shouldn't. See T387148 */ + protected ?User $current = null; - /** @var User|false */ - public $current; - - /** - * @param IResultWrapper $res - */ - public function __construct( $res ) { + public function __construct( IResultWrapper $res ) { $this->res = $res; - $this->key = 0; - $this->setCurrent( $this->res->current() ); + $this->rewind(); } /** - * @param stdClass|false $row + * @param stdClass|null|false $row * @return void */ protected function setCurrent( $row ) { - if ( $row === false ) { - $this->current = false; - } else { - $this->current = User::newFromRow( $row ); - } + $this->current = $row instanceof stdClass ? User::newFromRow( $row ) : null; } public function count(): int { @@ -70,8 +59,7 @@ class UserArrayFromResult extends UserArray { } public function next(): void { - $row = $this->res->fetchObject(); - $this->setCurrent( $row ); + $this->setCurrent( $this->res->fetchObject() ); $this->key++; } @@ -82,7 +70,7 @@ class UserArrayFromResult extends UserArray { } public function valid(): bool { - return $this->current !== false; + return (bool)$this->current; } } diff --git a/includes/widget/OrderedMultiselectWidget.php b/includes/widget/OrderedMultiselectWidget.php new file mode 100644 index 000000000000..fbf9e8a060c6 --- /dev/null +++ b/includes/widget/OrderedMultiselectWidget.php @@ -0,0 +1,32 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * Widget to select multiple options from a dropdown. + * + * @license MIT + */ +class OrderedMultiselectWidget extends TagMultiselectWidget { + + private array $mOptions; + + /** + * @param array $config Configuration options + * - array $config['options'] Grouped options for the dropdown menu + */ + public function __construct( $config ) { + $this->mOptions = $config['options']; + parent::__construct( $config ); + } + + public function getConfig( &$config ) { + $config['options'] = $this->mOptions; + + return parent::getConfig( $config ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.OrderedMultiselectWidget'; + } +} |