From: jenkins-bot Date: Mon, 27 Aug 2018 21:44:23 +0000 (+0000) Subject: Merge "resourceloader: Restore mw.loader.store update postponing logic" X-Git-Tag: 1.34.0-rc.0~4287 X-Git-Url: http://git.cyclocoop.org/%28%28?a=commitdiff_plain;h=9b140b4fc6d4aed0152f5717ccafb78bba1f08fc;hp=d269e1d91f76719fcf6f3e08175ba4a0a1ce0a17;p=lhc%2Fweb%2Fwiklou.git Merge "resourceloader: Restore mw.loader.store update postponing logic" --- diff --git a/.phpcs.xml b/.phpcs.xml index 944c3e20e1..65ddb73393 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -16,7 +16,6 @@ - @@ -137,7 +136,6 @@ */maintenance/7zip.inc */maintenance/CodeCleanerGlobalsPass.inc */maintenance/archives/upgradeLogging\.php - */maintenance/backup.inc */maintenance/benchmarks/bench_HTTP_HTTPS\.php */maintenance/benchmarks/bench_Wikimedia_base_convert\.php */maintenance/benchmarks/bench_delete_truncate\.php diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32 index 0ad2e41b65..1deca12eeb 100644 --- a/RELEASE-NOTES-1.32 +++ b/RELEASE-NOTES-1.32 @@ -40,6 +40,8 @@ production. * The $wgPasswordSenderName setting, ignored since 1.23 by MediaWiki and almost all extensions, is no longer set at all. Instead, you can modify the system message `emailsender`. +* A new configuration setting, $wgRawHtmlMessages, is added, for listing + messages which are displayed as raw HTML. === New features in 1.32 === * (T112474) Generalized the ResourceLoader mechanism for overriding modules @@ -75,6 +77,11 @@ production. render diffs between two Content objects, and DifferenceEngine::setRevisions() to render diffs between two custom (potentially multi-content) revisions. Added GetSlotDiffRenderer hook which works like GetDifferenceEngine for slots. +* Added a temporary action=mcrundo to the web UI, as the normal undo logic + can't yet handle MCR and deadlines are forcing is to put off fixing that. + This action should be considered deprecated and should not be used directly. +* Extensions overriding ContentHandler::getUndoContent() will need to be + updated for the changed method signature. === External library changes in 1.32 === * … @@ -136,6 +143,18 @@ production. * action=query&prop=deletedrevisions, action=query&list=allrevisions, and action=query&list=alldeletedrevisions are changed similarly to &prop=revisions (see the three previous items). +* (T174032) action=compare now supports multi-content revisions. + * It has a 'slots' parameter to select diffing of individual slots. The + default behavior is to return one combined diff. + * The 'fromtext', 'fromsection', 'fromcontentmodel', 'fromcontentformat', + 'totext', 'tosection', 'tocontentmodel', and 'tocontentformat' parameters + are deprecated. Specify the new 'fromslots' and 'toslots' to identify which + slots have text supplied and the corresponding templated parameters for + each slot. + * The behavior of 'fromsection' and 'tosection' of extracting one section's + content is not being preserved. 'fromsection-{slot}' and 'tosection-{slot}' + instead expand the given text as if for a section edit. This effectively + declines T183823 in favor of T185723. === Action API internal changes in 1.32 === * Added 'ApiParseMakeOutputPage' hook. @@ -377,6 +396,8 @@ because of Phabricator reports. MediaWikiServices. * mw.user.stickyRandomId was renamed to the more explicit mw.user.getPageviewToken to better capture its function. +* Passing Revision objects to ContentHandler::getUndoContent() is deprecated, + Content object should be passed instead. === Other changes in 1.32 === * (T198811) The following tables have had their UNIQUE indexes turned into diff --git a/api.php b/api.php index 9c5ac95716..9cf75787ba 100644 --- a/api.php +++ b/api.php @@ -72,7 +72,11 @@ try { if ( !$processor instanceof ApiMain ) { throw new MWException( 'ApiBeforeMain hook set $processor to a non-ApiMain class' ); } -} catch ( Exception $e ) { +} catch ( Exception $e ) { // @todo Remove this block when HHVM is no longer supported + // Crap. Try to report the exception in API format to be friendly to clients. + ApiMain::handleApiBeforeMainException( $e ); + $processor = false; +} catch ( Throwable $e ) { // Crap. Try to report the exception in API format to be friendly to clients. ApiMain::handleApiBeforeMainException( $e ); $processor = false; @@ -99,7 +103,9 @@ if ( $wgAPIRequestLog ) { try { $manager = $processor->getModuleManager(); $module = $manager->getModule( $wgRequest->getVal( 'action' ), 'action' ); - } catch ( Exception $ex ) { + } catch ( Exception $ex ) { // @todo Remove this block when HHVM is no longer supported + $module = null; + } catch ( Throwable $ex ) { $module = null; } if ( !$module || $module->mustBePosted() ) { diff --git a/autoload.php b/autoload.php index e960f42274..10aab64e8a 100644 --- a/autoload.php +++ b/autoload.php @@ -174,7 +174,7 @@ $wgAutoloadLocalClasses = [ 'AvroValidator' => __DIR__ . '/includes/utils/AvroValidator.php', 'BacklinkCache' => __DIR__ . '/includes/cache/BacklinkCache.php', 'BacklinkJobUtils' => __DIR__ . '/includes/jobqueue/utils/BacklinkJobUtils.php', - 'BackupDumper' => __DIR__ . '/maintenance/backup.inc', + 'BackupDumper' => __DIR__ . '/maintenance/includes/BackupDumper.php', 'BackupReader' => __DIR__ . '/maintenance/importDump.php', 'BadRequestError' => __DIR__ . '/includes/exception/BadRequestError.php', 'BadTitleError' => __DIR__ . '/includes/exception/BadTitleError.php', @@ -188,7 +188,6 @@ $wgAutoloadLocalClasses = [ 'BcryptPassword' => __DIR__ . '/includes/password/BcryptPassword.php', 'BenchHttpHttps' => __DIR__ . '/maintenance/benchmarks/bench_HTTP_HTTPS.php', 'BenchIfSwitch' => __DIR__ . '/maintenance/benchmarks/bench_if_switch.php', - 'BenchStrtrStrReplace' => __DIR__ . '/maintenance/benchmarks/bench_strtr_str_replace.php', 'BenchUtf8TitleCheck' => __DIR__ . '/maintenance/benchmarks/bench_utf8_title_check.php', 'BenchWfIsWindows' => __DIR__ . '/maintenance/benchmarks/bench_wfIsWindows.php', 'BenchWikimediaBaseConvert' => __DIR__ . '/maintenance/benchmarks/bench_Wikimedia_base_convert.php', @@ -201,6 +200,7 @@ $wgAutoloadLocalClasses = [ 'BenchmarkParse' => __DIR__ . '/maintenance/benchmarks/benchmarkParse.php', 'BenchmarkPurge' => __DIR__ . '/maintenance/benchmarks/benchmarkPurge.php', 'BenchmarkSanitizer' => __DIR__ . '/maintenance/benchmarks/benchmarkSanitizer.php', + 'BenchmarkStringReplacement' => __DIR__ . '/maintenance/benchmarks/benchmarkStringReplacement.php', 'BenchmarkTidy' => __DIR__ . '/maintenance/benchmarks/benchmarkTidy.php', 'BenchmarkTitleValue' => __DIR__ . '/maintenance/benchmarks/benchmarkTitleValue.php', 'Benchmarker' => __DIR__ . '/maintenance/benchmarks/Benchmarker.php', @@ -851,6 +851,7 @@ $wgAutoloadLocalClasses = [ 'MappedIterator' => __DIR__ . '/includes/libs/MappedIterator.php', 'MarkpatrolledAction' => __DIR__ . '/includes/actions/MarkpatrolledAction.php', 'McTest' => __DIR__ . '/maintenance/mctest.php', + 'McrUndoAction' => __DIR__ . '/includes/actions/McrUndoAction.php', 'MediaHandler' => __DIR__ . '/includes/media/MediaHandler.php', 'MediaHandlerFactory' => __DIR__ . '/includes/media/MediaHandlerFactory.php', 'MediaStatisticsPage' => __DIR__ . '/includes/specials/SpecialMediaStatistics.php', @@ -936,6 +937,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Special\\SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php', 'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php', 'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php', + 'MediaWiki\\Widget\\CheckMatrixWidget' => __DIR__ . '/includes/widget/CheckMatrixWidget.php', 'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php', 'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php', 'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php', @@ -1485,7 +1487,7 @@ $wgAutoloadLocalClasses = [ 'ThrottledError' => __DIR__ . '/includes/exception/ThrottledError.php', 'ThumbnailImage' => __DIR__ . '/includes/media/MediaTransformOutput.php', 'ThumbnailRenderJob' => __DIR__ . '/includes/jobqueue/jobs/ThumbnailRenderJob.php', - 'TidyUpBug37714' => __DIR__ . '/maintenance/tidyUpBug37714.php', + 'TidyUpT39714' => __DIR__ . '/maintenance/tidyUpT39714.php', 'TiffHandler' => __DIR__ . '/includes/media/TiffHandler.php', 'Timing' => __DIR__ . '/includes/libs/Timing.php', 'Title' => __DIR__ . '/includes/Title.php', diff --git a/docs/extension.schema.v1.json b/docs/extension.schema.v1.json index c9a887dd9c..0ff169c3c2 100644 --- a/docs/extension.schema.v1.json +++ b/docs/extension.schema.v1.json @@ -668,6 +668,13 @@ "type": "string" } }, + "RawHtmlMessages": { + "type": "array", + "description": "Messages which are rendered as raw HTML", + "items": { + "type": "string" + } + }, "callback": { "type": [ "array", diff --git a/docs/extension.schema.v2.json b/docs/extension.schema.v2.json index 24212a9a54..7de5ed5f9a 100644 --- a/docs/extension.schema.v2.json +++ b/docs/extension.schema.v2.json @@ -690,6 +690,13 @@ "type": "string" } }, + "RawHtmlMessages": { + "type": "array", + "description": "Messages which are rendered as raw HTML", + "items": { + "type": "string" + } + }, "callback": { "type": [ "array", diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index fdac10a53e..ea368bc6b5 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -3792,6 +3792,16 @@ $wgResourceLoaderMaxQueryLength = false; */ $wgResourceLoaderValidateJS = true; +/** + * When enabled, execution of JavaScript modules is profiled client-side. + * + * Instrumentation happens in mw.loader.profiler. + * Use `mw.inspect('time')` from the browser console to display the data. + * + * @since 1.32 + */ +$wgResourceLoaderEnableJSProfiler = false; + /** * Whether ResourceLoader should attempt to persist modules in localStorage on * browsers that support the Web Storage API. @@ -7998,6 +8008,7 @@ $wgActions = [ 'history' => true, 'info' => true, 'markpatrolled' => true, + 'mcrundo' => McrUndoAction::class, 'protect' => true, 'purge' => true, 'raw' => true, @@ -8834,6 +8845,22 @@ $wgCSPHeader = false; */ $wgCSPReportOnlyHeader = false; +/** + * List of messages which might contain raw HTML. + * Extensions should add their messages here. The list is used for access control: + * changing messages listed here will require editsitecss and editsitejs rights. + * + * @since 1.32 + * @var string[] + */ +$wgRawHtmlMessages = [ + 'copyright', + 'history_copyright', + 'googlesearch', + 'feedback-terms', + 'feedback-termsofuse', +]; + /** * Mapping of event channels (or channel categories) to EventRelayer configuration. * diff --git a/includes/EditPage.php b/includes/EditPage.php index d1f874ead7..e087a6e217 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -684,7 +684,10 @@ class EditPage { # checking, etc. if ( 'initial' == $this->formtype || $this->firsttime ) { if ( $this->initialiseForm() === false ) { - $this->noSuchSectionPage(); + $out = $this->context->getOutput(); + if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it + $this->noSuchSectionPage(); + } return; } @@ -1220,8 +1223,13 @@ class EditPage { !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev ) ) { - // Cannot yet undo edits that involve anything other the main slot. - $undoMsg = 'main-slot-only'; + // Hack for undo while EditPage can't handle multi-slot editing + $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [ + 'action' => 'mcrundo', + 'undo' => $undo, + 'undoafter' => $undoafter, + ] ) ); + return false; } else { $content = $this->page->getUndoContent( $undorev, $oldrev ); diff --git a/includes/Linker.php b/includes/Linker.php index 08a57247a6..7e565224cf 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -431,7 +431,11 @@ class Linker { $s = $thumb->toHtml( $params ); } if ( $frameParams['align'] != '' ) { - $s = "
{$s}
"; + $s = Html::rawElement( + 'div', + [ 'class' => 'float' . $frameParams['align'] ], + $s + ); } return str_replace( "\n", ' ', $prefix . $s . $postfix ); } diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 1a19465ed5..99b2942ef6 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -72,7 +72,7 @@ return [ 'BlobStoreFactory' => function ( MediaWikiServices $services ) : BlobStoreFactory { return new BlobStoreFactory( - $services->getDBLoadBalancer(), + $services->getDBLoadBalancerFactory(), $services->getMainWANObjectCache(), $services->getMainConfig(), $services->getContentLanguage() diff --git a/includes/Storage/BlobStoreFactory.php b/includes/Storage/BlobStoreFactory.php index 63ca74def4..4e1f97ffe1 100644 --- a/includes/Storage/BlobStoreFactory.php +++ b/includes/Storage/BlobStoreFactory.php @@ -23,7 +23,7 @@ namespace MediaWiki\Storage; use Config; use Language; use WANObjectCache; -use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\LBFactory; /** * Service for instantiating BlobStores @@ -35,9 +35,9 @@ use Wikimedia\Rdbms\LoadBalancer; class BlobStoreFactory { /** - * @var LoadBalancer + * @var LBFactory */ - private $loadBalancer; + private $lbFactory; /** * @var WANObjectCache @@ -55,12 +55,12 @@ class BlobStoreFactory { private $contLang; public function __construct( - LoadBalancer $loadBalancer, + LBFactory $lbFactory, WANObjectCache $cache, Config $mainConfig, Language $contLang ) { - $this->loadBalancer = $loadBalancer; + $this->lbFactory = $lbFactory; $this->cache = $cache; $this->config = $mainConfig; $this->contLang = $contLang; @@ -85,8 +85,9 @@ class BlobStoreFactory { * @return SqlBlobStore */ public function newSqlBlobStore( $wikiId = false ) { + $lb = $this->lbFactory->getMainLB( $wikiId ); $store = new SqlBlobStore( - $this->loadBalancer, + $lb, $this->cache, $wikiId ); diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index a00766fa74..dacec96f99 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -1408,21 +1408,16 @@ class DerivedPageDataUpdater implements IDBAccessObject { $recursive = $this->options['changed']; // T52785 $updates = $this->getSecondaryDataUpdates( $recursive ); + $triggeringUser = $this->options['triggeringuser'] ?? $this->user; + if ( !$triggeringUser instanceof User ) { + $triggeringUser = User::newFromIdentity( $triggeringUser ); + } foreach ( $updates as $update ) { // TODO: make an $option field for the cause - $update->setCause( 'edit-page', $this->user->getName() ); + $update->setCause( 'edit-page', $triggeringUser->getName() ); if ( $update instanceof LinksUpdate ) { $update->setRevision( $legacyRevision ); - - if ( !empty( $this->options['triggeringuser'] ) ) { - /** @var UserIdentity|User $triggeringUser */ - $triggeringUser = $this->options['triggeringuser']; - if ( !$triggeringUser instanceof User ) { - $triggeringUser = User::newFromIdentity( $triggeringUser ); - } - - $update->setTriggeringUser( $triggeringUser ); - } + $update->setTriggeringUser( $triggeringUser ); } DeferredUpdates::addUpdate( $update ); } diff --git a/includes/Storage/NameTableStore.php b/includes/Storage/NameTableStore.php index 52e8f5b993..6c7919d254 100644 --- a/includes/Storage/NameTableStore.php +++ b/includes/Storage/NameTableStore.php @@ -27,7 +27,6 @@ use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; -use Wikimedia\Rdbms\LoadBalancer; /** * @author Addshore @@ -35,7 +34,7 @@ use Wikimedia\Rdbms\LoadBalancer; */ class NameTableStore { - /** @var LoadBalancer */ + /** @var ILoadBalancer */ private $loadBalancer; /** @var WANObjectCache */ @@ -159,11 +158,13 @@ class NameTableStore { if ( $searchResult === false ) { $id = $this->store( $name ); if ( $id === null ) { - // RACE: $name was already in the db, probably just inserted, so load from master - // Use DBO_TRX to avoid missing inserts due to other threads or REPEATABLE-READs - $table = $this->loadTable( - $this->getDBConnection( DB_MASTER, LoadBalancer::CONN_TRX_AUTOCOMMIT ) - ); + // RACE: $name was already in the db, probably just inserted, so load from master. + // Use DBO_TRX to avoid missing inserts due to other threads or REPEATABLE-READs. + // ...but not during unit tests, because we need the fake DB tables of the default + // connection. + $connFlags = defined( 'MW_PHPUNIT_TEST' ) ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT; + $table = $this->reloadMap( $connFlags ); + $searchResult = array_search( $name, $table, true ); if ( $searchResult === false ) { // Insert failed due to IGNORE flag, but DB_MASTER didn't give us the data @@ -172,14 +173,15 @@ class NameTableStore { $this->logger->error( $m ); throw new NameTableAccessException( $m ); } - $this->purgeWANCache( - function () { - $this->cache->reap( $this->getCacheKey(), INF ); - } - ); + } elseif ( isset( $table[$id] ) ) { + throw new NameTableAccessException( + "Expected unused ID from database insert for '$name' " + . " into '{$this->table}', but ID $id is already associated with" + . " the name '{$table[$id]}'! This may indicate database corruption!" ); } else { $table[$id] = $name; $searchResult = $id; + // As store returned an ID we know we inserted so delete from WAN cache $this->purgeWANCache( function () { @@ -193,6 +195,31 @@ class NameTableStore { return $searchResult; } + /** + * Reloads the name table from the master database, and purges the WAN cache entry. + * + * @note This should only be called in situations where the local cache has been detected + * to be out of sync with the database. There should be no reason to call this method + * from outside the NameTabelStore during normal operation. This method may however be + * useful in unit tests. + * + * @param int $connFlags ILoadBalancer::CONN_XXX flags. Optional. + * + * @return \string[] The freshly reloaded name map + */ + public function reloadMap( $connFlags = 0 ) { + $this->tableCache = $this->loadTable( + $this->getDBConnection( DB_MASTER, $connFlags ) + ); + $this->purgeWANCache( + function () { + $this->cache->reap( $this->getCacheKey(), INF ); + } + ); + + return $this->tableCache; + } + /** * Get the id of the given name. * If the name doesn't exist this will throw. diff --git a/includes/Storage/PageUpdater.php b/includes/Storage/PageUpdater.php index c6795ea83a..838efcd7e2 100644 --- a/includes/Storage/PageUpdater.php +++ b/includes/Storage/PageUpdater.php @@ -351,6 +351,15 @@ class PageUpdater { $this->slotsUpdate->modifyContent( $role, $content ); } + /** + * Set the new slot for the given slot role + * + * @param SlotRecord $slot + */ + public function setSlot( SlotRecord $slot ) { + $this->slotsUpdate->modifySlot( $slot ); + } + /** * Explicitly inherit a slot from some earlier revision. * diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php index 5769527796..d219267298 100644 --- a/includes/Storage/RevisionStore.php +++ b/includes/Storage/RevisionStore.php @@ -746,6 +746,76 @@ class RevisionStore if ( !isset( $revisionRow['rev_id'] ) ) { // only if auto-increment was used $revisionRow['rev_id'] = intval( $dbw->insertId() ); + + if ( $dbw->getType() === 'mysql' ) { + // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the + // auto-increment value to disk, so on server restart it might reuse IDs from deleted + // revisions. We can fix that with an insert with an explicit rev_id value, if necessary. + + $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) ); + $table = 'archive'; + if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) { + $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) ); + if ( $maxRevId2 >= $maxRevId ) { + $maxRevId = $maxRevId2; + $table = 'slots'; + } + } + + if ( $maxRevId >= $revisionRow['rev_id'] ) { + $this->logger->debug( + '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.' + . ' Trying to fix it.', + [ + 'revid' => $revisionRow['rev_id'], + 'table' => $table, + 'maxrevid' => $maxRevId, + ] + ); + + if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) { + throw new MWException( 'Failed to get database lock for T202032' ); + } + $fname = __METHOD__; + $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) { + $dbw->unlock( 'fix-for-T202032', $fname ); + } ); + + $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ ); + + // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction + // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing + // inserts too, though, at least on MariaDB 10.1.29. + // + // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent + // transactions in this code path thanks to the row lock from the original ->insert() above. + // + // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning + // that's for non-MySQL DBs. + $row1 = $dbw->query( + $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE' + )->fetchObject(); + if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) { + $row2 = $dbw->query( + $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ ) + . ' FOR UPDATE' + )->fetchObject(); + } else { + $row2 = null; + } + $maxRevId = max( + $maxRevId, + $row1 ? intval( $row1->v ) : 0, + $row2 ? intval( $row2->v ) : 0 + ); + + // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent + // transactions will throw a duplicate key error here. It doesn't seem worth trying + // to avoid that. + $revisionRow['rev_id'] = $maxRevId + 1; + $dbw->insert( 'revision', $revisionRow, __METHOD__ ); + } + } } $commentCallback( $revisionRow['rev_id'] ); @@ -1477,6 +1547,10 @@ class RevisionStore $slots = []; foreach ( $res as $row ) { + // resolve role names and model names from in-memory cache, instead of joining. + $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); + $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); + $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) { return $this->loadSlotContent( $slot, null, null, null, $queryFlags ); }; @@ -2174,7 +2248,9 @@ class RevisionStore // NOTE: even when this class is set to not read from the old schema, callers // should still be able to join against the text table, as long as we are still // writing the old schema for compatibility. - wfDeprecated( __METHOD__ . ' with `text` option', '1.32' ); + // TODO: This should trigger a deprecation warning eventually (T200918), but not + // before all known usages are removed (see T198341 and T201164). + // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' ); } $ret['tables'][] = 'text'; @@ -2196,6 +2272,9 @@ class RevisionStore * * @param array $options Any combination of the following strings * - 'content': Join with the content table, and select content meta-data fields + * - 'model': Join with the content_models table, and select the model_name field. + * Only applicable if 'content' is also set. + * - 'role': Join with the slot_roles table, and select the role_name field * * @return array With three keys: * - tables: (string[]) to include in the `$table` to `IDatabase->select()` @@ -2232,26 +2311,39 @@ class RevisionStore } } else { $ret['tables'][] = 'slots'; - $ret['tables'][] = 'slot_roles'; $ret['fields'] = array_merge( $ret['fields'], [ 'slot_revision_id', 'slot_content_id', 'slot_origin', - 'role_name' + 'slot_role_id', ] ); - $ret['joins']['slot_roles'] = [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ]; + + if ( in_array( 'role', $options, true ) ) { + // Use left join to attach role name, so we still find the revision row even + // if the role name is missing. This triggers a more obvious failure mode. + $ret['tables'][] = 'slot_roles'; + $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ]; + $ret['fields'][] = 'role_name'; + } if ( in_array( 'content', $options, true ) ) { $ret['tables'][] = 'content'; - $ret['tables'][] = 'content_models'; $ret['fields'] = array_merge( $ret['fields'], [ 'content_size', 'content_sha1', 'content_address', - 'model_name' + 'content_model', ] ); $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ]; - $ret['joins']['content_models'] = [ 'INNER JOIN', [ 'content_model = model_id' ] ]; + + if ( in_array( 'model', $options, true ) ) { + // Use left join to attach model name, so we still find the revision row even + // if the model name is missing. This triggers a more obvious failure mode. + $ret['tables'][] = 'content_models'; + $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ]; + $ret['fields'][] = 'model_name'; + } + } } diff --git a/includes/Title.php b/includes/Title.php index e74824c9b8..895cc0e635 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -134,8 +134,15 @@ class Title implements LinkTarget { /** @var bool Boolean for initialisation on demand */ public $mRestrictionsLoaded = false; - /** @var string Text form including namespace/interwiki, initialised on demand */ - protected $mPrefixedText = null; + /** + * Text form including namespace/interwiki, initialised on demand + * + * Only public to share cache with TitleFormatter + * + * @private + * @var string + */ + public $prefixedText = null; /** @var mixed Cached value for getTitleProtection (create protection) */ public $mTitleProtection; @@ -1473,6 +1480,22 @@ class Title implements LinkTarget { ); } + /** + * Is this a message which can contain raw HTML? + * + * @return bool + * @since 1.32 + */ + public function isRawHtmlMessage() { + global $wgRawHtmlMessages; + + if ( $this->inNamespace( NS_MEDIAWIKI ) ) { + return false; + } + $message = lcfirst( $this->getRootText() ); + return in_array( $message, $wgRawHtmlMessages, true ); + } + /** * Is this a talk page of some sort? * @@ -1669,12 +1692,12 @@ class Title implements LinkTarget { * @return string The prefixed title, with spaces */ public function getPrefixedText() { - if ( $this->mPrefixedText === null ) { + if ( $this->prefixedText === null ) { $s = $this->prefix( $this->mTextform ); $s = strtr( $s, '_', ' ' ); - $this->mPrefixedText = $s; + $this->prefixedText = $s; } - return $this->mPrefixedText; + return $this->prefixedText; } /** @@ -2385,6 +2408,13 @@ class Title implements LinkTarget { $error = [ 'sitejsonprotected', $action ]; } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) { $error = [ 'sitejsprotected', $action ]; + } elseif ( $this->isRawHtmlMessage() ) { + // Raw HTML can be used to deploy CSS or JS so require rights for both. + if ( !$user->isAllowed( 'editsitejs' ) ) { + $error = [ 'sitejsprotected', $action ]; + } elseif ( !$user->isAllowed( 'editsitecss' ) ) { + $error = [ 'sitecssprotected', $action ]; + } } if ( $error ) { @@ -2416,25 +2446,34 @@ class Title implements LinkTarget { # Protect css/json/js subpages of user pages # XXX: this might be better using restrictions - if ( $action != 'patrol' ) { - if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { - if ( - $this->isUserCssConfigPage() - && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) - ) { - $errors[] = [ 'mycustomcssprotected', $action ]; - } elseif ( - $this->isUserJsonConfigPage() - && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' ) - ) { - $errors[] = [ 'mycustomjsonprotected', $action ]; - } elseif ( - $this->isUserJsConfigPage() - && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) - ) { - $errors[] = [ 'mycustomjsprotected', $action ]; - } - } else { + if ( $action === 'patrol' ) { + return []; + } + + if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { + // Users need editmyuser* to edit their own CSS/JSON/JS subpages. + if ( + $this->isUserCssConfigPage() + && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) + ) { + $errors[] = [ 'mycustomcssprotected', $action ]; + } elseif ( + $this->isUserJsonConfigPage() + && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' ) + ) { + $errors[] = [ 'mycustomjsonprotected', $action ]; + } elseif ( + $this->isUserJsConfigPage() + && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) + ) { + $errors[] = [ 'mycustomjsprotected', $action ]; + } + } else { + // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for + // deletion/suppression which cannot be used for attacks and we want to avoid the + // situation where an unprivileged user can post abusive content on their subpages + // and only very highly privileged users could remove it. + if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) { if ( $this->isUserCssConfigPage() && !$user->isAllowed( 'editusercss' ) diff --git a/includes/actions/McrUndoAction.php b/includes/actions/McrUndoAction.php new file mode 100644 index 0000000000..90d1f686cd --- /dev/null +++ b/includes/actions/McrUndoAction.php @@ -0,0 +1,376 @@ +persist(); + + // Some stuff copied from EditAction + $this->useTransactionalTimeLimit(); + + $out = $this->getOutput(); + $out->setRobotPolicy( 'noindex,nofollow' ); + if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $out->addModuleStyles( [ + 'mediawiki.ui.input', + 'mediawiki.ui.checkbox', + ] ); + } + + // IP warning headers copied from EditPage + // (should more be copied?) + if ( wfReadOnly() ) { + $out->wrapWikiMsg( + "
\n$1\n
", + [ 'readonlywarning', wfReadOnlyReason() ] + ); + } elseif ( $this->context->getUser()->isAnon() ) { + if ( !$this->getRequest()->getCheck( 'wpPreview' ) ) { + $out->wrapWikiMsg( + "
\n$1\n
", + [ 'anoneditwarning', + // Log-in link + SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ + 'returnto' => $this->getTitle()->getPrefixedDBkey() + ] ), + // Sign-up link + SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ + 'returnto' => $this->getTitle()->getPrefixedDBkey() + ] ) + ] + ); + } else { + $out->wrapWikiMsg( "
\n$1
", + 'anonpreviewwarning' + ); + } + } + + parent::show(); + } + + protected function checkCanExecute( User $user ) { + parent::checkCanExecute( $user ); + + $this->undoafter = $this->getRequest()->getInt( 'undoafter' ); + $this->undo = $this->getRequest()->getInt( 'undo' ); + + if ( $this->undo == 0 || $this->undoafter == 0 ) { + throw new ErrorPageError( 'mcrundofailed', 'mcrundo-missingparam' ); + } + + $curRev = $this->page->getRevision(); + if ( !$curRev ) { + throw new ErrorPageError( 'mcrundofailed', 'nopagetext' ); + } + $this->curRev = $curRev->getRevisionRecord(); + $this->cur = $this->getRequest()->getInt( 'cur', $this->curRev->getId() ); + + $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); + + $undoRev = $revisionLookup->getRevisionById( $this->undo ); + $oldRev = $revisionLookup->getRevisionById( $this->undoafter ); + + if ( $undoRev === null || $oldRev === null || + $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) || + $oldRev->isDeleted( RevisionRecord::DELETED_TEXT ) + ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-norev' ); + } + + return true; + } + + /** + * @return MutableRevisionRecord + */ + private function getNewRevision() { + $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); + + $undoRev = $revisionLookup->getRevisionById( $this->undo ); + $oldRev = $revisionLookup->getRevisionById( $this->undoafter ); + $curRev = $this->curRev; + + $isLatest = $curRev->getId() === $undoRev->getId(); + + if ( $undoRev === null || $oldRev === null || + $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) || + $oldRev->isDeleted( RevisionRecord::DELETED_TEXT ) + ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-norev' ); + } + + if ( $isLatest ) { + // Short cut! Undoing the current revision means we just restore the old. + return MutableRevisionRecord::newFromParentRevision( $oldRev ); + } + + $newRev = MutableRevisionRecord::newFromParentRevision( $curRev ); + + // Figure out the roles that need merging by first collecting all roles + // and then removing the ones that don't. + $rolesToMerge = array_unique( array_merge( + $oldRev->getSlotRoles(), + $undoRev->getSlotRoles(), + $curRev->getSlotRoles() + ) ); + + // Any roles with the same content in $oldRev and $undoRev can be + // inherited because undo won't change them. + $rolesToMerge = array_intersect( + $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $undoRev->getSlots() ) + ); + if ( !$rolesToMerge ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' ); + } + + // Any roles with the same content in $oldRev and $curRev were already reverted + // and so can be inherited. + $rolesToMerge = array_intersect( + $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() ) + ); + if ( !$rolesToMerge ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' ); + } + + // Any roles with the same content in $undoRev and $curRev weren't + // changed since and so can be reverted to $oldRev. + $diffRoles = array_intersect( + $rolesToMerge, $undoRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() ) + ); + foreach ( array_diff( $rolesToMerge, $diffRoles ) as $role ) { + if ( $oldRev->hasSlot( $role ) ) { + $newRev->inheritSlot( $oldRev->getSlot( $role, RevisionRecord::RAW ) ); + } else { + $newRev->removeSlot( $role ); + } + } + $rolesToMerge = $diffRoles; + + // Any slot additions or removals not handled by the above checks can't be undone. + // There will be only one of the three revisions missing the slot: + // - !old means it was added in the undone revisions and modified after. + // Should it be removed entirely for the undo, or should the modified version be kept? + // - !undo means it was removed in the undone revisions and then readded with different content. + // Which content is should be kept, the old or the new? + // - !cur means it was changed in the undone revisions and then deleted after. + // Did someone delete vandalized content instead of undoing (meaning we should ideally restore + // it), or should it stay gone? + foreach ( $rolesToMerge as $role ) { + if ( !$oldRev->hasSlot( $role ) || !$undoRev->hasSlot( $role ) || !$curRev->hasSlot( $role ) ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-failure' ); + } + } + + // Try to merge anything that's left. + foreach ( $rolesToMerge as $role ) { + $oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent(); + $undoContent = $undoRev->getSlot( $role, RevisionRecord::RAW )->getContent(); + $curContent = $curRev->getSlot( $role, RevisionRecord::RAW )->getContent(); + $newContent = $undoContent->getContentHandler() + ->getUndoContent( $curContent, $undoContent, $oldContent, $isLatest ); + if ( !$newContent ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-failure' ); + } + $newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) ); + } + + return $newRev; + } + + private function generateDiff() { + $newRev = $this->getNewRevision(); + if ( $newRev->hasSameContent( $this->curRev ) ) { + throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' ); + } + + $diffEngine = new DifferenceEngine( $this->context ); + $diffEngine->setRevisions( $this->curRev, $newRev ); + + $oldtitle = $this->context->msg( 'currentrev' )->parse(); + $newtitle = $this->context->msg( 'yourtext' )->parse(); + + if ( $this->getRequest()->getCheck( 'wpPreview' ) ) { + $diffEngine->renderNewRevision(); + return ''; + } else { + $diffText = $diffEngine->getDiff( $oldtitle, $newtitle ); + $diffEngine->showDiffStyle(); + return '
' . $diffText . '
'; + } + } + + public function onSubmit( $data ) { + global $wgUseRCPatrol; + + if ( !$this->getRequest()->getCheck( 'wpSave' ) ) { + // Diff or preview + return false; + } + + $updater = $this->page->getPage()->newPageUpdater( $this->context->getUser() ); + $curRev = $updater->grabParentRevision(); + if ( !$curRev ) { + throw new ErrorPageError( 'mcrundofailed', 'nopagetext' ); + } + + if ( $this->cur !== $curRev->getId() ) { + return Status::newFatal( 'mcrundo-changed' ); + } + + $newRev = $this->getNewRevision(); + if ( !$newRev->hasSameContent( $curRev ) ) { + // Copy new slots into the PageUpdater, and remove any removed slots. + // TODO: This interface is awful, there should be a way to just pass $newRev. + // TODO: MCR: test this once we can store multiple slots + foreach ( $newRev->getSlots()->getSlots() as $slot ) { + $updater->setSlot( $slot ); + } + foreach ( $curRev->getSlotRoles() as $role ) { + if ( !$newRev->hasSlot( $role ) ) { + $updater->removeSlot( $role ); + } + } + + $updater->setOriginalRevisionId( false ); + $updater->setUndidRevisionId( $this->undo ); + + // TODO: Ugh. + if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $this->getUser() ) ) { + $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED ); + } + + $updater->saveRevision( + CommentStoreComment::newUnsavedComment( trim( $this->getRequest()->getVal( 'wpSummary' ) ) ), + EDIT_AUTOSUMMARY | EDIT_UPDATE + ); + + return $updater->getStatus(); + } + + return Status::newGood(); + } + + protected function usesOOUI() { + return true; + } + + protected function getFormFields() { + $request = $this->getRequest(); + $config = $this->context->getConfig(); + $oldCommentSchema = $config->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD; + $ret = [ + 'diff' => [ + 'type' => 'info', + 'vertical-label' => true, + 'raw' => true, + 'default' => function () { + return $this->generateDiff(); + } + ], + 'summary' => [ + 'type' => 'text', + 'id' => 'wpSummary', + 'name' => 'wpSummary', + 'cssclass' => 'mw-summary', + 'label-message' => 'summary', + 'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT, + 'value' => $request->getVal( 'wpSummary', '' ), + 'size' => 60, + 'spellcheck' => 'true', + ], + 'summarypreview' => [ + 'type' => 'info', + 'label-message' => 'summary-preview', + 'raw' => true, + ], + ]; + + if ( $request->getCheck( 'wpSummary' ) ) { + $ret['summarypreview']['default'] = Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], + Linker::commentBlock( trim( $request->getVal( 'wpSummary' ) ), $this->getTitle(), false ) + ); + } else { + unset( $ret['summarypreview'] ); + } + + return $ret; + } + + protected function alterForm( HTMLForm $form ) { + $form->setWrapperLegendMsg( 'confirm-mcrundo-title' ); + + $labelAsPublish = $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' ); + + $form->setSubmitName( 'wpSave' ); + $form->setSubmitTooltip( $labelAsPublish ? 'publish' : 'save' ); + $form->setSubmitTextMsg( $labelAsPublish ? 'publishchanges' : 'savechanges' ); + $form->showCancel( true ); + $form->setCancelTarget( $this->getTitle() ); + $form->addButton( [ + 'name' => 'wpPreview', + 'value' => '1', + 'label-message' => 'showpreview', + 'attribs' => Linker::tooltipAndAccesskeyAttribs( 'preview' ), + ] ); + $form->addButton( [ + 'name' => 'wpDiff', + 'value' => '1', + 'label-message' => 'showdiff', + 'attribs' => Linker::tooltipAndAccesskeyAttribs( 'diff' ), + ] ); + + $form->addHiddenField( 'undo', $this->undo ); + $form->addHiddenField( 'undoafter', $this->undoafter ); + $form->addHiddenField( 'cur', $this->curRev->getId() ); + } + + public function onSuccess() { + $this->getOutput()->redirect( $this->getTitle()->getFullURL() ); + } + + protected function preText() { + return '
'; + } +} diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 93c35d3d23..6bfa35dd6e 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -19,141 +19,136 @@ * @file */ +use MediaWiki\MediaWikiServices; +use MediaWiki\Storage\MutableRevisionRecord; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\RevisionStore; + class ApiComparePages extends ApiBase { - private $guessed = false, $guessedTitle, $guessedModel, $props; + /** @var RevisionStore */ + private $revisionStore; + + private $guessedTitle = false, $props; + + public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) { + parent::__construct( $mainModule, $moduleName, $modulePrefix ); + $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); + } public function execute() { $params = $this->extractRequestParams(); // Parameter validation - $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' ); - $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' ); + $this->requireAtLeastOneParameter( + $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots' + ); + $this->requireAtLeastOneParameter( + $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots' + ); $this->props = array_flip( $params['prop'] ); // Cache responses publicly by default. This may be overridden later. $this->getMain()->setCacheMode( 'public' ); - // Get the 'from' Revision and Content - list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params ); + // Get the 'from' RevisionRecord + list( $fromRev, $fromRelRev, $fromValsRev ) = $this->getDiffRevision( 'from', $params ); - // Get the 'to' Revision and Content + // Get the 'to' RevisionRecord if ( $params['torelative'] !== null ) { - if ( !$relRev ) { + if ( !$fromRelRev ) { $this->dieWithError( 'apierror-compare-relative-to-nothing' ); } switch ( $params['torelative'] ) { case 'prev': // Swap 'from' and 'to' - $toRev = $fromRev; - $toContent = $fromContent; - $fromRev = $relRev->getPrevious(); - $fromContent = $fromRev - ? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) - : $toContent->getContentHandler()->makeEmptyContent(); - if ( !$fromContent ) { - $this->dieWithError( - [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' - ); - } + list( $toRev, $toRelRev2, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ]; + $fromRev = $this->revisionStore->getPreviousRevision( $fromRelRev ); + $fromRelRev = $fromRev; + $fromValsRev = $fromRev; break; case 'next': - $toRev = $relRev->getNext(); - $toContent = $toRev - ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) - : $fromContent; - if ( !$toContent ) { - $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' ); - } + $toRev = $this->revisionStore->getNextRevision( $fromRelRev ); + $toRelRev = $toRev; + $toValsRev = $toRev; break; case 'cur': - $title = $relRev->getTitle(); - $id = $title->getLatestRevID(); - $toRev = $id ? Revision::newFromId( $id ) : null; + $title = $fromRelRev->getPageAsLinkTarget(); + $toRev = $this->revisionStore->getRevisionByTitle( $title ); if ( !$toRev ) { + $title = Title::newFromLinkTarget( $title ); $this->dieWithError( [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid' ); } - $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); - if ( !$toContent ) { - $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' ); - } + $toRelRev = $toRev; + $toValsRev = $toRev; break; } - $relRev2 = null; } else { - list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params ); + list( $toRev, $toRelRev, $toValsRev ) = $this->getDiffRevision( 'to', $params ); } - // Should never happen, but just in case... - if ( !$fromContent || !$toContent ) { + // Handle missing from or to revisions + if ( !$fromRev || !$toRev ) { $this->dieWithError( 'apierror-baddiff' ); } - // Extract sections, if told to - if ( isset( $params['fromsection'] ) ) { - $fromContent = $fromContent->getSection( $params['fromsection'] ); - if ( !$fromContent ) { - $this->dieWithError( - [ 'apierror-compare-nosuchfromsection', wfEscapeWikiText( $params['fromsection'] ) ], - 'nosuchfromsection' - ); - } + // Handle revdel + if ( !$fromRev->audienceCan( + RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser() + ) ) { + $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' ); } - if ( isset( $params['tosection'] ) ) { - $toContent = $toContent->getSection( $params['tosection'] ); - if ( !$toContent ) { - $this->dieWithError( - [ 'apierror-compare-nosuchtosection', wfEscapeWikiText( $params['tosection'] ) ], - 'nosuchtosection' - ); - } + if ( !$toRev->audienceCan( + RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser() + ) ) { + $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' ); } // Get the diff $context = new DerivativeContext( $this->getContext() ); - if ( $relRev && $relRev->getTitle() ) { - $context->setTitle( $relRev->getTitle() ); - } elseif ( $relRev2 && $relRev2->getTitle() ) { - $context->setTitle( $relRev2->getTitle() ); + if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) { + $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) ); + } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) { + $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) ); } else { - $this->guessTitleAndModel(); - if ( $this->guessedTitle ) { - $context->setTitle( $this->guessedTitle ); + $guessedTitle = $this->guessTitle(); + if ( $guessedTitle ) { + $context->setTitle( $guessedTitle ); } } - $de = $fromContent->getContentHandler()->createDifferenceEngine( - $context, - $fromRev ? $fromRev->getId() : 0, - $toRev ? $toRev->getId() : 0, - /* $rcid = */ null, - /* $refreshCache = */ false, - /* $unhide = */ true - ); - $de->setContent( $fromContent, $toContent ); - $difftext = $de->getDiffBody(); - if ( $difftext === false ) { - $this->dieWithError( 'apierror-baddiff' ); + $de = new DifferenceEngine( $context ); + $de->setRevisions( $fromRev, $toRev ); + if ( $params['slots'] === null ) { + $difftext = $de->getDiffBody(); + if ( $difftext === false ) { + $this->dieWithError( 'apierror-baddiff' ); + } + } else { + $difftext = []; + foreach ( $params['slots'] as $role ) { + $difftext[$role] = $de->getDiffBodyForRole( $role ); + } } // Fill in the response $vals = []; - $this->setVals( $vals, 'from', $fromRev ); - $this->setVals( $vals, 'to', $toRev ); + $this->setVals( $vals, 'from', $fromValsRev ); + $this->setVals( $vals, 'to', $toValsRev ); if ( isset( $this->props['rel'] ) ) { - if ( $fromRev ) { - $rev = $fromRev->getPrevious(); + if ( !$fromRev instanceof MutableRevisionRecord ) { + $rev = $this->revisionStore->getPreviousRevision( $fromRev ); if ( $rev ) { $vals['prev'] = $rev->getId(); } } - if ( $toRev ) { - $rev = $toRev->getNext(); + if ( !$toRev instanceof MutableRevisionRecord ) { + $rev = $this->revisionStore->getNextRevision( $toRev ); if ( $rev ) { $vals['next'] = $rev->getId(); } @@ -161,10 +156,18 @@ class ApiComparePages extends ApiBase { } if ( isset( $this->props['diffsize'] ) ) { - $vals['diffsize'] = strlen( $difftext ); + $vals['diffsize'] = 0; + foreach ( (array)$difftext as $text ) { + $vals['diffsize'] += strlen( $text ); + } } if ( isset( $this->props['diff'] ) ) { - ApiResult::setContentValue( $vals, 'body', $difftext ); + if ( is_array( $difftext ) ) { + ApiResult::setArrayType( $difftext, 'kvp', 'diff' ); + $vals['bodies'] = $difftext; + } else { + ApiResult::setContentValue( $vals, 'body', $difftext ); + } } // Diffs can be really big and there's little point in having @@ -174,49 +177,55 @@ class ApiComparePages extends ApiBase { } /** - * Guess an appropriate default Title and content model for this request + * Load a revision by ID * - * Fills in $this->guessedTitle based on the first of 'fromrev', - * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and - * valid. + * Falls back to checking the archive table if appropriate. + * + * @param int $id + * @return RevisionRecord|null + */ + private function getRevisionById( $id ) { + $rev = $this->revisionStore->getRevisionById( $id ); + if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) { + // Try the 'archive' table + $arQuery = $this->revisionStore->getArchiveQueryInfo(); + $row = $this->getDB()->selectRow( + $arQuery['tables'], + array_merge( + $arQuery['fields'], + [ 'ar_namespace', 'ar_title' ] + ), + [ 'ar_rev_id' => $id ], + __METHOD__, + [], + $arQuery['joins'] + ); + if ( $row ) { + $rev = $this->revisionStore->newRevisionFromArchiveRow( $row ); + $rev->isArchive = true; + } + } + return $rev; + } + + /** + * Guess an appropriate default Title for this request * - * Fills in $this->guessedModel based on the Revision or Title used to - * determine $this->guessedTitle, or the 'fromcontentmodel' or - * 'tocontentmodel' parameters if no title was guessed. + * @return Title|null */ - private function guessTitleAndModel() { - if ( $this->guessed ) { - return; + private function guessTitle() { + if ( $this->guessedTitle !== false ) { + return $this->guessedTitle; } - $this->guessed = true; + $this->guessedTitle = null; $params = $this->extractRequestParams(); foreach ( [ 'from', 'to' ] as $prefix ) { if ( $params["{$prefix}rev"] !== null ) { - $revId = $params["{$prefix}rev"]; - $rev = Revision::newFromId( $revId ); - if ( !$rev ) { - // Titles of deleted revisions aren't secret, per T51088 - $arQuery = Revision::getArchiveQueryInfo(); - $row = $this->getDB()->selectRow( - $arQuery['tables'], - array_merge( - $arQuery['fields'], - [ 'ar_namespace', 'ar_title' ] - ), - [ 'ar_rev_id' => $revId ], - __METHOD__, - [], - $arQuery['joins'] - ); - if ( $row ) { - $rev = Revision::newFromArchiveRow( $row ); - } - } + $rev = $this->getRevisionById( $params["{$prefix}rev"] ); if ( $rev ) { - $this->guessedTitle = $rev->getTitle(); - $this->guessedModel = $rev->getContentModel(); + $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); break; } } @@ -238,38 +247,84 @@ class ApiComparePages extends ApiBase { } } - if ( !$this->guessedModel ) { - if ( $this->guessedTitle ) { - $this->guessedModel = $this->guessedTitle->getContentModel(); - } elseif ( $params['fromcontentmodel'] !== null ) { - $this->guessedModel = $params['fromcontentmodel']; - } elseif ( $params['tocontentmodel'] !== null ) { - $this->guessedModel = $params['tocontentmodel']; + return $this->guessedTitle; + } + + /** + * Guess an appropriate default content model for this request + * @param string $role Slot for which to guess the model + * @return string|null Guessed content model + */ + private function guessModel( $role ) { + $params = $this->extractRequestParams(); + + $title = null; + foreach ( [ 'from', 'to' ] as $prefix ) { + if ( $params["{$prefix}rev"] !== null ) { + $rev = $this->getRevisionById( $params["{$prefix}rev"] ); + if ( $rev ) { + if ( $rev->hasSlot( $role ) ) { + return $rev->getSlot( $role, RevisionRecord::RAW )->getModel(); + } + } + } + } + + $guessedTitle = $this->guessTitle(); + if ( $guessedTitle && $role === 'main' ) { + // @todo: Use SlotRoleRegistry and do this for all slots + return $guessedTitle->getContentModel(); + } + + if ( isset( $params["fromcontentmodel-$role"] ) ) { + return $params["fromcontentmodel-$role"]; + } + if ( isset( $params["tocontentmodel-$role"] ) ) { + return $params["tocontentmodel-$role"]; + } + + if ( $role === 'main' ) { + if ( isset( $params['fromcontentmodel'] ) ) { + return $params['fromcontentmodel']; + } + if ( isset( $params['tocontentmodel'] ) ) { + return $params['tocontentmodel']; } } + + return null; } /** - * Get the Revision and Content for one side of the diff + * Get the RevisionRecord for one side of the diff * - * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst', - * 'contentmodel', and 'contentformat' parameters to determine what content + * This uses the appropriate set of parameters to determine what content * should be diffed. * * Returns three values: - * - The revision used to retrieve the content, if any - * - The content to be diffed - * - The revision specified, if any, even if not used to retrieve the - * Content + * - A RevisionRecord holding the content + * - The revision specified, if any, even if content was supplied + * - The revision to pass to setVals(), if any * * @param string $prefix 'from' or 'to' * @param array $params - * @return array [ Revision|null, Content, Revision|null ] + * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ] */ - private function getDiffContent( $prefix, array $params ) { + private function getDiffRevision( $prefix, array $params ) { + // Back compat params + $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" ); + $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" ); + if ( $params["{$prefix}text"] !== null ) { + $params["{$prefix}slots"] = [ 'main' ]; + $params["{$prefix}text-main"] = $params["{$prefix}text"]; + $params["{$prefix}section-main"] = null; + $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"]; + $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"]; + } + $title = null; $rev = null; - $suppliedContent = $params["{$prefix}text"] !== null; + $suppliedContent = $params["{$prefix}slots"] !== null; // Get the revision and title, if applicable $revId = null; @@ -308,94 +363,146 @@ class ApiComparePages extends ApiBase { } } if ( $revId !== null ) { - $rev = Revision::newFromId( $revId ); - if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) { - // Try the 'archive' table - $arQuery = Revision::getArchiveQueryInfo(); - $row = $this->getDB()->selectRow( - $arQuery['tables'], - array_merge( - $arQuery['fields'], - [ 'ar_namespace', 'ar_title' ] - ), - [ 'ar_rev_id' => $revId ], - __METHOD__, - [], - $arQuery['joins'] - ); - if ( $row ) { - $rev = Revision::newFromArchiveRow( $row ); - $rev->isArchive = true; - } - } + $rev = $this->getRevisionById( $revId ); if ( !$rev ) { $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] ); } - $title = $rev->getTitle(); + $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); // If we don't have supplied content, return here. Otherwise, // continue on below with the supplied content. if ( !$suppliedContent ) { - $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); - if ( !$content ) { - $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' ); + $newRev = $rev; + + // Deprecated 'fromsection'/'tosection' + if ( isset( $params["{$prefix}section"] ) ) { + $section = $params["{$prefix}section"]; + $newRev = MutableRevisionRecord::newFromParentRevision( $rev ); + $content = $rev->getContent( 'main', RevisionRecord::FOR_THIS_USER, $this->getUser() ); + if ( !$content ) { + $this->dieWithError( + [ 'apierror-missingcontent-revid-role', $rev->getId(), 'main' ], 'missingcontent' + ); + } + $content = $content ? $content->getSection( $section ) : null; + if ( !$content ) { + $this->dieWithError( + [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ], + "nosuch{$prefix}section" + ); + } + $newRev->setContent( 'main', $content ); } - return [ $rev, $content, $rev ]; + + return [ $newRev, $rev, $rev ]; } } // Override $content based on supplied text - $model = $params["{$prefix}contentmodel"]; - $format = $params["{$prefix}contentformat"]; - - if ( !$model && $rev ) { - $model = $rev->getContentModel(); - } - if ( !$model && $title ) { - $model = $title->getContentModel(); - } - if ( !$model ) { - $this->guessTitleAndModel(); - $model = $this->guessedModel; - } - if ( !$model ) { - $model = CONTENT_MODEL_WIKITEXT; - $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] ); - } - if ( !$title ) { - $this->guessTitleAndModel(); - $title = $this->guessedTitle; + $title = $this->guessTitle(); } - - try { - $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format ); - } catch ( MWContentSerializationException $ex ) { - $this->dieWithException( $ex, [ - 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' ) + if ( $rev ) { + $newRev = MutableRevisionRecord::newFromParentRevision( $rev ); + } else { + $newRev = $this->revisionStore->newMutableRevisionFromArray( [ + 'title' => $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ ) ] ); } + foreach ( $params["{$prefix}slots"] as $role ) { + $text = $params["{$prefix}text-{$role}"]; + if ( $text === null ) { + $newRev->removeSlot( $role ); + continue; + } + + $model = $params["{$prefix}contentmodel-{$role}"]; + $format = $params["{$prefix}contentformat-{$role}"]; - if ( $params["{$prefix}pst"] ) { - if ( !$title ) { - $this->dieWithError( 'apierror-compare-no-title' ); + if ( !$model && $rev && $rev->hasSlot( $role ) ) { + $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel(); + } + if ( !$model && $title && $role === 'main' ) { + // @todo: Use SlotRoleRegistry and do this for all slots + $model = $title->getContentModel(); + } + if ( !$model ) { + $model = $this->guessModel( $role ); + } + if ( !$model ) { + $model = CONTENT_MODEL_WIKITEXT; + $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] ); + } + + try { + $content = ContentHandler::makeContent( $text, $title, $model, $format ); + } catch ( MWContentSerializationException $ex ) { + $this->dieWithException( $ex, [ + 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' ) + ] ); + } + + if ( $params["{$prefix}pst"] ) { + if ( !$title ) { + $this->dieWithError( 'apierror-compare-no-title' ); + } + $popts = ParserOptions::newFromContext( $this->getContext() ); + $content = $content->preSaveTransform( $title, $this->getUser(), $popts ); + } + + $section = $params["{$prefix}section-{$role}"]; + if ( $section !== null && $section !== '' ) { + if ( !$rev ) { + $this->dieWithError( "apierror-compare-no{$prefix}revision" ); + } + $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getUser() ); + if ( !$oldContent ) { + $this->dieWithError( + [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ], + 'missingcontent' + ); + } + if ( !$oldContent->getContentHandler()->supportsSections() ) { + $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] ); + } + try { + $content = $oldContent->replaceSection( $section, $content, '' ); + } catch ( Exception $ex ) { + // Probably a content model mismatch. + $content = null; + } + if ( !$content ) { + $this->dieWithError( [ 'apierror-sectionreplacefailed' ] ); + } + } + + // Deprecated 'fromsection'/'tosection' + if ( $role === 'main' && isset( $params["{$prefix}section"] ) ) { + $section = $params["{$prefix}section"]; + $content = $content->getSection( $section ); + if ( !$content ) { + $this->dieWithError( + [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ], + "nosuch{$prefix}section" + ); + } } - $popts = ParserOptions::newFromContext( $this->getContext() ); - $content = $content->preSaveTransform( $title, $this->getUser(), $popts ); - } - return [ null, $content, $rev ]; + $newRev->setContent( $role, $content ); + } + return [ $newRev, $rev, null ]; } /** - * Set value fields from a Revision object + * Set value fields from a RevisionRecord object + * * @param array &$vals Result array to set data into * @param string $prefix 'from' or 'to' - * @param Revision|null $rev + * @param RevisionRecord|null $rev */ private function setVals( &$vals, $prefix, $rev ) { if ( $rev ) { - $title = $rev->getTitle(); + $title = $rev->getPageAsLinkTarget(); if ( isset( $this->props['ids'] ) ) { $vals["{$prefix}id"] = $title->getArticleID(); $vals["{$prefix}revid"] = $rev->getId(); @@ -408,41 +515,42 @@ class ApiComparePages extends ApiBase { } $anyHidden = false; - if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) { $vals["{$prefix}texthidden"] = true; $anyHidden = true; } - if ( $rev->isDeleted( Revision::DELETED_USER ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) { $vals["{$prefix}userhidden"] = true; $anyHidden = true; } - if ( isset( $this->props['user'] ) && - $rev->userCan( Revision::DELETED_USER, $this->getUser() ) - ) { - $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW ); - $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW ); + if ( isset( $this->props['user'] ) ) { + $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getUser() ); + if ( $user ) { + $vals["{$prefix}user"] = $user->getName(); + $vals["{$prefix}userid"] = $user->getId(); + } } - if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) { $vals["{$prefix}commenthidden"] = true; $anyHidden = true; } - if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) { - if ( isset( $this->props['comment'] ) ) { - $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW ); - } - if ( isset( $this->props['parsedcomment'] ) ) { + if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) { + $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getUser() ); + if ( $comment !== null ) { + if ( isset( $this->props['comment'] ) ) { + $vals["{$prefix}comment"] = $comment->text; + } $vals["{$prefix}parsedcomment"] = Linker::formatComment( - $rev->getComment( Revision::RAW ), - $rev->getTitle() + $comment->text, Title::newFromLinkTarget( $title ) ); } } if ( $anyHidden ) { $this->getMain()->setCacheMode( 'private' ); - if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) { + if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) { $vals["{$prefix}suppressed"] = true; } } @@ -455,6 +563,12 @@ class ApiComparePages extends ApiBase { } public function getAllowedParams() { + $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap(); + if ( !in_array( 'main', $slotRoles, true ) ) { + $slotRoles[] = 'main'; + } + sort( $slotRoles, SORT_STRING ); + // Parameters for the 'from' and 'to' content $fromToParams = [ 'title' => null, @@ -464,24 +578,58 @@ class ApiComparePages extends ApiBase { 'rev' => [ ApiBase::PARAM_TYPE => 'integer' ], - 'text' => [ - ApiBase::PARAM_TYPE => 'text' + + 'slots' => [ + ApiBase::PARAM_TYPE => $slotRoles, + ApiBase::PARAM_ISMULTI => true, + ], + 'text-{slot}' => [ + ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below + ApiBase::PARAM_TYPE => 'text', + ], + 'section-{slot}' => [ + ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below + ApiBase::PARAM_TYPE => 'string', + ], + 'contentformat-{slot}' => [ + ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ], + 'contentmodel-{slot}' => [ + ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below + ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), ], - 'section' => null, 'pst' => false, + + 'text' => [ + ApiBase::PARAM_TYPE => 'text', + ApiBase::PARAM_DEPRECATED => true, + ], 'contentformat' => [ ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ApiBase::PARAM_DEPRECATED => true, ], 'contentmodel' => [ ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), - ] + ApiBase::PARAM_DEPRECATED => true, + ], + 'section' => [ + ApiBase::PARAM_DFLT => null, + ApiBase::PARAM_DEPRECATED => true, + ], ]; $ret = []; foreach ( $fromToParams as $k => $v ) { + if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) { + $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots'; + } $ret["from$k"] = $v; } foreach ( $fromToParams as $k => $v ) { + if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) { + $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots'; + } $ret["to$k"] = $v; } @@ -508,6 +656,12 @@ class ApiComparePages extends ApiBase { ApiBase::PARAM_HELP_MSG_PER_VALUE => [], ]; + $ret['slots'] = [ + ApiBase::PARAM_TYPE => $slotRoles, + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_ALL => true, + ]; + return $ret; } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 03d29524a8..3b305f9e5c 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -534,7 +534,11 @@ class ApiMain extends ApiBase { MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime ); - } catch ( Exception $e ) { + } catch ( Exception $e ) { // @todo Remove this block when HHVM is no longer supported + $this->handleException( $e ); + $this->logRequest( microtime( true ) - $t, $e ); + $isError = true; + } catch ( Throwable $e ) { $this->handleException( $e ); $this->logRequest( microtime( true ) - $t, $e ); $isError = true; @@ -558,9 +562,9 @@ class ApiMain extends ApiBase { * Handle an exception as an API response * * @since 1.23 - * @param Exception $e + * @param Exception|Throwable $e */ - protected function handleException( Exception $e ) { + protected function handleException( $e ) { // T65145: Rollback any open database transactions if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) { // UsageExceptions are intentional, so don't rollback if that's the case @@ -600,7 +604,10 @@ class ApiMain extends ApiBase { foreach ( $ex->getStatusValue()->getErrors() as $error ) { try { $this->mPrinter->addWarning( $error ); - } catch ( Exception $ex2 ) { + } catch ( Exception $ex2 ) { // @todo Remove this block when HHVM is no longer supported + // WTF? + $this->addWarning( $error ); + } catch ( Throwable $ex2 ) { // WTF? $this->addWarning( $error ); } @@ -631,17 +638,20 @@ class ApiMain extends ApiBase { * friendly to clients. If it fails, it will rethrow the exception. * * @since 1.23 - * @param Exception $e - * @throws Exception + * @param Exception|Throwable $e + * @throws Exception|Throwable */ - public static function handleApiBeforeMainException( Exception $e ) { + public static function handleApiBeforeMainException( $e ) { ob_start(); try { $main = new self( RequestContext::getMain(), false ); $main->handleException( $e ); $main->logRequest( 0, $e ); - } catch ( Exception $e2 ) { + } catch ( Exception $e2 ) { // @todo Remove this block when HHVM is no longer supported + // Nope, even that didn't work. Punt. + throw $e; + } catch ( Throwable $e2 ) { // Nope, even that didn't work. Punt. throw $e; } @@ -1009,7 +1019,7 @@ class ApiMain extends ApiBase { * text around the exception's (presumably English) message as a single * error (no warnings). * - * @param Exception $e + * @param Exception|Throwable $e * @param string $type 'error' or 'warning' * @return ApiMessage[] * @since 1.27 @@ -1054,7 +1064,7 @@ class ApiMain extends ApiBase { /** * Replace the result data with the information about an exception. - * @param Exception $e + * @param Exception|Throwable $e * @return string[] Error codes */ protected function substituteResultWithError( $e ) { @@ -1609,7 +1619,7 @@ class ApiMain extends ApiBase { /** * Log the preceding request * @param float $time Time in seconds - * @param Exception|null $e Exception caught while processing the request + * @param Exception|Throwable|null $e Exception caught while processing the request */ protected function logRequest( $time, $e = null ) { $request = $this->getRequest(); diff --git a/includes/api/ApiQueryDeletedRevisions.php b/includes/api/ApiQueryDeletedRevisions.php index c3af71bff8..48d6f300c7 100644 --- a/includes/api/ApiQueryDeletedRevisions.php +++ b/includes/api/ApiQueryDeletedRevisions.php @@ -289,7 +289,7 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase { protected function getExamplesMessages() { return [ 'action=query&prop=deletedrevisions&titles=Main%20Page|Talk:Main%20Page&' . - 'drvprop=user|comment|content' + 'drvslots=*&drvprop=user|comment|content' => 'apihelp-query+deletedrevisions-example-titles', 'action=query&prop=deletedrevisions&revids=123456' => 'apihelp-query+deletedrevisions-example-revids', diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 5e7b864b54..8c26024812 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -486,7 +486,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { protected function getExamplesMessages() { return [ 'action=query&prop=revisions&titles=API|Main%20Page&' . - 'rvprop=timestamp|user|comment|content' + 'rvslots=*&rvprop=timestamp|user|comment|content' => 'apihelp-query+revisions-example-content', 'action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' . 'rvprop=timestamp|user|comment' diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index a3e3e575e7..ab9ae8e4e1 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -234,6 +234,7 @@ class ApiStashEdit extends ApiBase { return self::ERROR_CACHE; } } else { + // @todo Doesn't seem reachable, see @todo in buildStashValue $logger->info( "Uncacheable parser output for key '{cachekey}' ('{title}') [{code}].", [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] ); return self::ERROR_UNCACHEABLE; @@ -410,6 +411,9 @@ class ApiStashEdit extends ApiBase { } if ( $ttl <= 0 ) { + // @todo It doesn't seem like this can occur, because it would mean an entry older than + // getCacheExpiry() seconds, which is much longer than PRESUME_FRESH_TTL_SEC, and + // anything older than PRESUME_FRESH_TTL_SEC will have been thrown out already. return [ null, 0, 'no_ttl' ]; } diff --git a/includes/api/i18n/ar.json b/includes/api/i18n/ar.json index 241e71afd8..a1740a969b 100644 --- a/includes/api/i18n/ar.json +++ b/includes/api/i18n/ar.json @@ -64,20 +64,20 @@ "apihelp-compare-param-fromtitle": "العنوان الأول للمقارنة.", "apihelp-compare-param-fromid": "رقم الصفحة الأول للمقارنة.", "apihelp-compare-param-fromrev": "أول مراجعة للمقارنة.", - "apihelp-compare-param-fromtext": "استخدم هذا النص بدلا من محتوى المراجعة المحدد بواسطة fromtitle، fromid أو fromrev.", - "apihelp-compare-param-fromsection": "استخدم فقط القسم المحدد في المحتوى 'من' المحدد.", "apihelp-compare-param-frompst": "قم بإجراء تحويل ما قبل الحفظ على fromtext.", + "apihelp-compare-param-fromtext": "استخدم هذا النص بدلا من محتوى المراجعة المحدد بواسطة fromtitle، fromid أو fromrev.", "apihelp-compare-param-fromcontentmodel": "نموذج محتوى fromtext، إذا لم يتم توفيره، فسيتم تخمينه استنادا إلى الوسائط الأخرى.", "apihelp-compare-param-fromcontentformat": "تنسيق محتوى تسلسل fromtext.", + "apihelp-compare-param-fromsection": "استخدم فقط القسم المحدد في المحتوى 'من' المحدد.", "apihelp-compare-param-totitle": "العنوان الثاني للمقارنة.", "apihelp-compare-param-toid": "رقم الصفحة الثاني للمقارنة.", "apihelp-compare-param-torev": "المراجعة الثانية للمقارنة.", "apihelp-compare-param-torelative": "استخدم مراجعة متعلقة بالمراجعة المحددة من fromtitle أو fromid أو fromrev، سيتم تجاهل جميع خيارات 'إلى' الأخرى.", - "apihelp-compare-param-totext": "استخدم هذا النص بدلا من محتوى المراجعة المحدد بواسطة totitle أو toid أو torev.", - "apihelp-compare-param-tosection": "استخدم فقط القسم المحدد في المحتوى 'إلى' المحدد.", "apihelp-compare-param-topst": "قم بإجراء تحويل ما قبل الحفظ على totext.", + "apihelp-compare-param-totext": "استخدم هذا النص بدلا من محتوى المراجعة المحدد بواسطة totitle أو toid أو torev.", "apihelp-compare-param-tocontentmodel": "نموذج محتوى totext، إذا لم يتم توفيره، فسيتم تخمينه استنادا إلى الوسائط الأخرى.", "apihelp-compare-param-tocontentformat": "تنسيق محتوى تسلسل totext.", + "apihelp-compare-param-tosection": "استخدم فقط القسم المحدد في المحتوى 'إلى' المحدد.", "apihelp-compare-param-prop": "أية قطعة من المعلومات للحصول عليها.", "apihelp-compare-paramvalue-prop-diff": "HTML الفرق.", "apihelp-compare-paramvalue-prop-diffsize": "حجم HTML الفرق، بالبايت.", diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index 5bdc3c900e..c3b75c327a 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -824,6 +824,7 @@ "apihelp-query+protectedtitles-paramvalue-prop-level": "Ergänzt den Schutzstatus.", "apihelp-query+protectedtitles-example-simple": "Listet geschützte Titel auf.", "apihelp-query+querypage-param-limit": "Anzahl der zurückzugebenden Ergebnisse.", + "apihelp-query+random-summary": "Ruft einen Satz an zufälligen Seiten ab.", "apihelp-query+recentchanges-summary": "Listet die letzten Änderungen auf.", "apihelp-query+recentchanges-param-user": "Listet nur Änderungen von diesem Benutzer auf.", "apihelp-query+recentchanges-param-excludeuser": "Listet keine Änderungen von diesem Benutzer auf.", @@ -1085,6 +1086,8 @@ "apierror-badparameter": "Ungültiger Wert für den Parameter $1.", "apierror-badquery": "Ungültige Abfrage.", "apierror-cannot-async-upload-file": "Die Parameter async und file können nicht kombiniert werden. Falls du eine asynchrone Verarbeitung deiner hochgeladenen Datei wünschst, lade sie zuerst mithilfe des Parameters stash auf den Speicher hoch. Veröffentliche anschließend die gespeicherte Datei asynchron mithilfe filekey und async.", + "apierror-compare-nofromrevision": "Keine Version „from“. fromrev, fromtitle oder fromid angeben.", + "apierror-compare-notorevision": "Keine Version „to“. torev, totitle oder toid angeben.", "apierror-emptypage": "Das Erstellen neuer leerer Seiten ist nicht erlaubt.", "apierror-filedoesnotexist": "Die Datei ist nicht vorhanden.", "apierror-import-unknownerror": "Unbekannter Fehler beim Importieren: $1.", @@ -1093,6 +1096,7 @@ "apierror-invaliduserid": "Die Benutzerkennung $1 ist nicht gültig.", "apierror-maxbytes": "Der Parameter $1 kann nicht länger sein als {{PLURAL:$2|ein Byte|$2 Bytes}}", "apierror-maxchars": "Der Parameter $1 kann nicht länger sein als {{PLURAL:$2|ein|$2}} Zeichen", + "apierror-missingcontent-revid-role": "Fehlender Inhalt für die Versionskennung $1 für die Rolle $2.", "apierror-nosuchsection": "Es gibt keinen Abschnitt $1.", "apierror-nosuchuserid": "Es gibt keinen Benutzer mit der Kennung $1.", "apierror-offline": "Aufgrund von Problemen bei der Netzwerkverbindung kannst du nicht weitermachen. Stelle sicher, dass du eine funktionierende Internetverbindung hast und versuche es erneut.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 3c74f25feb..ae2ffd3ddb 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -64,20 +64,30 @@ "apihelp-compare-param-fromtitle": "First title to compare.", "apihelp-compare-param-fromid": "First page ID to compare.", "apihelp-compare-param-fromrev": "First revision to compare.", - "apihelp-compare-param-fromtext": "Use this text instead of the content of the revision specified by fromtitle, fromid or fromrev.", + "apihelp-compare-param-frompst": "Do a pre-save transform on fromtext-{slot}.", + "apihelp-compare-param-fromslots": "Override content of the revision specified by fromtitle, fromid or fromrev.\n\nThis parameter specifies the slots that are to be modified. Use fromtext-{slot}, fromcontentmodel-{slot}, and fromcontentformat-{slot} to specify content for each slot.", + "apihelp-compare-param-fromtext-{slot}": "Text of the specified slot. If omitted, the slot is removed from the revision.", + "apihelp-compare-param-fromsection-{slot}": "When fromtext-{slot} is the content of a single section, this is the section number. It will be merged into the revision specified by fromtitle, fromid or fromrev as if for a section edit.", + "apihelp-compare-param-fromcontentmodel-{slot}": "Content model of fromtext-{slot}. If not supplied, it will be guessed based on the other parameters.", + "apihelp-compare-param-fromcontentformat-{slot}": "Content serialization format of fromtext-{slot}.", + "apihelp-compare-param-fromtext": "Specify fromslots=main and use fromtext-main instead.", + "apihelp-compare-param-fromcontentmodel": "Specify fromslots=main and use fromcontentmodel-main instead.", + "apihelp-compare-param-fromcontentformat": "Specify fromslots=main and use fromcontentformat-main instead.", "apihelp-compare-param-fromsection": "Only use the specified section of the specified 'from' content.", - "apihelp-compare-param-frompst": "Do a pre-save transform on fromtext.", - "apihelp-compare-param-fromcontentmodel": "Content model of fromtext. If not supplied, it will be guessed based on the other parameters.", - "apihelp-compare-param-fromcontentformat": "Content serialization format of fromtext.", "apihelp-compare-param-totitle": "Second title to compare.", "apihelp-compare-param-toid": "Second page ID to compare.", "apihelp-compare-param-torev": "Second revision to compare.", "apihelp-compare-param-torelative": "Use a revision relative to the revision determined from fromtitle, fromid or fromrev. All of the other 'to' options will be ignored.", - "apihelp-compare-param-totext": "Use this text instead of the content of the revision specified by totitle, toid or torev.", - "apihelp-compare-param-tosection": "Only use the specified section of the specified 'to' content.", "apihelp-compare-param-topst": "Do a pre-save transform on totext.", - "apihelp-compare-param-tocontentmodel": "Content model of totext. If not supplied, it will be guessed based on the other parameters.", - "apihelp-compare-param-tocontentformat": "Content serialization format of totext.", + "apihelp-compare-param-toslots": "Override content of the revision specified by totitle, toid or torev.\n\nThis parameter specifies the slots that are to be modified. Use totext-{slot}, tocontentmodel-{slot}, and tocontentformat-{slot} to specify content for each slot.", + "apihelp-compare-param-totext-{slot}": "Text of the specified slot. If omitted, the slot is removed from the revision.", + "apihelp-compare-param-tosection-{slot}": "When totext-{slot} is the content of a single section, this is the section number. It will be merged into the revision specified by totitle, toid or torev as if for a section edit.", + "apihelp-compare-param-tocontentmodel-{slot}": "Content model of totext-{slot}. If not supplied, it will be guessed based on the other parameters.", + "apihelp-compare-param-tocontentformat-{slot}": "Content serialization format of totext-{slot}.", + "apihelp-compare-param-totext": "Specify toslots=main and use totext-main instead.", + "apihelp-compare-param-tocontentmodel": "Specify toslots=main and use tocontentmodel-main instead.", + "apihelp-compare-param-tocontentformat": "Specify toslots=main and use tocontentformat-main instead.", + "apihelp-compare-param-tosection": "Only use the specified section of the specified 'to' content.", "apihelp-compare-param-prop": "Which pieces of information to get.", "apihelp-compare-paramvalue-prop-diff": "The diff HTML.", "apihelp-compare-paramvalue-prop-diffsize": "The size of the diff HTML, in bytes.", @@ -88,6 +98,7 @@ "apihelp-compare-paramvalue-prop-comment": "The comment on the 'from' and 'to' revisions.", "apihelp-compare-paramvalue-prop-parsedcomment": "The parsed comment on the 'from' and 'to' revisions.", "apihelp-compare-paramvalue-prop-size": "The size of the 'from' and 'to' revisions.", + "apihelp-compare-param-slots": "Return individual diffs for these slots, rather than one combined diff for all slots.", "apihelp-compare-example-1": "Create a diff between revision 1 and 2.", "apihelp-createaccount-summary": "Create a new user account.", @@ -1709,6 +1720,8 @@ "apierror-compare-no-title": "Cannot pre-save transform without a title. Try specifying fromtitle or totitle.", "apierror-compare-nosuchfromsection": "There is no section $1 in the 'from' content.", "apierror-compare-nosuchtosection": "There is no section $1 in the 'to' content.", + "apierror-compare-nofromrevision": "No 'from' revision. Specify fromrev, fromtitle, or fromid.", + "apierror-compare-notorevision": "No 'to' revision. Specify torev, totitle, or toid.", "apierror-compare-relative-to-nothing": "No 'from' revision for torelative to be relative to.", "apierror-contentserializationexception": "Content serialization failed: $1", "apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.", @@ -1756,6 +1769,7 @@ "apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.", "apierror-missingcontent-pageid": "Missing content for page ID $1.", "apierror-missingcontent-revid": "Missing content for revision ID $1.", + "apierror-missingcontent-revid-role": "Missing content for revision ID $1 for role $2.", "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|The parameter|At least one of the parameters}} $1 is required.", "apierror-missingparam-one-of": "{{PLURAL:$2|The parameter|One of the parameters}} $1 is required.", "apierror-missingparam": "The $1 parameter must be set.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index a62b2bae4f..2862da7ab3 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -84,20 +84,30 @@ "apihelp-compare-param-fromtitle": "Premier titre à comparer.", "apihelp-compare-param-fromid": "ID de la première page à comparer.", "apihelp-compare-param-fromrev": "Première révision à comparer.", - "apihelp-compare-param-fromtext": "Utiliser ce texte au lieu du contenu de la révision spécifié par fromtitle, fromid ou fromrev.", + "apihelp-compare-param-frompst": "Faire une transformation avant enregistrement sur fromtext-{slot}.", + "apihelp-compare-param-fromslots": "Substituer le contenu de la révision spécifiée par fromtitle, fromid ou fromrev.\n\nCe paramètre spécifie les intervalles à modifier. Utilisez fromtext-{slot}, fromcontentmodel-{slot}, et fromcontentformat-{slot} pour spécifier le contenu de chaque intervalle.", + "apihelp-compare-param-fromtext-{slot}": "Texte de l'intervalle spécifié. Si absent, l'intervalle est supprimé de la révision.", + "apihelp-compare-param-fromsection-{slot}": "Si fromtext-{slot} est le contenu d'une seule section, c'est le numéro de la section. Il sera fusionné dans la révision spécifiée par fromtitle, fromid ou fromrev comme pour les modifications de section.", + "apihelp-compare-param-fromcontentmodel-{slot}": "Modèle de contenu de fromtext-{slot}. Si non fourni, il sera déduit en fonction de la valeur des autres paramètres.", + "apihelp-compare-param-fromcontentformat-{slot}": "Format de sérialisation de contenu de fromtext-{slot}.", + "apihelp-compare-param-fromtext": "Spécifiez fromslots=main et utilisez fromtext-main à la place.", + "apihelp-compare-param-fromcontentmodel": "Spécifiez fromslots=main et utilisez fromcontentmodel-main à la place.", + "apihelp-compare-param-fromcontentformat": "Spécifiez fromslots=main et utilisez fromcontentformat-main à la place.", "apihelp-compare-param-fromsection": "N'utiliser que la section spécifiée du contenu 'from'.", - "apihelp-compare-param-frompst": "Faire une transformation avant enregistrement sur fromtext.", - "apihelp-compare-param-fromcontentmodel": "Modèle de contenu de fromtext. Si non fourni, il sera déduit d’après les autres paramètres.", - "apihelp-compare-param-fromcontentformat": "Sérialisation du contenu de fromtext.", "apihelp-compare-param-totitle": "Second titre à comparer.", "apihelp-compare-param-toid": "ID de la seconde page à comparer.", "apihelp-compare-param-torev": "Seconde révision à comparer.", "apihelp-compare-param-torelative": "Utiliser une révision relative à la révision déterminée de fromtitle, fromid ou fromrev. Toutes les autres options 'to' seront ignorées.", - "apihelp-compare-param-totext": "Utiliser ce texte au lieu du contenu de la révision spécifié par totitle, toid ou torev.", - "apihelp-compare-param-tosection": "N'utiliser que la section spécifiée du contenu 'to'.", "apihelp-compare-param-topst": "Faire une transformation avant enregistrement sur totext.", - "apihelp-compare-param-tocontentmodel": "Modèle de contenu de totext. Si non fourni, il sera deviné d’après les autres paramètres.", - "apihelp-compare-param-tocontentformat": "Format de sérialisation du contenu de totext.", + "apihelp-compare-param-toslots": "Substitue le contenu de la révision spécifiée par totitle, toid ou torev.\n\nCe paramètre spécifie les intervalles qui vont être modifiés. Utilisez totext-{slot}, tocontentmodel-{slot}, et tocontentformat-{slot} pour spécifier le contenue de chaque intervalle.", + "apihelp-compare-param-totext-{slot}": "Texte de la relation spécifiée. Si absent, le lien est supprimé de la révision.", + "apihelp-compare-param-tosection-{slot}": "Si totext-{slot} est le contenu d'une seule section, c'est le numéro de la section. Il sera fusionné dans la révision spécifiée par totitle, toid ou torev comme pour les modifications de section.", + "apihelp-compare-param-tocontentmodel-{slot}": "Modèle de contenu de totext-{slot}. Si non fourni, il sera déduit en fonction de la valeur des autres paramètres.", + "apihelp-compare-param-tocontentformat-{slot}": "Format de sérialisation du contenu de totext-{slot}.", + "apihelp-compare-param-totext": "Spécifiez toslots=main et utilisez totext-main à la place.", + "apihelp-compare-param-tocontentmodel": "Spécifiez toslots=main et utilisez tocontentmodel-main à la place.", + "apihelp-compare-param-tocontentformat": "Spécifiez toslots=main et utilisez tocontentformat-main à la place.", + "apihelp-compare-param-tosection": "N'utiliser que la section spécifiée du contenu 'to'.", "apihelp-compare-param-prop": "Quelles informations obtenir.", "apihelp-compare-paramvalue-prop-diff": "Le diff HTML.", "apihelp-compare-paramvalue-prop-diffsize": "La taille du diff HTML en octets.", @@ -108,6 +118,7 @@ "apihelp-compare-paramvalue-prop-comment": "Le commentaire des révisions 'depuis' et 'vers'.", "apihelp-compare-paramvalue-prop-parsedcomment": "Le commentaire analysé des révisions 'depuis' et 'vers'.", "apihelp-compare-paramvalue-prop-size": "La taille des révisions 'depuis' et 'vers'.", + "apihelp-compare-param-slots": "Retourne les diffs individuels pour ces intervalles, plutôt qu'un diff combiné pour tous les intervalles.", "apihelp-compare-example-1": "Créer une différence entre les révisions 1 et 2", "apihelp-createaccount-summary": "Créer un nouveau compte utilisateur.", "apihelp-createaccount-param-preservestate": "Si [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] a retourné true pour hasprimarypreservedstate, les demandes marquées comme primary-required doivent être omises. Si elle a retourné une valeur non vide pour preservedusername, ce nom d'utilisateur doit être utilisé pour le paramètre username.", @@ -1607,6 +1618,8 @@ "apierror-compare-no-title": "Impossible de faire une transformation avant enregistrement sans titre. Essayez de spécifier fromtitle ou totitle.", "apierror-compare-nosuchfromsection": "Il n'y a pas de section $1 dans le contenu 'from'.", "apierror-compare-nosuchtosection": "Il n'y a pas de section $1 dans le contenu 'to'.", + "apierror-compare-nofromrevision": "Aucune révision 'from'. Spécifiez fromrev, fromtitle, ou fromid.", + "apierror-compare-notorevision": "Aucune révision 'to'. Spécifiez torev, totitle, ou toid.", "apierror-compare-relative-to-nothing": "Pas de révision 'depuis' pour torelative à laquelle se rapporter.", "apierror-contentserializationexception": "Échec de sérialisation du contenu : $1", "apierror-contenttoobig": "Le contenu que vous avez fourni dépasse la limite de taille d’un article, qui est de $1 {{PLURAL:$1|kilooctet|kilooctets}}.", @@ -1654,6 +1667,7 @@ "apierror-mimesearchdisabled": "La recherche MIME est désactivée en mode Misère.", "apierror-missingcontent-pageid": "Contenu manquant pour la page d’ID $1.", "apierror-missingcontent-revid": "Contenu de la révision d’ID $1 manquant.", + "apierror-missingcontent-revid-role": "Contenu absent pour l'ID de révision $1 pour le rôle $2.", "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|Le paramètre|Au moins un des paramètres}} $1 est obligatoire.", "apierror-missingparam-one-of": "{{PLURAL:$2|Le paramètre|Un des paramètres}} $1 est obligatoire.", "apierror-missingparam": "Le paramètre $1 doit être défini.", diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index cb018dc696..4bfb522552 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -68,20 +68,20 @@ "apihelp-compare-param-fromtitle": "כותרת ראשונה להשוואה.", "apihelp-compare-param-fromid": "מס׳ זיהוי של הדף הראשון להשוואה.", "apihelp-compare-param-fromrev": "גרסה ראשונה להשוואה.", - "apihelp-compare-param-fromtext": "להשתמש בטקסט הזה במקום תוכן הגרסה שהוגדרה על־ידי fromtitle, fromid או fromrev.", - "apihelp-compare-param-fromsection": "יש להשתמש רק בפסקה שצוינה בתוכן של הפרמטר 'from'.", "apihelp-compare-param-frompst": "לעשות התמרה לפני שמירה ב־fromtext.", + "apihelp-compare-param-fromtext": "להשתמש בטקסט הזה במקום תוכן הגרסה שהוגדרה על־ידי fromtitle, fromid או fromrev.", "apihelp-compare-param-fromcontentmodel": "מודל התוכן של fromtext. אם זה לא סופק, ייעשה ניחוש על סמך פרמטרים אחרים.", "apihelp-compare-param-fromcontentformat": "תסדיר הסדרת תוכן של fromtext.", + "apihelp-compare-param-fromsection": "יש להשתמש רק בפסקה שצוינה בתוכן של הפרמטר 'from'.", "apihelp-compare-param-totitle": "כותרת שנייה להשוואה.", "apihelp-compare-param-toid": "מס׳ מזהה של הדף השני להשוואה.", "apihelp-compare-param-torev": "גרסה שנייה להשוואה.", "apihelp-compare-param-torelative": "להשתמש בגרסה יחסית לגרסה שהוסקה מfromtitle, fromid או fromrev. לכל אפשריות ה־\"to\" האחרות לא תהיה השפעה.", - "apihelp-compare-param-totext": "להשתמש בטקסט הזה במקום התוכן של הגרסה שהוגדר ב־totitle, toid or torev.", - "apihelp-compare-param-tosection": "יש להשתמש רק בפסקה שצוינה בתוכן של הפרמטר 'to'.", "apihelp-compare-param-topst": "לעשות התמרה לפני שמירה ב־totext.", + "apihelp-compare-param-totext": "להשתמש בטקסט הזה במקום התוכן של הגרסה שהוגדר ב־totitle, toid or torev.", "apihelp-compare-param-tocontentmodel": "מודל התוכן של totext. אם זה לא סופק, ייעשה ניחוש על סמך פרמטרים אחרים.", "apihelp-compare-param-tocontentformat": "תסדיר הסדרת תוכן של fromtext.", + "apihelp-compare-param-tosection": "יש להשתמש רק בפסקה שצוינה בתוכן של הפרמטר 'to'.", "apihelp-compare-param-prop": "אילו פריטי מידע לקבל.", "apihelp-compare-paramvalue-prop-diff": "ה־HTML של ההשוואה.", "apihelp-compare-paramvalue-prop-diffsize": "גודל ה־HTML של ההשוואה, בבתים.", diff --git a/includes/api/i18n/hu.json b/includes/api/i18n/hu.json index 4451f194e9..1211693b63 100644 --- a/includes/api/i18n/hu.json +++ b/includes/api/i18n/hu.json @@ -10,7 +10,7 @@ "Dj" ] }, - "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentáció]]\n* [[mw:Special:MyLanguage/API:FAQ|GYIK]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Levelezőlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-bejelentések]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Hibabejelentések és kérések]\n
\nStátusz: Minden ezen a lapon látható funkciónak működnie kell, de az API jelenleg is aktív fejlesztés alatt áll, és bármikor változhat. Iratkozz fel a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce levelezőlistára] a frissítések követéséhez.\n\nHibás kérések: Ha az API hibás kérést kap, egy HTTP-fejlécet küld vissza „MediaWiki-API-Error” kulccsal, és a fejléc értéke és a visszaküldött hibakód ugyanarra az értékre lesz állítva. További információért lásd: [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Hibák és figyelmeztetések]].\n\n

Tesztelés: Az API-kérések könnyebb teszteléséhez használható az [[Special:ApiSandbox|API-homokozó]].

", + "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentáció]]\n* [[mw:Special:MyLanguage/API:FAQ|GYIK]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Levelezőlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-bejelentések]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Hibabejelentések és kérések]\n
\nÁllapot: A MediaWiki API egy érett és stabil interfész, ami aktív támogatásban és fejlesztésben részesül. Bár próbáljuk elkerülni, de néha szükség van visszafelé nem kompatibilis változtatásokra; iratkozz fel a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce levelezőlistára] a frissítések követéséhez.\n\nHibás kérések: Ha az API hibás kérést kap, egy HTTP-fejlécet küld vissza „MediaWiki-API-Error” kulccsal, és a fejléc értéke és a visszaküldött hibakód ugyanarra az értékre lesz állítva. További információért lásd: [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Hibák és figyelmeztetések]].\n\n

Tesztelés: Az API-kérések könnyebb teszteléséhez használható az [[Special:ApiSandbox|API-homokozó]].

", "apihelp-main-param-action": "Milyen műveletet hajtson végre.", "apihelp-main-param-format": "A kimenet formátuma.", "apihelp-main-param-smaxage": "Az s-maxage gyorsítótár-vezérlő HTTP-fejléc beállítása ennyi másodpercre. A hibák soha nincsenek gyorsítótárazva.", diff --git a/includes/api/i18n/ja.json b/includes/api/i18n/ja.json index 399fc1fd17..0d2d2d19e9 100644 --- a/includes/api/i18n/ja.json +++ b/includes/api/i18n/ja.json @@ -13,7 +13,8 @@ "Kkairri", "ネイ", "Omotecho", - "Yusuke1109" + "Yusuke1109", + "Suyama" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|説明文書]]\n* [[mw:Special:MyLanguage/API:FAQ|よくある質問]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api メーリングリスト]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 告知]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R バグの報告とリクエスト]\n
\n状態: MediaWiki APIは、積極的にサポートされ、改善された成熟した安定したインターフェースです。避けようとはしていますが、時には壊れた変更が加えられるかもしれません。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n誤ったリクエスト: 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\n

テスト: API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。

", @@ -62,16 +63,16 @@ "apihelp-compare-param-fromtitle": "比較する1つ目のページ名。", "apihelp-compare-param-fromid": "比較する1つ目のページID。", "apihelp-compare-param-fromrev": "比較する1つ目の版。", - "apihelp-compare-param-fromtext": "fromtitle, fromid or fromrev で指定された版の内容の代わりに、このテキストを使用します。", - "apihelp-compare-param-fromsection": "'from' の内容のうち指定された節のみを使用します。", "apihelp-compare-param-frompst": "fromtextに保存前変換を行います。", + "apihelp-compare-param-fromtext": "fromtitle, fromid or fromrev で指定された版の内容の代わりに、このテキストを使用します。", "apihelp-compare-param-fromcontentmodel": "fromtextのコンテンツモデル。指定されていない場合は、他のパラメータに基づいて推測されます。", + "apihelp-compare-param-fromsection": "'from' の内容のうち指定された節のみを使用します。", "apihelp-compare-param-totitle": "比較する2つ目のページ名。", "apihelp-compare-param-toid": "比較する2つ目のページID。", "apihelp-compare-param-torev": "比較する2つ目の版。", - "apihelp-compare-param-tosection": "'to' の内容のうち指定された節のみを使用します。", "apihelp-compare-param-topst": "totextに保存前変換を行います。", "apihelp-compare-param-tocontentmodel": "totext のコンテンツモデル。指定されていない場合は、他のパラメータに基づいて推測されます。", + "apihelp-compare-param-tosection": "'to' の内容のうち指定された節のみを使用します。", "apihelp-compare-param-prop": "どの情報を取得するか:", "apihelp-compare-paramvalue-prop-diff": "差分HTML。", "apihelp-compare-paramvalue-prop-diffsize": "差分HTMLのサイズ (バイト数)。", @@ -1020,6 +1021,7 @@ "api-help-permissions": "{{PLURAL:$1|権限}}:", "api-help-permissions-granted-to": "{{PLURAL:$1|権限を持つグループ}}: $2", "api-help-open-in-apisandbox": "[サンドボックスで開く]", + "apierror-botsnotsupported": "この API インターフェースはボットをサポートしていません。", "apierror-filedoesnotexist": "ファイルが存在しません。", "apierror-invaliduser": "無効なユーザー名「$1」。", "apierror-missingparam": "パラメーター $1 を設定してください。", diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index e37bbbc5cf..5280644346 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -69,20 +69,26 @@ "apihelp-compare-param-fromtitle": "비교할 첫 이름.", "apihelp-compare-param-fromid": "비교할 첫 문서 ID.", "apihelp-compare-param-fromrev": "비교할 첫 판.", - "apihelp-compare-param-fromtext": "fromtitle, fromid 또는 fromrev로 지정된 판의 내용 대신 이 텍스트를 사용합니다.", + "apihelp-compare-param-frompst": "fromtext-{slot}에 사전 저장 변환을 수행합니다.", + "apihelp-compare-param-fromtext-{slot}": "지정된 슬롯의 텍스트입니다. 생략할 경우 판에서 슬롯이 제거됩니다.", + "apihelp-compare-param-fromcontentmodel-{slot}": "fromtext-{slot}의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.", + "apihelp-compare-param-fromcontentformat-{slot}": "fromtext-{slot}의 콘텐츠 직렬화 포맷입니다.", + "apihelp-compare-param-fromtext": "fromslots=main을 지정하고 fromtext-main을 대신 사용합니다.", + "apihelp-compare-param-fromcontentmodel": "fromslots=main을 지정하고 fromcontentmodel-main을 대신 사용합니다.", + "apihelp-compare-param-fromcontentformat": "fromslots=main을 지정하고 fromcontentformat-main을 대신 사용합니다.", "apihelp-compare-param-fromsection": "지정된 'from' 내용의 지정된 문단만 사용합니다.", - "apihelp-compare-param-frompst": "fromtext에 사전 저장 변환을 수행합니다.", - "apihelp-compare-param-fromcontentmodel": "fromtext의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.", - "apihelp-compare-param-fromcontentformat": "fromtext의 콘텐츠 직렬화 포맷입니다.", "apihelp-compare-param-totitle": "비교할 두 번째 제목.", "apihelp-compare-param-toid": "비교할 두 번째 문서 ID.", "apihelp-compare-param-torev": "비교할 두 번째 판.", "apihelp-compare-param-torelative": "fromtitle, fromid 또는 fromrev에서 결정된 판과 상대적인 판을 사용합니다. 다른 'to' 옵션들은 모두 무시됩니다.", - "apihelp-compare-param-totext": "totitle, toid 또는 torev로 지정된 판의 내용 대신 이 텍스트를 사용합니다.", - "apihelp-compare-param-tosection": "지정된 'to' 내용의 지정된 문단만 사용합니다.", "apihelp-compare-param-topst": "totext에 사전 저장 변환을 수행합니다.", - "apihelp-compare-param-tocontentmodel": "totext의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.", - "apihelp-compare-param-tocontentformat": "totext의 콘텐츠 직렬화 포맷입니다.", + "apihelp-compare-param-totext-{slot}": "지정된 슬롯의 텍스트입니다. 생략하면 이 슬롯은 판에서 제거됩니다.", + "apihelp-compare-param-tocontentmodel-{slot}": "totext-{slot}의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.", + "apihelp-compare-param-tocontentformat-{slot}": "totext-{slot}의 콘텐츠 직렬화 포맷입니다.", + "apihelp-compare-param-totext": "toslots=main을 지정하고 totext-main을 대신 사용합니다.", + "apihelp-compare-param-tocontentmodel": "toslots=main을 지정하고 tocontentmodel-main을 대신 사용합니다.", + "apihelp-compare-param-tocontentformat": "toslots=main을 지정하고 tocontentformat-main을 대신 사용합니다.", + "apihelp-compare-param-tosection": "지정된 'to' 내용의 지정된 문단만 사용합니다.", "apihelp-compare-param-prop": "가져올 정보입니다.", "apihelp-compare-paramvalue-prop-diff": "HTML의 차이입니다.", "apihelp-compare-paramvalue-prop-diffsize": "HTML 차이의 크기(바이트 단위)입니다.", @@ -857,6 +863,7 @@ "apierror-maxlag-generic": "데이터베이스 서버 대기 중: $1 {{PLURAL:$1|초}} 지연되었습니다.", "apierror-maxlag": "$2 대기 중: $1 {{PLURAL:$1|초}} 지연되었습니다.", "apierror-missingcontent-revid": "ID $1 판에 해당하는 내용이 없습니다.", + "apierror-missingcontent-revid-role": "역할 $2에 대해 판 ID $1의 내용이 없습니다.", "apierror-missingparam": "$1 변수는 설정해야 합니다.", "apierror-missingtitle": "지정한 페이지가 존재하지 않습니다.", "apierror-missingtitle-byname": "$1 문서가 존재하지 않습니다.", diff --git a/includes/api/i18n/nb.json b/includes/api/i18n/nb.json index be6e798f5d..8edddda330 100644 --- a/includes/api/i18n/nb.json +++ b/includes/api/i18n/nb.json @@ -59,16 +59,16 @@ "apihelp-compare-param-fromtitle": "Første tittel å sammenligne.", "apihelp-compare-param-fromid": "Første side-ID å sammenligne.", "apihelp-compare-param-fromrev": "Første revisjon å sammenligne.", - "apihelp-compare-param-fromtext": "Bruk denne teksten i stedet for innholdet i revisjonen som angis med fromtitle, fromid eller fromrev.", "apihelp-compare-param-frompst": "Gjør en transformering av fromtext før lagring.", + "apihelp-compare-param-fromtext": "Bruk denne teksten i stedet for innholdet i revisjonen som angis med fromtitle, fromid eller fromrev.", "apihelp-compare-param-fromcontentmodel": "Innholdsmodell for fromtext. Om den ikke angis vil den gjettes basert på de andre parameterne.", "apihelp-compare-param-fromcontentformat": "Innholdsserialiseringsformat for fromtext.", "apihelp-compare-param-totitle": "Andre tittel å sammenligne.", "apihelp-compare-param-toid": "Andre side-ID å sammenligne.", "apihelp-compare-param-torev": "Andre revisjon å sammenligne.", "apihelp-compare-param-torelative": "Bruk en revisjon som er relativ til revisjonen som hentes fra fromtitle, fromid eller fromrev. Alle de andre «to»-alternativene vil ignoreres.", - "apihelp-compare-param-totext": "Bruk denne teksten i stedet for innholdet i revisjonen spesifisert av totitle, toid eller torev.", "apihelp-compare-param-topst": "Gjør en transformering av totext før lagring.", + "apihelp-compare-param-totext": "Bruk denne teksten i stedet for innholdet i revisjonen spesifisert av totitle, toid eller torev.", "apihelp-compare-param-tocontentmodel": "Innholdsmodellen til totext. Om denne ikke angis vil den bli gjettet ut fra andre parametere.", "apihelp-compare-param-tocontentformat": "Innholdsserialiseringsformat for totext.", "apihelp-compare-param-prop": "Hvilke informasjonsdeler som skal hentes.", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index 1ded789288..a0429ac0bd 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -53,7 +53,7 @@ "apihelp-checktoken-example-simple": "Sprawdź poprawność tokenu csrf.", "apihelp-clearhasmsg-summary": "Czyści flagę hasmsg dla bieżącego użytkownika.", "apihelp-clearhasmsg-example-1": "Wyczyść flagę hasmsg dla bieżącego użytkownika.", - "apihelp-compare-summary": "Zauważ różnicę między dwoma stronami", + "apihelp-compare-summary": "Pokaż porównanie dwóch stron.", "apihelp-compare-param-fromtitle": "Pierwszy tytuł do porównania.", "apihelp-compare-param-fromid": "ID pierwszej strony do porównania.", "apihelp-compare-param-fromrev": "Pierwsza wersja do porównania.", diff --git a/includes/api/i18n/pt-br.json b/includes/api/i18n/pt-br.json index 7378ac45f8..26c9a1759f 100644 --- a/includes/api/i18n/pt-br.json +++ b/includes/api/i18n/pt-br.json @@ -67,20 +67,29 @@ "apihelp-compare-param-fromtitle": "Primeiro título para comparar.", "apihelp-compare-param-fromid": "Primeiro ID de página para comparar.", "apihelp-compare-param-fromrev": "Primeira revisão para comparar.", - "apihelp-compare-param-fromtext": "Use este texto em vez do conteúdo da revisão especificada por fromtitle, fromid ou fromrev.", + "apihelp-compare-param-frompst": "Fazer uma transformação anterior à gravação, de fromtext-{slot}.", + "apihelp-compare-param-fromslots": "Substituir o conteúdo da revisão especificada por fromtitle, fromid ou fromrev.\n\nEste parâmetro especifica os segmentos que deverão ser modificados. Use fromtext-{slot}, fromcontentmodel-{slot} e fromcontentformat-{slot} para especificar conteúdo para cada segmento.", + "apihelp-compare-param-fromtext-{slot}": "Texto do slot especificado. Se omitido, o slot é removido da revisão.", + "apihelp-compare-param-fromsection-{slot}": "Quando fromtext-{slot} é o conteúdo de uma única secção, este é o número da seção. Será fundido na revisão especificada por fromtitle, fromid ou fromrev tal como acontece na edição de uma secção.", + "apihelp-compare-param-fromcontentmodel-{slot}": "Modelo de conteúdo de fromtext-{slot}. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.", + "apihelp-compare-param-fromcontentformat-{slot}": "Formato de serialização de conteúdo de fromtext-{slot}.", + "apihelp-compare-param-fromtext": "Especificar fromslots=main e usar fromtext-main.", + "apihelp-compare-param-fromcontentmodel": "Especificar fromslots=main e usar fromcontentmodel-main.", + "apihelp-compare-param-fromcontentformat": "Especificar fromslots=main e usar fromcontentformat-main.", "apihelp-compare-param-fromsection": "Utilizar apenas a secção especificada do conteúdo 'from' especificado.", - "apihelp-compare-param-frompst": "Faz uma transformação pré-salvar em fromtext.", - "apihelp-compare-param-fromcontentmodel": "Modelo de conteúdo de fromtext. Se não for fornecido, será adivinhado com base nos outros parâmetros.", - "apihelp-compare-param-fromcontentformat": "Formato de serialização de conteúdo de fromtext.", "apihelp-compare-param-totitle": "Segundo título para comparar.", "apihelp-compare-param-toid": "Segundo ID de página para comparar.", "apihelp-compare-param-torev": "Segunda revisão para comparar.", "apihelp-compare-param-torelative": "Use uma revisão relativa à revisão determinada de fromtitle, fromid ou fromrev. Todas as outras opções 'to' serão ignoradas.", - "apihelp-compare-param-totext": "Use este texto em vez do conteúdo da revisão especificada por totitle, toid ou torev.", - "apihelp-compare-param-tosection": "Utilizar apenas a secção especificada do conteúdo 'to' especificado.", "apihelp-compare-param-topst": "Faz uma transformação pré-salvar em totext.", - "apihelp-compare-param-tocontentmodel": "Modelo de conteúdo de totext. Se não for fornecido, será adivinhado com base nos outros parâmetros.", - "apihelp-compare-param-tocontentformat": "Formato de serialização de conteúdo de totext.", + "apihelp-compare-param-totext-{slot}": "Texto do slot especificado. Se omitido, o slot é removido da revisão.", + "apihelp-compare-param-tosection-{slot}": "Quando totext-{slot} é o conteúdo de uma única secção, este é o número da secção. Será fundido na revisão especificada por totitle, toid ou torev tal como acontece na edição de uma secção.", + "apihelp-compare-param-tocontentmodel-{slot}": "Modelo de conteúdo de totext-{slot}. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.", + "apihelp-compare-param-tocontentformat-{slot}": "Formato de seriação do conteúdo de totext-{slot}.", + "apihelp-compare-param-totext": "Especificar toslots=main e usar totext-main.", + "apihelp-compare-param-tocontentmodel": "Especificar toslots=main e usar tocontentmodel-main.", + "apihelp-compare-param-tocontentformat": "Especificar toslots=main e usar tocontentformat-main.", + "apihelp-compare-param-tosection": "Utilizar apenas a secção especificada do conteúdo 'to' especificado.", "apihelp-compare-param-prop": "Quais peças de informação incluir.", "apihelp-compare-paramvalue-prop-diff": "O dif do HTML.", "apihelp-compare-paramvalue-prop-diffsize": "O tamanho do diff HTML, em bytes.", @@ -91,6 +100,7 @@ "apihelp-compare-paramvalue-prop-comment": "O comentário das revisões 'from' e 'to'.", "apihelp-compare-paramvalue-prop-parsedcomment": "O comentário analisado sobre as revisões 'from' e 'to'.", "apihelp-compare-paramvalue-prop-size": "O tamanho das revisões 'from' e 'to'.", + "apihelp-compare-param-slots": "Devolve os diffs individuais para estes slots, em vez de um diff combinado para todos os slots.", "apihelp-compare-example-1": "Criar um diff entre a revisão 1 e 2.", "apihelp-createaccount-summary": "Criar uma nova conta de usuário.", "apihelp-createaccount-param-preservestate": "Se [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] retornar true para hasprimarypreservedstate, pedidos marcados como hasprimarypreservedstate devem ser omitidos. Se retornou um valor não vazio para preservedusername, esse nome de usuário deve ser usado pelo parâmetro username.", @@ -1590,6 +1600,8 @@ "apierror-compare-no-title": "Não é possível pré-salvar a transformação sem um título. Tente especificar fromtitle ou totitle.", "apierror-compare-nosuchfromsection": "Não há nenhuma secção $1 no conteúdo 'from'.", "apierror-compare-nosuchtosection": "Não há nenhuma seção $1 no conteúdo 'to'.", + "apierror-compare-nofromrevision": "Não foi especificada uma revisão 'from'. Especificar fromrev, fromtitle ou fromid.", + "apierror-compare-notorevision": "Não foi especificada uma revisão 'to'. Especificar torev, totitle ou toid.", "apierror-compare-relative-to-nothing": "Nenhuma revisão 'from' para torelative 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|kilobyte|kilobytes}}.", @@ -1637,6 +1649,7 @@ "apierror-mimesearchdisabled": "A pesquisa MIME está desativada no Miser Mode.", "apierror-missingcontent-pageid": "Falta conteúdo para a ID da página $1.", "apierror-missingcontent-revid": "Falta conteúdo para a ID de revisão $1.", + "apierror-missingcontent-revid-role": "Conteúdo ausente para o ID de revisão $1 para a função $2.", "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|O parâmetro|Ao menos um dos parâmetros}} $1 é necessário.", "apierror-missingparam-one-of": "{{PLURAL:$2|O parâmetro|Um dos parâmetros}} $1 é necessário.", "apierror-missingparam": "O parâmetro $1 precisa ser definido.", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 0733a2a212..c157ec01e6 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -62,20 +62,30 @@ "apihelp-compare-param-fromtitle": "Primeiro título a comparar.", "apihelp-compare-param-fromid": "Primeiro identificador de página a comparar.", "apihelp-compare-param-fromrev": "Primeira revisão a comparar.", - "apihelp-compare-param-fromtext": "Usar este texto em vez do conteúdo da revisão especificada por fromtitle, fromid ou fromrev.", + "apihelp-compare-param-frompst": "Fazer uma transformação anterior à gravação, de fromtext-{slot}.", + "apihelp-compare-param-fromslots": "Substituir o conteúdo da revisão especificada por fromtitle, fromid ou fromrev.\n\nEste parâmetro especifica os segmentos que deverão ser modificados. Use fromtext-{slot}, fromcontentmodel-{slot} e fromcontentformat-{slot} para especificar conteúdo para cada segmento.", + "apihelp-compare-param-fromtext-{slot}": "Texto do segmento especificado. Se for omitido, o segmento é removido da revisão.", + "apihelp-compare-param-fromsection-{slot}": "Quando fromtext-{slot} é o conteúdo de uma única secção, este é o número da secção. Será fundido na revisão especificada por fromtitle, fromid ou fromrev tal como acontece na edição de uma secção.", + "apihelp-compare-param-fromcontentmodel-{slot}": "Modelo de conteúdo de fromtext-{slot}. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.", + "apihelp-compare-param-fromcontentformat-{slot}": "Formato de seriação do conteúdo de fromtext-{slot}.", + "apihelp-compare-param-fromtext": "Especificar fromslots=main e usar fromtext-main.", + "apihelp-compare-param-fromcontentmodel": "Especificar fromslots=main e usar fromcontentmodel-main.", + "apihelp-compare-param-fromcontentformat": "Especificar fromslots=main e usar fromcontentformat-main.", "apihelp-compare-param-fromsection": "Utilizar apenas a secção especificada do conteúdo 'from' especificado.", - "apihelp-compare-param-frompst": "Fazer uma transformação anterior à gravação, de fromtext.", - "apihelp-compare-param-fromcontentmodel": "Modelo de conteúdo de fromtext. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.", - "apihelp-compare-param-fromcontentformat": "Formato de seriação do conteúdo de fromtext.", "apihelp-compare-param-totitle": "Segundo título a comparar.", "apihelp-compare-param-toid": "Segundo identificador de página a comparar.", "apihelp-compare-param-torev": "Segunda revisão a comparar.", "apihelp-compare-param-torelative": "Usar uma revisão relativa à revisão determinada a partir de fromtitle, fromid ou fromrev. Todas as outras opções 'to' serão ignoradas.", - "apihelp-compare-param-totext": "Usar este texto em vez do conteúdo da revisão especificada por totitle, toid ou torev.", - "apihelp-compare-param-tosection": "Utilizar apenas a secção especificada do conteúdo 'to' especificado.", "apihelp-compare-param-topst": "Fazer uma transformação anterior à gravação, de totext.", - "apihelp-compare-param-tocontentmodel": "Modelo de conteúdo de totext. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.", - "apihelp-compare-param-tocontentformat": "Formato de seriação do conteúdo de totext.", + "apihelp-compare-param-toslots": "Especificar o conteúdo para ser usado em vez do conteúdo da revisão especificada em totitle, toid ou torev.\n\nEste parâmetro especifica os segmentos que têm conteúdo. Use totext-{slot}, tocontentmodel-{slot} e tocontentformat-{slot} para especificar conteúdo para cada segmento.", + "apihelp-compare-param-totext-{slot}": "Texto do segmento especificado.", + "apihelp-compare-param-tosection-{slot}": "Quando totext-{slot} é o conteúdo de uma única secção, este é o número da secção. Será fundido na revisão especificada por totitle, toid ou torev tal como acontece na edição de uma secção.", + "apihelp-compare-param-tocontentmodel-{slot}": "Modelo de conteúdo de totext-{slot}. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.", + "apihelp-compare-param-tocontentformat-{slot}": "Formato de seriação do conteúdo de totext-{slot}.", + "apihelp-compare-param-totext": "Especificar toslots=main e usar totext-main.", + "apihelp-compare-param-tocontentmodel": "Especificar toslots=main e usar tocontentmodel-main.", + "apihelp-compare-param-tocontentformat": "Especificar toslots=main e usar tocontentformat-main.", + "apihelp-compare-param-tosection": "Utilizar apenas a secção especificada do conteúdo 'to' especificado.", "apihelp-compare-param-prop": "As informações que devem ser obtidas.", "apihelp-compare-paramvalue-prop-diff": "O HTML da lista de diferenças.", "apihelp-compare-paramvalue-prop-diffsize": "O tamanho do HTML da lista de diferenças, em bytes.", @@ -86,6 +96,7 @@ "apihelp-compare-paramvalue-prop-comment": "O comentário das revisões 'from' e 'to'.", "apihelp-compare-paramvalue-prop-parsedcomment": "O comentário após análise sintática, das revisões 'from' e 'to'.", "apihelp-compare-paramvalue-prop-size": "O tamanho das revisões 'from' e 'to'.", + "apihelp-compare-param-slots": "Devolver as diferenças individuais destes segmentos, em vez de uma lista combinada para todos os segmentos.", "apihelp-compare-example-1": "Criar uma lista de diferenças entre as revisões 1 e 2.", "apihelp-createaccount-summary": "Criar uma conta de utilizador nova.", "apihelp-createaccount-param-preservestate": "Se [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] devolveu o valor verdadeiro para hasprimarypreservedstate, pedidos marcados como primary-required devem ser omitidos. Se devolveu um valor não vazio em preservedusername, esse nome de utilizador tem de ser usado no parâmetro username.", @@ -1585,6 +1596,8 @@ "apierror-compare-no-title": "Não é possível transformar antes da gravação, sem um título. Tente especificar fromtitle ou totitle.", "apierror-compare-nosuchfromsection": "Não há nenhuma secção $1 no conteúdo 'from'.", "apierror-compare-nosuchtosection": "Não há nenhuma secção $1 no conteúdo 'to'.", + "apierror-compare-nofromrevision": "Não foi especificada uma revisão 'from'. Especificar fromrev, fromtitle ou fromid.", + "apierror-compare-notorevision": "Não foi especificada uma revisão 'to'. Especificar torev, totitle ou toid.", "apierror-compare-relative-to-nothing": "Não existe uma revisão 'from' em relação à qual torelative possa ser relativo.", "apierror-contentserializationexception": "A seriação do conteúdo falhou: $1", "apierror-contenttoobig": "O conteúdo que forneceu excede o tamanho máximo dos artigos que é $1 {{PLURAL:$1|kilobyte|kilobytes}}.", @@ -1632,6 +1645,7 @@ "apierror-mimesearchdisabled": "A pesquisa MIME é desativada no modo avarento.", "apierror-missingcontent-pageid": "Conteúdo em falta para a página com o identificador $1.", "apierror-missingcontent-revid": "Conteúdo em falta para a revisão com o identificador $1.", + "apierror-missingcontent-revid-role": "O identificador de revisão $1 para a função $2 não tem conteúdo.", "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|O parâmetro|Pelo menos um dos parâmetros}} $1 é obrigatório.", "apierror-missingparam-one-of": "{{PLURAL:$2|O parâmetro|Um dos parâmetros}} $1 é obrigatório.", "apierror-missingparam": "O parâmetro $1 tem de ser definido.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index f158f27dd3..33f6613ada 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -67,20 +67,30 @@ "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}", "apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}", "apihelp-compare-param-fromrev": "{{doc-apihelp-param|compare|fromrev}}", - "apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}", - "apihelp-compare-param-fromsection": "{{doc-apihelp-param|compare|fromsection}}", "apihelp-compare-param-frompst": "{{doc-apihelp-param|compare|frompst}}", + "apihelp-compare-param-fromslots": "{{doc-apihelp-param|compare|fromslots}}", + "apihelp-compare-param-fromtext-{slot}": "{{doc-apihelp-param|compare|fromtext-{slot} }}", + "apihelp-compare-param-fromsection-{slot}": "{{doc-apihelp-param|compare|fromsection-{slot} }}", + "apihelp-compare-param-fromcontentmodel-{slot}": "{{doc-apihelp-param|compare|fromcontentmodel-{slot} }}", + "apihelp-compare-param-fromcontentformat-{slot}": "{{doc-apihelp-param|compare|fromcontentformat-{slot} }}", + "apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}", "apihelp-compare-param-fromcontentmodel": "{{doc-apihelp-param|compare|fromcontentmodel}}", "apihelp-compare-param-fromcontentformat": "{{doc-apihelp-param|compare|fromcontentformat}}", + "apihelp-compare-param-fromsection": "{{doc-apihelp-param|compare|fromsection}}", "apihelp-compare-param-totitle": "{{doc-apihelp-param|compare|totitle}}", "apihelp-compare-param-toid": "{{doc-apihelp-param|compare|toid}}", "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}", "apihelp-compare-param-torelative": "{{doc-apihelp-param|compare|torelative}}", - "apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}", - "apihelp-compare-param-tosection": "{{doc-apihelp-param|compare|tosection}}", "apihelp-compare-param-topst": "{{doc-apihelp-param|compare|topst}}", + "apihelp-compare-param-toslots": "{{doc-apihelp-param|compare|toslots}}", + "apihelp-compare-param-totext-{slot}": "{{doc-apihelp-param|compare|totext-{slot} }}", + "apihelp-compare-param-tosection-{slot}": "{{doc-apihelp-param|compare|tosection-{slot} }}", + "apihelp-compare-param-tocontentmodel-{slot}": "{{doc-apihelp-param|compare|tocontentmodel-{slot} }}", + "apihelp-compare-param-tocontentformat-{slot}": "{{doc-apihelp-param|compare|tocontentformat-{slot} }}", + "apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}", "apihelp-compare-param-tocontentmodel": "{{doc-apihelp-param|compare|tocontentmodel}}", "apihelp-compare-param-tocontentformat": "{{doc-apihelp-param|compare|tocontentformat}}", + "apihelp-compare-param-tosection": "{{doc-apihelp-param|compare|tosection}}", "apihelp-compare-param-prop": "{{doc-apihelp-param|compare|prop}}", "apihelp-compare-paramvalue-prop-diff": "{{doc-apihelp-paramvalue|compare|prop|diff}}", "apihelp-compare-paramvalue-prop-diffsize": "{{doc-apihelp-paramvalue|compare|prop|diffsize}}", @@ -91,6 +101,7 @@ "apihelp-compare-paramvalue-prop-comment": "{{doc-apihelp-paramvalue|compare|prop|comment}}", "apihelp-compare-paramvalue-prop-parsedcomment": "{{doc-apihelp-paramvalue|compare|prop|parsedcomment}}", "apihelp-compare-paramvalue-prop-size": "{{doc-apihelp-paramvalue|compare|prop|size}}", + "apihelp-compare-param-slots": "{{doc-apihelp-param|compare|slots}}", "apihelp-compare-example-1": "{{doc-apihelp-example|compare}}", "apihelp-createaccount-summary": "{{doc-apihelp-summary|createaccount}}", "apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}", @@ -1597,6 +1608,8 @@ "apierror-compare-no-title": "{{doc-apierror}}", "apierror-compare-nosuchfromsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.", "apierror-compare-nosuchtosection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.", + "apierror-compare-nofromrevision": "{{doc-apierror}}", + "apierror-compare-notorevision": "{{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 kilobytes.", @@ -1644,6 +1657,7 @@ "apierror-mimesearchdisabled": "{{doc-apierror}}", "apierror-missingcontent-pageid": "{{doc-apierror}}\n\nParameters:\n* $1 - Page ID number.", "apierror-missingcontent-revid": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number", + "apierror-missingcontent-revid-role": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number\n* $2 - Role name", "apierror-missingparam-at-least-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.", "apierror-missingparam-one-of": "{{doc-apierror}}\n\nParameters:\n* $1 - List of parameter names.\n* $2 - Number of parameters.", "apierror-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.", diff --git a/includes/api/i18n/ru.json b/includes/api/i18n/ru.json index d712765cb1..4450b6cbb5 100644 --- a/includes/api/i18n/ru.json +++ b/includes/api/i18n/ru.json @@ -88,20 +88,21 @@ "apihelp-compare-param-fromtitle": "Заголовок первой сравниваемой страницы.", "apihelp-compare-param-fromid": "Идентификатор первой сравниваемой страницы.", "apihelp-compare-param-fromrev": "Первая сравниваемая версия.", - "apihelp-compare-param-fromtext": "Используйте этот текст вместо содержимого версии, заданной fromtitle, fromid или fromrev.", - "apihelp-compare-param-fromsection": "Использовать только указанную секцию из содержимого «from».", "apihelp-compare-param-frompst": "Выполнить преобразование перед записью правки (PST) над fromtext.", + "apihelp-compare-param-fromtext": "Используйте этот текст вместо содержимого версии, заданной fromtitle, fromid или fromrev.", "apihelp-compare-param-fromcontentmodel": "Модель содержимого fromtext. Если не задана, будет угадана по другим параметрам.", "apihelp-compare-param-fromcontentformat": "Формат сериализации содержимого fromtext.", + "apihelp-compare-param-fromsection": "Использовать только указанную секцию из содержимого «from».", "apihelp-compare-param-totitle": "Заголовок второй сравниваемой страницы.", "apihelp-compare-param-toid": "Идентификатор второй сравниваемой страницы.", "apihelp-compare-param-torev": "Вторая сравниваемая версия.", "apihelp-compare-param-torelative": "Использовать версию, относящуюся к определённой fromtitle, fromid или fromrev. Все другие опции 'to' будут проигнорированы.", - "apihelp-compare-param-totext": "Используйте этот текст вместо содержимого версии, заданной totitle, toid или torev.", - "apihelp-compare-param-tosection": "Использовать только указанную секцию из содержимого «to».", "apihelp-compare-param-topst": "Выполнить преобразование перед записью правки (PST) над totext.", + "apihelp-compare-param-tocontentmodel-{slot}": "Модель содержимого totext-{slot}. Если не задана, будет угадана по другим параметрам.", + "apihelp-compare-param-totext": "Используйте этот текст вместо содержимого версии, заданной totitle, toid или torev.", "apihelp-compare-param-tocontentmodel": "Модель содержимого totext. Если не задана, будет угадана по другим параметрам.", "apihelp-compare-param-tocontentformat": "Формат сериализации содержимого totext.", + "apihelp-compare-param-tosection": "Использовать только указанную секцию из содержимого «to».", "apihelp-compare-param-prop": "Какую информацию получить.", "apihelp-compare-paramvalue-prop-diff": "HTML-код разницы.", "apihelp-compare-paramvalue-prop-diffsize": "Размер HTML-кода разницы в байтах.", diff --git a/includes/api/i18n/sv.json b/includes/api/i18n/sv.json index 20dc919f8a..1cb6f5c38f 100644 --- a/includes/api/i18n/sv.json +++ b/includes/api/i18n/sv.json @@ -16,7 +16,8 @@ "Rockyfelle", "Macofe", "Magol", - "Bengtsson96" + "Bengtsson96", + "Larske" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Vanliga frågor]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Sändlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-nyheter]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Buggar och begäran]\n
\nStatus: Alla funktioner som visas på denna sida bör fungera, men API:et är fortfarande under utveckling och kan ändras när som helst. Prenumerera på [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ sändlistan mediawiki-api-announce] för uppdateringsaviseringar.\n\nFelaktiga begäran: När felaktiga begäran skickas till API:et kommer en HTTP-header skickas med nyckeln \"MediaWiki-API-Error\" och sedan kommer både värdet i headern och felkoden som skickades tillbaka anges som samma värde. För mer information se [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fel och varningar]].\n\n

Testning: För enkelt testning av API-begäran, se [[Special:ApiSandbox]].

", @@ -447,7 +448,7 @@ "apihelp-query+images-param-limit": "Hur många filer att returnera.", "apihelp-query+images-param-dir": "Riktningen att lista mot.", "apihelp-query+images-example-simple": "Hämta en lista över filer som används på [[Main Page]].", - "apihelp-query+imageusage-summary": "Hitta alla sidor som användare angiven bildtitel.", + "apihelp-query+imageusage-summary": "Hitta alla sidor som använder angiven bildtitel.", "apihelp-query+imageusage-param-dir": "Riktningen att lista mot.", "apihelp-query+imageusage-example-simple": "Visa sidor med hjälp av [[:File:Albert Einstein Head.jpg]].", "apihelp-query+imageusage-example-generator": "Hämta information om sidor med hjälp av [[:File:Albert Einstein Head.jpg]].", diff --git a/includes/api/i18n/uk.json b/includes/api/i18n/uk.json index e9078e0b65..cd1ccc50ad 100644 --- a/includes/api/i18n/uk.json +++ b/includes/api/i18n/uk.json @@ -66,20 +66,20 @@ "apihelp-compare-param-fromtitle": "Перший заголовок для порівняння.", "apihelp-compare-param-fromid": "Перший ID сторінки для порівняння.", "apihelp-compare-param-fromrev": "Перша версія для порівняння.", - "apihelp-compare-param-fromtext": "Використати цей текст замість контенту версії, вказаної через fromtitle, fromid або fromrev.", - "apihelp-compare-param-fromsection": "Використовувати лише вказану секцію із заданого вмісту «from».", "apihelp-compare-param-frompst": "Зробити трансформацію перед збереженням на fromtext.", + "apihelp-compare-param-fromtext": "Використати цей текст замість контенту версії, вказаної через fromtitle, fromid або fromrev.", "apihelp-compare-param-fromcontentmodel": "Контентна модель fromtext. Якщо не вказано, буде використано припущення на основі інших параметрів.", "apihelp-compare-param-fromcontentformat": "Формат серіалізації контенту fromtext.", + "apihelp-compare-param-fromsection": "Використовувати лише вказану секцію із заданого вмісту «from».", "apihelp-compare-param-totitle": "Другий заголовок для порівняння.", "apihelp-compare-param-toid": "Другий ID сторінки для порівняння.", "apihelp-compare-param-torev": "Друга версія для порівняння.", "apihelp-compare-param-torelative": "Використати версію, яка стосується версії, визначеної через fromtitle, fromid або fromrev. Усі інші опції 'to' буде проігноровано.", - "apihelp-compare-param-totext": "Використати цей текст замість контенту версії, вказаної через totitle, toid або torev.", - "apihelp-compare-param-tosection": "Використовувати лише вказану секцію із заданого вмісту «to».", "apihelp-compare-param-topst": "Виконати трансформацію перед збереженням на totext.", + "apihelp-compare-param-totext": "Використати цей текст замість контенту версії, вказаної через totitle, toid або torev.", "apihelp-compare-param-tocontentmodel": "Контентна модель totext. Якщо не вказано, буде використано припущення на основі інших параметрів.", "apihelp-compare-param-tocontentformat": "Формат серіалізації контенту totext.", + "apihelp-compare-param-tosection": "Використовувати лише вказану секцію із заданого вмісту «to».", "apihelp-compare-param-prop": "Які уривки інформації отримати.", "apihelp-compare-paramvalue-prop-diff": "HTML різниці версій.", "apihelp-compare-paramvalue-prop-diffsize": "Розмір HTML різниці версій, у байтах.", diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index 8d618ea068..5cba292e53 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -78,20 +78,20 @@ "apihelp-compare-param-fromtitle": "要比较的第一个标题。", "apihelp-compare-param-fromid": "要比较的第一个页面 ID。", "apihelp-compare-param-fromrev": "要比较的第一个修订版本。", - "apihelp-compare-param-fromtext": "使用该文本而不是由fromtitle、fromid或fromrev指定的修订版本内容。", - "apihelp-compare-param-fromsection": "只使用指定“from”内容的指定章节。", "apihelp-compare-param-frompst": "在fromtext执行预保存转变。", + "apihelp-compare-param-fromtext": "使用该文本而不是由fromtitle、fromid或fromrev指定的修订版本内容。", "apihelp-compare-param-fromcontentmodel": "fromtext的内容模型。如果未指定,这将基于其他参数猜想。", "apihelp-compare-param-fromcontentformat": "fromtext的内容序列化格式。", + "apihelp-compare-param-fromsection": "只使用指定“from”内容的指定章节。", "apihelp-compare-param-totitle": "要比较的第二个标题。", "apihelp-compare-param-toid": "要比较的第二个页面 ID。", "apihelp-compare-param-torev": "要比较的第二个修订版本。", "apihelp-compare-param-torelative": "使用与定义自fromtitle、fromid或fromrev的修订版本相关的修订版本。所有其他“to”的选项将被忽略。", - "apihelp-compare-param-totext": "使用该文本而不是由totitle、toid或torev指定的修订版本内容。", - "apihelp-compare-param-tosection": "只使用指定“to”内容的指定章节。", "apihelp-compare-param-topst": "在totext执行预保存转换。", + "apihelp-compare-param-totext": "使用该文本而不是由totitle、toid或torev指定的修订版本内容。", "apihelp-compare-param-tocontentmodel": "totext的内容模型。如果未指定,这将基于其他参数猜想。", "apihelp-compare-param-tocontentformat": "totext的内容序列化格式。", + "apihelp-compare-param-tosection": "只使用指定“to”内容的指定章节。", "apihelp-compare-param-prop": "要获取的信息束。", "apihelp-compare-paramvalue-prop-diff": "差异HTML。", "apihelp-compare-paramvalue-prop-diffsize": "差异HTML的大小(字节)。", @@ -1050,7 +1050,7 @@ "apihelp-query+redirects-example-generator": "获取所有重定向至[[Main Page]]的信息。", "apihelp-query+revisions-summary": "获取修订版本信息。", "apihelp-query+revisions-extended-description": "可用于以下几个方面:\n# 通过设置标题或页面ID获取一批页面(最新修订)的数据。\n# 通过使用带start、end或limit的标题或页面ID获取给定页面的多个修订。\n# 通过revid设置一批修订的ID获取它们的数据。", - "apihelp-query+revisions-paraminfo-singlepageonly": "可能只能与单一页面使用(模式#2)。", + "apihelp-query+revisions-paraminfo-singlepageonly": "只能在单一页面模式中使用(模式#2)。", "apihelp-query+revisions-param-startid": "从这个修订版本时间戳开始列举。修订版本必须存在,但未必与该页面相关。", "apihelp-query+revisions-param-endid": "在这个修订版本时间戳停止列举。修订版本必须存在,但未必与该页面相关。", "apihelp-query+revisions-param-start": "从哪个修订版本时间戳开始列举。", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index 1e590c9bd7..293fac3732 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -156,6 +156,9 @@ "apihelp-help-example-query": "兩個查詢子模組的說明。", "apihelp-imagerotate-summary": "旋轉一張或多張圖片。", "apihelp-imagerotate-param-rotation": "順時針旋轉圖片的度數。", + "apihelp-imagerotate-param-tags": "在更新日誌裡套用到項目的標籤。", + "apihelp-imagerotate-example-simple": "90 度旋轉 File:Example.png。", + "apihelp-imagerotate-example-generator": "180 度旋轉所有在 Category:Flip 裡的圖片。", "apihelp-import-summary": "從其它 wiki 或 XML 檔案來匯入頁面。", "apihelp-import-param-summary": "匯入摘要。", "apihelp-import-param-xml": "上載的 XML 檔。", @@ -198,6 +201,7 @@ "apihelp-opensearch-param-suggest": "若[[mw:Special:MyLanguage/Manual:$wgEnableOpenSearchSuggest|$wgEnableOpenSearchSuggest]]設定為false,則不做任何事。", "apihelp-opensearch-param-redirects": "如何處理重定向:\n;return:傳回重定向本身。\n;resolve:傳回目標頁面,傳回的結果數目可能少於$1limit。\n由於歷史原因,$1format=json的預設值為「return」,其他格式則為「resolve」。", "apihelp-opensearch-param-format": "輸出的格式。", + "apihelp-opensearch-example-te": "找出以 Te 為開頭的頁面。", "apihelp-options-summary": "更改目前使用者的偏好設定。", "apihelp-options-param-reset": "重設偏好設定為網站預設值。", "apihelp-options-example-reset": "重設所有偏好設定", @@ -259,6 +263,10 @@ "apihelp-query+alldeletedrevisions-param-user": "此列出由該使用者作出的修訂。", "apihelp-query+alldeletedrevisions-param-excludeuser": "不要列出由該使用者作出的修訂。", "apihelp-query+alldeletedrevisions-param-namespace": "僅列出此命名空間的頁面。", + "apihelp-query+allfileusages-summary": "列出所有檔案用途,包含不存在的。", + "apihelp-query+allfileusages-param-from": "要起始列舉的檔案標題。", + "apihelp-query+allfileusages-param-to": "要終止列舉的檔案標題。", + "apihelp-query+allfileusages-param-prefix": "搜尋以此值為開頭的所有檔案標題。", "apihelp-query+allfileusages-param-prop": "要包含到的資訊部份:", "apihelp-query+allfileusages-paramvalue-prop-title": "添加檔案標題。", "apihelp-query+allfileusages-param-limit": "要回傳的項目總數。", @@ -275,6 +283,8 @@ "apihelp-query+allimages-param-sha1base36": "以 base 36 的圖片 SHA1 雜湊值(使用在 MediaWiki)。", "apihelp-query+allimages-param-mime": "所要搜尋的 MIME 類型,例如:image/jpeg。", "apihelp-query+allimages-param-limit": "要回傳的圖片總數。", + "apihelp-query+allimages-example-B": "搜尋以字母 B 為開頭的所有檔案清單。", + "apihelp-query+allimages-example-recent": "顯示近期已上傳檔案的清單,類似於 [[Special:NewFiles]]。", "apihelp-query+alllinks-param-from": "要起始列舉的連結標題。", "apihelp-query+alllinks-param-to": "要終止列舉的連結標題。", "apihelp-query+alllinks-param-prop": "要包含的資訊部份:", @@ -282,6 +292,7 @@ "apihelp-query+alllinks-param-namespace": "要列舉的命名空間。", "apihelp-query+alllinks-param-limit": "要回傳的項目總數。", "apihelp-query+alllinks-param-dir": "列出時所採用的方向。", + "apihelp-query+alllinks-example-unique": "列出唯一的連結標題。", "apihelp-query+alllinks-example-unique-generator": "取得所有已連結標題,標記為遺失。", "apihelp-query+alllinks-example-generator": "取得包含連結的頁面。", "apihelp-query+allmessages-summary": "返回來自該網站的訊息。", @@ -336,6 +347,7 @@ "apihelp-query+allusers-param-prop": "要包含的資訊部份:", "apihelp-query+allusers-paramvalue-prop-rights": "列出使用者所擁有的權限。", "apihelp-query+allusers-paramvalue-prop-editcount": "添加使用者的編輯次數。", + "apihelp-query+allusers-param-witheditsonly": "僅列出有做過編輯的使用者。", "apihelp-query+allusers-example-Y": "列出以Y開頭的使用者。", "apihelp-query+authmanagerinfo-summary": "取得目前身分核對狀態的資訊。", "apihelp-query+backlinks-summary": "找出連結至指定頁面的所有頁面。", @@ -351,13 +363,18 @@ "apihelp-query+blocks-paramvalue-prop-userid": "添加已封鎖使用者的使用者 ID。", "apihelp-query+blocks-paramvalue-prop-by": "添加進行封鎖中的使用者之使用者名稱。", "apihelp-query+blocks-paramvalue-prop-byid": "添加進行封鎖中的使用者之使用者 ID。", + "apihelp-query+blocks-paramvalue-prop-reason": "添加封鎖的原因。", "apihelp-query+blocks-example-simple": "列出封鎖。", "apihelp-query+blocks-example-users": "列出使用者 Alice 與 Bob 的封鎖。", "apihelp-query+categories-summary": "列出頁面隸屬的所有分類。", + "apihelp-query+categories-param-show": "要顯示出的分類種類。", "apihelp-query+categories-param-limit": "要回傳的分類數量。", + "apihelp-query+categories-param-dir": "列出時所採用的方向。", "apihelp-query+categoryinfo-summary": "回傳有關指定分類的資訊。", "apihelp-query+categorymembers-summary": "在指定的分類中列出所有頁面。", + "apihelp-query+categorymembers-param-prop": "要包含的資訊部份:", "apihelp-query+categorymembers-paramvalue-prop-ids": "添加頁面 ID。", + "apihelp-query+categorymembers-paramvalue-prop-title": "添加標題與頁面的命名空間 ID。", "apihelp-query+categorymembers-param-limit": "回傳的頁面數量上限。", "apihelp-query+categorymembers-param-sort": "作為排序順序的屬性。", "apihelp-query+categorymembers-param-startsortkey": "請改用 $1starthexsortkey。", @@ -374,6 +391,7 @@ "apihelp-query+deletedrevs-param-end": "終止列舉的時間戳記。", "apihelp-query+deletedrevs-param-from": "在此標題開始列出。", "apihelp-query+deletedrevs-param-to": "在此標題停止列出。", + "apihelp-query+deletedrevs-param-tag": "僅列出以此標籤所標記的修訂。", "apihelp-query+deletedrevs-param-user": "此列出由該使用者作出的修訂。", "apihelp-query+deletedrevs-param-excludeuser": "不要列出由該使用者作出的修訂。", "apihelp-query+deletedrevs-param-namespace": "僅列出此命名空間的頁面。", @@ -420,31 +438,49 @@ "apihelp-query+imageinfo-paramvalue-prop-mime": "替檔案添加 MIME 類型。", "apihelp-query+imageinfo-paramvalue-prop-mediatype": "添加檔案的媒體類型。", "apihelp-query+imageinfo-param-limit": "每個檔案要回傳的檔案修訂數量。", + "apihelp-query+imageinfo-param-start": "列出的起始時間戳記。", + "apihelp-query+imageinfo-param-end": "列出的終止時間戳記。", + "apihelp-query+imageinfo-param-urlheight": "與 $1urlwidth 相似。", "apihelp-query+images-summary": "回傳指定頁面中包含的所有檔案。", "apihelp-query+images-param-limit": "要回傳的檔案數量。", "apihelp-query+images-param-dir": "列出時所採用的方向。", "apihelp-query+images-example-simple": "取得使用在 [[Main Page]] 的檔案清單。", + "apihelp-query+imageusage-param-title": "要搜尋的標題。不能與 $1pageid 一起使用。", + "apihelp-query+imageusage-param-pageid": "要搜尋的頁面 ID。不能與 $1title 一起使用。", "apihelp-query+imageusage-param-namespace": "要列舉的命名空間。", "apihelp-query+imageusage-param-dir": "列出時所採用的方向。", "apihelp-query+info-summary": "取得基本頁面訊息。", "apihelp-query+info-param-prop": "要取得的額外屬性:", "apihelp-query+info-paramvalue-prop-protection": "列出各頁面的保護層級。", "apihelp-query+info-paramvalue-prop-readable": "使用者是否可閱讀此頁面。", + "apihelp-query+iwbacklinks-param-prefix": "跨 wiki 前綴。", + "apihelp-query+iwbacklinks-param-limit": "要回傳的頁面總數。", "apihelp-query+iwbacklinks-param-prop": "要取得的屬性。", + "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "添加跨 wiki 前綴。", + "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "添加跨 wiki 標題。", + "apihelp-query+iwbacklinks-param-dir": "列出時所採用的方向。", + "apihelp-query+iwbacklinks-example-simple": "取得連結至 [[wikibooks:Test]] 的頁面。", "apihelp-query+iwlinks-summary": "回傳指定頁面的所有 interwiki 連結。", "apihelp-query+iwlinks-paramvalue-prop-url": "添加完整的 URL。", "apihelp-query+iwlinks-param-limit": "要回傳的跨 Wiki 連結數量。", "apihelp-query+iwlinks-param-dir": "列出時所採用的方向。", + "apihelp-query+langbacklinks-param-lang": "用於語言的語言連結。", + "apihelp-query+langbacklinks-param-title": "要搜尋的語言連結。必須與$1lang一同使用。", "apihelp-query+langbacklinks-param-limit": "要回傳的頁面總數。", "apihelp-query+langbacklinks-param-prop": "要取得的屬性。", + "apihelp-query+langbacklinks-paramvalue-prop-lllang": "添加用於語言連結的語言代碼。", + "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "添加語言連結標題。", "apihelp-query+langbacklinks-param-dir": "列出時所採用的方向。", + "apihelp-query+langbacklinks-example-simple": "取得連結至 [[:fr:Test]] 的頁面。", "apihelp-query+langlinks-summary": "回傳指定頁面的所有跨語言連結。", "apihelp-query+langlinks-param-limit": "要回傳的 langlinks 數量。", "apihelp-query+langlinks-paramvalue-prop-url": "添加完整的 URL。", + "apihelp-query+langlinks-paramvalue-prop-autonym": "添加本地語言名稱。", "apihelp-query+langlinks-param-dir": "列出時所採用的方向。", "apihelp-query+langlinks-param-inlanguagecode": "用於本地化語言名稱的語言代碼。", "apihelp-query+links-summary": "回傳指定頁面的所有連結。", "apihelp-query+links-param-limit": "要回傳的連結數量。", + "apihelp-query+links-param-dir": "列出時所採用的方向。", "apihelp-query+linkshere-param-prop": "要取得的屬性。", "apihelp-query+linkshere-paramvalue-prop-pageid": "各頁面的頁面 ID。", "apihelp-query+linkshere-paramvalue-prop-title": "各頁面的標題。", @@ -453,12 +489,15 @@ "apihelp-query+linkshere-param-limit": "要回傳的數量。", "apihelp-query+logevents-summary": "從日誌中獲取事件。", "apihelp-query+logevents-param-prop": "要取得的屬性。", + "apihelp-query+logevents-paramvalue-prop-ids": "添加日誌事件的 ID。", "apihelp-query+logevents-param-start": "起始列舉的時間戳記。", "apihelp-query+logevents-param-end": "結束列舉的時間戳記。", "apihelp-query+logevents-param-limit": "要回傳的事件項目總數。", + "apihelp-query+logevents-example-simple": "列出近期日誌事件。", "apihelp-query+pagepropnames-param-limit": "回傳的名稱數量上限。", "apihelp-query+pagepropnames-example-simple": "取得前 10 個屬性名稱。", "apihelp-query+pageswithprop-paramvalue-prop-ids": "添加頁面 ID。", + "apihelp-query+pageswithprop-paramvalue-prop-value": "添加頁面屬性的值。", "apihelp-query+pageswithprop-param-limit": "回傳的頁面數量上限。", "apihelp-query+prefixsearch-param-search": "搜尋字串。", "apihelp-query+prefixsearch-param-namespace": "搜尋的命名空間。若 $1search 以有效的命名空間前綴為開頭則會被忽略。", @@ -493,6 +532,7 @@ "apihelp-query+redirects-paramvalue-prop-title": "各重新導向的標題。", "apihelp-query+redirects-param-namespace": "僅包含這些命名空間的頁面。", "apihelp-query+redirects-param-limit": "要回傳的重新導向數量。", + "apihelp-query+redirects-example-simple": "取得 [[Main Page]] 的重新導向清單", "apihelp-query+revisions-summary": "取得修訂的資訊。", "apihelp-query+revisions-example-content": "取得用於標題 API 與 Main Page 最新修訂內容的資料。", "apihelp-query+revisions-example-last5": "取得 Main Page 的最近 5 筆修訂。", @@ -501,6 +541,7 @@ "apihelp-query+revisions-example-first5-not-localhost": "取得 Main Page 裡並非由匿名使用者 127.0.0.1 所做出的最早前 5 筆修訂。", "apihelp-query+revisions-example-first5-user": "取得 Main Page 裡由使用者 MediaWiki default 所做出的最早前 5 筆修訂。", "apihelp-query+revisions+base-paramvalue-prop-ids": "修訂 ID。", + "apihelp-query+revisions+base-paramvalue-prop-user": "做出修訂的使用者。", "apihelp-query+revisions+base-paramvalue-prop-tags": "修訂標籤。", "apihelp-query+search-summary": "執行全文搜尋。", "apihelp-query+search-param-what": "要執行的搜尋類型。", @@ -512,11 +553,15 @@ "apihelp-query+search-paramvalue-prop-score": "已忽略", "apihelp-query+search-paramvalue-prop-hasrelated": "已忽略", "apihelp-query+search-param-limit": "要回傳的頁面總數。", + "apihelp-query+search-example-simple": "搜尋 meaning。", + "apihelp-query+search-example-text": "搜尋 meaning 的文字。", + "apihelp-query+siteinfo-param-prop": "要取得的資訊:", "apihelp-query+siteinfo-paramvalue-prop-general": "全面系統資訊。", "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "特殊頁面別名清單。", "apihelp-query+siteinfo-param-numberingroup": "列出在使用者群組裡的使用者數目。", "apihelp-query+siteinfo-example-simple": "索取站台資訊。", "apihelp-query+siteinfo-example-interwiki": "索取本地端跨 wiki 前綴的清單。", + "apihelp-query+siteinfo-example-replag": "檢查目前的響應延遲。", "apihelp-query+stashimageinfo-summary": "回傳多筆儲藏檔案的檔案資訊。", "apihelp-query+stashimageinfo-example-simple": "回傳儲藏檔案的檔案資訊。", "apihelp-query+tags-summary": "列出變更標記。", @@ -544,6 +589,7 @@ "apihelp-query+usercontribs-paramvalue-prop-comment": "添加編輯的註釋。", "apihelp-query+usercontribs-paramvalue-prop-parsedcomment": "添加編輯的已解析註解。", "apihelp-query+usercontribs-paramvalue-prop-size": "添加編輯的新大小。", + "apihelp-query+usercontribs-paramvalue-prop-tags": "列出編輯的標籤。", "apihelp-query+userinfo-summary": "取得目前使用者的資訊。", "apihelp-query+userinfo-param-prop": "要包含的資訊部份:", "apihelp-query+userinfo-paramvalue-prop-realname": "添加使用者的真實姓名。", @@ -555,22 +601,30 @@ "apihelp-query+users-param-prop": "要包含的資訊部份:", "apihelp-query+watchlist-param-start": "起始列舉的時間戳記。", "apihelp-query+watchlist-param-end": "結束列舉的時間戳記。", + "apihelp-query+watchlist-param-user": "此列出由該使用者作出的更改。", + "apihelp-query+watchlist-param-excludeuser": "不要列出由該使用者作出的更改。", "apihelp-query+watchlist-param-limit": "每個請求要回傳的結果總數。", + "apihelp-query+watchlist-param-prop": "要取得的額外屬性:", "apihelp-query+watchlist-paramvalue-prop-title": "添加頁面標題。", "apihelp-query+watchlist-paramvalue-prop-flags": "添加編輯的標籤。", "apihelp-query+watchlist-paramvalue-prop-tags": "列出項目的標籤。", + "apihelp-query+watchlist-paramvalue-type-edit": "一般頁面編輯。", "apihelp-query+watchlist-paramvalue-type-new": "頁面建立。", "apihelp-query+watchlist-paramvalue-type-log": "日誌項目。", "apihelp-query+watchlist-paramvalue-type-categorize": "分類成員更改。", "apihelp-query+watchlistraw-param-limit": "每個請求要回傳的結果總數。", + "apihelp-query+watchlistraw-param-prop": "要取得的額外屬性:", "apihelp-query+watchlistraw-param-dir": "列出時所採用的方向。", + "apihelp-query+watchlistraw-example-simple": "列出在目前使用者的監視清單裡頭頁面。", "apihelp-removeauthenticationdata-summary": "為目前使用者移除身分核對資料。", + "apihelp-resetpassword-summary": "寄送重新設定密碼的電子郵件給使用者。", "apihelp-revisiondelete-summary": "刪除和取消刪除修訂。", "apihelp-rollback-summary": "撤修頁面的最後一次編輯。", "apihelp-setpagelanguage-summary": "更改頁面的語言。", "apihelp-setpagelanguage-param-reason": "變更的原因。", "apihelp-stashedit-param-title": "正在編輯此頁面的標題。", "apihelp-stashedit-param-text": "頁面內容。", + "apihelp-tag-param-reason": "變更的原因。", "apihelp-tokens-summary": "取得資料修改動作的密鑰。", "apihelp-tokens-extended-description": "此模組已因支援 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 而停用。", "apihelp-unblock-summary": "解除封鎖一位使用者。", @@ -578,11 +632,15 @@ "apihelp-unblock-example-id": "解除封銷 ID #105。", "apihelp-undelete-param-title": "要恢復的頁面標題。", "apihelp-undelete-param-reason": "還原的原因。", + "apihelp-undelete-example-page": "取消刪除頁面 Main Page。", + "apihelp-undelete-example-revisions": "取消刪除 Main Page 的兩筆修訂。", "apihelp-upload-param-filename": "目標檔案名稱。", "apihelp-upload-param-comment": "上傳註釋。如果 $1text 未指定的話,也會作為新檔案用的初始頁面文字。", + "apihelp-upload-param-text": "用於新檔案的初始頁面文字。", "apihelp-upload-param-watch": "監視頁面。", "apihelp-upload-param-ignorewarnings": "忽略所有警告。", "apihelp-upload-param-file": "檔案內容。", + "apihelp-upload-param-url": "索取檔案的來源 URL。", "apihelp-upload-example-url": "從 URL 上傳。", "apihelp-userrights-summary": "變更一位使用者的群組成員。", "apihelp-userrights-param-user": "使用者名稱。", @@ -591,7 +649,10 @@ "apihelp-userrights-param-remove": "從這些群組移除使用者。", "apihelp-userrights-param-reason": "變更的原因。", "apihelp-validatepassword-param-password": "要驗證的密碼。", + "apihelp-validatepassword-param-email": "電子郵件地址,用於當測試帳號建立時使用。", + "apihelp-validatepassword-param-realname": "真實姓名,用於當測試帳號建立時使用。", "apihelp-watch-example-watch": "監視頁面 Main Page。", + "apihelp-watch-example-unwatch": "取消監視頁面 Main Page。", "apihelp-format-example-generic": "以 $1 格式傳回查詢結果。", "apihelp-json-summary": "使用 JSON 格式輸出資料。", "apihelp-jsonfm-summary": "使用 JSON 格式輸出資料 (使用 HTML 格式顯示)。", @@ -600,6 +661,7 @@ "apihelp-phpfm-summary": "使用序列化 PHP 格式輸出資料 (使用 HTML 格式顯示)。", "apihelp-rawfm-summary": "使用 JSON 格式的除錯元素輸出資料 (使用 HTML 格式顯示)。", "apihelp-xml-summary": "使用 XML 格式輸出資料。", + "apihelp-xml-param-includexmlnamespace": "若有指定,添加一個 XML 命名空間。", "apihelp-xmlfm-summary": "使用 XML 格式輸出資料 (使用 HTML 格式顯示)。", "api-format-title": "MediaWiki API 結果", "api-format-prettyprint-header": "這是$1格式的HTML呈現。HTML適合用於除錯,但不適合應用程式使用。\n\n指定format參數以更改輸出格式。要檢視$1格式的非HTML呈現,設定format=$2。\n\n參考 [[mw:Special:MyLanguage/API|完整說明文件]] 或 [[Special:ApiHelp/main|API說明]] 以取得更多資訊。", @@ -612,6 +674,7 @@ "api-help-title": "MediaWiki API 說明", "api-help-lead": "此頁為自動產生的 MediaWiki API 說明文件頁面。\n\n說明文件與範例:https://www.mediawiki.org/wiki/API", "api-help-main-header": "主要模組", + "api-help-undocumented-module": "沒有用於模組 $1 的說明文件。", "api-help-flag-deprecated": "此模組已停用。", "api-help-flag-internal": "此模組是內部的或不穩定的。它的操作可能更改而不另行通知。", "api-help-flag-readrights": "此模組需要讀取權限。", @@ -667,16 +730,23 @@ "api-help-authmanagerhelper-returnurl": "為第三方身份驗證流程傳回URL,必須為絕對值。需要此值或$1continue兩者之一。\n\n在接收REDIRECT回應時,一般狀況下您將打開瀏覽器或網站瀏覽功能到特定的redirecttarget URL以進行第三方身份驗證流程。當它完成時,第三方會將瀏覽器或網站瀏覽功能送至此URL。您應當提取任何來自URL的查詢或POST參數,並將之作為$1continue請求傳遞至此API模組。", "api-help-authmanagerhelper-continue": "此請求是在先前的UI或REDIRECT回應之後的後續動作。必須為此值或$1returnurl。", "api-help-authmanagerhelper-additional-params": "此模組允許額外參數,取決於可用的身份驗證請求。使用[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]与amirequestsfor=$1(或之前來自此模組的回應,如果合適)以決定可用請求及其使用的欄位。", + "apierror-badgenerator-unknown": "未知的 generator=$1。", "apierror-badip": "IP 參數無效。", "apierror-badmd5": "提供的 MD5 雜湊不正確。", "apierror-badquery": "無效的查詢。", + "apierror-cantblock": "您沒有權限來解封使用者。", + "apierror-cantimport": "您沒有權限來匯入頁面。", + "apierror-changeauth-norequest": "建立更改請求失敗。", + "apierror-contentserializationexception": "內容序列化失敗:$1", "apierror-copyuploadbadurl": "不允許從此 URL 來上傳。", + "apierror-csp-report": "處理 CSP 報告時錯誤:$1。", "apierror-filedoesnotexist": "檔案不存在。", "apierror-filenopath": "無法取得本地端檔案路徑。", "apierror-filetypecannotberotated": "無法旋轉的檔案類型。", "apierror-imageusage-badtitle": "$1的標題必須是檔案。", "apierror-import-unknownerror": "未知的匯入錯誤:$1", "apierror-invalidsha1hash": "所提供的 SHA1 雜湊無效。", + "apierror-invalidtitle": "錯誤標題「$1」。", "apierror-invaliduser": "無效的使用者名稱「$1」。", "apierror-invaliduserid": "使用者 ID $1 無效。", "apierror-missingparam": "$1參數必須被設定。", @@ -685,6 +755,7 @@ "apierror-mustbeloggedin-generic": "您必須登入。", "apierror-mustbeloggedin-linkaccounts": "您必須登入到連結帳號。", "apierror-mustbeloggedin-removeauth": "必須登入,才能移除身分核對資取。", + "apierror-mustbeloggedin": "您必須登入至$1。", "apierror-nodeleteablefile": "沒有這樣檔案的舊版本。", "apierror-noedit-anon": "匿名使用者不可編輯頁面。", "apierror-noedit": "您沒有權限來編輯頁面。", @@ -693,6 +764,8 @@ "apierror-permissiondenied": "您沒有權限$1。", "apierror-permissiondenied-generic": "權限不足。", "apierror-permissiondenied-unblock": "您沒有權限來解封使用者。", + "apierror-protect-invalidaction": "無效的保護類型「$1」。", + "apierror-protect-invalidlevel": "無效的保護層級「$1」。", "apierror-readapidenied": "您需要有閱讀權限來使用此模組。", "apierror-readonly": "Wiki 目前為唯讀模式。", "apierror-reauthenticate": "於本工作階段還未核對身分,請重新核對。", diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index b3286a9a29..344d0406bd 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -1,9 +1,4 @@ createDifferenceEngine( $context ); if ( get_class( $differenceEngine ) !== DifferenceEngine::class ) { // TODO turn this into a deprecation warning in a later release - LoggerFactory::getInstance( 'diff' )->notice( + LoggerFactory::getInstance( 'diff' )->info( 'Falling back to DifferenceEngineSlotDiffRenderer', [ 'modelID' => $this->getModelID(), 'DifferenceEngine' => get_class( $differenceEngine ), @@ -1129,31 +1130,52 @@ abstract class ContentHandler { * must exist and must not be deleted. * * @since 1.21 + * @since 1.32 accepts Content objects for all parameters instead of Revision objects. + * Passing Revision objects is deprecated. * - * @param Revision $current The current text - * @param Revision $undo The revision to undo - * @param Revision $undoafter Must be an earlier revision than $undo + * @param Revision|Content $current The current text + * @param Revision|Content $undo The content of the revision to undo + * @param Revision|Content $undoafter Must be from an earlier revision than $undo + * @param bool $undoIsLatest Set true if $undo is from the current revision (since 1.32) * * @return mixed Content on success, false on failure */ - public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) { - $cur_content = $current->getContent(); + public function getUndoContent( $current, $undo, $undoafter, $undoIsLatest = false ) { + Assert::parameterType( Revision::class . '|' . Content::class, $current, '$current' ); + if ( $current instanceof Content ) { + Assert::parameter( $undo instanceof Content, '$undo', + 'Must be Content when $current is Content' ); + Assert::parameter( $undoafter instanceof Content, '$undoafter', + 'Must be Content when $current is Content' ); + $cur_content = $current; + $undo_content = $undo; + $undoafter_content = $undoafter; + } else { + Assert::parameter( $undo instanceof Revision, '$undo', + 'Must be Revision when $current is Revision' ); + Assert::parameter( $undoafter instanceof Revision, '$undoafter', + 'Must be Revision when $current is Revision' ); - if ( empty( $cur_content ) ) { - return false; // no page - } + $cur_content = $current->getContent(); - $undo_content = $undo->getContent(); - $undoafter_content = $undoafter->getContent(); + if ( empty( $cur_content ) ) { + return false; // no page + } + + $undo_content = $undo->getContent(); + $undoafter_content = $undoafter->getContent(); + + if ( !$undo_content || !$undoafter_content ) { + return false; // no content to undo + } - if ( !$undo_content || !$undoafter_content ) { - return false; // no content to undo + $undoIsLatest = $current->getId() === $undo->getId(); } try { $this->checkModelID( $cur_content->getModel() ); $this->checkModelID( $undo_content->getModel() ); - if ( $current->getId() !== $undo->getId() ) { + if ( !$undoIsLatest ) { // If we are undoing the most recent revision, // its ok to revert content model changes. However // if we are undoing a revision in the middle, then diff --git a/includes/context/DerivativeContext.php b/includes/context/DerivativeContext.php index acf6fcb9fc..9817c3fc4f 100644 --- a/includes/context/DerivativeContext.php +++ b/includes/context/DerivativeContext.php @@ -296,6 +296,7 @@ class DerivativeContext extends ContextSource implements MutableContext { public function msg( $key ) { $args = func_get_args(); + // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage return wfMessage( ...$args )->setContext( $this ); } } diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 1564fab225..5f09555e7a 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -52,6 +52,9 @@ class CloneDatabase { public function __construct( IMaintainableDatabase $db, array $tablesToClone, $newTablePrefix, $oldTablePrefix = null, $dropCurrentTables = true ) { + if ( !$tablesToClone ) { + throw new InvalidArgumentException( 'Empty list of tables to clone' ); + } $this->db = $db; $this->tablesToClone = $tablesToClone; $this->newTablePrefix = $newTablePrefix; diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 49777627af..876b9bb9fa 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -81,15 +81,6 @@ class DatabaseOracle extends Database { return false; } - /** - * Usually aborts on failure - * @param string $server - * @param string $user - * @param string $password - * @param string $dbName - * @throws DBConnectionError - * @return resource|null - */ function open( $server, $user, $password, $dbName ) { global $wgDBOracleDRCP; if ( !function_exists( 'oci_connect' ) ) { @@ -173,7 +164,7 @@ class DatabaseOracle extends Database { $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); $this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' ); - return $this->conn; + return (bool)$this->conn; } /** diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 2ceda216d0..025445871a 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -57,29 +57,41 @@ class DifferenceEngine extends ContextSource { */ const DIFF_VERSION = '1.12'; - /** @var int Revision ID or 0 for current */ + /** + * Revision ID for the old revision. 0 for the revision previous to $mNewid, false + * if the diff does not have an old revision (e.g. 'oldid=&diff=prev'), + * or the revision does not exist, null if the revision is unsaved. + * @var int|false|null + */ protected $mOldid; - /** @var int|string Revision ID or null for current or an alias such as 'next' */ + /** + * Revision ID for the new revision. 0 for the last revision of the current page + * (as defined by the request context), false if the revision does not exist, null + * if it is unsaved, or an alias such as 'next'. + * @var int|string|false|null + */ protected $mNewid; - private $mOldTags; - private $mNewTags; - /** * Old revision (left pane). * Allowed to be an unsaved revision, unlikely that's ever needed though. - * Null when the old revision does not exist; this can happen when using - * diff=prev on the first revision. + * False when the old revision does not exist; this can happen when using + * diff=prev on the first revision. Null when the revision should exist but + * doesn't (e.g. load failure); loadRevisionData() will return false in that + * case. Also null until lazy-loaded. Ignored completely when isContentOverridden + * is set. * Since 1.32 public access is deprecated. - * @var Revision|null + * @var Revision|null|false */ protected $mOldRev; /** * New revision (right pane). * Note that this might be an unsaved revision (e.g. for edit preview). - * Null only in case of load failure; diff methods will just return an error message in that case. + * Null in case of load failure; diff methods will just return an error message in that case, + * and loadRevisionData() will return false. Also null until lazy-loaded. Ignored completely + * when isContentOverridden is set. * Since 1.32 public access is deprecated. * @var Revision|null */ @@ -99,6 +111,18 @@ class DifferenceEngine extends ContextSource { */ protected $mNewPage; + /** + * Change tags of $mOldRev or null if it does not exist / is not saved. + * @var string[]|null + */ + private $mOldTags; + + /** + * Change tags of $mNewRev or null if it does not exist / is not saved. + * @var string[]|null + */ + private $mNewTags; + /** * @var Content|null * @deprecated since 1.32, content slots are now handled by the corresponding SlotDiffRenderer. @@ -244,7 +268,7 @@ class DifferenceEngine extends ContextSource { /** * Get the old and new content objects for all slots. * This method does not do any permission checks. - * @return array [ role => [ 'old' => SlotRecord, 'new' => SlotRecord ], ... ] + * @return array [ role => [ 'old' => SlotRecord|null, 'new' => SlotRecord|null ], ... ] */ protected function getSlotContents() { if ( $this->isContentOverridden ) { @@ -254,16 +278,21 @@ class DifferenceEngine extends ContextSource { 'new' => $this->mNewContent, ] ]; + } elseif ( !$this->loadRevisionData() ) { + return []; } - $oldRev = $this->mOldRev->getRevisionRecord(); - $newRev = $this->mNewRev->getRevisionRecord(); + $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots(); + if ( $this->mOldRev ) { + $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots(); + } else { + $oldSlots = []; + } // The order here will determine the visual order of the diff. The current logic is - // changed first, then added, then deleted. This is ad hoc and should not be relied on - // - in the future we may want the ordering to depend on the page type. - $roles = array_merge( $newRev->getSlotRoles(), $oldRev->getSlotRoles() ); - $oldSlots = $oldRev->getSlots()->getSlots(); - $newSlots = $newRev->getSlots()->getSlots(); + // slots of the new revision first in natural order, then deleted ones. This is ad hoc + // and should not be relied on - in the future we may want the ordering to depend + // on the page type. + $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) ); $slots = []; foreach ( $roles as $role ) { @@ -311,7 +340,11 @@ class DifferenceEngine extends ContextSource { } /** - * @return int + * Get the ID of old revision (left pane) of the diff. 0 for the revision + * previous to getNewid(), false if the old revision does not exist, null + * if it's unsaved. + * To get a real revision ID instead of 0, call loadRevisionData() first. + * @return int|false|null */ public function getOldid() { $this->loadRevisionIds(); @@ -320,7 +353,10 @@ class DifferenceEngine extends ContextSource { } /** - * @return bool|int + * Get the ID of new revision (right pane) of the diff. 0 for the current revision, + * false if the new revision does not exist, null if it's unsaved. + * To get a real revision ID instead of 0, call loadRevisionData() first. + * @return int|false|null */ public function getNewid() { $this->loadRevisionIds(); @@ -1014,6 +1050,34 @@ class DifferenceEngine extends ContextSource { return $difftext; } + /** + * Get the diff table body for one slot, without header + * + * @param string $role + * @return string|false + */ + public function getDiffBodyForRole( $role ) { + $diffRenderers = $this->getSlotDiffRenderers(); + if ( !isset( $diffRenderers[$role] ) ) { + return false; + } + + $slotContents = $this->getSlotContents(); + $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'], + $slotContents[$role]['new'] ); + if ( !$slotDiff ) { + return false; + } + + if ( $role !== 'main' ) { + // TODO use human-readable role name at least + $slotTitle = $role; + $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff; + } + + return $this->localiseDiff( $slotDiff ); + } + /** * Get a slot header for inclusion in a diff body (as a table row). * @@ -1548,7 +1612,8 @@ class DifferenceEngine extends ContextSource { $this->mOldContent = $oldRevision ? $oldRevision->getContent( 'main', RevisionRecord::FOR_THIS_USER, $this->getUser() ) : null; } else { - $this->mOldRev = $this->mOldid = $this->mOldPage = null; + $this->mOldPage = null; + $this->mOldRev = $this->mOldid = false; } $this->mNewRev = new Revision( $newRevision ); $this->mNewid = $newRevision->getId(); @@ -1582,7 +1647,7 @@ class DifferenceEngine extends ContextSource { * @param int $old Revision id, e.g. from URL parameter 'oldid' * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff' * - * @return int[] List of two revision ids, older first, later second. + * @return array List of two revision ids, older first, later second. * Zero signifies invalid argument passed. * false signifies that there is no previous/next revision ($old is the oldest/newest one). */ @@ -1630,20 +1695,21 @@ class DifferenceEngine extends ContextSource { } /** - * Load revision metadata for the specified articles. If newid is 0, then compare - * the old article in oldid to the current article; if oldid is 0, then - * compare the current article to the immediately previous one (ignoring the - * value of newid). + * Load revision metadata for the specified revisions. If newid is 0, then compare + * the old revision in oldid to the current revision of the current page (as defined + * by the request context); if oldid is 0, then compare the revision in newid to the + * immediately previous one. * * If oldid is false, leave the corresponding revision object set - * to false. This is impossible via ordinary user input, and is provided for - * API convenience. + * to false. This can happen with 'diff=prev' pointing to a non-existent revision, + * and is also used directly by the API. * - * @return bool Whether both revisions were loaded successfully. + * @return bool Whether both revisions were loaded successfully. Setting mOldRev + * to false counts as successful loading. */ public function loadRevisionData() { if ( $this->mRevisionsLoaded ) { - return $this->isContentOverridden || $this->mNewRev && $this->mOldRev; + return $this->isContentOverridden || $this->mNewRev && !is_null( $this->mOldRev ); } // Whether it succeeds or fails, we don't want to try again @@ -1724,12 +1790,16 @@ class DifferenceEngine extends ContextSource { /** * Load the text of the revisions, as well as revision data. + * When the old revision is missing (mOldRev is false), loading mOldContent is not attempted. * * @return bool Whether the content of both revisions could be loaded successfully. + * (When mOldRev is false, that still counts as a success.) + * */ public function loadText() { if ( $this->mTextLoaded == 2 ) { - return $this->loadRevisionData() && $this->mOldContent && $this->mNewContent; + return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent ) + && $this->mNewContent; } // Whether it succeeds or fails, we don't want to try again @@ -1746,12 +1816,10 @@ class DifferenceEngine extends ContextSource { } } - if ( $this->mNewRev ) { - $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); - Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] ); - if ( $this->mNewContent === null ) { - return false; - } + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); + Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] ); + if ( $this->mNewContent === null ) { + return false; } return true; diff --git a/includes/diff/TextSlotDiffRenderer.php b/includes/diff/TextSlotDiffRenderer.php index baedcf05ee..9c60705587 100644 --- a/includes/diff/TextSlotDiffRenderer.php +++ b/includes/diff/TextSlotDiffRenderer.php @@ -209,7 +209,8 @@ class TextSlotDiffRenderer extends SlotDiffRenderer { $wikidiff2Version = phpversion( 'wikidiff2' ); if ( $wikidiff2Version !== false && - version_compare( $wikidiff2Version, '1.5.0', '>=' ) + version_compare( $wikidiff2Version, '1.5.0', '>=' ) && + version_compare( $wikidiff2Version, '1.8.0', '<' ) ) { $text = wikidiff2_do_diff( $oldText, @@ -218,7 +219,7 @@ class TextSlotDiffRenderer extends SlotDiffRenderer { $this->wikiDiff2MovedParagraphDetectionCutoff ); } else { - // Don't pass the 4th parameter for compatibility with older versions of wikidiff2 + // Don't pass the 4th parameter introduced in version 1.5.0 and removed in version 1.8.0 $text = wikidiff2_do_diff( $oldText, $newText, diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index 0fa0406396..43c9ee05d6 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -57,10 +57,12 @@ use Wikimedia\ObjectFactory; * 'cssclass' -- CSS class * 'csshelpclass' -- CSS class used to style help text * 'dir' -- Direction of the element. - * 'options' -- associative array mapping labels to values. + * 'options' -- associative array mapping raw text labels to values. * Some field types support multi-level arrays. + * Overwrites 'options-message'. * 'options-messages' -- associative array mapping message keys to values. * Some field types support multi-level arrays. + * Overwrites 'options' and 'options-message'. * 'options-message' -- message key or object to be parsed to extract the list of * options (like 'ipbreason-dropdown'). * 'label-message' -- message key or object for a message to use as the label. diff --git a/includes/htmlform/fields/HTMLCheckMatrix.php b/includes/htmlform/fields/HTMLCheckMatrix.php index da68a626a6..a679e45951 100644 --- a/includes/htmlform/fields/HTMLCheckMatrix.php +++ b/includes/htmlform/fields/HTMLCheckMatrix.php @@ -129,7 +129,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { $thisAttribs['class'] = 'checkmatrix-forced checkmatrix-forced-on'; } - $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs ); + $checkbox = $this->getOneCheckboxHTML( $checked, $attribs + $thisAttribs ); $rowContents .= Html::rawElement( 'td', @@ -148,24 +148,35 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { return $html; } - protected function getOneCheckbox( $checked, $attribs ) { - if ( $this->mParent instanceof OOUIHTMLForm ) { - return new OOUI\CheckboxInputWidget( [ - 'name' => "{$this->mName}[]", - 'selected' => $checked, - ] + OOUI\Element::configFromHtmlAttributes( - $attribs - ) ); - } else { - $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs ); - if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { - $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) . - $checkbox . - Html::element( 'label', [ 'for' => $attribs['id'] ] ) . - Html::closeElement( 'div' ); - } - return $checkbox; + public function getInputOOUI( $value ) { + $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] ); + + return new MediaWiki\Widget\CheckMatrixWidget( + [ + 'name' => $this->mName, + 'infusable' => true, + 'id' => $this->mID, + 'rows' => $this->mParams['rows'], + 'columns' => $this->mParams['columns'], + 'tooltips' => $this->mParams['tooltips'], + 'forcedOff' => isset( $this->mParams['force-options-off'] ) ? + $this->mParams['force-options-off'] : [], + 'forcedOn' => isset( $this->mParams['force-options-on'] ) ? + $this->mParams['force-options-on'] : [], + 'values' => $value + ] + OOUI\Element::configFromHtmlAttributes( $attribs ) + ); + } + + protected function getOneCheckboxHTML( $checked, $attribs ) { + $checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs ); + if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { + $checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) . + $checkbox . + Html::element( 'label', [ 'for' => $attribs['id'] ] ) . + Html::closeElement( 'div' ); } + return $checkbox; } protected function isTagForcedOff( $tag ) { @@ -262,4 +273,12 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { return $res; } + + protected function getOOUIModules() { + return [ 'mediawiki.widgets.CheckMatrixWidget' ]; + } + + protected function shouldInfuseOOUI() { + return true; + } } diff --git a/includes/installer/MysqlInstaller.php b/includes/installer/MysqlInstaller.php index 45f932a83b..1b0780bc8c 100644 --- a/includes/installer/MysqlInstaller.php +++ b/includes/installer/MysqlInstaller.php @@ -485,18 +485,32 @@ class MysqlInstaller extends DatabaseInstaller { /** @var Database $conn */ $conn = $status->value; $dbName = $this->getVar( 'wgDBname' ); - if ( !$conn->selectDB( $dbName ) ) { + if ( !$this->databaseExists( $dbName ) ) { $conn->query( "CREATE DATABASE " . $conn->addIdentifierQuotes( $dbName ) . "CHARACTER SET utf8", __METHOD__ ); - $conn->selectDB( $dbName ); } + $conn->selectDB( $dbName ); $this->setupSchemaVars(); return $status; } + /** + * Try to see if a given database exists + * @param string $dbName Database name to check + * @return bool + */ + private function databaseExists( $dbName ) { + $encDatabase = $this->db->addQuotes( $dbName ); + + return $this->db->query( + "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = $encDatabase", + __METHOD__ + )->numRows() > 0; + } + /** * @return Status */ diff --git a/includes/installer/i18n/pl.json b/includes/installer/i18n/pl.json index 1d4d515a56..ae4ce21127 100644 --- a/includes/installer/i18n/pl.json +++ b/includes/installer/i18n/pl.json @@ -246,9 +246,9 @@ "config-email-watchlist": "Włącz powiadomienie o zmianach stron obserwowanych", "config-email-watchlist-help": "Pozwól użytkownikom otrzymywać powiadomienia o zmianach na stronach obserwowanych, jeśli będą mieć włączoną tę funkcję w swoich preferencjach.", "config-email-auth": "Włącz uwierzytelnianie e‐mailem", - "config-email-auth-help": "Jeśli ta opcja jest włączona, użytkownicy będą musieli potwierdzić swoje adresy e-mail przy użyciu wysłanego do nich łącza, gdy będą je ustawiać lub zmieniać.\nTylko uwierzytelnione adresy e-mail mogą otrzymywać wiadomości od innych użytkowników lub mailowe powiadomienia o zmianach.\nUstawienie tej opcji jest'''zalecane''' na publicznych wiki ze względu na potencjalne nadużycia funkcji poczty e-mail.", + "config-email-auth-help": "Jeśli ta opcja jest włączona, użytkownicy będą musieli potwierdzić swoje adresy e-mail przy użyciu wysłanego do nich łącza, gdy będą je ustawiać lub zmieniać.\nTylko uwierzytelnione adresy e-mail mogą otrzymywać wiadomości od innych użytkowników lub mailowe powiadomienia o zmianach.\nUstawienie tej opcji jest zalecane na publicznych wiki ze względu na potencjalne nadużycia funkcji poczty e-mail.", "config-email-sender": "Zwrotny adres e‐mail", - "config-email-sender-help": "Wprowadź adres e-mail używany jako adres zwrotny wiadomości wychodzących.\nTo tam będą wysyłane szturchnięcia.\nWiele serwerów poczty wymaga, by co najmniej część nazwy domeny była prawidłowa.", + "config-email-sender-help": "Wprowadź adres e-mail używany jako adres zwrotny wiadomości wychodzących.\nTo tam będą wysyłane zwroty z serwerów pocztowych.\nWiele serwerów poczty wymaga, by co najmniej część nazwy domeny była prawidłowa.", "config-upload-settings": "Przesyłanie obrazków i plików", "config-upload-enable": "Włącz przesyłanie plików na serwer", "config-upload-help": "Przesyłanie plików potencjalnie wystawia serwer na zagrożenia.\nWięcej informacji na ten temat można znaleźć w [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security sekcji zabezpieczeń] podręcznika.\n\nAby włączyć przesyłanie plików, zmień właściwości podkatalogu images katalogu głównego MediaWiki tak, aby serwer sieci web mógł zapisywać do niego.\nNastępnie włącz tę opcję.", diff --git a/includes/installer/i18n/tr.json b/includes/installer/i18n/tr.json index 93d44e4c60..ada4d365c0 100644 --- a/includes/installer/i18n/tr.json +++ b/includes/installer/i18n/tr.json @@ -19,7 +19,8 @@ "Elftrkn", "Vito Genovese", "Incelemeelemani", - "Hedda" + "Hedda", + "By erdo can" ] }, "config-desc": "MediaWiki yükleyicisi", @@ -92,6 +93,7 @@ "config-using-uri": "Sunucu URLsi olarak \"$1$2\" kullanılıyor.", "config-uploads-not-safe": "Uyarı: Yüklemeler için varsayılan dizininiz $1, rastgele komut dosyalarının yürütülmesine karşı savunmasızdır.\nMediaWiki, karşıya yüklenen tüm dosyaları güvenlik tehditlerine karşı denetlese de, yüklemeleri etkinleştirmeden önce [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security bu güvenlik açığını kapatmanız] önemle tavsiye edilir.", "config-no-cli-uploads-check": "Uyarı: Yüklemeler için varsayılan dizininiz ($1), CLI yüklemesi sırasında rastgele kod yürütme güvenlik açığı açısından denetlenmez.", + "config-brokenlibxml": "Sisteminizde, \"buggy\" olan ve MediaWiki ve diğer web uygulamalarında gizli veri bozulmasına neden olabilecek PHP ve libxml2 sürümlerinin bir kombinasyonu vardır.\nLibxml2 2.7.3 veya sonraki bir sürüme yükseltin ([https://bugs.php.net/bug.php?id=45996 PHP ile dosyalanmış hata]).\nKurulum iptal edildi.", "config-db-type": "Veritabanı tipi:", "config-db-host": "Veritabanı sunucusu:", "config-db-host-help": "Veritabanı sunucunuz farklı bir sunucu üzerinde ise, ana bilgisayar adını veya IP adresini buraya girin.\n\nPaylaşılan ağ barındırma hizmeti kullanıyorsanız, barındırma sağlayıcınız size doğru bir ana bilgisayar adını kendi belgelerinde vermiştir.\n\nEğer MySQL kullanan bir Windows sunucusuna yükleme yapıyorsanız, sunucu adı olarak \"localhost\" kullanırsanız çalışmayabilir. Çalışmazsa, yerel IP adresi için \"127.0.0.1\" deneyin.\n\nPostgreSQL kullanıyorsanız, bu alanı bir Unix soketi ile bağlanmak için boş bırakın.", @@ -251,6 +253,6 @@ "config-extension-link": "Vikinizin [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions eklentileri] desteklediğini biliyor musunuz?\n\n[https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category Eklentileri kategorilerine göre] inceleyebilir ya da tüm eklentilerin listesini görmek için [https://www.mediawiki.org/wiki/Extension_Matrix Eklenti Matrisine] bakabilirsiniz.", "config-skins-screenshots": "$1 (ekran görüntüleri: $2)", "config-screenshot": "ekran görüntüsü", - "mainpagetext": "'''MediaWiki başarı ile kuruldu.'''", - "mainpagedocfooter": "Viki yazılımının kullanımı hakkında bilgi almak için [https://meta.wikimedia.org/wiki/Help:Contents kullanıcı rehberine] bakınız.\n\n== Yeni Başlayanlar ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Yapılandırma ayarlarının listesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki SSS]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-posta listesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Kendi diliniz için MediaWiki yerelleştirmesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Kendi vikinizde spam ile nasıl savaşılacağını öğrennin]" + "mainpagetext": "MediaWiki başarı ile kuruldu.", + "mainpagedocfooter": "Viki yazılımının kullanımı hakkında bilgi almak için [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents kullanıcı rehberine] bakınız.\n\n== Yeni Başlayanlar ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Yapılandırma ayarlarının listesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki SSS]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki e-posta listesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Kendi diliniz için MediaWiki yerelleştirmesi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Kendi vikinizde spam ile nasıl savaşılacağını öğrennin]" } diff --git a/includes/libs/RiffExtractor.php b/includes/libs/RiffExtractor.php index 304b99b8a4..c060380a71 100644 --- a/includes/libs/RiffExtractor.php +++ b/includes/libs/RiffExtractor.php @@ -96,4 +96,4 @@ class RiffExtractor { public static function extractUInt32( $string ) { return unpack( 'V', $string )[1]; } -}; +} diff --git a/includes/libs/filebackend/fsfile/TempFSFile.php b/includes/libs/filebackend/fsfile/TempFSFile.php index 00d2028790..321424f94b 100644 --- a/includes/libs/filebackend/fsfile/TempFSFile.php +++ b/includes/libs/filebackend/fsfile/TempFSFile.php @@ -61,7 +61,7 @@ class TempFSFile extends FSFile { if ( !is_string( $tmpDirectory ) ) { $tmpDirectory = self::getUsableTempDirectory(); } - $path = wfTempDir() . '/' . $prefix . $hex . $ext; + $path = $tmpDirectory . '/' . $prefix . $hex . $ext; Wikimedia\suppressWarnings(); $newFileHandle = fopen( $path, 'x' ); Wikimedia\restoreWarnings(); diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index 716641fcbc..3af820b883 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -27,6 +27,8 @@ use Psr\Log\NullLogger; /** * Multi-datacenter aware caching interface * + * ### Using WANObjectCache + * * All operations go to the local datacenter cache, except for delete(), * touchCheckKey(), and resetCheckKey(), which broadcast to all datacenters. * @@ -36,34 +38,63 @@ use Psr\Log\NullLogger; * The preferred way to do this logic is through getWithSetCallback(). * When querying the store on cache miss, the closest DB replica * should be used. Try to avoid heavyweight DB master or quorum reads. - * When the source data changes, a purge method should be called. - * Since purges are expensive, they should be avoided. One can do so if: - * - a) The object cached is immutable; or - * - b) Validity is checked against the source after get(); or - * - c) Using a modest TTL is reasonably correct and performant * + * To ensure consumers of the cache see new values in a timely manner, + * you either need to follow either the validation strategy, or the + * purge strategy. + * + * The validation strategy refers to the natural avoidance of stale data + * by one of the following means: + * + * - A) The cached value is immutable. + * If the consumer has access to an identifier that uniquely describes a value, + * cached value need not change. Instead, the key can change. This also allows + * all servers to access their perceived current version. This is important + * in context of multiple deployed versions of your application and/or cross-dc + * database replication, to ensure deterministic values without oscillation. + * - B) Validity is checked against the source after get(). + * This is the inverse of A. The unique identifier is embedded inside the value + * and validated after on retreival. If outdated, the value is recomputed. + * - C) The value is cached with a modest TTL (without validation). + * If value recomputation is reasonably performant, and the value is allowed to + * be stale, one should consider using TTL only – using the value's age as + * method of validation. + * + * The purge strategy refers to the the approach whereby your application knows that + * source data has changed and can react by purging the relevant cache keys. + * As purges are expensive, this strategy should be avoided if possible. * The simplest purge method is delete(). * - * There are three supported ways to handle broadcasted operations: - * - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint - * that has subscribed listeners on the cache servers applying the cache updates. - * - b) Ommit the 'purge' EventRelayer parameter and set up mcrouter as the underlying cache + * No matter which strategy you choose, callers must not rely on updates or purges + * being immediately visible to other servers. It should be treated similarly as + * one would a database replica. + * + * The need for immediate updates should be avoided. If needed, solutions must be + * sought outside WANObjectCache. + * + * ### Deploying WANObjectCache + * + * There are three supported ways to set up broadcasted operations: + * + * - A) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint + * that has subscribed listeners on the cache servers applying the cache updates. + * - B) Omit the 'purge' EventRelayer parameter and set up mcrouter as the underlying cache * backend, using a memcached BagOStuff class for the 'cache' parameter. The 'region' - * and 'cluster' parameters must be provided and 'mcrouterAware' must be set to 'true'. + * and 'cluster' parameters must be provided and 'mcrouterAware' must be set to `true`. * Configure mcrouter as follows: * - 1) Use Route Prefixing based on region (datacenter) and cache cluster. - * See https://github.com/facebook/mcrouter/wiki/Routing-Prefix and - * https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup + * See https://github.com/facebook/mcrouter/wiki/Routing-Prefix and + * https://github.com/facebook/mcrouter/wiki/Multi-cluster-broadcast-setup. * - 2) To increase the consistency of delete() and touchCheckKey() during cache - * server membership changes, you can use the OperationSelectorRoute to - * configure 'set' and 'delete' operations to go to all servers in the cache - * cluster, instead of just one server determined by hashing. - * See https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles - * - c) Ommit the 'purge' EventRelayer parameter and set up dynomite as cache middleware - * between the web servers and either memcached or redis. This will also broadcast all - * key setting operations, not just purges, which can be useful for cache warming. - * Writes are eventually consistent via the Dynamo replication model. - * See https://github.com/Netflix/dynomite + * server membership changes, you can use the OperationSelectorRoute to + * configure 'set' and 'delete' operations to go to all servers in the cache + * cluster, instead of just one server determined by hashing. + * See https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles. + * - C) Omit the 'purge' EventRelayer parameter and set up dynomite as cache middleware + * between the web servers and either memcached or redis. This will broadcast all + * key setting operations, not just purges, which can be useful for cache warming. + * Writes are eventually consistent via the Dynamo replication model. + * See https://github.com/Netflix/dynomite. * * Broadcasted operations like delete() and touchCheckKey() are done asynchronously * in all datacenters this way, though the local one should likely be near immediate. diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index eba1657f29..0de90c9d04 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -178,10 +178,6 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - public function open( $server, $user, $password, $dbName ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - public function fetchObject( $res ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index e35e0827b0..e276d09992 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -266,7 +266,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var int[] Prior flags member variable values */ private $priorFlags = []; - /** @var object|string Class name or object With profileIn/profileOut methods */ + /** @var mixed Class name or object With profileIn/profileOut methods */ protected $profiler; /** @var TransactionProfiler */ protected $trxProfiler; @@ -373,6 +373,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } + /** + * Open a new connection to the database (closing any existing one) + * + * @param string $server Database server host + * @param string $user Database user name + * @param string $password Database user password + * @param string $dbName Database name + * @return bool + * @throws DBConnectionError + */ + abstract protected function open( $server, $user, $password, $dbName ); + /** * Construct a Database subclass instance given a database type and parameters * @@ -3496,7 +3508,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware list( $phpCallback ) = $callback; $phpCallback( $this ); } catch ( Exception $ex ) { - $this->errorLogger( $ex ); + ( $this->errorLogger )( $ex ); $e = $e ?: $ex; } } @@ -4018,7 +4030,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * a wrapper. Nowadays, raw database objects are never exposed to external * callers, so this is unnecessary in external code. * - * @param bool|ResultWrapper|resource|object $result + * @param bool|ResultWrapper|resource $result * @return bool|ResultWrapper */ protected function resultObject( $result ) { diff --git a/includes/libs/rdbms/database/DatabaseMssql.php b/includes/libs/rdbms/database/DatabaseMssql.php index fed6f14616..1246e4463f 100644 --- a/includes/libs/rdbms/database/DatabaseMssql.php +++ b/includes/libs/rdbms/database/DatabaseMssql.php @@ -77,16 +77,7 @@ class DatabaseMssql extends Database { parent::__construct( $params ); } - /** - * Usually aborts on failure - * @param string $server - * @param string $user - * @param string $password - * @param string $dbName - * @throws DBConnectionError - * @return bool|resource|null - */ - public function open( $server, $user, $password, $dbName ) { + protected function open( $server, $user, $password, $dbName ) { # Test for driver support, to avoid suppressed fatal error if ( !function_exists( 'sqlsrv_connect' ) ) { throw new DBConnectionError( @@ -130,7 +121,7 @@ class DatabaseMssql extends Database { $this->opened = true; - return $this->conn; + return (bool)$this->conn; } /** @@ -243,7 +234,7 @@ class DatabaseMssql extends Database { } /** - * @param MssqlResultWrapper $res + * @param IResultWrapper $res * @return stdClass */ public function fetchObject( $res ) { @@ -252,7 +243,7 @@ class DatabaseMssql extends Database { } /** - * @param MssqlResultWrapper $res + * @param IResultWrapper $res * @return array */ public function fetchRow( $res ) { diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index 57fab54936..0f575516b4 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -120,15 +120,7 @@ abstract class DatabaseMysqlBase extends Database { return 'mysql'; } - /** - * @param string $server - * @param string $user - * @param string $password - * @param string $dbName - * @throws Exception|DBConnectionError - * @return bool - */ - public function open( $server, $user, $password, $dbName ) { + protected function open( $server, $user, $password, $dbName ) { # Close/unset connection handle $this->close(); diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index a959d72ba8..3c2f145656 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -86,7 +86,7 @@ class DatabasePostgres extends Database { return false; } - public function open( $server, $user, $password, $dbName ) { + protected function open( $server, $user, $password, $dbName ) { # Test for Postgres support, to avoid suppressed fatal error if ( !function_exists( 'pg_connect' ) ) { throw new DBConnectionError( diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php index 25fbba09e5..1b9675add6 100644 --- a/includes/libs/rdbms/database/DatabaseSqlite.php +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@ -155,24 +155,14 @@ class DatabaseSqlite extends Database { return false; } - /** Open an SQLite database and return a resource handle to it - * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases - * - * @param string $server - * @param string $user Unused - * @param string $pass - * @param string $dbName - * - * @throws DBConnectionError - * @return bool - */ - function open( $server, $user, $pass, $dbName ) { + protected function open( $server, $user, $pass, $dbName ) { $this->close(); $fileName = self::generateFileName( $this->dbDir, $dbName ); if ( !is_readable( $fileName ) ) { $this->conn = false; throw new DBConnectionError( $this, "SQLite database not accessible" ); } + // Only $dbName is used, the other parameters are irrelevant for SQLite databases $this->openFile( $fileName, $dbName ); return (bool)$this->conn; diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 7da259d9f0..f97db3a7ca 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -370,18 +370,6 @@ interface IDatabase { */ public function getType(); - /** - * Open a new connection to the database (closing any existing one) - * - * @param string $server Database server host - * @param string $user Database user name - * @param string $password Database user password - * @param string $dbName Database name - * @return bool - * @throws DBConnectionError - */ - public function open( $server, $user, $password, $dbName ); - /** * Fetch the next row from the given result object, in object form. * Fields can be retrieved with $row->fieldname, with fields acting like diff --git a/includes/libs/rdbms/lbfactory/LBFactorySingle.php b/includes/libs/rdbms/lbfactory/LBFactorySingle.php index 2c1a782a53..60044baa9c 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySingle.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySingle.php @@ -56,7 +56,11 @@ class LBFactorySingle extends LBFactory { * @since 1.28 */ public static function newFromConnection( IDatabase $db, array $params = [] ) { - return new static( [ 'connection' => $db ] + $params ); + return new static( array_merge( + [ 'localDomain' => $db->getDomainID() ], + $params, + [ 'connection' => $db ] + ) ); } /** diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index fbc3be96be..00b413017b 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -938,10 +938,6 @@ class LoadBalancer implements ILoadBalancer { $server = $this->servers[$i]; $server['serverIndex'] = $i; $server['autoCommitOnly'] = $autoCommit; - if ( $this->localDomain->getDatabase() !== null ) { - // Use the local domain table prefix if the local domain is specified - $server['tablePrefix'] = $this->localDomain->getTablePrefix(); - } $conn = $this->reallyOpenConnection( $server, $this->localDomain ); $host = $this->getServerName( $i ); if ( $conn->isOpen() ) { @@ -1037,7 +1033,6 @@ class LoadBalancer implements ILoadBalancer { $this->errorConnection = $conn; $conn = false; } else { - $conn->tablePrefix( $prefix ); // as specified // Note that if $domain is an empty string, getDomainID() might not match it $this->conns[$connInUseKey][$i][$conn->getDomainID()] = $conn; $this->connLogger->debug( __METHOD__ . ": opened new connection for $i/$domain" ); @@ -1081,20 +1076,20 @@ class LoadBalancer implements ILoadBalancer { * Returns a Database object whether or not the connection was successful. * * @param array $server - * @param DatabaseDomain $domainOverride Use an unspecified domain to not select any database + * @param DatabaseDomain $domain Domain the connection is for, possibly unspecified * @return Database * @throws DBAccessError * @throws InvalidArgumentException */ - protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) { + protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) { if ( $this->disabled ) { throw new DBAccessError(); } - // Handle $domainOverride being a specified or an unspecified domain - if ( $domainOverride->getDatabase() === null ) { - // Normally, an RDBMS requires a DB name specified on connection and the $server - // configuration array is assumed to already specify an appropriate DB name. + if ( $domain->getDatabase() === null ) { + // The database domain does not specify a DB name and some database systems require a + // valid DB specified on connection. The $server configuration array contains a default + // DB name to use for connections in such cases. if ( $server['type'] === 'mysql' ) { // For MySQL, DATABASE and SCHEMA are synonyms, connections need not specify a DB, // and the DB name in $server might not exist due to legacy reasons (the default @@ -1102,10 +1097,16 @@ class LoadBalancer implements ILoadBalancer { $server['dbname'] = null; } } else { - $server['dbname'] = $domainOverride->getDatabase(); - $server['schema'] = $domainOverride->getSchema(); + $server['dbname'] = $domain->getDatabase(); + } + + if ( $domain->getSchema() !== null ) { + $server['schema'] = $domain->getSchema(); } + // It is always possible to connect with any prefix, even the empty string + $server['tablePrefix'] = $domain->getTablePrefix(); + // Let the handle know what the cluster master is (e.g. "db1052") $masterName = $this->getServerName( $this->getWriterIndex() ); $server['clusterMasterHost'] = $masterName; diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php index 1b72502b14..5c0af119be 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php @@ -54,7 +54,8 @@ class LoadBalancerSingle extends LoadBalancer { ], 'trxProfiler' => $params['trxProfiler'] ?? null, 'srvCache' => $params['srvCache'] ?? null, - 'wanCache' => $params['wanCache'] ?? null + 'wanCache' => $params['wanCache'] ?? null, + 'localDomain' => $params['localDomain'] ?? $this->db->getDomainID() ] ); if ( isset( $params['readOnlyReason'] ) ) { @@ -69,7 +70,11 @@ class LoadBalancerSingle extends LoadBalancer { * @since 1.28 */ public static function newFromConnection( IDatabase $db, array $params = [] ) { - return new static( [ 'connection' => $db ] + $params ); + return new static( array_merge( + [ 'localDomain' => $db->getDomainID() ], + $params, + [ 'connection' => $db ] + ) ); } protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) { diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 147c9f311d..24cc8b5779 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -1961,7 +1961,7 @@ class WikiPage implements Page, IDBAccessObject { * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * - * @deprecated since 1.32, use PageUpdater::doEditUpdates instead. + * @deprecated since 1.32, use PageUpdater::doUpdates instead. * * @param Revision $revision * @param User $user User object that did the revision @@ -3152,6 +3152,9 @@ class WikiPage implements Page, IDBAccessObject { // Image redirects RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); + + // Purge cross-wiki cache entities referencing this page + self::purgeInterwikiCheckKey( $title ); } /** @@ -3190,14 +3193,41 @@ class WikiPage implements Page, IDBAccessObject { // Clear file cache for this page only HTMLFileCache::clearFileCache( $title ); + // Purge ?action=info cache $revid = $revision ? $revision->getId() : null; DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) { InfoAction::invalidateCache( $title, $revid ); } ); + + // Purge cross-wiki cache entities referencing this page + self::purgeInterwikiCheckKey( $title ); } /**#@-*/ + /** + * Purge the check key for cross-wiki cache entries referencing this page + * + * @param Title $title + */ + private static function purgeInterwikiCheckKey( Title $title ) { + global $wgEnableScaryTranscluding; + + if ( !$wgEnableScaryTranscluding ) { + return; // @todo: perhaps this wiki is only used as a *source* for content? + } + + DeferredUpdates::addCallableUpdate( function () use ( $title ) { + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $cache->resetCheckKey( + // Do not include the namespace since there can be multiple aliases to it + // due to different namespace text definitions on different wikis. This only + // means that some cache invalidations happen that are not strictly needed. + $cache->makeGlobalKey( 'interwiki-page', wfWikiID(), $title->getDBkey() ) + ); + } ); + } + /** * Returns a list of categories this page is a member of. * Results will include hidden categories diff --git a/includes/pager/IndexPager.php b/includes/pager/IndexPager.php index b00ec3a893..7ce125de1b 100644 --- a/includes/pager/IndexPager.php +++ b/includes/pager/IndexPager.php @@ -472,7 +472,7 @@ abstract class IndexPager extends ContextSource implements Pager { } if ( in_array( $type, [ 'asc', 'desc' ] ) ) { - $attrs['title'] = wfMessage( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text(); + $attrs['title'] = $this->msg( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text(); } if ( $type ) { diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php index 1f5308fbdc..d44ac8c8f0 100644 --- a/includes/parser/CoreParserFunctions.php +++ b/includes/parser/CoreParserFunctions.php @@ -98,11 +98,6 @@ class CoreParserFunctions { $args = array_slice( func_get_args(), 2 ); $message = wfMessage( $part1, $args ) ->inLanguage( $parser->getOptions()->getUserLangObj() ); - if ( !$message->exists() ) { - // When message does not exists, the message name is surrounded by angle - // and can result in a tag, therefore escape the angles - return $message->escaped(); - } return [ $message->plain(), 'noparse' => false ]; } else { return [ 'found' => false ]; diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 6bee1692c4..78265e830d 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -3783,57 +3783,68 @@ class Parser { * Transclude an interwiki link. * * @param Title $title - * @param string $action + * @param string $action Usually one of (raw, render) * * @return string */ public function interwikiTransclude( $title, $action ) { - global $wgEnableScaryTranscluding; + global $wgEnableScaryTranscluding, $wgTranscludeCacheExpiry; if ( !$wgEnableScaryTranscluding ) { return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text(); } $url = $title->getFullURL( [ 'action' => $action ] ); - - if ( strlen( $url ) > 255 ) { + if ( strlen( $url ) > 1024 ) { return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text(); } - return $this->fetchScaryTemplateMaybeFromCache( $url ); - } - /** - * @param string $url - * @return mixed|string - */ - public function fetchScaryTemplateMaybeFromCache( $url ) { - global $wgTranscludeCacheExpiry; - $dbr = wfGetDB( DB_REPLICA ); - $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry ); - $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ], - [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] ); - if ( $obj ) { - return $obj->tc_contents; - } - - $req = MWHttpRequest::factory( $url, [], __METHOD__ ); - $status = $req->execute(); // Status object - if ( $status->isOK() ) { - $text = $req->getContent(); - } elseif ( $req->getStatus() != 200 ) { + $wikiId = $title->getTransWikiID(); // remote wiki ID or false + + $fname = __METHOD__; + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + + $data = $cache->getWithSetCallback( + $cache->makeGlobalKey( + 'interwiki-transclude', + ( $wikiId !== false ) ? $wikiId : 'external', + sha1( $url ) + ), + $wgTranscludeCacheExpiry, + function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) { + $req = MWHttpRequest::factory( $url, [], $fname ); + + $status = $req->execute(); // Status object + if ( !$status->isOK() ) { + $ttl = $cache::TTL_UNCACHEABLE; + } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) { + $ttl = min( $cache::TTL_LAGGED, $ttl ); + } + + return [ + 'text' => $status->isOK() ? $req->getContent() : null, + 'code' => $req->getStatus() + ]; + }, + [ + 'checkKeys' => ( $wikiId !== false ) + ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ] + : [], + 'pcGroup' => 'interwiki-transclude:5', + 'pcTTL' => $cache::TTL_PROC_LONG + ] + ); + + if ( is_string( $data['text'] ) ) { + $text = $data['text']; + } elseif ( $data['code'] != 200 ) { // Though we failed to fetch the content, this status is useless. - return wfMessage( 'scarytranscludefailed-httpstatus' ) - ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text(); + $text = wfMessage( 'scarytranscludefailed-httpstatus' ) + ->params( $url, $data['code'] )->inContentLanguage()->text(); } else { - return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text(); + $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text(); } - $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'transcache', [ 'tc_url' ], [ - 'tc_url' => $url, - 'tc_time' => $dbw->timestamp( time() ), - 'tc_contents' => $text - ] ); return $text; } diff --git a/includes/registration/ExtensionProcessor.php b/includes/registration/ExtensionProcessor.php index bf617792a9..eb56e1365e 100644 --- a/includes/registration/ExtensionProcessor.php +++ b/includes/registration/ExtensionProcessor.php @@ -45,6 +45,7 @@ class ExtensionProcessor implements Processor { 'MediaHandlers', 'PasswordPolicy', 'RateLimits', + 'RawHtmlMessages', 'RecentChangesFlags', 'RemoveCredentialsBlacklist', 'RemoveGroups', diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 2457fe8c72..99ffcd2a94 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -388,6 +388,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { if ( $context->getDebug() ) { $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/mediawiki.log.js" ); } + if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) { + $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" ); + } $mapToJson = function ( $value ) { $value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK ); @@ -397,9 +400,23 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { }; // Perform replacements for mediawiki.js - $mwLoaderCode = strtr( $mwLoaderCode, [ + $mwLoaderPairs = [ '$VARS.baseModules' => $mapToJson( $this->getBaseModules() ), - ] ); + ]; + $profilerStubs = [ + '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );', + '$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );', + '$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );', + '$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );', + ]; + if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) { + // When profiling is enabled, insert the calls. + $mwLoaderPairs += $profilerStubs; + } else { + // When disabled (by default), insert nothing. + $mwLoaderPairs += array_fill_keys( array_keys( $profilerStubs ), '' ); + } + $mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs ); // Perform replacements for startup.js $pairs = array_map( $mapToJson, [ @@ -434,13 +451,15 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { public function getDefinitionSummary( ResourceLoaderContext $context ) { global $IP; $summary = parent::getDefinitionSummary( $context ); - $summary[] = [ - // Detect changes to variables exposed in mw.config (T30899). + $startup = [ + // getScript() exposes these variables to mw.config (T30899). 'vars' => $this->getConfigSettings( $context ), - // Changes how getScript() creates mw.Map for mw.config + // getScript() uses this to decide how configure mw.Map for mw.config. 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ), - // Detect changes to the module registrations + // Detect changes to the module registrations output by getScript(). 'moduleHashes' => $this->getAllModuleHashes( $context ), + // Detect changes to base modules listed by getScript(). + 'baseModules' => $this->getBaseModules(), 'fileHashes' => [ $this->safeFileHash( "$IP/resources/src/startup/startup.js" ), @@ -448,6 +467,13 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $this->safeFileHash( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" ), ], ]; + if ( $context->getDebug() ) { + $startup['fileHashes'][] = $this->safeFileHash( "$IP/resources/src/startup/mediawiki.log.js" ); + } + if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) { + $startup['fileHashes'][] = $this->safeFileHash( "$IP/resources/src/startup/profiling.js" ); + } + $summary[] = $startup; return $summary; } diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index c603f2f3f3..b05fb0bcd9 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -533,7 +533,7 @@ abstract class Skin extends ContextSource { $t = $embed . implode( "{$pop}{$embed}", $allCats['normal'] ) . $pop; $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped(); - $linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text(); + $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text(); $title = Title::newFromText( $linkPage ); $link = $title ? Linker::link( $title, $msg ) : $msg; $s .= '