From: jenkins-bot Date: Sun, 18 Jun 2017 19:18:39 +0000 (+0000) Subject: Merge "Improve documentation for wfParseUrl" X-Git-Tag: 1.31.0-rc.0~2952 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=447ba0260189568b5fc01a5d516e58561bd85137;hp=dc01555a3f59011ceb8a71921c8ea4729063a90e;p=lhc%2Fweb%2Fwiklou.git Merge "Improve documentation for wfParseUrl" --- diff --git a/.travis.yml b/.travis.yml index baf7f033e8..5e2c7a00db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,11 +31,6 @@ matrix: php: hhvm-3.12 - env: dbtype=mysql dbuser=root php: 7 - allow_failures: - # Postgres support for unit tests is still buggy - # https://phabricator.wikimedia.org/T75174 - - env: dbtype=postgres dbuser=travis - php: 5.5 services: - mysql diff --git a/RELEASE-NOTES-1.30 b/RELEASE-NOTES-1.30 index 343c296c81..0f5cb47a7c 100644 --- a/RELEASE-NOTES-1.30 +++ b/RELEASE-NOTES-1.30 @@ -27,6 +27,7 @@ production. ParserOptions would pollute the parser cache. Callers should use WikiPage::makeParserOptions() to create the ParserOptions object and only change options that affect the parser cache key. +* (T45547) $wgUsePigLatinVariant added (off by default). === New features in 1.30 === * (T37247) Output from Parser::parse() will now be wrapped in a div with @@ -34,11 +35,12 @@ production. ParserOptions::setWrapOutputClass(). * Added 'ChangeTagsAllowedAdd' hook, enabling extensions to allow software- specific tags to be added by users. -* File storage backends that supports headers (eg. Swift) now store an - X-Content-Dimensions header for originals that contain the media's dimensions - as page ranges keyed by dimensions. * Added a 'ParserOptionsRegister' hook to allow extensions to register additional parser options. +* (T45547) Included Pig Latin, a language game in English, as a + LanguageConverter variant. This allows English-speaking developers + to develop and test LanguageConverter more easily. Pig Latin can be + enabled by setting $wgUsePigLatinVariant to true. === Languages updated in 1.30 === @@ -79,6 +81,11 @@ changes to languages because of Phabricator reports. * … +==== Pig Latin added ==== +* (T45547) Added Pig Latin, a made-up English variant (en-x-piglatin), + for easier variant development and testing. Disabled by default. It can be + enabled by setting $wgUsePigLatinVariant to true. + === Other changes in 1.30 === * The use of an associative array for $wgProxyList, where the IP address is in the key instead of the value, is deprecated (e.g. [ '127.0.0.1' => 'value' ]). @@ -99,6 +106,14 @@ changes to languages because of Phabricator reports. or wikilinks. * (T163966) Page moves are now counted as edits for the purposes of autopromotion, i.e., they increment the user_editcount field in the database. +* Two new hooks, LogEventsListLineEnding and NewPagesLineEnding were added for + manipulating Special:Log and Special:NewPages lines. +* The OldChangesListRecentChangesLine, EnhancedChangesListModifyLineData, + PageHistoryLineEnding, ContributionsLineEnding and DeletedContributionsLineEnding + hooks have an additional parameter, for manipulating HTML data attributes of + RC/history lines. EnhancedChangesListModifyBlockLineData can do that via the + $data['attribs'] subarray. +* (T130632) The OutputPage::enableTOC() method was removed. == Compatibility == MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for diff --git a/autoload.php b/autoload.php index 0264435761..293bf6a829 100644 --- a/autoload.php +++ b/autoload.php @@ -426,6 +426,7 @@ $wgAutoloadLocalClasses = [ 'EmailNotification' => __DIR__ . '/includes/mail/EmailNotification.php', 'EmaillingJob' => __DIR__ . '/includes/jobqueue/jobs/EmaillingJob.php', 'EmptyBagOStuff' => __DIR__ . '/includes/libs/objectcache/EmptyBagOStuff.php', + 'EnConverter' => __DIR__ . '/languages/classes/LanguageEn.php', 'EncryptedPassword' => __DIR__ . '/includes/password/EncryptedPassword.php', 'EnhancedChangesList' => __DIR__ . '/includes/changes/EnhancedChangesList.php', 'EnotifNotifyJob' => __DIR__ . '/includes/jobqueue/jobs/EnotifNotifyJob.php', @@ -701,6 +702,7 @@ $wgAutoloadLocalClasses = [ 'LanguageConverter' => __DIR__ . '/languages/LanguageConverter.php', 'LanguageCu' => __DIR__ . '/languages/classes/LanguageCu.php', 'LanguageDsb' => __DIR__ . '/languages/classes/LanguageDsb.php', + 'LanguageEn' => __DIR__ . '/languages/classes/LanguageEn.php', 'LanguageEs' => __DIR__ . '/languages/classes/LanguageEs.php', 'LanguageEt' => __DIR__ . '/languages/classes/LanguageEt.php', 'LanguageFi' => __DIR__ . '/languages/classes/LanguageFi.php', @@ -979,6 +981,7 @@ $wgAutoloadLocalClasses = [ 'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php', 'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php', 'MessageContent' => __DIR__ . '/includes/content/MessageContent.php', + 'MessageLocalizer' => __DIR__ . '/languages/MessageLocalizer.php', 'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php', 'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php', 'MigrateUserGroup' => __DIR__ . '/maintenance/migrateUserGroup.php', @@ -1070,6 +1073,7 @@ $wgAutoloadLocalClasses = [ 'PackedOverlayImageGallery' => __DIR__ . '/includes/gallery/PackedOverlayImageGallery.php', 'Page' => __DIR__ . '/includes/page/Page.php', 'PageArchive' => __DIR__ . '/includes/page/PageArchive.php', + 'PageDataRequestHandler' => __DIR__ . '/includes/linkeddata/PageDataRequestHandler.php', 'PageExists' => __DIR__ . '/maintenance/pageExists.php', 'PageLangLogFormatter' => __DIR__ . '/includes/logging/PageLangLogFormatter.php', 'PageProps' => __DIR__ . '/includes/PageProps.php', @@ -1378,6 +1382,7 @@ $wgAutoloadLocalClasses = [ 'SpecialNewpages' => __DIR__ . '/includes/specials/SpecialNewpages.php', 'SpecialPage' => __DIR__ . '/includes/specialpage/SpecialPage.php', 'SpecialPageAction' => __DIR__ . '/includes/actions/SpecialPageAction.php', + 'SpecialPageData' => __DIR__ . '/includes/specials/SpecialPageData.php', 'SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php', 'SpecialPageLanguage' => __DIR__ . '/includes/specials/SpecialPageLanguage.php', 'SpecialPagesWithProp' => __DIR__ . '/includes/specials/SpecialPagesWithProp.php', @@ -1606,6 +1611,8 @@ $wgAutoloadLocalClasses = [ 'WikiRevision' => __DIR__ . '/includes/import/WikiRevision.php', 'WikiStatsOutput' => __DIR__ . '/maintenance/language/StatOutputs.php', 'WikiTextStructure' => __DIR__ . '/includes/content/WikiTextStructure.php', + 'Wikimedia\\Http\\HttpAcceptNegotiator' => __DIR__ . '/includes/libs/http/HttpAcceptNegotiator.php', + 'Wikimedia\\Http\\HttpAcceptParser' => __DIR__ . '/includes/libs/http/HttpAcceptParser.php', 'Wikimedia\\Rdbms\\Blob' => __DIR__ . '/includes/libs/rdbms/encasing/Blob.php', 'Wikimedia\\Rdbms\\ChronologyProtector' => __DIR__ . '/includes/libs/rdbms/ChronologyProtector.php', 'Wikimedia\\Rdbms\\ConnectionManager' => __DIR__ . '/includes/libs/rdbms/connectionmanager/ConnectionManager.php', diff --git a/docs/hooks.txt b/docs/hooks.txt index 0e8b50829d..3d310c3508 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1155,6 +1155,9 @@ $page: SpecialPage object for contributions &$ret: the HTML line $row: the DB row for this line &$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks': Change tool links above Special:Contributions $id: User identifier @@ -1200,6 +1203,9 @@ $page: SpecialPage object for DeletedContributions &$ret: the HTML line $row: the DB row for this line &$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText': called in DifferenceEngine::loadNewText() after the new revision's content has been loaded into the class member variable @@ -1512,6 +1518,9 @@ $changesList: EnhancedChangesList object $block: An array of RecentChange objects in that block $rc: The RecentChange object for this line &$classes: An array of classes to change +&$attribs: associative array of other HTML attributes for the element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'EnhancedChangesListModifyBlockLineData': to alter data used to build a non-grouped recent change line in EnhancedChangesList. @@ -1999,6 +2008,16 @@ $file: the File object or false if broken link &$attribs: the attributes to be applied &$ret: the value to return if your hook returns false +'LogEventsListLineEnding': Called before a Special:Log line is finished +$page: the LogEventsList object +&$ret: the HTML line +$entry: the DatabaseLogEntry object for this row +&$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). + + 'HtmlPageLinkRendererBegin': Used when generating internal and interwiki links in LinkRenderer, before processing starts. Return false to skip default @@ -2284,6 +2303,16 @@ $title: the diff page title (nullable) $old: the ?old= param value from the url $new: the ?new= param value from the url +'NewPagesLineEnding': Called before a NewPages line is finished. +$page: the SpecialNewPages object +&$ret: the HTML line +$row: the database row for this page (the recentchanges record and a few extras - see + NewPagesPager::getQueryInfo) +&$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). + 'NewRevisionFromEditComplete': Called when a revision was inserted due to an edit. $wikiPage: the WikiPage edited @@ -2296,7 +2325,10 @@ return false to omit the line from RecentChanges and Watchlist special pages. &$changeslist: The OldChangesList instance. &$s: HTML of the form "
  • ...
  • " containing one RC entry. $rc: The RecentChange object. -&$classes: array of css classes for the
  • element +&$classes: array of css classes for the
  • element. +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'OpenSearchUrls': Called when constructing the OpenSearch description XML. Hooks can alter or append to the array of URLs for search & suggestion formats. @@ -2404,6 +2436,9 @@ $historyAction: the action object &$row: the revision row for this line &$s: the string representing this parsed line &$classes: array containing the
  • element classes +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'PageHistoryPager::doBatchLookups': Called after the pager query was run, before any output is generated, to allow batch lookups for prefetching information diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 5b7ca3ecc9..00e26d9f07 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1455,6 +1455,8 @@ $wgGalleryOptions = [ 'captionLength' => true, // Show the filesize in bytes in categories 'showBytes' => true, + // Show the dimensions (width x height) in categories + 'showDimensions' => true, 'mode' => 'traditional', ]; @@ -3056,6 +3058,12 @@ $wgDisableTitleConversion = false; */ $wgDefaultLanguageVariant = false; +/** + * Whether to enable the pig latin variant of English (en-x-piglatin), + * used to ease variant development work. + */ +$wgUsePigLatinVariant = false; + /** * Disabled variants array of language variant conversion. * @@ -6767,6 +6775,12 @@ $wgUseRCPatrol = true; */ $wgStructuredChangeFiltersEnableSaving = true; +/** + * Whether to show the new experimental views (like namespaces, tags, and users) in + * RecentChanges filters + */ +$wgStructuredChangeFiltersEnableExperimentalViews = false; + /** * Use new page patrolling to check new pages on Special:Newpages */ @@ -8559,6 +8573,15 @@ $wgPopularPasswordFile = __DIR__ . '/../serialized/commonpasswords.cdb'; */ $wgMaxUserDBWriteDuration = false; +/* + * Max time (in seconds) a job-generated transaction can spend in writes. + * If exceeded, the transaction is rolled back with an error instead of being committed. + * + * @var int|bool Disabled if false + * @since 1.30 + */ +$wgMaxJobDBWriteDuration = false; + /** * Mapping of event channels (or channel categories) to EventRelayer configuration. * diff --git a/includes/EditPage.php b/includes/EditPage.php index f79a2863e7..6be8771121 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -4335,8 +4335,6 @@ HTML $buttonLabel = $this->context->msg( $this->getSaveButtonLabel() )->text(); $attribs = [ - 'id' => 'wpSaveWidget', - 'inputId' => 'wpSave', 'name' => 'wpSave', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'save' ); @@ -4344,6 +4342,8 @@ HTML if ( $this->oouiEnabled ) { $saveConfig = OOUI\Element::configFromHtmlAttributes( $attribs ); $buttons['save'] = new OOUI\ButtonInputWidget( [ + 'id' => 'wpSaveWidget', + 'inputId' => 'wpSave', // Support: IE 6 – Use , otherwise it can't distinguish which button was clicked 'useInputTag' => true, 'flags' => [ 'constructive', 'primary' ], @@ -4354,20 +4354,20 @@ HTML } else { $buttons['save'] = Html::submitButton( $buttonLabel, - $attribs, + $attribs + [ 'id' => 'wpSave' ], [ 'mw-ui-progressive' ] ); } $attribs = [ - 'id' => 'wpPreviewWidget', - 'inputId' => 'wpPreview', 'name' => 'wpPreview', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'preview' ); if ( $this->oouiEnabled ) { $previewConfig = OOUI\Element::configFromHtmlAttributes( $attribs ); $buttons['preview'] = new OOUI\ButtonInputWidget( [ + 'id' => 'wpPreviewWidget', + 'inputId' => 'wpPreview', // Support: IE 6 – Use , otherwise it can't distinguish which button was clicked 'useInputTag' => true, 'label' => $this->context->msg( 'showpreview' )->text(), @@ -4377,18 +4377,18 @@ HTML } else { $buttons['preview'] = Html::submitButton( $this->context->msg( 'showpreview' )->text(), - $attribs + $attribs + [ 'id' => 'wpPreview' ] ); } $attribs = [ - 'id' => 'wpDiffWidget', - 'inputId' => 'wpDiff', 'name' => 'wpDiff', 'tabindex' => ++$tabindex, ] + Linker::tooltipAndAccesskeyAttribs( 'diff' ); if ( $this->oouiEnabled ) { $diffConfig = OOUI\Element::configFromHtmlAttributes( $attribs ); $buttons['diff'] = new OOUI\ButtonInputWidget( [ + 'id' => 'wpDiffWidget', + 'inputId' => 'wpDiff', // Support: IE 6 – Use , otherwise it can't distinguish which button was clicked 'useInputTag' => true, 'label' => $this->context->msg( 'showdiff' )->text(), @@ -4398,7 +4398,7 @@ HTML } else { $buttons['diff'] = Html::submitButton( $this->context->msg( 'showdiff' )->text(), - $attribs + $attribs + [ 'id' => 'wpDiff' ] ); } diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index f7978c544f..089ed81d78 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -203,6 +203,38 @@ function wfArrayDiff2_cmp( $a, $b ) { } } +/** + * Like array_filter with ARRAY_FILTER_USE_BOTH, but works pre-5.6. + * + * @param array $arr + * @param callable $callback Will be called with the array value and key (in that order) and + * should return a bool which will determine whether the array element is kept. + * @return array + */ +function wfArrayFilter( array $arr, callable $callback ) { + if ( defined( 'ARRAY_FILTER_USE_BOTH' ) ) { + return array_filter( $arr, $callback, ARRAY_FILTER_USE_BOTH ); + } + $filteredKeys = array_filter( array_keys( $arr ), function ( $key ) use ( $arr, $callback ) { + return call_user_func( $callback, $arr[$key], $key ); + } ); + return array_intersect_key( $arr, array_fill_keys( $filteredKeys, true ) ); +} + +/** + * Like array_filter with ARRAY_FILTER_USE_KEY, but works pre-5.6. + * + * @param array $arr + * @param callable $callback Will be called with the array key and should return a bool which + * will determine whether the array element is kept. + * @return array + */ +function wfArrayFilterByKey( array $arr, callable $callback ) { + return wfArrayFilter( $arr, function ( $val, $key ) use ( $callback ) { + return call_user_func( $callback, $key ); + } ); +} + /** * Appends to second array if $value differs from that in $default * diff --git a/includes/Linker.php b/includes/Linker.php index bed9957f00..b133ecdbbe 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1561,7 +1561,7 @@ class Linker { $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); return '
    ' - . '

    ' . $title . "

    \n" + . '

    ' . $title . "

    \n" . $toc . "\n
    \n"; } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 2125c23499..364ed86edf 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -898,9 +898,8 @@ class MediaWiki { __METHOD__ ); - // Push lazilly-pushed jobs // Important: this must be the last deferred update added (T100085, T154425) - DeferredUpdates::addCallableUpdate( [ 'JobQueueGroup', 'pushLazyJobs' ] ); + DeferredUpdates::addCallableUpdate( [ JobQueueGroup::class, 'pushLazyJobs' ] ); // Do any deferred jobs DeferredUpdates::doUpdates( 'enqueue' ); diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index c4883ba289..8920e92f43 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -782,15 +782,12 @@ class Sanitizer { # Allow any attribute beginning with "data-" # However: - # * data-ooui is reserved for ooui - # * data-mw and data-parsoid are reserved for parsoid - # * data-mw- is reserved for extensions (or core) if - # they need to communicate some data to the client and want to be - # sure that it isn't coming from an untrusted user. + # * Disallow data attributes used by MediaWiki code # * Ensure that the attribute is not namespaced by banning # colons. - if ( !preg_match( '/^data-(?!ooui|mw|parsoid)[^:]*$/i', $attribute ) + if ( !preg_match( '/^data-[^:]*$/i', $attribute ) && !isset( $whitelist[$attribute] ) + || self::isReservedDataAttribute( $attribute ) ) { continue; } @@ -858,6 +855,24 @@ class Sanitizer { return $out; } + /** + * Given an attribute name, checks whether it is a reserved data attribute + * (such as data-mw-foo) which is unavailable to user-generated HTML so MediaWiki + * core and extension code can safely use it to communicate with frontend code. + * @param string $attr Attribute name. + * @return bool + */ + public static function isReservedDataAttribute( $attr ) { + // data-ooui is reserved for ooui. + // data-mw and data-parsoid are reserved for parsoid. + // data-mw- is reserved for extensions (or core) if + // they need to communicate some data to the client and want to be + // sure that it isn't coming from an untrusted user. + // We ignore the possibility of namespaces since user-generated HTML + // can't use them anymore. + return (bool)preg_match( '/^data-(ooui|mw|parsoid)/i', $attr ); + } + /** * Merge two sets of HTML attributes. Conflicting items in the second set * will override those in the first, except for 'class' attributes which @@ -1192,7 +1207,7 @@ class Sanitizer { ]; $id = urlencode( strtr( $id, ' ', '_' ) ); - $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); + $id = strtr( $id, $replace ); if ( !preg_match( '/^[a-zA-Z]/', $id ) && !in_array( 'noninitial', $options ) ) { // Initial character must be a letter! diff --git a/includes/Title.php b/includes/Title.php index a8cfad8748..c9f09f79e2 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1419,13 +1419,22 @@ class Title implements LinkTarget { * @return string The prefixed text */ private function prefix( $name ) { + global $wgContLang; + $p = ''; if ( $this->isExternal() ) { $p = $this->mInterwiki . ':'; } if ( 0 != $this->mNamespace ) { - $p .= $this->getNsText() . ':'; + $nsText = $this->getNsText(); + + if ( $nsText === false ) { + // See T165149. Awkward, but better than erroneously linking to the main namespace. + $nsText = $wgContLang->getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}"; + } + + $p .= $nsText . ':'; } return $p . $name; } diff --git a/includes/actions/Action.php b/includes/actions/Action.php index f06f828204..844a0d6048 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -34,7 +34,7 @@ * format (protect, delete, move, etc), and the just-do-something format (watch, rollback, * patrol, etc). The FormAction and FormlessAction classes represent these two groups. */ -abstract class Action { +abstract class Action implements MessageLocalizer { /** * Page on which we're performing the action @@ -253,7 +253,7 @@ abstract class Action { * * @return Message */ - final public function msg() { + final public function msg( $key ) { $params = func_get_args(); return call_user_func_array( [ $this->getContext(), 'msg' ], $params ); } diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index d1be7d4b1b..7460340a96 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -780,9 +780,11 @@ class HistoryPager extends ReverseChronologicalPager { $s .= ' . . ' . $s2; } - Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes ] ); + $attribs = [ 'data-mw-revid' => $rev->getId() ]; + + Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); - $attribs = []; if ( $classes ) { $attribs['class'] = implode( ' ', $classes ); } diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index a6c4b2ad19..5332d7e07f 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -1863,6 +1863,23 @@ abstract class ApiBase extends ContextSource { throw new MWException( 'Successful status passed to ApiBase::dieStatus' ); } + // ApiUsageException needs a fatal status, but this method has + // historically accepted any non-good status. Convert it if necessary. + $status->setOK( false ); + if ( !$status->getErrorsByType( 'error' ) ) { + $newStatus = Status::newGood(); + foreach ( $status->getErrorsByType( 'warning' ) as $err ) { + call_user_func_array( + [ $newStatus, 'fatal' ], + array_merge( [ $err['message'] ], $err['params'] ) + ); + } + if ( !$newStatus->getErrorsByType( 'error' ) ) { + $newStatus->fatal( 'unknownerror-nocode' ); + } + $status = $newStatus; + } + throw new ApiUsageException( $this, $status ); } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index eb23bd63ac..36247dd981 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -219,7 +219,14 @@ abstract class ApiFormatBase extends ApiBase { if ( !$this->getIsWrappedHtml() ) { // When the format without suffix 'fm' is defined, there is a non-html version if ( $this->getMain()->getModuleManager()->isDefined( $lcformat, 'format' ) ) { - $msg = $context->msg( 'api-format-prettyprint-header' )->params( $format, $lcformat ); + if ( !$this->getRequest()->wasPosted() ) { + $nonHtmlUrl = strtok( $this->getRequest()->getFullRequestURL(), '?' ) + . '?' . $this->getRequest()->appendQueryValue( 'format', $lcformat ); + $msg = $context->msg( 'api-format-prettyprint-header-hyperlinked' ) + ->params( $format, $lcformat, $nonHtmlUrl ); + } else { + $msg = $context->msg( 'api-format-prettyprint-header' )->params( $format, $lcformat ); + } } else { $msg = $context->msg( 'api-format-prettyprint-header-only-html' )->params( $format ); } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index dc1b4e73af..ff65d0e29d 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -25,7 +25,6 @@ */ use MediaWiki\MediaWikiServices; -use Wikimedia\Rdbms\Database; /** * @ingroup API diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 91e49abc9c..b2e03c80ee 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -38,6 +38,9 @@ class ApiParse extends ApiBase { /** @var Content $pstContent */ private $pstContent = null; + /** @var bool */ + private $contentIsDeleted = false, $contentIsSuppressed = false; + public function execute() { // The data is hot but user-dependent, like page views, so we set vary cookies $this->getMain()->setCacheMode( 'anon-public-user-private' ); @@ -85,6 +88,9 @@ class ApiParse extends ApiBase { $redirValues = null; + $needContent = isset( $prop['wikitext'] ) || + isset( $prop['parsetree'] ) || $params['generatexml']; + // Return result $result = $this->getResult(); @@ -110,27 +116,9 @@ class ApiParse extends ApiBase { $wgTitle = $titleObj; $pageObj = WikiPage::factory( $titleObj ); list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params ); - - // If for some reason the "oldid" is actually the current revision, it may be cached - // Deliberately comparing $pageObj->getLatest() with $rev->getId(), rather than - // checking $rev->isCurrent(), because $pageObj is what actually ends up being used, - // and if its ->getLatest() is outdated, $rev->isCurrent() won't tell us that. - if ( !$suppressCache && $rev->getId() == $pageObj->getLatest() ) { - // May get from/save to parser cache - $p_result = $this->getParsedContent( $pageObj, $popts, - $pageid, isset( $prop['wikitext'] ) ); - } else { // This is an old revision, so get the text differently - $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); - - if ( $this->section !== false ) { - $this->content = $this->getSectionContent( - $this->content, $this->msg( 'revid', $rev->getId() ) - ); - } - - // Should we save old revision parses to the parser cache? - $p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts ); - } + $p_result = $this->getParsedContent( + $pageObj, $popts, $suppressCache, $pageid, $rev, $needContent + ); } else { // Not $oldid, but $pageid or $page if ( $params['redirects'] ) { $reqParams = [ @@ -172,25 +160,9 @@ class ApiParse extends ApiBase { } list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params ); - - // Don't pollute the parser cache when setting options that aren't - // in ParserOptions::optionsHash() - /// @todo: This should be handled closer to the actual cache instead of here, see T110269 - $suppressCache = $suppressCache || - $params['disablepp'] || - $params['disablelimitreport'] || - $params['preview'] || - $params['sectionpreview'] || - $params['disabletidy']; - - if ( $suppressCache ) { - $this->content = $this->getContent( $pageObj, $pageid ); - $p_result = $this->content->getParserOutput( $titleObj, null, $popts ); - } else { - // Potentially cached - $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, - isset( $prop['wikitext'] ) ); - } + $p_result = $this->getParsedContent( + $pageObj, $popts, $suppressCache, $pageid, null, $needContent + ); } } else { // Not $oldid, $pageid, $page. Hence based on $text $titleObj = Title::newFromText( $title ); @@ -249,6 +221,12 @@ class ApiParse extends ApiBase { if ( $params['onlypst'] ) { // Build a result and bail out $result_array = []; + if ( $this->contentIsDeleted ) { + $result_array['textdeleted'] = true; + } + if ( $this->contentIsSuppressed ) { + $result_array['textsuppressed'] = true; + } $result_array['text'] = $this->pstContent->serialize( $format ); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; if ( isset( $prop['wikitext'] ) ) { @@ -279,6 +257,12 @@ class ApiParse extends ApiBase { $result_array['title'] = $titleObj->getPrefixedText(); $result_array['pageid'] = $pageid ?: $pageObj->getId(); + if ( $this->contentIsDeleted ) { + $result_array['textdeleted'] = true; + } + if ( $this->contentIsSuppressed ) { + $result_array['textsuppressed'] = true; + } if ( $params['disabletoc'] ) { $p_result->setTOCEnabled( false ); @@ -541,64 +525,72 @@ class ApiParse extends ApiBase { Hooks::run( 'ApiMakeParserOptions', [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] ); + // Force cache suppression when $popts aren't cacheable. + $suppressCache = $suppressCache || !$popts->isSafeToCache(); + return [ $popts, $reset, $suppressCache ]; } /** * @param WikiPage $page * @param ParserOptions $popts + * @param bool $suppressCache * @param int $pageId - * @param bool $getWikitext + * @param Revision|null $rev + * @param bool $getContent * @return ParserOutput */ - private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) { - $this->content = $this->getContent( $page, $pageId ); + private function getParsedContent( + WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent + ) { + $revId = $rev ? $rev->getId() : null; + $isDeleted = $rev && $rev->isDeleted( Revision::DELETED_TEXT ); + + if ( $getContent || $this->section !== false || $isDeleted ) { + if ( $rev ) { + $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); + if ( !$this->content ) { + $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] ); + } + } else { + $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() ); + if ( !$this->content ) { + $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] ); + } + } + $this->contentIsDeleted = $isDeleted; + $this->contentIsSuppressed = $rev && + $rev->isDeleted( Revision::DELETED_TEXT | Revision::DELETED_RESTRICTED ); + } - if ( $this->section !== false && $this->content !== null ) { - // Not cached (save or load) - return $this->content->getParserOutput( $page->getTitle(), null, $popts ); + if ( $this->section !== false ) { + $this->content = $this->getSectionContent( + $this->content, + $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId ) + ); + return $this->content->getParserOutput( $page->getTitle(), $revId, $popts ); } - // Try the parser cache first - // getParserOutput will save to Parser cache if able - $pout = $page->getParserOutput( $popts ); - if ( !$pout ) { - $this->dieWithError( [ 'apierror-nosuchrevid', $page->getLatest() ] ); + if ( $isDeleted ) { + // getParserOutput can't do revdeled revisions + $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts ); + } else { + // getParserOutput will save to Parser cache if able + $pout = $page->getParserOutput( $popts, $revId, $suppressCache ); } - if ( $getWikitext ) { - $this->content = $page->getContent( Revision::RAW ); + if ( !$pout ) { + $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); } return $pout; } - /** - * Get the content for the given page and the requested section. - * - * @param WikiPage $page - * @param int $pageId - * @return Content - */ - private function getContent( WikiPage $page, $pageId = null ) { - $content = $page->getContent( Revision::RAW ); // XXX: really raw? - - if ( $this->section !== false && $content !== null ) { - $content = $this->getSectionContent( - $content, - !is_null( $pageId ) - ? $this->msg( 'pageid', $pageId ) - : $page->getTitle()->getPrefixedText() - ); - } - return $content; - } - /** * Extract the requested section from the given Content * * @param Content $content * @param string|Message $what Identifies the content in error messages, e.g. page title. - * @return Content|bool + * @return Content */ private function getSectionContent( Content $content, $what ) { // Not cached (save or load) diff --git a/includes/api/i18n/cs.json b/includes/api/i18n/cs.json index a6f1a28cc4..164a7c2292 100644 --- a/includes/api/i18n/cs.json +++ b/includes/api/i18n/cs.json @@ -10,7 +10,8 @@ "Macofe", "Danny B.", "LordMsz", - "Dvorapa" + "Dvorapa", + "Matěj Suchánek" ] }, "apihelp-main-description": "
    \n* [[mw:Special:MyLanguage/API:Main_page|Dokumentace]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-mailová konference]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Oznámení k API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Chyby a požadavky]\n
    \nStav: Všechny funkce uvedené na této stránce by měly fungovat, ale API se stále aktivně vyvíjí a může se kdykoli změnit. Upozornění na změny získáte přihlášením se k [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-mailové konferenci mediawiki-api-announce].\n\nChybné požadavky: Pokud jsou do API zaslány chybné požadavky, bude vrácena HTTP hlavička s klíčem „MediaWiki-API-Error“ a hodnota této hlavičky a chybový kód budou nastaveny na stejnou hodnotu. Více informací najdete [[mw:Special:MyLanguage/API:Errors_and_warnings|v dokumentaci]].\n\nTestování: Pro jednoduché testování požadavků na API zkuste [[Special:ApiSandbox]].", @@ -80,7 +81,7 @@ "apihelp-edit-param-nocreate": "Pokud stránka neexistuje, vrátit chybu.", "apihelp-edit-param-watch": "Přidat stránku na seznam sledovaných.", "apihelp-edit-param-unwatch": "Odstranit stránku ze seznamu sledovaných.", - "apihelp-edit-param-watchlist": "Bezpodmíněnečně přidat nebo odstranit stránku ze sledovaných stránek aktuálního uživatele, použít nastavení nebo neměnit sledování.", + "apihelp-edit-param-watchlist": "Bezpodmínečně přidat nebo odstranit stránku ze sledovaných stránek aktuálního uživatele, použít nastavení nebo neměnit sledování.", "apihelp-edit-param-redirect": "Automaticky opravit přesměrování.", "apihelp-edit-example-edit": "Upravit stránku.", "apihelp-emailuser-description": "Poslat uživateli e-mail.", diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index 264c9d2c38..f15e55d9ea 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -71,6 +71,7 @@ "apihelp-compare-param-totitle": "Zweiter zu vergleichender Titel.", "apihelp-compare-param-toid": "Zweite zu vergleichende Seitennummer.", "apihelp-compare-param-torev": "Zweite zu vergleichende Version.", + "apihelp-compare-paramvalue-prop-title": "Die Seitentitel der Versionen „Von“ und „Nach“.", "apihelp-compare-example-1": "Unterschied zwischen Version 1 und 2 abrufen", "apihelp-createaccount-description": "Erstellen eines neuen Benutzerkontos.", "apihelp-createaccount-param-preservestate": "Falls [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] für hasprimarypreservedstate wahr ausgegeben hat, sollten Anfragen, die als primary-required markiert wurden, ausgelassen werden. Falls ein nicht-leerer Wert für preservedusername zurückgegeben wurde, muss dieser Benutzername für den Parameter username verwendet werden.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index ed3f25f3d9..5554105804 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1513,6 +1513,7 @@ "api-format-title": "MediaWiki API result", "api-format-prettyprint-header": "This is the HTML representation of the $1 format. HTML is good for debugging, but is unsuitable for application use.\n\nSpecify the format parameter to change the output format. To see the non-HTML representation of the $1 format, set format=$2.\n\nSee the [[mw:Special:MyLanguage/API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", "api-format-prettyprint-header-only-html": "This is an HTML representation intended for debugging, and is unsuitable for application use.\n\nSee the [[mw:Special:MyLanguage/API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", + "api-format-prettyprint-header-hyperlinked": "This is the HTML representation of the $1 format. HTML is good for debugging, but is unsuitable for application use.\n\nSpecify the format parameter to change the output format. To see the non-HTML representation of the $1 format, set [$3 format=$2].\n\nSee the [[mw:API|complete documentation]], or the [[Special:ApiHelp/main|API help]] for more information.", "api-format-prettyprint-status": "This response would be returned with HTTP status $1 $2.", "api-login-fail-aborted": "Authentication requires user interaction, which is not supported by action=login. To be able to login with action=login, see [[Special:BotPasswords]]. To continue using main-account login, see [[Special:ApiHelp/clientlogin|action=clientlogin]].", diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index c7bcf02ac2..e6414b1ddc 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -63,6 +63,7 @@ "apihelp-compare-param-fromtitle": "Primeiro título para comparar.", "apihelp-compare-param-fromid": "Identificador da primeira páxina a comparar.", "apihelp-compare-param-fromrev": "Primeira revisión a comparar.", + "apihelp-compare-param-fromtext": "Uso este texto en vez do contido da revisión especificada por fromtitle, fromid ou fromrev.", "apihelp-compare-param-totitle": "Segundo título para comparar.", "apihelp-compare-param-toid": "Identificador da segunda páxina a comparar.", "apihelp-compare-param-torev": "Segunda revisión a comparar.", diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index b6a5b09beb..74d83e62e7 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -67,9 +67,28 @@ "apihelp-compare-param-fromtitle": "כותרת ראשונה להשוואה.", "apihelp-compare-param-fromid": "מס׳ זיהוי של העמוד הראשון להשוואה.", "apihelp-compare-param-fromrev": "גרסה ראשונה להשוואה.", + "apihelp-compare-param-fromtext": "להשתמש בטקסט הזה במקום תוכן הגרסה שהוגדרה על־ידי fromtitle, fromid או fromrev.", + "apihelp-compare-param-frompst": "לעשות התמרה לפני שמירה ב־fromtext.", + "apihelp-compare-param-fromcontentmodel": "מודל התוכן של fromtext. אם זה לא סופק, ייעשה ניחוש על סמך פרמטרים אחרים.", + "apihelp-compare-param-fromcontentformat": "תסדיר הסדרת תוכן של fromtext.", "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-topst": "לעשות התמרה לפני שמירה ב־totext.", + "apihelp-compare-param-tocontentmodel": "מודל התוכן של totext. אם זה לא סופק, ייעשה ניחוש על סמך פרמטרים אחרים.", + "apihelp-compare-param-tocontentformat": "תסדיר הסדרת תוכן של fromtext.", + "apihelp-compare-param-prop": "אילו פריטי מידע לקבל.", + "apihelp-compare-paramvalue-prop-diff": "ה־HTML של ההשוואה.", + "apihelp-compare-paramvalue-prop-diffsize": "גודל ה־HTML של ההשוואה, בבתים.", + "apihelp-compare-paramvalue-prop-rel": "מזהי הגרסאות של הגרסאות לפני \"from\" ואחרי \"to\", אם יש כאלה.", + "apihelp-compare-paramvalue-prop-ids": "מזהי הדף והגרסה של גרסאות ה־\"from\" וה־\"to\".", + "apihelp-compare-paramvalue-prop-title": "כותרות הדפים של גרסאות ה־\"from\" וה־\"to\".", + "apihelp-compare-paramvalue-prop-user": "השם והמזהה של המשתמש של גרסאות ה־\"from\" וה־\"to\".", + "apihelp-compare-paramvalue-prop-comment": "התקציר על גרסאות ה־\"from\" וה־\"to\".", + "apihelp-compare-paramvalue-prop-parsedcomment": "התקציר המפוענח על גרסאות ה־\"from\" וה־\"to\".", + "apihelp-compare-paramvalue-prop-size": "הגודל של גרסאות ה־\"from\" וה־\"to\".", "apihelp-compare-example-1": "יצירת תיעוד שינוי בין גרסה 1 ל־2.", "apihelp-createaccount-description": "יצירת חשבון משתמש חדש.", "apihelp-createaccount-param-preservestate": "אם [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] החזיר true עבור hasprimarypreservedstate, בקשות שמסומנות בתור primary-required אמורות להיות מושמטות. אם מוחזר ערך לא ריק ל־preservedusername, שם המשתמש הזה ישמש לפרמטר username.", @@ -1509,7 +1528,8 @@ "apierror-changeauth-norequest": "יצירת בקשת השינוי נכשלה.", "apierror-chunk-too-small": "גודל הפלח המזערי הוא {{PLURAL:$1|בית אחד|$1 בתים}} בשביל פלחים לא סופיים.", "apierror-cidrtoobroad": "טווחי CIDR של $1 שרחבים יותר מ־/$2 אינם קבילים.", - "apierror-compare-inputneeded": "כותרת, מזהה דף, או מספר גרסה נחוצים בשביל הפרמטרים from ו־to.", + "apierror-compare-no-title": "לא ניתן לעשות התמרה לפני שמירה ללא כותרת. נא לנסות לציין fromtitle או totitle.", + "apierror-compare-relative-to-nothing": "אין גרסת \"from\" עבור torelative שתהיה יחסית.", "apierror-contentserializationexception": "הסדרת התוכן נכשלה: $1", "apierror-contenttoobig": "התוכן שסיפקת חורג מגודל הערך המרבי של {{PLURAL:$1|קילובייט אחד|$1 קילובייטים}}.", "apierror-copyuploadbaddomain": "העלאות לפי URL אינם מורשות מהמתחם הזה.", @@ -1553,10 +1573,12 @@ "apierror-maxlag": "ממתין ל־$2: שיהוי של {{PLURAL:$1|שנייה אחת|$1 שניות}}.", "apierror-mimesearchdisabled": "חיפוש MIME כבוי במצב קמצן.", "apierror-missingcontent-pageid": "תוכן חסר עבור מזהה הדף $1.", + "apierror-missingcontent-revid": "תוכן חסר עבור מזהה הגרסה $1.", "apierror-missingparam-at-least-one-of": "דרוש {{PLURAL:$2|הפרמטר|לפחות אחד מהפרמטרים}} $1.", "apierror-missingparam-one-of": "דרוש {{PLURAL:$2|הפרמטר|אחד מהפרמטרים}} $1.", "apierror-missingparam": "הפרמטר $1 צריך להיות מוגדר.", "apierror-missingrev-pageid": "אין גרסה נוכחית של דף עם המזהה $1.", + "apierror-missingrev-title": "אין גרסה נוכחית לכותרת $1.", "apierror-missingtitle-createonly": "כותרות חסרות יכולות להיות מוגנות עם create.", "apierror-missingtitle": "הדף שנתת אינו קיים.", "apierror-missingtitle-byname": "הדף $1 אינו קיים.", @@ -1658,6 +1680,7 @@ "apiwarn-badurlparam": "לא היה אפשר לפענח את $1urlparam עבור $2. משתמשים רק ב־width ו־height.", "apiwarn-badutf8": "הערך הערך שהועבר ל־$1 מכיל נתונים בלתי־תקינים או בלתי־מנורמלים. נתונים טקסט אמורים להיות תקינים, מנורמלי NFC ללא תווי בקרה C0 למעט HT (\\t)‏, LF (\\n), ו־CR (\\r).", "apiwarn-checktoken-percentencoding": "נא לבדוק שסימנים כמו \"+\" באסימון מקודדים עם אחוזים בצורה נכונה ב־URL.", + "apiwarn-compare-nocontentmodel": "לא היה אפשר לקבוע את מודל התוכן, נניח שזה $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs הוצהר בתור מיושן. נא להשתמש ב־ prop=deletedrevisions או ב־list=alldeletedrevisions במקום זה.", "apiwarn-deprecation-expandtemplates-prop": "מכיוון שלא ניתנו ערכים לפרמטר prop, תסדיר מיושן ישמש לפלט. התסדיר הזה מיושן, ובעתיד יינתן ערך בררת מחדל לפרמטר prop, כך שתמיד ישמש התסדיר החדש.", "apiwarn-deprecation-httpsexpected": "משמש HTTP כשהיה צפוי HTTPS.", diff --git a/includes/api/i18n/hu.json b/includes/api/i18n/hu.json index 5ae31c1ef0..5d997bed70 100644 --- a/includes/api/i18n/hu.json +++ b/includes/api/i18n/hu.json @@ -1083,6 +1083,36 @@ "apihelp-resetpassword-param-email": "A visszaállítandó felhasználó e-mail-címe.", "apihelp-resetpassword-example-user": "Jelszó-visszaállító e-mail küldése Example felhasználónak.", "apihelp-resetpassword-example-email": "Jelszó-visszaállító e-mail küldése az összes user@example.com e-mail-című felhasználónak.", + "apihelp-revisiondelete-description": "Változatok törlése és helyreállítása.", + "apihelp-revisiondelete-param-ids": "A törlendő lapváltozatok azonosítói.", + "apihelp-revisiondelete-param-reason": "A törlés vagy helyreállítás indoklása.", + "apihelp-revisiondelete-example-revision": "A 12345 lapváltozat tartalmának elrejtése a Main Page lapon.", + "apihelp-revisiondelete-example-log": "A 67890 naplóbejegyzés összes adatának elrejtése BLP violation indoklással.", + "apihelp-rollback-description": "A lap legutóbbi változtatásának visszavonása.\n\nHa a lap utolsó szerkesztője egymás után több szerkesztést végzett, az összes visszavonása.", + "apihelp-rollback-param-title": "A visszaállítandó lap címe. Nem használható együtt a $1pageid paraméterrel.", + "apihelp-rollback-param-pageid": "A visszaállítandó lap lapazonosítója. Nem használható együtt a $1title paraméterrel.", + "apihelp-rollback-param-summary": "Egyéni szerkesztési összefoglaló. Ha üres, az alapértelmezett összefoglaló lesz használatban.", + "apihelp-rollback-param-markbot": "A visszavont és a visszavonó szerkesztések botszerkesztésnek jelölése.", + "apihelp-rollback-param-watchlist": "A lap hozzáadása a figyelőlistához vagy eltávolítása onnan feltétel nélkül, a beállítások használata vagy a figyelőlista érintetlenül hagyása.", + "apihelp-rsd-description": "Egy RSD-séma (Really Simple Discovery) exportálása.", + "apihelp-rsd-example-simple": "Az RSD-séma exportálása.", + "apihelp-setnotificationtimestamp-description": "A figyelt lapok értesítési időbélyegének frissítése.\n\nEz érinti a módosított lapok kiemelését a figyelőlistán és a laptörténetekben, valamint az e-mail-küldést a „{{int:tog-enotifwatchlistpages}}” beállítás engedélyezése esetén.", + "apihelp-setnotificationtimestamp-param-entirewatchlist": "Dolgozás az összes figyelt lapon.", + "apihelp-setnotificationtimestamp-param-timestamp": "Az értesítési időbélyeg állítása erre az időbélyegre.", + "apihelp-setnotificationtimestamp-param-torevid": "Az értesítési időbélyeg állítása erre a lapváltozatra (csak egy lap esetén).", + "apihelp-setnotificationtimestamp-param-newerthanrevid": "Az értesítési időbélyeg állítása ennél a lapváltozatnál újabbra (csak egy lap esetén).", + "apihelp-setnotificationtimestamp-example-all": "Az értesítési állapot visszaállítása a teljes figyelőlistára.", + "apihelp-setnotificationtimestamp-example-page": "A Main page értesítési állapotának visszaállítása.", + "apihelp-setnotificationtimestamp-example-pagetimestamp": "A Main page értesítési időbélyegének módosítása, hogy a 2012. január 1-jét követő szerkesztések nem megtekintettek legyenek.", + "apihelp-setnotificationtimestamp-example-allpages": "A {{ns:user}} névtérbeli lapok értesítési állapotának visszaállítása.", + "apihelp-setpagelanguage-description": "Egy lap nyelvének módosítása.", + "apihelp-setpagelanguage-description-disabled": "A lapnyelv módosítása nem engedélyezett ezen a wikin.\n\nEngedélyezd a [[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]] PHP-változót ezen művelet használatához.", + "apihelp-setpagelanguage-param-title": "A módosítandó lap címe. Nem használható együtt a $1pageid paraméterrel.", + "apihelp-setpagelanguage-param-pageid": "A módosítandó lap azonosítója. Nem használható együtt a $1title paraméterrel.", + "apihelp-setpagelanguage-param-lang": "A lap nyelvének módosítása erre a nyelvkódra. Használd a default értéket a wiki alapértelmezett tartalomnyelvére való visszaállításhoz.", + "apihelp-setpagelanguage-param-reason": "A módosítás oka.", + "apihelp-setpagelanguage-example-language": "A Main Page nyelvének módosítása baszkra.", + "apihelp-setpagelanguage-example-default": "A 123 azonosítójú lap nyelvének módosítása a wiki alapértelmezett tartalomnyelvére.", "apihelp-userrights-param-userid": "Felhasználói azonosító.", "api-help-title": "MediaWiki API súgó", "api-help-lead": "Ez egy automatikusan generált MediaWiki API-dokumentációs lap.\n\nDokumentáció és példák: https://www.mediawiki.org/wiki/API", diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index 06a5aeeca5..4458f36851 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -526,6 +526,9 @@ "apihelp-rollback-example-simple": "Project:대문 문서의 예시의 마지막 판을 되돌리기", "apihelp-setpagelanguage-description": "문서의 언어를 변경합니다.", "apihelp-setpagelanguage-description-disabled": "이 위키에서 문서의 언어 변경은 허용되지 않습니다.\n\n이 동작을 사용하려면 [[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]을 활성화하십시오.", + "apihelp-setpagelanguage-param-title": "언어를 변경하려는 문서의 제목입니다. $1pageid와 함께 사용할 수 없습니다.", + "apihelp-setpagelanguage-param-pageid": "언어를 변경하려는 문서의 ID입니다. $1title과 함께 사용할 수 없습니다.", + "apihelp-setpagelanguage-param-lang": "문서를 변경할 언어의 언어 코드입니다. 문서를 위키의 기본 콘텐츠 언어로 재설정하려면 default를 사용하십시오.", "apihelp-setpagelanguage-param-reason": "변경 이유.", "apihelp-setpagelanguage-example-language": "Main Page의 언어를 바스크어로 변경합니다.", "apihelp-stashedit-param-sectiontitle": "새 문단을 위한 제목.", @@ -555,6 +558,7 @@ "apihelp-undelete-example-revisions": "대문 문서의 두 판을 복구합니다.", "apihelp-unlinkaccount-description": "현재 사용자에 연결된 타사 계정을 제거합니다.", "apihelp-unlinkaccount-example-simple": "FooAuthenticationRequest와 연결된 제공자에 대한 현재 사용자의 토론 링크 제거를 시도합니다.", + "apihelp-upload-description": "파일을 업로드하거나 대기 중인 업로드 상태를 가져옵니다.\n\n몇 가지 방식을 사용할 수 있습니다:\n* $1file 변수를 사용하여 파일의 내용을 직접 업로드합니다.\n* $1filesize, $1chunk, $1offset 변수를 사용하여 파일을 부분적으로 업로드합니다.\n* $1url 변수를 사용하여 미디어위키 서버가 URL로부터 파일을 가져오게 합니다.\n* $1filekey 변수를 사용하여 경고로 실패한 과거의 업로드를 완료합니다.\n$1file을(를) 보낼 때 HTTP POST는 파일 업로드로 끝나야 합니다. (예: multipart/form-data를 사용하여)", "apihelp-upload-param-filename": "대상 파일 이름.", "apihelp-upload-param-comment": "업로드 주석입니다. 또, $1text가 지정되지 않은 경우 새로운 파일들의 초기 페이지 텍스트로 사용됩니다.", "apihelp-upload-param-tags": "업로드 기록 항목과 파일 문서 판에 적용할 태그를 변경합니다.", diff --git a/includes/api/i18n/lt.json b/includes/api/i18n/lt.json index 0935c59ce7..f923e1869b 100644 --- a/includes/api/i18n/lt.json +++ b/includes/api/i18n/lt.json @@ -14,6 +14,7 @@ "apihelp-compare-param-fromid": "Pirmojo lyginamo puslapio ID.", "apihelp-compare-param-totitle": "Antrasis pavadinimas palyginimui.", "apihelp-compare-param-toid": "Antrojo lyginamo puslapio ID.", + "apihelp-compare-param-prop": "Kokią informaciją gauti.", "apihelp-createaccount-description": "Kurti naują vartotojo paskyrą.", "apihelp-createaccount-param-name": "Naudotojo vardas.", "apihelp-createaccount-param-email": "Vartotojo el. pašto adresas (nebūtina).", diff --git a/includes/api/i18n/nb.json b/includes/api/i18n/nb.json index f2705ba673..72a5fda8d0 100644 --- a/includes/api/i18n/nb.json +++ b/includes/api/i18n/nb.json @@ -24,9 +24,11 @@ "apihelp-main-param-origin": "Når man aksesserer API-en som bruker en domene-kryssende AJAX-forespørsel (CORS), sett denne til det opprinnelige domenet. Denne må tas med i alle pre-flight-forespørsler, og derfor være en del av spørre-URI-en (ikke POST-kroppen).\n\nFor autentiserte forespørsler må denne stemme helt med en av de opprinnelige i Origin-headeren, slik at den må settes til noe a la https://en.wikipedia.org eller https://meta.wikimedia.org. Hvis denne parameteren ikke stemmer med Origin-headeren, returneres et 403-svar. Hvis denne parameteren stemmer med Origin-headeren og originalen er hvitlistet, vil Access-Control-Allow-Origin og Access-Control-Allow-Credentials-headere bli satt.\n\nFor ikke-autentiserte forepørsler, spesifiser *. Denne vil gjøre at Access-Control-Allow-Origin-headeren blir satt, men Access-Control-Allow-Credentials blir false og alle bruerspesifikke data blir begrenset.", "apihelp-main-param-uselang": "Språk å bruke for meldingsoversettelser. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] med siprop=languages returnerer en liste over språkkoder, eller spesifiser user for å bruke den nåværende brukerens språkpreferanser, eller spesifiser content for å bruke denne wikiens innholdsspråk.", "apihelp-main-param-errorformat": "Formater som kan brukes for advarsels- og feiltekster.\n; plaintext: Wikitext der HTML-tagger er fjernet og elementer byttet ut.\n; wikitext: Ubehandlet wikitext.\n; html: HTML.\n; raw: Meldingsnøkler og -parametre.\n; none: Ingen tekst, bare feilkoder.\n; bc: Format brukt før MediaWiki 1.29. errorlang og errorsuselocal ses bort fra.", + "apihelp-main-param-errorlang": "Språk som skal brukes for advarsler og feil. [[Specia:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] med siprop=languages/ returnerer ei liste over språkkoder, eller angi content for å bruke wikiens innholdsspråk, eller angi uselang for å bruke samme verdi som uselang-parameteren.", "apihelp-main-param-errorsuselocal": "Hvis gitt, vil feiltekster bruke lokalt tilpassede meldinger fra {{ns:MediaWiki}}-navnerommet.", "apihelp-block-description": "Blokker en bruker.", "apihelp-block-param-user": "Brukernavn, IP-adresse eller IP-intervall som skal blokkeres. Kan ikke brukes sammen med $1userid", + "apihelp-block-param-userid": "Bruker-ID som skal blokkeres. Kan ikke brukes sammen med $1user.", "apihelp-block-param-expiry": "Utløpstid. Kan være relativ (f.eks. 5 months eller 2 weeks) eller absolutt (f.eks. 2014-09-18T12:34:56Z). Om den er satt til infinite, indefinite eller never vil blokkeringen ikke ha noen utløpsdato.", "apihelp-block-param-reason": "Årsak for blokkering.", "apihelp-block-param-anononly": "Blokker bare anonyme brukere (dvs. hindre anonyme redigeringer fra denne IP-adressen).", @@ -56,6 +58,9 @@ "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-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.", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 749136a4b1..dc9516e62d 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -1403,6 +1403,7 @@ "api-format-title": "Resultado da API do MediaWiki.", "api-format-prettyprint-header": "Esta é a representação em HTML do formato $1. O HTML é bom para o despiste de erros, mas inadequado para uso na aplicação.\n\nEspecifique o parâmetro format para alterar o formato de saída. Para ver a representação que não é em HTML do formato $1, defina format=$2.\n\nConsulte a [[mw:Special:MyLanguage/API|documentação completa]], ou a [[Special:ApiHelp/main|ajuda da API]] para mais informação.", "api-format-prettyprint-header-only-html": "Esta é uma representação em HTML para ser usada no despiste de erros, mas inadequada para uso na aplicação.\n\nConsulte a [[mw:Special:MyLanguage/API|documentação completa]], ou a [[Special:ApiHelp/main|ajuda da API]] para mais informação.", + "api-format-prettyprint-header-hyperlinked": "Esta é a representação em HTML do formato $1. O HTML é bom para o despiste de erros, mas inadequado para uso na aplicação.\n\nEspecifique o parâmetro format para alterar o formato de saída. Para ver a representação que não é em HTML do formato $1, defina [$3 format=$2].\n\nConsulte a [[mw:API|documentação completa]], ou a [[Special:ApiHelp/main|ajuda da API]] para mais informação.", "api-format-prettyprint-status": "Esta resposta seria devolvida com o estado de HTTP: $1 $2.", "api-login-fail-aborted": "A autenticação requer interação com o utilizador, que não é suportada por action=login. Para poder entrar com action=login, consulte [[Special:BotPasswords]]. Para continuar a usar a autenticação da conta principal, consulte [[Special:ApiHelp/clientlogin|action=clientlogin]].", "api-login-fail-aborted-nobotpw": "A autenticação requer interação com o utilizador, que não é suportada por action=login. Para entrar, consulte [[Special:ApiHelp/clientlogin|action=clientlogin]].", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index e53ece6a6f..2f6041c18a 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1407,8 +1407,9 @@ "apihelp-xml-param-includexmlnamespace": "{{doc-apihelp-param|xml|includexmlnamespace}}", "apihelp-xmlfm-description": "{{doc-apihelp-description|xmlfm|seealso=* {{msg-mw|apihelp-xml-description}}}}", "api-format-title": "{{technical}}\nPage title when API output is pretty-printed in HTML.", - "api-format-prettyprint-header": "{{technical}} Displayed as a header when API output is pretty-printed in HTML.\n\nParameters:\n* $1 - Format name\n* $2 - Non-pretty-printing module name", + "api-format-prettyprint-header": "{{technical}} Displayed as a header when API output is pretty-printed in HTML, but a post request is received.\n\nParameters:\n* $1 - Format name\n* $2 - Non-pretty-printing module name", "api-format-prettyprint-header-only-html": "{{technical}} Displayed as a header when API output is pretty-printed in HTML, but there is no non-html module.\n\nParameters:\n* $1 - Format name", + "api-format-prettyprint-header-hyperlinked": "{{technical}} Displayed as a header when API output is pretty-printed in HTML.\n\nParameters:\n* $1 - Format name\n* $2 - Non-pretty-printing module name\n* $3 - URL to Non-pretty-printing module", "api-format-prettyprint-status": "{{technical}} Displayed as a header when API pretty-printed output is used for a response that uses an unusual HTTP status code.\n\nParameters:\n* $1 - HTTP status code (integer)\n* $2 - Standard English text for the status code.", "api-login-fail-aborted": "{{technical}} Displayed as an error when API login fails due to AuthManager requiring user interaction.\n\nSee also:\n* {{msg-mw|api-login-fail-aborted-nobotpw}}", "api-login-fail-aborted-nobotpw": "{{technical}} Displayed as an error when API login fails due to AuthManager requiring user interaction. Used when BotPasswords is disabled.\n\nSee also:\n* {{msg-mw|api-login-fail-aborted}}", diff --git a/includes/api/i18n/ru.json b/includes/api/i18n/ru.json index 74ea9934f0..9fece21a34 100644 --- a/includes/api/i18n/ru.json +++ b/includes/api/i18n/ru.json @@ -78,9 +78,28 @@ "apihelp-compare-param-fromtitle": "Заголовок первой сравниваемой страницы.", "apihelp-compare-param-fromid": "Идентификатор первой сравниваемой страницы.", "apihelp-compare-param-fromrev": "Первая сравниваемая версия.", + "apihelp-compare-param-fromtext": "Используйте этот текст вместо содержимого версии, заданной fromtitle, fromid или fromrev.", + "apihelp-compare-param-frompst": "Выполнить преобразование перед записью правки (PST) над fromtext.", + "apihelp-compare-param-fromcontentmodel": "Модель содержимого fromtext. Если не задана, будет угадана по другим параметрам.", + "apihelp-compare-param-fromcontentformat": "Формат сериализации содержимого fromtext.", "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-topst": "Выполнить преобразование перед записью правки (PST) над totext.", + "apihelp-compare-param-tocontentmodel": "Модель содержимого totext. Если не задана, будет угадана по другим параметрам.", + "apihelp-compare-param-tocontentformat": "Формат сериализации содержимого totext.", + "apihelp-compare-param-prop": "Какую информацию получить.", + "apihelp-compare-paramvalue-prop-diff": "HTML разницы.", + "apihelp-compare-paramvalue-prop-diffsize": "Размер HTML разницы в байтах.", + "apihelp-compare-paramvalue-prop-rel": "Идентификаторы предыдущей к 'from' и следующей за 'to' версий.", + "apihelp-compare-paramvalue-prop-ids": "Идентификаторы страниц и версий 'from' и 'to'.", + "apihelp-compare-paramvalue-prop-title": "Названия страниц для версий 'from' и 'to'.", + "apihelp-compare-paramvalue-prop-user": "Имя и идентификатор участника для версий 'from' и 'to'.", + "apihelp-compare-paramvalue-prop-comment": "Описания правок для версий 'from' и 'to'.", + "apihelp-compare-paramvalue-prop-parsedcomment": "Распарсенные описания правок для версий 'from' и 'to'.", + "apihelp-compare-paramvalue-prop-size": "Размер версий 'from' и 'to'.", "apihelp-compare-example-1": "Создать разницу между версиями 1 и 2.", "apihelp-createaccount-description": "Создание новой учётной записи.", "apihelp-createaccount-param-preservestate": "Если запрос [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] возвращает true для hasprimarypreservedstate, то запросы, отмеченные как primary-required, должны быть пропущены. Если запрос возвращает непустое значение поля preservedusername, то это значение должно быть использовано в параметре username.", @@ -1403,6 +1422,7 @@ "api-format-title": "Результат MediaWiki API", "api-format-prettyprint-header": "Это HTML-представление формата $1. HTML хорош для отладки, но неудобен для практического применения.\n\nУкажите параметр format для изменения формата вывода. Для отображения не-HTML-представления формата $1, присвойте format=$2.\n\nСм. [[mw:Special:MyLanguage/API|полную документацию]] или [[Special:ApiHelp/main|справку API]] для получения дополнительной информации.", "api-format-prettyprint-header-only-html": "Это HTML-представление для отладки, не рассчитанное на практическое применение.\n\nСм. [[mw:Special:MyLanguage/API|полную документацию]] или [[Special:ApiHelp/main|справку API]] для получения дополнительной информации.", + "api-format-prettyprint-header-hyperlinked": "Это HTML-представление формата $1. HTML хорош для отладки, но неудобен для практического применения.\n\nУкажите параметр format для изменения формата вывода. Для отображения не-HTML-представления формата $1, присвойте [$3 format=$2].\n\nСм. [[mw:API|полную документацию]] или [[Special:ApiHelp/main|справку API]] для получения дополнительной информации.", "api-format-prettyprint-status": "Этот ответ будет возвращён HTTP статусом $1 $2.", "api-login-fail-aborted": "Аутентификация требует взаимодействия с пользователем, что не поддерживается action=login. Чтобы авторизовываться через action=login, см. [[Special:BotPasswords]]. Для продолжения использования авторизации основного аккаунта см. [[Special:ApiHelp/clientlogin|action=clientlogin]].", "api-login-fail-aborted-nobotpw": "Аутентификация требует взаимодействия с пользователем, что не поддерживается action=login. Для авторизации см. [[Special:ApiHelp/clientlogin|action=clientlogin]].", @@ -1520,6 +1540,8 @@ "apierror-changeauth-norequest": "Попытка создать запрос правки провалилась.", "apierror-chunk-too-small": "Минимальный размер кусочка — $1 {{PLURAL:$1|байт|байта|байтов}}, если кусочек не является последним.", "apierror-cidrtoobroad": "Диапазоны $1 CIDR, шире /$2, не разрешены.", + "apierror-compare-no-title": "Невозможно выполнить преобразование перед записью правки без заголовка. Попробуйте задать fromtitle или totitle.", + "apierror-compare-relative-to-nothing": "Нет версии 'from', к которой относится torelative.", "apierror-contentserializationexception": "Сериализация содержимого провалилась: $1", "apierror-contenttoobig": "Предоставленное вами содержимое превышает максимальный размер страницы в $1 {{PLURAL:$1|килобайт|килобайта|килобайтов}}.", "apierror-copyuploadbaddomain": "Загрузка по ссылке недоступна с этого домена.", @@ -1563,10 +1585,12 @@ "apierror-maxlag": "Ожидание $2: $1 {{PLURAL:$1|секунда|секунды|секунд}} задержки.", "apierror-mimesearchdisabled": "Поиск по MIME отключен в жадном режиме.", "apierror-missingcontent-pageid": "Отсутствует содержимое страницы с идентификатором $1.", + "apierror-missingcontent-revid": "Отсутствует содержимое версии с идентификатором $1.", "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|Параметр|Как минимум один из параметров}} $1 обязателен.", "apierror-missingparam-one-of": "{{PLURAL:$2|Параметр|Один из параметров}} $1 обязателен.", "apierror-missingparam": "Параметр $1 должен быть задан.", "apierror-missingrev-pageid": "Нет текущей версии страницы с идентификатором $1.", + "apierror-missingrev-title": "Нет текущей версии для заголовка $1.", "apierror-missingtitle-createonly": "Несуществующие названия страниц могут быть защищены только с помощью create.", "apierror-missingtitle": "Указанная вами страница не существует.", "apierror-missingtitle-byname": "Страница $1 не существует.", @@ -1668,6 +1692,7 @@ "apiwarn-badurlparam": "Невозможно распарсить $2 из $1urlparam. Используется только ширина и высота.", "apiwarn-badutf8": "Значение, переданное $1, содержит некорректные или ненормализованные данные. Текстовые данные должны быть корректным NFC-нормализованным Юникодом без символов управления C0, кроме HT (\\t), LF (\\n) и CR (\\r).", "apiwarn-checktoken-percentencoding": "Проверьте, что символы вроде «+» в токене корректно закодированы %-последовательностями в ссылке.", + "apiwarn-compare-nocontentmodel": "Модель содержимого не может быть определена, предполагается $1.", "apiwarn-deprecation-deletedrevs": "list=deletedrevs не поддерживается. Пожалуйста, вместо него используйте prop=deletedrevisions или list=alldeletedrevisions.", "apiwarn-deprecation-expandtemplates-prop": "Поскольку никакие значения не были указаны в параметре prop, был использован наследованный формат. Этот формат является устаревшим, и в будущем параметру prop будет присвоено значение по умолчанию, что приведёт к повсеместному использованию нового формата.", "apiwarn-deprecation-httpsexpected": "Использован HTTP, где ожидался HTTPS.", diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index 34d4a8dad8..6166b00679 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -1405,6 +1405,7 @@ "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帮助]]以获取更多信息。", "api-format-prettyprint-header-only-html": "这是用来调试的HTML实现,不适合实际使用。\n\n参见[[mw:Special:MyLanguage/API|完整文档]],或[[Special:ApiHelp/main|API帮助]]以获取更多信息。", + "api-format-prettyprint-header-hyperlinked": "这是$1格式的HTML实现。HTML对调试很有用,但不适合应用程序使用。\n\n指定format参数以更改输出格式。要查看$1格式的非HTML实现,设置[$3 format=$2]。\n\n参见[[mw:API|完整文档]],或[[Special:ApiHelp/main|API帮助]]以获取更多信息。", "api-format-prettyprint-status": "此响应将会返回HTTP状态$1 $2。", "api-login-fail-aborted": "身份验证需要用户交互,而其不被action=login支持。要通过action=login登录,请参见[[Special:BotPasswords]]。要继续使用主账户登录,请参见[[Special:ApiHelp/clientlogin|action=clientlogin]]。", "api-login-fail-aborted-nobotpw": "身份验证需要用户交互,而其不被action=login支持。要登录,请参见[[Special:ApiHelp/clientlogin|action=clientlogin]]。", @@ -1522,6 +1523,7 @@ "apierror-changeauth-norequest": "创建更改请求失败。", "apierror-chunk-too-small": "对于非最终块,最小块大小为$1{{PLURAL:$1|字节}}。", "apierror-cidrtoobroad": "比/$2更宽的$1 CIDR地址段不被接受。", + "apierror-compare-relative-to-nothing": "没有与torelative的“来源”修订版本相对的版本。", "apierror-contentserializationexception": "内容序列化失败:$1", "apierror-contenttoobig": "您提供的内容超过了$1{{PLURAL:$1|千字节}}的条目大小限制。", "apierror-copyuploadbaddomain": "不允许从此域名通过URL上传。", @@ -1670,6 +1672,7 @@ "apiwarn-alldeletedrevisions-performance": "当生成标题时,为获得更好性能,请设置$1dir=newer。", "apiwarn-badurlparam": "不能为$2解析$1urlparam。请只使用宽和高。", "apiwarn-badutf8": "$1通过的值包含无效或非标准化数据。正文数据应为有效的NFC标准化Unicode,没有除HT(\\t)、LF(\\n)和CR(\\r)以外的C0控制字符。", + "apiwarn-compare-nocontentmodel": "没有可以定义的模型,假定为$1。", "apiwarn-deprecation-deletedrevs": "list=deletedrevs已被弃用。请改用prop=deletedrevisions或list=alldeletedrevisions。", "apiwarn-deprecation-expandtemplates-prop": "因为没有为prop参数指定值,所以在输出上使用了遗留格式。这种格式已弃用,并且将来会为prop参数设置默认值,这会导致新格式总会被使用。", "apiwarn-deprecation-httpsexpected": "当应为HTTPS时,HTTP被使用。", diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index 92a3d3f2e2..5aa693ddd9 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -177,6 +177,8 @@ class ChangesList extends ContextSource { } else { $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' . $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); + $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' . + $rc->mAttribs['rc_namespace'] ); } // Indicate watched status on the line to allow for more @@ -739,4 +741,26 @@ class ChangesList extends ContextSource { && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0; } + /** + * Get recommended data attributes for a change line. + * @param RecentChange $rc + * @return string[] attribute name => value + */ + protected function getDataAttributes( RecentChange $rc ) { + $type = $rc->getAttribute( 'rc_source' ); + switch ( $type ) { + case RecentChange::SRC_EDIT: + case RecentChange::SRC_NEW: + return [ + 'data-mw-revid' => $rc->mAttribs['rc_this_oldid'], + ]; + case RecentChange::SRC_LOG: + return [ + 'data-mw-logid' => $rc->mAttribs['rc_logid'], + 'data-mw-logaction' => $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'], + ]; + default: + return []; + } + } } diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index b34a33fdcf..03f63f673f 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -447,13 +447,16 @@ class EnhancedChangesList extends ChangesList { # Tags $data['tags'] = $this->getTags( $rcObj, $classes ); + $attribs = $this->getDataAttributes( $rcObj ); + // give the hook a chance to modify the data $success = Hooks::run( 'EnhancedChangesListModifyLineData', - [ $this, &$data, $block, $rcObj, &$classes ] ); + [ $this, &$data, $block, $rcObj, &$classes, &$attribs ] ); if ( !$success ) { // skip entry if hook aborted it return []; } + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); $lineParams['recentChangesFlagsRaw'] = []; if ( isset( $data['recentChangesFlags'] ) ) { @@ -469,6 +472,7 @@ class EnhancedChangesList extends ChangesList { } $lineParams['classes'] = array_values( $classes ); + $lineParams['attribs'] = Html::expandAttributes( $attribs ); // everything else: makes it easier for extensions to add or remove data $lineParams['data'] = array_values( $data ); @@ -671,6 +675,8 @@ class EnhancedChangesList extends ChangesList { # Show how many people are watching this if enabled $data['watchingUsers'] = $this->numberofWatchingusers( $rcObj->numberofWatchingusers ); + $data['attribs'] = array_merge( $this->getDataAttributes( $rcObj ), [ 'class' => $classes ] ); + // give the hook a chance to modify the data $success = Hooks::run( 'EnhancedChangesListModifyBlockLineData', [ $this, &$data, $rcObj ] ); @@ -678,9 +684,11 @@ class EnhancedChangesList extends ChangesList { // skip entry if hook aborted it return ''; } + $attribs = $data['attribs']; + unset( $data['attribs'] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); - $line = Html::openElement( 'table', [ 'class' => $classes ] ) . - Html::openElement( 'tr' ); + $line = Html::openElement( 'table', $attribs ) . Html::openElement( 'tr' ); $line .= ''; if ( isset( $data['recentChangesFlags'] ) ) { diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index a5d5191da8..2a53d6694d 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -50,16 +50,23 @@ class OldChangesList extends ChangesList { $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); } + $attribs = $this->getDataAttributes( $rc ); + // Avoid PHP 7.1 warning from passing $this by reference $list = $this; - if ( !Hooks::run( 'OldChangesListRecentChangesLine', [ &$list, &$html, $rc, &$classes ] ) ) { + if ( !Hooks::run( 'OldChangesListRecentChangesLine', + [ &$list, &$html, $rc, &$classes, &$attribs ] ) + ) { return false; } + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); $dateheader = ''; // $html now contains only
  • ...
  • , for hooks' convenience. $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); - return "$dateheader
  • " . $html . "
  • \n"; + $attribs['class'] = implode( ' ', $classes ); + + return $dateheader . Html::rawElement( 'li', $attribs, $html ) . "\n"; } /** diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index ff6a8730c5..6ba9c10a70 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -120,6 +120,32 @@ class ChangeTags { return $msg->parse(); } + /** + * Get the message object for the tag's long description. + * + * Checks if message key "mediawiki:tag-$tag-description" exists. If it does not, + * or if message is disabled, returns false. Otherwise, returns the message object + * for the long description. + * + * @param string $tag Tag + * @param IContextSource $context + * @return Message|bool Message object of the tag long description or false if + * there is no description. + */ + public static function tagLongDescriptionMessage( $tag, IContextSource $context ) { + $msg = $context->msg( "tag-$tag-description" ); + if ( !$msg->exists() ) { + return false; + } + if ( $msg->isDisabled() ) { + // The message exists but is disabled, hide the description. + return false; + } + + // Message exists and isn't disabled, use it. + return $msg; + } + /** * Add tags to a change given its rc_id, rev_id and/or log_id * diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php index 2264670cfa..36d6df2c57 100644 --- a/includes/context/ContextSource.php +++ b/includes/context/ContextSource.php @@ -181,10 +181,12 @@ abstract class ContextSource implements IContextSource { * Parameters are the same as wfMessage() * * @since 1.18 + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, + * or a MessageSpecifier. * @param mixed ... * @return Message */ - public function msg( /* $args */ ) { + public function msg( $key /* $args */ ) { $args = func_get_args(); return call_user_func_array( [ $this->getContext(), 'msg' ], $args ); diff --git a/includes/context/DerivativeContext.php b/includes/context/DerivativeContext.php index 29395101d0..9c3c42a92d 100644 --- a/includes/context/DerivativeContext.php +++ b/includes/context/DerivativeContext.php @@ -324,10 +324,12 @@ class DerivativeContext extends ContextSource implements MutableContext { * it would set only the original context, and not take * into account any changes. * + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, + * or a MessageSpecifier. * @param mixed $args,... Arguments to wfMessage * @return Message */ - public function msg() { + public function msg( $key ) { $args = func_get_args(); return call_user_func_array( 'wfMessage', $args )->setContext( $this ); diff --git a/includes/context/IContextSource.php b/includes/context/IContextSource.php index 8e9fc6f6a9..d13e1a5705 100644 --- a/includes/context/IContextSource.php +++ b/includes/context/IContextSource.php @@ -52,7 +52,7 @@ use Liuggio\StatsdClient\Factory\StatsdDataFactory; * belong here either. Session state changes should only be propagated on * shutdown by separate persistence handler objects, for example. */ -interface IContextSource { +interface IContextSource extends MessageLocalizer { /** * Get the WebRequest object * @@ -143,14 +143,6 @@ interface IContextSource { */ public function getTiming(); - /** - * Get a Message object with context set. See wfMessage for parameters. - * - * @param mixed ... - * @return Message - */ - public function msg(); - /** * Export the resolved user IP, HTTP headers, user ID, and session ID. * The result will be reasonably sized to allow for serialization. diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index 0e1de504e5..2cabfe1013 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -449,10 +449,12 @@ class RequestContext implements IContextSource, MutableContext { * Get a Message object with context set * Parameters are the same as wfMessage() * + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, + * or a MessageSpecifier. * @param mixed ... * @return Message */ - public function msg() { + public function msg( $key ) { $args = func_get_args(); return call_user_func_array( 'wfMessage', $args )->setContext( $this ); diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index b728786896..556fe75547 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -558,19 +558,9 @@ class DatabaseOracle extends Database { } function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = [], $selectOptions = [] + $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ) { $destTable = $this->tableName( $destTable ); - if ( !is_array( $selectOptions ) ) { - $selectOptions = [ $selectOptions ]; - } - list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = - $this->makeSelectOptions( $selectOptions ); - if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) ); - } else { - $srcTable = $this->tableName( $srcTable ); - } $sequenceData = $this->getSequenceData( $destTable ); if ( $sequenceData !== false && @@ -585,13 +575,16 @@ class DatabaseOracle extends Database { $val = $val . ' field' . ( $i++ ); } - $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . - " SELECT $startOpts " . implode( ',', $varMap ) . - " FROM $srcTable $useIndex $ignoreIndex "; - if ( $conds != '*' ) { - $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); - } - $sql .= " $tailOpts"; + $selectSql = $this->selectSQLText( + $srcTable, + array_values( $varMap ), + $conds, + $fname, + $selectOptions, + $selectJoinConds + ); + + $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' . $selectSql; if ( in_array( 'IGNORE', $insertOptions ) ) { $this->ignoreDupValOnIndex = true; diff --git a/includes/debug/logger/monolog/AvroFormatter.php b/includes/debug/logger/monolog/AvroFormatter.php index 2700daa66f..2a50566912 100644 --- a/includes/debug/logger/monolog/AvroFormatter.php +++ b/includes/debug/logger/monolog/AvroFormatter.php @@ -23,7 +23,6 @@ namespace MediaWiki\Logger\Monolog; use AvroIODatumWriter; use AvroIOBinaryEncoder; use AvroIOTypeException; -use AvroNamedSchemata; use AvroSchema; use AvroStringIO; use AvroValidator; diff --git a/includes/deferred/DeferredUpdates.php b/includes/deferred/DeferredUpdates.php index 51f5a28c3d..a3a37f6f2e 100644 --- a/includes/deferred/DeferredUpdates.php +++ b/includes/deferred/DeferredUpdates.php @@ -286,7 +286,7 @@ class DeferredUpdates { } // Avoiding running updates without them having outer scope - if ( !self::getBusyDbConnections() ) { + if ( !self::areDatabaseTransactionsActive() ) { self::doUpdates( $mode ); return true; } @@ -356,16 +356,19 @@ class DeferredUpdates { } /** - * @return IDatabase[] Connection where commit() cannot be called yet + * @return bool If a transaction round is active or connection is not ready for commit() */ - private static function getBusyDbConnections() { - $connsBusy = []; - + private static function areDatabaseTransactionsActive() { $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + if ( $lbFactory->hasTransactionRound() ) { + return true; + } + + $connsBusy = false; $lbFactory->forEachLB( function ( LoadBalancer $lb ) use ( &$connsBusy ) { $lb->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( &$connsBusy ) { if ( $conn->writesOrCallbacksPending() || $conn->explicitTrxActive() ) { - $connsBusy[] = $conn; + $connsBusy = true; } } ); } ); diff --git a/includes/filerepo/FileBackendDBRepoWrapper.php b/includes/filerepo/FileBackendDBRepoWrapper.php index 856dc36521..23947048de 100644 --- a/includes/filerepo/FileBackendDBRepoWrapper.php +++ b/includes/filerepo/FileBackendDBRepoWrapper.php @@ -24,7 +24,6 @@ */ use Wikimedia\Rdbms\DBConnRef; -use Wikimedia\Rdbms\MaintainableDBConnRef; /** * @brief Proxy backend that manages file layout rewriting for FileRepo. diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 53211febab..9aa2b186e5 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -2173,7 +2173,7 @@ abstract class File implements IDBAccessObject { $metadata = []; } - return $handler->getContentHeaders( $metadata, $this->getWidth(), $this->getHeight() ); + return $handler->getContentHeaders( $metadata ); } return []; diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index a90156fad1..8d715e824a 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1206,9 +1206,7 @@ class LocalFile extends File { $metadata = []; } - $options['headers'] = $handler->getContentHeaders( - $metadata, $props['width'], $props['height'] - ); + $options['headers'] = $handler->getContentHeaders( $metadata ); } else { $options['headers'] = []; } diff --git a/includes/gallery/ImageGalleryBase.php b/includes/gallery/ImageGalleryBase.php index 6884f65626..eeb8a8ff84 100644 --- a/includes/gallery/ImageGalleryBase.php +++ b/includes/gallery/ImageGalleryBase.php @@ -38,6 +38,11 @@ abstract class ImageGalleryBase extends ContextSource { */ protected $mShowBytes; + /** + * @var bool Whether to show the dimensions in categories + */ + protected $mShowDimensions; + /** * @var bool Whether to show the filename. Default: true */ @@ -136,6 +141,7 @@ abstract class ImageGalleryBase extends ContextSource { $galleryOptions = $this->getConfig()->get( 'GalleryOptions' ); $this->mImages = []; $this->mShowBytes = $galleryOptions['showBytes']; + $this->mShowDimensions = $galleryOptions['showDimensions']; $this->mShowFilename = true; $this->mParser = false; $this->mHideBadImages = false; @@ -283,6 +289,16 @@ abstract class ImageGalleryBase extends ContextSource { return empty( $this->mImages ); } + /** + * Enable/Disable showing of the dimensions of an image in the gallery. + * Enabled by default. + * + * @param bool $f Set to false to disable + */ + function setShowDimensions( $f ) { + $this->mShowDimensions = (bool)$f; + } + /** * Enable/Disable showing of the file size of an image in the gallery. * Enabled by default. diff --git a/includes/gallery/TraditionalImageGallery.php b/includes/gallery/TraditionalImageGallery.php index 1fd7b0a362..a0059cea1f 100644 --- a/includes/gallery/TraditionalImageGallery.php +++ b/includes/gallery/TraditionalImageGallery.php @@ -174,15 +174,20 @@ class TraditionalImageGallery extends ImageGalleryBase { // ":{$ut}" ); // $ul = Linker::link( $linkTarget, $ut ); - if ( $this->mShowBytes ) { - if ( $img ) { - $fileSize = htmlspecialchars( $lang->formatSize( $img->getSize() ) ); - } else { - $fileSize = $this->msg( 'filemissing' )->escaped(); + $meta = []; + if ( $img ) { + if ( $this->mShowDimensions ) { + $meta[] = $img->getDimensionsString(); } - $fileSize = "$fileSize
    \n"; - } else { - $fileSize = ''; + if ( $this->mShowBytes ) { + $meta[] = htmlspecialchars( $lang->formatSize( $img->getSize() ) ); + } + } elseif ( $this->mShowDimensions || $this->mShowBytes ) { + $meta[] = $this->msg( 'filemissing' )->escaped(); + } + $meta = $lang->semicolonList( $meta ); + if ( $meta ) { + $meta .= "
    \n"; } $textlink = $this->mShowFilename ? @@ -201,7 +206,7 @@ class TraditionalImageGallery extends ImageGalleryBase { ) . "\n" : ''; - $galleryText = $textlink . $text . $fileSize; + $galleryText = $textlink . $text . $meta; $galleryText = $this->wrapGalleryText( $galleryText, $thumb ); # Weird double wrapping (the extra div inside the li) needed due to FF2 bug diff --git a/includes/htmlform/OOUIHTMLForm.php b/includes/htmlform/OOUIHTMLForm.php index 6650321633..ed99802994 100644 --- a/includes/htmlform/OOUIHTMLForm.php +++ b/includes/htmlform/OOUIHTMLForm.php @@ -191,6 +191,10 @@ class OOUIHTMLForm extends HTMLForm { * @return string */ public function getErrorsOrWarnings( $elements, $elementsType ) { + if ( $elements === '' ) { + return ''; + } + if ( !in_array( $elementsType, [ 'error', 'warning' ], true ) ) { throw new DomainException( $elementsType . ' is not a valid type.' ); } diff --git a/includes/import/WikiImporter.php b/includes/import/WikiImporter.php index 06b579a7d9..2fc9f5e527 100644 --- a/includes/import/WikiImporter.php +++ b/includes/import/WikiImporter.php @@ -39,6 +39,7 @@ class WikiImporter { private $mNoticeCallback, $mDebug; private $mImportUploads, $mImageBasePath; private $mNoUpdates = false; + private $pageOffset = 0; /** @var Config */ private $config; /** @var ImportTitleFactory */ @@ -146,6 +147,16 @@ class WikiImporter { $this->mNoUpdates = $noupdates; } + /** + * Sets 'pageOffset' value. So it will skip the first n-1 pages + * and start from the nth page. It's 1-based indexing. + * @param int $nthPage + * @since 1.29 + */ + function setPageOffset( $nthPage ) { + $this->pageOffset = $nthPage; + } + /** * Set a callback that displays notice messages * @@ -562,9 +573,19 @@ class WikiImporter { $keepReading = $this->reader->read(); $skip = false; $rethrow = null; + $pageCount = 0; try { while ( $keepReading ) { $tag = $this->reader->localName; + if ( $this->pageOffset ) { + if ( $tag === 'page' ) { + $pageCount++; + } + if ( $pageCount < $this->pageOffset ) { + $keepReading = $this->reader->next(); + continue; + } + } $type = $this->reader->nodeType; if ( !Hooks::run( 'ImportHandleToplevelXMLTag', [ $this ] ) ) { diff --git a/includes/installer/WebInstallerLanguage.php b/includes/installer/WebInstallerLanguage.php index cfd4a862c2..bce07d3118 100644 --- a/includes/installer/WebInstallerLanguage.php +++ b/includes/installer/WebInstallerLanguage.php @@ -98,17 +98,19 @@ class WebInstallerLanguage extends WebInstallerPage { * @return string */ public function getLanguageSelector( $name, $label, $selectedCode, $helpHtml = '' ) { - global $wgDummyLanguageCodes; + global $wgExtraLanguageCodes; $output = $helpHtml; $select = new XmlSelect( $name, $name, $selectedCode ); $select->setAttribute( 'tabindex', $this->parent->nextTabIndex() ); + $unwantedLanguageCodes = $wgExtraLanguageCodes + + LanguageCode::getDeprecatedCodeMapping(); $languages = Language::fetchLanguageNames(); ksort( $languages ); foreach ( $languages as $code => $lang ) { - if ( isset( $wgDummyLanguageCodes[$code] ) ) { + if ( isset( $unwantedLanguageCodes[$code] ) ) { continue; } $select->addOption( "$code - $lang", $code ); diff --git a/includes/installer/i18n/ast.json b/includes/installer/i18n/ast.json index d47334cca9..1b2831fa0b 100644 --- a/includes/installer/i18n/ast.json +++ b/includes/installer/i18n/ast.json @@ -107,10 +107,30 @@ "config-db-schema-help": "Esti esquema de vezu va tar bien.\nCamúdalos solo si sabes que lo precises.", "config-pg-test-error": "Nun puede coneutase cola base de datos $1: $2", "config-sqlite-dir": "Direutoriu de datos SQLite:", + "config-oracle-def-ts": "Espaciu de tables predetermináu:", + "config-oracle-temp-ts": "Espaciu de tables temporal:", "config-type-mysql": "MySQL (o compatible)", "config-type-mssql": "Microsoft SQL Server", + "config-support-info": "MediaWiki ye compatible colos siguientes sistemes de bases de datos:\n\n$1\n\nSi nun atopes na llista el sistema de base de datos que tas intentando utilizar, sigue les instrucciones enllazaes enriba p'activar la compatibilidá.", + "config-header-mysql": "Configuración de MySQL", + "config-header-postgres": "Configuración de PostgreSQL", + "config-header-sqlite": "Configuración de SQLite", + "config-header-oracle": "Configuración d'Oracle", + "config-header-mssql": "Configuración de Microsoft SQL Server", "config-invalid-db-type": "Triba non válida de base de datos.", "config-missing-db-name": "Tienes d'introducir un valor pa «{{int:config-db-name}}».", + "config-missing-db-host": "Tienes d'escribir un valor pa «{{int:config-db-host}}».", + "config-missing-db-server-oracle": "Tienes d'escribir un valor pa «{{int:config-db-host-oracle}}».", + "config-invalid-db-server-oracle": "TNS inválidu pa la base de datos «$1».\nUsa una cadena «TNS Name» o «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Métodos de nomenclatura d'Oracle]).", + "config-invalid-db-name": "Nome inválidu de la base de datos «$1».\nUsa sólo lletres ASCII (a-z, A-Z), númberos (0-9), guiones baxos (_) y guiones (-).", + "config-invalid-db-prefix": "Prefixu inválidu pa la base de datos «$1».\nUsa sólo lletres ASCII (a-z, A-Z), númberos (0-9), guiones baxos (_) y guiones (-).", + "config-connection-error": "$1.\n\nComprueba'l sirvidor, el nome d'usuariu y la contraseña, y tenta nuevamente.", + "config-invalid-schema": "Esquema inválidu «$1» pa MediaWiki.\nUsa sólo lletres ASCII (a-z, A-Z), númberos (0-9) y guiones baxos (_).", + "config-db-sys-create-oracle": "L'instalador sólo almite l'emplegu d'una cuenta SYSDBA pa crear una cuenta nueva.", + "config-db-sys-user-exists-oracle": "La cuenta d'usuariu «$1» yá esiste. ¡SYSDBA sólo puede utilizase pa crear una nueva cuenta!", + "config-postgres-old": "Ríquese PostgreSQL $1 o posterior. Tienes la versión $2.", + "config-mssql-old": "Ríquese Microsoft SQL Server $1 o posterior. Tienes la versión $2.", + "config-sqlite-name-help": "Escueye'l nome qu'identifica la to wiki.\nNun uses espacios o guiones.\nEsti va usase como nome del ficheru de datos pa SQLite.", "config-mysql-innodb": "InnoDB", "config-mysql-myisam": "MyISAM", "config-mysql-utf8": "UTF-8", diff --git a/includes/installer/i18n/bs.json b/includes/installer/i18n/bs.json index a8abcec6f0..66139728c0 100644 --- a/includes/installer/i18n/bs.json +++ b/includes/installer/i18n/bs.json @@ -48,21 +48,38 @@ "config-env-php": "PHP $1 je instaliran.", "config-env-hhvm": "HHVM $1 je instaliran.", "config-no-db": "Ne mogu pronaći pogodan upravljački program za bazu podataka! Morate ga instalirati za PHP-bazu.\n{{PLURAL:$2|Podržana je sljedeća vrsta|Podržane su sljedeće vrste}} baze podataka: $1.\n\nAko se sami kompajlirali PHP, omogućite klijent baze podataka u postavkama koristeći, naprimjer, ./configure --with-mysqli.\nAko ste instalirali PHP iz paketa za Debian ili Ubuntu, onda također morate instalirati, naprimjer, paket php5-mysql.", + "config-memory-raised": "memory_limit za PHP iznosi $1, povišen na $2.", + "config-memory-bad": "Upozorenje: memory_limit za PHP iznosi $1.\nOvo je vjerovatno premalo.\nInstalacija možda neće uspjeti!", "config-xcache": "[http://xcache.lighttpd.net/ XCache] je instaliran", "config-apc": "[http://www.php.net/apc APC] je instaliran", + "config-apcu": "[http://www.php.net/apcu APCu] je instaliran", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] je instaliran", "config-diff3-bad": "GNU diff3 nije pronađen.", + "config-git": "Pronađen je Git program za kontrolu verzija: $1.", + "config-git-bad": "Nije pronađen Git program za kontrolu verzija.", + "config-imagemagick": "Pronađen je ImageMagick: $1.\nAko omogućite postavljanje, bit će omogućena minijaturizacija slika.", + "config-gd": "Utvrđeno je da je ugrađena grafička biblioteka GD.\nAko omogućite postavljanje, bit će omogućena minijaturizacija slika.", + "config-no-scaling": "Ne mogu pronaći biblioteku GD niti ImageMagick.\nMinijaturizacija slika bit će onemogućena.", + "config-no-uri": "Greška: Ne mogu utvrditi trenutni URI.\nInstalacija je prekinuta.", "config-db-type": "Vrsta baze podataka:", "config-db-host": "Domaćin baze podataka:", "config-db-wiki-settings": "Identificiraj ovu wiki", "config-db-name": "Naziv baze podataka:", "config-db-name-oracle": "Šema baze podataka:", + "config-db-install-account": "Korisnički račun za instalaciju", "config-db-username": "Korisničko ime baze podataka:", "config-db-password": "Lozinka baze podataka:", + "config-db-wiki-account": "Korisnički račun za redovan rad", + "config-db-prefix": "Prefiks tabele u bazi podataka:", + "config-mysql-old": "Zahtijeva se MySQL $1 ili noviji. Vi imate $2.", "config-db-port": "Port baze podataka:", "config-db-schema": "Šema za MediaWiki:", + "config-pg-test-error": "Ne mogu se povezati na bazu podataka $1: $2", + "config-sqlite-dir": "Folder za SQLite-podatke:", "config-oracle-def-ts": "Predodređeni tabelarni prostor:", "config-oracle-temp-ts": "Privremeni tabelarni prostor:", + "config-type-mysql": "MySQL (ili kompaktibilan)", + "config-type-mssql": "Microsoft SQL Server", "config-header-mysql": "Postavke MySQL-a", "config-header-postgres": "Postavke PostgreSQL-a", "config-header-sqlite": "Postavke SQLite-a", @@ -72,13 +89,19 @@ "config-missing-db-name": "Morate unijeti vrijednost za \"{{int:config-db-name}}\".", "config-missing-db-host": "Morate unijeti vrijednost za \"{{int:config-db-host}}\".", "config-missing-db-server-oracle": "Morate unijeti vrijednost za \"{{int:config-db-host-oracle}}\".", + "config-db-sys-create-oracle": "Program za instalaciju podržava samo upotrebu SYSDBA-računa za pravljenje novih računa.", + "config-postgres-old": "Zahtijeva se PostgreSQL $1 ili noviji. Vi imate $2.", + "config-mssql-old": "Zahtijeva se Microsoft SQL Server $1 ili noviji. Vi imate $2.", + "config-sqlite-name-help": "Izaberite ime koje će predstavljati Vaš wiki.\nNemojte koristiti razmake i crte.\nOvo će se koristiti za ime datoteke SQLite-podataka.", "config-sqlite-readonly": "Datoteka $1 nije zapisiva.", + "config-sqlite-cant-create-db": "Ne mogu napraviti datoteku $1 za bazu podataka.", "config-sqlite-fts3-downgrade": "PHP ne podržava FTS3, poništavam nadogradnju tabela.", "config-upgrade-done": "Nadogradnja završena.\n\nSada možete [$1 početi koristiti Vaš wiki].\n\nAko želite regenerirati Vašu datoteku LocalSettings.php, kliknite na dugme ispod.\nOvo nije preporučeno osim ako nemate problema s Vašim wikijem.", "config-upgrade-done-no-regenerate": "Nadogradnja završena.\n\nSad možete [$1 početi da koristite svoj wiki].", "config-regenerate": "Regeneriraj LocalSettings.php →", "config-unknown-collation": "Upozorenje: Baza podataka koristi nepoznatu kolaciju.", "config-db-web-account": "Račun baze podataka za mrežni pristup", + "config-db-web-account-same": "Koristi isti račun kao i za instalaciju", "config-db-web-create": "Napravi račun ako već ne postoji", "config-mysql-engine": "Skladišni pogon:", "config-mysql-innodb": "InnoDB", @@ -90,6 +113,7 @@ "config-site-name-blank": "Upišite ime sajta.", "config-project-namespace": "Imenski prostor projekta:", "config-ns-generic": "Projekt", + "config-ns-site-name": "Isti kao ime wikija: $1", "config-ns-other": "Drugo (navedite)", "config-ns-other-default": "MyWiki", "config-admin-box": "Administratorski račun", @@ -103,32 +127,55 @@ "config-admin-password-mismatch": "Lozinke koje ste upisali se ne poklapaju.", "config-admin-email": "Adresa e-pošte:", "config-admin-error-bademail": "Upisali ste neispravnu adresu e-pošte.", + "config-pingback": "Podijeli podatke o instalaciji s programerima MediaWikija.", + "config-optional-continue": "Postavi mi još pitanja.", "config-optional-skip": "Već mi je dosadilo, daj samo instaliraj wiki.", + "config-profile": "Profil korisničkih prava:", "config-profile-wiki": "Otvoren wiki", + "config-profile-no-anon": "Potrebno je napraviti račun", + "config-profile-fishbowl": "Samo ovlašteni korisnici", "config-profile-private": "Privatni wiki", "config-license": "Autorska prava i licenca:", "config-license-none": "Bez podnožja za licencu", "config-license-pd": "Javno vlasništvo", + "config-email-settings": "Postavke e-pošte", + "config-enable-email": "Omogući odlaznu e-poštu", + "config-upload-enable": "Omogući postavljanje datoteka", + "config-upload-deleted": "Folder za obrisane datoteke:", "config-logo": "URL logotipa:", + "config-instantcommons": "Omogući Instant Commons", "config-cc-again": "Izaberite ponovo...", "config-advanced-settings": "Napredna konfiguracija", "config-extensions": "Proširenja", "config-skins": "Teme", + "config-skins-use-as-default": "Koristi temu kao predodređenu", + "config-skins-missing": "Nije pronađena nijedna tema. MediaWiki će koristiti rezervnu temu dok ne instalirate druge.", + "config-skins-must-enable-some": "Morate izabrati barem jednu temu.", + "config-skins-must-enable-default": "Tema koju ste izabrali za predodređenu mora se omogućiti.", "config-install-alreadydone": "Upozorenje: Izgleda da već imate instaliran MediaWiki i da ga ponovo pokušavate instalirati.\nIdite na sljedeću stranicu.", "config-install-step-done": "završeno", "config-install-step-failed": "neuspješno", "config-install-extensions": "Uključujem proširenja", "config-install-database": "Postavljam bazu podataka", "config-install-schema": "Pravim šemu", + "config-install-pg-plpgsql": "Provjeravam jezik PL/pgSQL", + "config-pg-no-plpgsql": "Morate instalirati jezik PL/pgSQL u bazu podataka $1", "config-install-user": "Pravim korisnika baze podataka", "config-install-user-alreadyexists": "Korisnik \"$1\" već postoji", + "config-install-user-create-failed": "Pravljenje korisnika \"$1\" nije uspjelo: $2", + "config-install-user-grant-failed": "Dodjeljivanje dozvola korisniku \"$1\" nije uspjelo: $2", "config-install-user-missing": "Navedeni korisnik \"$1\" ne postoji.", "config-install-tables": "Pravim tabele", "config-install-interwiki": "Popunjavam predodređenu međuprojektnu tabelu", "config-install-stats": "Pokrećem statistiku", "config-install-keys": "Stvaram tajne ključeve", + "config-install-updates": "Spriječi pokretanje nepotrebnih ažuriranja", "config-install-sysop": "Pravim administratorski korisnički račun", + "config-install-subscribe-fail": "Ne mogu Vas pretplatiti na mediawiki-announce: $1", + "config-install-subscribe-notpossible": "cURL nije instaliran, a allow_url_fopen nije dostupno.", "config-install-mainpage": "Pravim početnu stranicu sa standardnim sadržajem", + "config-install-mainpage-exists": "Početna strana već postoji. Prelazim na sljedeće.", + "config-install-mainpage-failed": "Ne mogu umetnuti početnu stranu: $1", "config-download-localsettings": "Preuzmi LocalSettings.php", "config-help": "pomoć", "config-help-tooltip": "kliknite da proširite", diff --git a/includes/installer/i18n/ce.json b/includes/installer/i18n/ce.json index cb7e392d03..e11fae2374 100644 --- a/includes/installer/i18n/ce.json +++ b/includes/installer/i18n/ce.json @@ -29,7 +29,7 @@ "config-page-existingwiki": "Йолуш йолу вики", "config-copyright": "=== Авторан бакъонаш а хьал а ===\n\n$1\nMediaWiki ю маьрша программин латораг, шу йиш ю фондас арахецна йолу GNU General Public License лицензица и яржо я хийца а.\n\nMediaWiki яржош ю и шуна пайдане хир яц те аьлла, амма цхьа юкъарахилар доцуш. Хь. кхин. лицензи мадарра GNU General Public License .\n\nШоьга кхача езаш яра [{{SERVER}}{{SCRIPTPATH}}/COPYING копи GNU General Public License] хӀокху программица, кхаьчна яцахь язъе Free Software Foundation, Inc., адрес тӀе: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA я [//www.gnu.org/licenses/old-licenses/gpl-2.0.html еша и онлайнехь].", "config-no-fts3": "'''Тергам бе''': SQLite гулйина хуттург йоцуш [//sqlite.org/fts3.html FTS3] — лахар болхбеш хир дац оцу бухца.", - "config-no-cli-uri": "'''ДӀахьедар''': --scriptpath параметр язйина яц, иза Ӏад йитарца лелош ю: $1 .", + "config-no-cli-uri": "'''ДӀахьедар''': --scriptpath параметр язйина яц, иза Ӏадйитаран кепаца лелош ю: $1 .", "config-db-name": "Хаамийн базан цӀе:", "config-type-mssql": "Microsoft SQL Server", "config-header-mssql": "Microsoft SQL Server параметраш", @@ -66,9 +66,9 @@ "config-logo": "Логотипан URL:", "config-cc-again": "Хьаржа кхин цӀа…", "config-skins": "Кечяран тема", - "config-skins-use-as-default": "ХӀара тема Ӏад йитарца лелае", + "config-skins-use-as-default": "ХӀара тема Ӏадйитаран кепара лелае", "config-skins-must-enable-some": "Ахьа цхьаъ мукъа тема латина йита езаш ю.", - "config-skins-must-enable-default": "Ӏад йитарца йолу тема латина хила еза.", + "config-skins-must-enable-default": "Ӏадйитаран кепаца йолу тема латина хила еза.", "config-install-step-done": "кхочушдина", "config-install-step-failed": "тар цаделира", "config-install-user": "Декъашхочун хаамийн база кхоллар", diff --git a/includes/installer/i18n/lij.json b/includes/installer/i18n/lij.json index 0f692d4da0..0ecc09781a 100644 --- a/includes/installer/i18n/lij.json +++ b/includes/installer/i18n/lij.json @@ -134,5 +134,179 @@ "config-postgres-old": "Ghe voeu MySQL $1 ò 'na verscion succesciva. Ti ti g'hæ a $2.", "config-mssql-old": "Ghe voeu Microsoft SQL Server $1 ò succescivo. Ti ti g'hæ a verscion $2.", "config-sqlite-name-help": "Çerni un nomme ch'o l'identiffiche a to wiki.\nNo doeuviâ spaÇçi ò trattin.\nQuesto o serviâ pe-o nomme do file di dæti SQLite.", - "config-sqlite-parent-unwritable-group": "No se poeu creâ a directory dæti $1, percose a directory supeiô $2 a no l'è scrivibbile da-o webserver.\n\nO programma d'instalaÇion o l'ha determinou l'utente con chi o serviou web o l'è in esecuçion.\nDagghe a poscibilitæ de scrive inta directory $3 pe continoâ.\nInsce un scistema Unix/Linux fanni:\n\n
    cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
    " + "config-sqlite-parent-unwritable-group": "No se poeu creâ a directory dæti $1, percose a directory supeiô $2 a no l'è scrivibbile da-o webserver.\n\nO programma d'instalaÇion o l'ha determinou l'utente con chi o serviou web o l'è in esecuçion.\nDagghe a poscibilitæ de scrive inta directory $3 pe continoâ.\nInsce un scistema Unix/Linux fanni:\n\n
    cd $2\nmkdir $3\nchgrp $4 $3\nchmod g+w $3
    ", + "config-sqlite-parent-unwritable-nogroup": "No se poeu creâ a directory dæti $1, percose a directory supeiô $2 a no l'è scrivibbile da-o webserver.\n\nO programma d'instalaçion o no l'ha posciuo determinâ l'utente con chi o serviou web o l'è in esecuçion.\nRendi a directory $3 scrivibbile globalmente, da esso (e da atri) pe continoâ.\nInsce un scistema Unix/Linux fanni:\n\n
    cd $2\nmkdir $3\nchmod a+w $3
    ", + "config-sqlite-mkdir-error": "Errô durante a creaçion da directory dæti \"$1\".\nControlla a poxiçion e proeuva torna.", + "config-sqlite-dir-unwritable": "Imposcibile scrive inta directory \"$1\".\nCangia i aotoizaçioin de mainea che o webserver o ghe posse scrive e proeuva torna.", + "config-sqlite-connection-error": "$1.\n\nControlla a directory dæti e o nomme do database chì de sotta, e proeuva torna.", + "config-sqlite-readonly": "O file $1 o no l'è scrivibbile.", + "config-sqlite-cant-create-db": "Imposcibile creâ o file do database $1 .", + "config-sqlite-fts3-downgrade": "A-o PHP gh'amanca o suporto FTS3, declassamento tabelle in corso", + "config-can-upgrade": "Gh'è de tabelle da MediaWiki in questo database.\nPe agiornâle a MediaWiki $1, clicca insce '''continnoa'''.", + "config-upgrade-done": "Agiornamento completo.\n\nAoa ti poeu [$1 començâ a doeuviâ a to wiki].\n\nSe t'oeu rigenerâ o to file LocalSettings.php, clicca in sciô pomello de sotta. Questa opiaçion '''a no l'è racomandâ''', a meno che no ti gh'aggi di problemi co-a to wiki.", + "config-upgrade-done-no-regenerate": "Agiornamento completo.\n\nAoa ti poeu [$1 començâ a doeuviâ a to wiki].", + "config-regenerate": "Rigennera LocalSettings.php →", + "config-show-table-status": "A query SHOW TABLE STATUS a l'è fallia!", + "config-unknown-collation": "'''Atençion:''' o database o doeuvia de reggole de confronto non riconosciue.", + "config-db-web-account": "Account do database pe l'acesso web", + "config-db-web-help": "Seleçion-a o nomme utente e a password che o serviou web o l'adoeuviâ pe conettise a-o serviou de database, durante o normale fonçionamento da wiki.", + "config-db-web-account-same": "Doeuvia o mæximo account de l'instalaçion", + "config-db-web-create": "Crea l'account s'o no l'existe ancon", + "config-db-web-no-create-privs": "L'account doeuviou pe l'installaçion o no dispon-e di privileggi necessai pe creâ un atro account.\nL'account indicou chì o deve za existe.", + "config-mysql-engine": "Motô d'archiviaçion:", + "config-mysql-innodb": "InnoDB", + "config-mysql-myisam": "MyISAM", + "config-mysql-myisam-dep": "Atençion: t'hæ seleçionou MyISAM comme motô d'archiviaçion pe MySQL, ch'o no l'è racomandou pe l'uso con MediaWiki, percose:\n* o supporta debolmente a concorença pe-o blocco da tabella\n* o l'è ciu inclinou a-a corruçion di atri motoî\n* o codiçe de base MediaWiki o no gestisce sempre MyISAM comm'o doviæ\n\nSe a to instalaçion MySQL a supporta InnoDB, l'è atamente racomandou che ti o çerni a-o so posto.\nSe a to installaçion MySQL a no supporta InnoDB, foscia l'è o momento pe 'n agiornamento.", + "config-mysql-only-myisam-dep": "Atençion: MyISAM o l'è l'unnico motô d'archiviaçion disponibbile pe MySQL insce sta macchina, e questo no l'è consegiou pe doeuviâlo con MediaWiki, percose:\n* o supporta debolmente a concorenza pe-o blocco da tabella\n* o l''è ciu inclinou a-a corruçion di atri motoî\n* o coddiçe de base MediaWiki MyISAM o no-o gestisce sempre comm'o doviæ\n\nS'a to installaçion MySQL a no supporta InnoDB, foscia l'è o momento pe 'n agiornamento.", + "config-mysql-engine-help": "InnoDB o l'è quæxi sempre a megio opçion, in quante o g'ha 'n bon supporto da concorença.\n\nMyISAM o poriæ vese ciu veloçe inte installaçioin mono-utente ò in sola-lettua.\nI database MyISAM tendan a dannezâse ciu soventi di database InnoDB.", + "config-mysql-charset": "Set di caratteri do database:", + "config-mysql-binary": "Binaio", + "config-mysql-utf8": "UTF-8", + "config-mysql-charset-help": "In modalitæ binaia, MediaWiki a l'archivvia o testo UTF-8 into database in campi binai.\nQuest'o l'è ciu efficaçe che a modalitæ UTF-8 do MySQL, e o consente de doeuviâ a gamma completa de caratteri Unicode.\n\nIn modalitæ UTF-8, MySQL o saviâ inte quæ set de caratteri l'è che son i to dæti, e o poriâ presentâli e convertîli in moddo apropiou, ma o no te permetiâ de memorizâ i caratteri de d'ato a-o [https://en.wikipedia.org/wiki/Mapping_of_Unicode_character_planes Cian de base murtilenguistego].", + "config-mssql-auth": "Tipo d'aotenticaçion:", + "config-mssql-install-auth": "Seleçion-a o tipo d'aotenticaçion ch'o saiâ doeuviou pe conettise a-o database durante o processo de instalaçion.\nSe ti seleçion-i \"{{int:config-mssql-windowsauth}}\", saiâ doeuviou e credençiæ de quæ se segge utente segge aproeuv'a fâ giâ o serviou web.", + "config-mssql-web-auth": "Seleçion-a o tipo d'aotenticaçion che o serviou web o doeuviâ pe conettise a-o database. \nSe ti seleçion-i \"{{int:config-mssql-windowsauth}}\", saiâ doeuviou e credençiæ de quæ se segge utente segge aproeuv'a fâ giâ o serviou web.", + "config-mssql-sqlauth": "Aotenticaçion de SQL Server", + "config-mssql-windowsauth": "Aotenticaçion de Windows", + "config-site-name": "Nomme da wiki:", + "config-site-name-help": "Questo saiâ vixualizou inta bara do tittolo do navegatô e in atri varri recanti.", + "config-site-name-blank": "Inseisci o nomme de 'n scito.", + "config-project-namespace": "Namespace do progetto:", + "config-ns-generic": "Progetto", + "config-ns-site-name": "Pægio che o nomme do wiki: $1", + "config-ns-other": "Atro (specificâ)", + "config-ns-other-default": "MyWiki", + "config-project-namespace-help": "Aproeuvo a l'exempio da Wikipedia, molte wiki tegnan e so paggine co-e reggole separæ da-e paggine de contegnuo, inte 'n '''namespace de progetto'''.\nTutti i tittoli de paggine inte sto namespace començan co-in çerto prefisso, che ti poeu indicâ chie.\nA l'uzo, sto prefisso o deriva da-o nomme da wiki, ma o no poeu contegnî di caratteri de pontezatua comme \"#\" ò \":\".", + "config-ns-invalid": "O namespace indicou \"$1\" o no l'è vallido.\nSpecificâ un despægio namespace de progetto.", + "config-ns-conflict": "O namespace indicou \"$1\" o l'è in conflito co-in namespace predefinio MediaWiki.\nSpecificâ un despægio namespace de progetto.", + "config-admin-box": "Account amministratô", + "config-admin-name": "O to nomme utente:", + "config-admin-password": "Poula segretta:", + "config-admin-password-confirm": "Ripeti a poula segretta:", + "config-admin-help": "Inseisci chì o to nomme utente prefeio, presempio \"Giobatta Parodi\".\nSto chì o l'è o nomme che ti doeuviæ pe acede a-a wiki.", + "config-admin-name-blank": "Inseisci un nomme pe l'aministratô.", + "config-admin-name-invalid": "O nomme utente indicou \"$1\" o no l'è vallido.\nSpecificâ un nomme utente despægio.", + "config-admin-password-blank": "Inseisci 'na password pe l'account d'aministratô.", + "config-admin-password-mismatch": "E doe password inseie no corispondan.", + "config-admin-email": "Adresso e-mail:", + "config-admin-email-help": "Inseisci chì 'n adresso e-mail pe poei riçeive di e-mail da-i atri utenti da wiki, rimpostâ a to password, e vese informou de modiffiche aportæ a-e to paggine sotta oservaçion. Se no te interessa, ti poeu lasciâ voeuo questo campo.", + "config-admin-error-user": "Erô interno durante a creaçion de 'n aministratô co-o nomme \"$1\".", + "config-admin-error-password": "Erô interno durante l'impostaçion de 'na password pe aministratô \"$1\":
    $2
    ", + "config-admin-error-bademail": "T'hæ inseio un adresso e-mail non vallido.", + "config-subscribe": "Sottoscrivi a [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce mailing list di anonçi de release].", + "config-subscribe-help": "Se tratta de 'na mailing list a basso traffego dedicâ a-i anonçi de sciortie de noeuve verscioin, compreize de importante segnalaçioin pe-a segueçça.\nSe conseggia de inscrivise e agiornâ a proppia instalaçion de MediaWiki quande sciorte 'na noeuva verscion.", + "config-subscribe-noemail": "T'hæ provou a inscrivite a-a mailing list dedicâ a-i anonçi de noeuve verscioin sença fornî un adresso e-mail.\nInseisci un adresso e-mail se ti dexidei efetoâ l'inscriçion a-a mailing list.", + "config-pingback": "Condividdi i dæti insce questa installaçion co-i svilupatoî da MediaWiki.", + "config-pingback-help": "Se ti seleçion-i questa opçion, MediaWiki a contattiâ periodicamente https://www.mediawiki.org co-i dæti base insce questa instançia MediaWiki. Queta categoria de dæti a l'includde, prexempio, o tipo de scistema, a verscion de PHP e o database de backend çernuo. A Wikimedia Foundation a condividde questi dæti co-i sviluppatoî Mediawiki pe agiutâla a guidâ i futuri sforsci de sviluppo. Pe-o to scistema saiâ inviou i seguenti dæti:\n
    $1
    ", + "config-almost-done": "T'hæ quæxi a tio!\nAoa ti poeu sâtâ a restante parte da configuaçion e instalâ a wiki subbito.", + "config-optional-continue": "Famme di atre domande.", + "config-optional-skip": "Son za stuffo, installa a wiki e basta.", + "config-profile": "Profî di driti utente:", + "config-profile-wiki": "Wiki averta", + "config-profile-no-anon": "Creaçion utença obrigatoia", + "config-profile-fishbowl": "Solo utenti aotorizæ", + "config-profile-private": "Wiki privâ", + "config-profile-help": "E wiki fonçion-an megio se ti permetti a tante person-e de poeili modificâ.\nIn MediaWiki, l'è sempliçe controlâ i urtime modiffiche, e ripristinâ i danni caosæ da di utenti inesperti ò malintençionæ.\n\nTuttavia, tanti han trovou a MediaWiki uttile inte 'n'ampia varietæ de rolli, e de volte no l'è faççile convinçe tutti di vantaggi da modalitæ wiki.\nPerciò, fanni a to scelta.\n\nO modello {{int:config-profile-wiki}} o consente a chi se segge de modificâ, anche sença efetoâ l'acesso.\nUna wiki con {{int:config-profile-no-anon}} a l'ofre 'na magiô responsabilitæ, ma a poriæ scoragî i contributoî ocaxonæ.\n\nO scenario {{int:config-profile-fishbowl}} o consente a-i utenti aotorizæ de modificâ, ma o pubbrico o poeu vixualizâ e paggine, compreiso a cronologia.\nUn {{int:config-profile-private}} o consente solo ch'a-i utenti aotorizæ de vixualizâ e paggine, o mæximo groppo o poeu modificâle.\n\nDe configuaçioin di driti utente ciu complesse son disponibbile doppo l'instalaçion, amia a [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights parte relativa do manoâ].", + "config-license": "Driti d'aotô e liçençia:", + "config-license-none": "Nisciun pê de paggina pe-a liçençia", + "config-license-cc-by-sa": "Creative Commons Attribuçion-Condividdi pægio", + "config-license-cc-by": "Creative Commons Attribuçion", + "config-license-cc-by-nc-sa": "Creative Commons Attribuçion-Non comerciale-Condividdi Pægio", + "config-license-cc-0": "Creative Commons Zero (pubbrico dominnio)", + "config-license-gfdl": "GNU Free Documentation License 1.3 o verscioin sucescive", + "config-license-pd": "Pubbrico dominnio", + "config-license-cc-choose": "Seleçion-a un-a de liçençie Creative Commons", + "config-license-help": "Tante wiki pubbriche rilascian i so contributi co-ina [http://freedomdefined.org/Definition liçençia libbera]. Sto fæto o l'agiutta a creâ un senso de propietæ condivisa inta comunitæ e o l'incoragisce a contriboî a longo termine. O no l'è generalmente necessaio pe 'na wiki privâ ò aziendale.\n\nSe ti voeu doeuviâ di scriti da Wikipedia, ò ti dexiddei che a Wikipedia a posse vese in graddo de acetâ di scriti copiæ da-a to wiki, ti doviesci scellie {{int:config-license-cc-by-sa}}.\n\nIn precedença a Wikipedia a l'ha doeuviou a GNU Free Documentation License. A GFDL a l'è 'na liçençia vallida, ma a l'è difiççile da capî e a complica o riutilizzo di contegnui.", + "config-email-settings": "Impostaçioin e-mail", + "config-enable-email": "Abillita a sciortia da posta elettronica", + "config-enable-email-help": "Se ti voeu che fonçion-e l'e-mail, e [http://www.php.net/manual/en/mail.configuration.php PHP's impostaçioin della posta] dev'esan configuæ corettamente.\nSe non ti dexiddei arcun-a fonçionalitæ de posta eletronnica, ti a poeu disabilitâ chie.", + "config-email-user": "Abillita e-mail fra utenti", + "config-email-user-help": "Consente a tutti i utenti de inviâse l'un l'atro l'e-mail, se l'han abilitou inte so preferençe.", + "config-email-usertalk": "Abillita e notiffiche pe-e paggine de discuscion utente", + "config-email-usertalk-help": "Consente a-i utenti de riçeive de notiffiche pe-e modiffiche de so paggine de discuscion, se l'han abilitou inte so preferençe.", + "config-email-watchlist": "Abillita e notiffiche pe-a lista sott'oservaçion", + "config-email-watchlist-help": "Consente a-i utenti de riçeive de notiffiche pe-e pagine da lista sott'oservaçion, se l'han abilitou inte so preferençe.", + "config-email-auth": "Abillita aotenticaçion via e-mail", + "config-email-auth-help": "Se questa opçion a l'è attivâ, i utenti dovian confermâ o so adresso e-mail doeouviando un ingancio ch'o ven inviou ogni votta che l'impostan ò o cangian.\nSolo i adressi de posta elettronica aotenticæ poeuan riçeive de e-mail da di atri utenti ò cangiâ e e-mail de notiffica.\nImpostâ st'opçion l'è raccomandou pe-e wiki pubbriche pe via do potençiale abuso dee fonçioin de posta elettronica.", + "config-email-sender": "Adresso e-mail de ritorno:", + "config-email-sender-help": "Inseisci l'adresso e-mail da doeuviâ comme adresso de ritorno pe-a posta ch'a sciorte.\nChì l'è donde ghe saiâ inviou i eventoali eroî.\nMolti server de posta richiedan che armeno a parte do nomme de dominnio a segge vallida.", + "config-upload-settings": "Caregamenti de inmaggine e file", + "config-upload-enable": "Consentî o caregamento di file", + "config-upload-help": "O caregamento di file o poriæ espon-e o to serviou a di reizeghi de segueçça.\nPe magioî informaçioin, lezi a [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security seçion in sciâ segueçça] into manoâ.\n\nPe consentî o caregamento di file, modiffica a modalitæ inta sottodirectory images da directory prinçipâ da MediaWiki coscì che o serviou web o posse scrive lì.\nPoi attiva questa opçion.", + "config-upload-deleted": "Directory pe-i file scassæ:", + "config-upload-deleted-help": "Çerni 'na directory onde archiviâ i file scassæ.\nIdealmente, questa a no doviæ ese accescibbile da-o web.", + "config-logo": "URL do logo:", + "config-logo-help": "O tema predefinio da MediaWiki o l'includde o spaççio pe 'n logo de 135 x 160 pixel sorve o menù laterâ.\nCarrega 'n'inmaggine de dimenscioin apropiæ e inseisci l'URL chie.\n\nL'è poscibbile doeuviâ $wgStylePath o $wgScriptPath se o logo o l'è relativo a sti percorsci.\n\nSe un logo no ti o voeu, lascia sta casella voeua.", + "config-instantcommons": "Abillita Instant Commons", + "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] a l'è 'na fonçionalitæ ch'a consente a-i wiki de doeuviâ inmaggine, soin e atri file murtimediæ ch'atroæ 'n sciô scito [https://commons.wikimedia.org/ Wikimedia Commons].\nPe fâ questo, a MediaWiki a richiede l'accesso a l'Internet.\n\nPe ciu informaçioin insce sta fonçionalitæ, con tanto de instruçioin sciu comme configuâlo pe de wiki despæge da-a Wikimedia Commons, consurtæ [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos o manoâ].", + "config-cc-error": "O selettô de liçençie Creative Commons o no l'ha reizo arcun resultato.\nInseisci manoalmente o nomme da liçençia.", + "config-cc-again": "Çerni torna...", + "config-cc-not-chosen": "Çerni quæ liçençia Creative Commons ti voeu e sciacca \"proceed\".", + "config-advanced-settings": "Configuaçion avançâ", + "config-cache-options": "Impostaçioin pe-a cache di ogetti:", + "config-cache-help": "A memorizaçion di ogetti inta cache a serve pe fâ anâ ciu fito a MediaWiki sarvando inta cache i dæti che ti doeuvi soventi.\nPe di sciti de dimenscioin mezan-e e grende l'è cadamente consegiou attivâ a cache, ma pe-i piccin ascì se ne vediâ i benefiççi.", + "config-cache-none": "Nisciun-a memorizaçion inta cache (nisciun-a fonçionalitæ a l'è impedia, ma in scî sciti wiki ciu grendi a veloçitæ a poriæ risentîne)", + "config-cache-accel": "Mette in cache ogetti PHP (APC, APCu, XCache ò WinCache)", + "config-cache-memcached": "Doeuvia Memcached (a richiede urteioî attivitæ de instalaçion e configuaçion)", + "config-memcached-servers": "Serviou de Memcached:", + "config-memcached-help": "Lista de adressi IP da doeuviâ pe Memcached.\nTi doviesci specificâne un pe riga e indicâ a porta da doeuviâ. Presempio:\n 127.0.0.1:11211\n 192.168.1.25:1234", + "config-memcache-needservers": "L'è stæto seleçionou o tipo de caching Memcached, ma no l'è stæto impostou arcun serviou.", + "config-memcache-badip": "L'è stæto inseio un adresso IP non vallido pe Memcached: $1.", + "config-memcache-noport": "Non l'è stæto specificou 'na porta da doeuviâ pe-o serviou Memcached: $1.\nSe no ti sæ quæ a l'è a porta, o valô pe difetto o l'è 11211.", + "config-memcache-badport": "I nummeri de porta pe Memcached dovieivan ese tra $1 e $2.", + "config-extensions": "Estenscioin", + "config-extensions-help": "I estenscioin elencæ de d'ato son stæte rilevæ inta to directory ./extensions.\n\nQueste poririvan richiede 'n urteiô configuaçion, ma l'è poscibbile attivâle aoa", + "config-skins": "Pelle", + "config-skins-help": "E pelle elencæ de d'ato son stæte rilevæ inta to directory ./skins. Ti devi attivâne aomanco un-a e scellie quella predefinia.", + "config-skins-use-as-default": "Doeuvia sta pelle comme predefinia", + "config-skins-missing": "No l'è stæto trovou arcun-a pelle; a MediaWiki a l'adoeuviâ 'na soluçion de ripiego pe scin che no ti ne instaliæ un-a apropiâ.", + "config-skins-must-enable-some": "Ti devi çerne armeno 'na pelle da attivâ.", + "config-skins-must-enable-default": "A pelle çernua comme predefinia a dev'ese attivâ.", + "config-install-alreadydone": "'''Attençion:''' pâ che t'aggi za instalou a MediaWiki e ti çerchi de instalâla torna.\nProcedi a-a paggina succesciva.", + "config-install-begin": "Sciacando \"{{int:config-continue}}\", t'inçiæ l'instalaçion da MediaWiki.\nSe primma ti voesci fâ di atri cangiamenti, premmi \"{{int:config-back}}\".", + "config-install-step-done": "fæto", + "config-install-step-failed": "no ariescio", + "config-install-extensions": "Estenscioin compreize", + "config-install-database": "Configuaçion do database", + "config-install-schema": "Creaçion do schema", + "config-install-pg-schema-not-exist": "O schema PostgreSQL o no l'existe.", + "config-install-pg-schema-failed": "Creaçion tabelle non riuscia.\nAsseguite che l'utente \"$1\" o posse scrive into schema \"$2\".", + "config-install-pg-commit": "Commetti i cangiamenti", + "config-install-pg-plpgsql": "Controllo do lenguaggio PL/pgSQL", + "config-pg-no-plpgsql": "Bezoeugna che t'installi o lenguaggio PL/pgSQL into database $1", + "config-pg-no-create-privs": "L'utença indicâ pe l'instalaçion a no dispon-e di privileggi necessai pe creâ 'n'utença.", + "config-pg-not-in-role": "L'utença indicâ pe l'utente web a l'existe za.\nL'utença indicâ pe l'instalaçion a no l'è un super-utente e a no l'è un membro do rollo di utenti web, quindi a no l'è in graddo de creâ di ogetti de propietæ de l'utente web.\n\nMediaWiki atoalmente a richiede che e tabelle seggian de propietæ de l'utente web. Indica 'n atro account web, ò clicca \"inderê\" e speciffica un utente pe l'instalaçion oportunamente privilegiou.", + "config-install-user": "Creaçion de utente do database", + "config-install-user-alreadyexists": "L'utente $1 o l'existe za.", + "config-install-user-create-failed": "Creaçion de l'utente \"$1\" no ariescia: $2", + "config-install-user-grant-failed": "Erô durante a concescion de l'aotorizaçion a l'utente \"$1\": $2", + "config-install-user-missing": "L'utente indicou \"$1\" o no l'existe.", + "config-install-user-missing-create": "L'utente indicou \"$1\" o no l'existe.\nSeleçion-a a casella \"crea utença\" chì de sotta, se ti a voeu creâ.", + "config-install-tables": "Creaçion tabelle", + "config-install-tables-exist": "'''Atençion:''' pâ che e tabelle da MediaWiki ghe seggian za.\nSato a creaçion.", + "config-install-tables-failed": "'''Erô''': a creaçion da tabella a no l'è ariescia: $1", + "config-install-interwiki": "Impimento da tabella interwiki predefinia", + "config-install-interwiki-list": "Imposcibbile leze o file interwiki.list.", + "config-install-interwiki-exists": "'''Atençion:''' pâ che inta tabella interwiki ghe segge za di elementi.\nA lista predefinia a se sata.", + "config-install-stats": "Iniçializaçion de statisteghe", + "config-install-keys": "Generaçion de ciave segrette", + "config-insecure-keys": "'''Atençion:''' {{PLURAL:$2|Una ciave segûa|De ciave segûe}} ($1) {{PLURAL:$2|generâ|generæ}} durante l'instalaçion {{PLURAL:$2|a|}} no {{PLURAL:$2|l'è|son}} completamente {{PLURAL:$2|segûa|segûe}}. Consciddera de cangiâ{{PLURAL:$2|la|le}} manoalmente.", + "config-install-updates": "Impedî l'esecuçion di agiornamenti non necessai", + "config-install-updates-failed": "Erô: l'inseimento de ciave de agiornamento inte tabelle o no l'è ariescio pe-o seguente erô: $1", + "config-install-sysop": "Creaçion de l'utença pe l'aministratô", + "config-install-subscribe-fail": "Imposcibbile sottoscrive mediawiki-announce: $1", + "config-install-subscribe-notpossible": "cURL o no l'è instalou e allow_url_fopen o no l'è disponibbile.", + "config-install-mainpage": "Creaçion da paggina prinçipâ con contegnuo predefinio", + "config-install-mainpage-exists": "A paggina prinçipâ a l'existe za, ignorou", + "config-install-extension-tables": "Creaçion de tabelle pe i estenscioin attivæ", + "config-install-mainpage-failed": "Imposcibbile insei a paggina prinçipâ: $1", + "config-install-done": "Complimenti!\nT'hæ instalou MediaWiki.\n\nO programma d'instalaçion o l'ha generou un file LocalSettings.php ch'o conten tutte e impostaçioin.\n\nTi devi scaregâlo e inseilo inta directory base da to wiki (a mæxima dovve gh'è index.php). O download o doviæ partî aotomaticamente.\n\nSe o download o no s'inandia, ò s'o l'è stæto annulou, ti poeu ricomençâ clicando in sce l'ingancio chì aproeuvo:\n\n$3\n\nNotta: se ti sciorti aoa da l'installaçion sença scaregâ o file de configuaçion ch'o l'è stæto generou, questo doppo o no saiâ ciu disponibbile.\n\nQuande t'hæ fæto, ti poeu [$2 intrâ inta to wiki].", + "config-install-done-path": "Complimenti!\nT'hæ instalou MediaWiki.\n\nO programma d'instalaçion o l'ha generou un file LocalSettings.php ch'o conten tutte e impostaçioin.\n\nTi devi scaregâlo e inseilo in $4. O download o doviæ partî aotomaticamente.\n\nSe o download o no s'inandia, ò s'o l'è stæto annulou, ti poeu inandiâlo torna clicando in sce l'ingancio chì de sotta:\n\n$3\n\nNotta: se ti sciorti aoa da l'installaçion sença scaregâ o file de configuaçion ch'o l'è stæto generou, questo doppo o no saiâ ciu disponibbile.\n\nQuande t'hæ fæto, ti poeu [$2 intrâ inta to wiki].", + "config-download-localsettings": "Scarega LocalSettings.php", + "config-help": "agiutto", + "config-help-tooltip": "clicca pe espande", + "config-nofile": "O file \"$1\" o no poeu vese atrovou. O l'è stæto eliminou?", + "config-extension-link": "Ti o saveivi che o to wiki o suporta i [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions estenscioin]?\n\nTi poeu navegâ tra i [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category estenscioin pe categoria].", + "mainpagetext": "MediaWiki o l'è stæto instalou.", + "mainpagedocfooter": "Consurta a [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents guidda utente] pe ciu informaçioin in sce l'uso de questo software wiki.\n\n== Pe començâ ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Impostaçioin de configuaçion]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Domande frequente sciu MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Mailing list anonçi MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Troeuva MediaWiki inta to lengoa]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Imprendi a combatte o spam in sciâ to wiki]" } diff --git a/includes/installer/i18n/mk.json b/includes/installer/i18n/mk.json index b8dab21924..728a9a8708 100644 --- a/includes/installer/i18n/mk.json +++ b/includes/installer/i18n/mk.json @@ -3,7 +3,8 @@ "authors": [ "Bjankuloski06", "아라", - "Macofe" + "Macofe", + "Srdjan m" ] }, "config-desc": "Воспоставувачот на МедијаВики", @@ -58,7 +59,7 @@ "config-pcre-old": "'''Кобно:''' Се бара PCRE $1 или понова верзија.\nВашиот PHP-бинарен е сврзан со PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Повеќе информации].", "config-pcre-no-utf8": "Кобно: PCRE-модулот на PHP е срочен без поддршка за PCRE_UTF8.\nМедијаВики бара поддршка за UTF-8 за да може да работи правилно.", "config-memory-raised": "memory_limit за PHP изнесува $1, зголемен на $2.", - "config-memory-bad": "'''Предупредување:''' memory_limit за PHP изнесува $1.\nОва е веројатно премалку.\nВоспоставката може да не успее!", + "config-memory-bad": "Предупредување: memory_limit за PHP изнесува $1.\nОва е веројатно премалку.\nВоспоставката може да не успее!", "config-xcache": "[http://xcache.lighttpd.net/ XCache] е воспоставен", "config-apc": "[http://www.php.net/apc APC] е воспоставен", "config-apcu": "[http://www.php.net/apcu APCu] е воспоставен", @@ -71,7 +72,7 @@ "config-imagemagick": "Пронајден е ImageMagick: $1.\nАко овозможите подигање, тогаш ќе биде овозможена минијатуризација на сликите.", "config-gd": "Утврдив дека има вградена GD графичка библиотека.\nАко овозможите подигање, тогаш ќе биде овозможена минијатураизација на сликите.", "config-no-scaling": "Не можев да пронајдам GD-библиотека или ImageMagick.\nМинијатуризацијата на сликите ќе биде оневозможена.", - "config-no-uri": "'''Грешка:''' Не можев да го утврдам тековниот URI.\nВоспоставката е откажана.", + "config-no-uri": "Грешка: Не можев да го утврдам тековниот URI.\nВоспоставката е откажана.", "config-no-cli-uri": "'''Предупредување''': Нема наведено --scriptpath. Ќе се користи основниот: $1.", "config-using-server": "Користите опслужувач под името „$1“.", "config-using-uri": "Користите опслужувач со URL-адреса „$1$2“.", @@ -104,7 +105,7 @@ "config-db-port": "Порта на базата:", "config-db-schema": "Шема за МедијаВики", "config-db-schema-help": "Оваа шема обично по правило ќе работи нормално.\nСменете ја само ако знаете дека треба да се смени.", - "config-pg-test-error": "Не можам да се поврзам со базата '''$1''': $2", + "config-pg-test-error": "Не можам да се поврзам со базата $1: $2", "config-sqlite-dir": "Папка на SQLite-податоци:", "config-sqlite-dir-help": "SQLite ги складира сите податоци во една податотека.\n\nПапката што ќе ја наведете мора да е запислива од мрежниот опслужувач во текот на воспоставката.\n\nТаа '''не''' смее да биде достапна преку семрежјето, и затоа не ја ставаме кајшто ви се наоѓаат PHP-податотеките.\n\nВоспоставувачот воедно ќе создаде податотека .htaccess, но ако таа не функционира како што треба, тогаш некој ќе може да ви влезе во вашата необработена (сирова) база на податоци.\nТука спаѓаат необработени кориснички податоци (е-поштенски адреси, хеширани лозинки) како и избришани преработки и други податоци за викито до кои се има ограничен пристап.\n\nСе препорачува целата база да ја сместите некаде, како на пр. /var/lib/mediawiki/вашетовики.", "config-oracle-def-ts": "Стандарден таблеарен простор:", diff --git a/includes/installer/i18n/pt.json b/includes/installer/i18n/pt.json index 779a32fe8f..d68e5a2e8f 100644 --- a/includes/installer/i18n/pt.json +++ b/includes/installer/i18n/pt.json @@ -24,11 +24,11 @@ "config-desc": "O instalador do MediaWiki", "config-title": "Instalação da MediaWiki $1", "config-information": "Informação", - "config-localsettings-upgrade": "Foi detectado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, por favor introduza o valor de $wgUpgradeKey na caixa abaixo.\nEncontra este valor em LocalSettings.php.", - "config-localsettings-cli-upgrade": "Foi detectado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, execute o update.php, por favor", + "config-localsettings-upgrade": "Foi detetado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, por favor introduza o valor de $wgUpgradeKey na caixa abaixo.\nEncontra este valor em LocalSettings.php.", + "config-localsettings-cli-upgrade": "Foi detetado um ficheiro LocalSettings.php.\nPara atualizar esta instalação, execute o update.php, por favor", "config-localsettings-key": "Chave de atualização:", "config-localsettings-badkey": "A chave de atualização que forneceu está incorreta.", - "config-upgrade-key-missing": "Foi detectada uma instalação existente do MediaWiki.\nPara atualizar esta instalação, por favor coloque a seguinte linha no final do seu LocalSettings.php:\n\n$1", + "config-upgrade-key-missing": "Foi detetada uma instalação existente do MediaWiki.\nPara atualizar esta instalação, por favor coloque a seguinte linha no final do seu LocalSettings.php:\n\n$1", "config-localsettings-incomplete": "O ficheiro LocalSettings.php existente parece estar incompleto.\nA variável $1 não está definida.\nPor favor, defina esta variável no LocalSettings.php e clique \"{{int:Config-continue}}\".", "config-localsettings-connection-error": "Ocorreu um erro ao ligar à base de dados usando as configurações especificadas no LocalSettings.php. Por favor corrija essas configurações e tente novamente.\n\n$1", "config-session-error": "Erro ao iniciar a sessão: $1", @@ -37,7 +37,7 @@ "config-your-language": "A sua língua:", "config-your-language-help": "Selecione o idioma que será usado durante o processo de instalação.", "config-wiki-language": "Língua da wiki:", - "config-wiki-language-help": "Selecione o idioma que será predominante na wiki.", + "config-wiki-language-help": "Selecione a língua que será predominante na wiki.", "config-back": "← Voltar", "config-continue": "Continuar →", "config-page-language": "Língua", @@ -275,7 +275,7 @@ "config-memcache-noport": "Não especificou a porta a usar para o servidor Memcached: $1.\nSe não sabe qual é a porta, a predefinida é a 11211.", "config-memcache-badport": "Os números das portas do Memcached devem estar entre $1 e $2.", "config-extensions": "Extensões", - "config-extensions-help": "Foi detectada a existência das extensões listadas acima, no seu diretório ./extensions.\n\nEstas talvez necessitem de configurações adicionais, mas pode ativá-las agora", + "config-extensions-help": "Foi detetada a existência das extensões listadas acima, no seu diretório ./extensions.\n\nEstas talvez necessitem de configurações adicionais, mas pode ativá-las agora.", "config-skins": "Temas", "config-skins-help": "Os temas listados abaixo foram detetados no seu diretório ./skins. Deverá ativar pelo menos um e escolher qual o escolhido por padrão.", "config-skins-use-as-default": "Usar este tema como padrão", diff --git a/includes/jobqueue/JobQueueDB.php b/includes/jobqueue/JobQueueDB.php index 5e457300e9..9b9928d1f3 100644 --- a/includes/jobqueue/JobQueueDB.php +++ b/includes/jobqueue/JobQueueDB.php @@ -185,13 +185,16 @@ class JobQueueDB extends JobQueue { * @return void */ protected function doBatchPush( array $jobs, $flags ) { - DeferredUpdates::addUpdate( new AutoCommitUpdate( - wfGetDB( DB_MASTER ), - __METHOD__, - function ( IDatabase $dbw, $fname ) use ( $jobs, $flags ) { - $this->doBatchPushInternal( $dbw, $jobs, $flags, $fname ); - } - ) ); + DeferredUpdates::addUpdate( + new AutoCommitUpdate( + $this->getMasterDB(), + __METHOD__, + function ( IDatabase $dbw, $fname ) use ( $jobs, $flags ) { + $this->doBatchPushInternal( $dbw, $jobs, $flags, $fname ); + } + ), + DeferredUpdates::PRESEND + ); } /** diff --git a/includes/jobqueue/JobRunner.php b/includes/jobqueue/JobRunner.php index 0a0e9e0eb0..49b7a459a6 100644 --- a/includes/jobqueue/JobRunner.php +++ b/includes/jobqueue/JobRunner.php @@ -38,6 +38,8 @@ use Wikimedia\Rdbms\DBReplicationWaitError; * @since 1.24 */ class JobRunner implements LoggerAwareInterface { + /** @var Config */ + protected $config; /** @var callable|null Debug output handler */ protected $debug; @@ -74,6 +76,7 @@ class JobRunner implements LoggerAwareInterface { $logger = LoggerFactory::getInstance( 'runJobs' ); } $this->setLogger( $logger ); + $this->config = MediaWikiServices::getInstance()->getMainConfig(); } /** @@ -101,7 +104,8 @@ class JobRunner implements LoggerAwareInterface { * @return array Summary response that can easily be JSON serialized */ public function run( array $options ) { - global $wgJobClasses, $wgTrxProfilerLimits; + $jobClasses = $this->config->get( 'JobClasses' ); + $profilerLimits = $this->config->get( 'TrxProfilerLimits' ); $response = [ 'jobs' => [], 'reached' => 'none-ready' ]; @@ -111,7 +115,7 @@ class JobRunner implements LoggerAwareInterface { $noThrottle = isset( $options['throttle'] ) && !$options['throttle']; // Bail if job type is invalid - if ( $type !== false && !isset( $wgJobClasses[$type] ) ) { + if ( $type !== false && !isset( $jobClasses[$type] ) ) { $response['reached'] = 'none-possible'; return $response; } @@ -136,7 +140,7 @@ class JobRunner implements LoggerAwareInterface { // Catch huge single updates that lead to replica DB lag $trxProfiler = Profiler::instance()->getTransactionProfiler(); $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) ); - $trxProfiler->setExpectations( $wgTrxProfilerLimits['JobRunner'], __METHOD__ ); + $trxProfiler->setExpectations( $profilerLimits['JobRunner'], __METHOD__ ); // Some jobs types should not run until a certain timestamp $backoffs = []; // map of (type => UNIX expiry) @@ -289,9 +293,8 @@ class JobRunner implements LoggerAwareInterface { $status = $job->run(); $error = $job->getLastError(); $this->commitMasterChanges( $lbFactory, $job, $fnameTrxOwner ); - // Push lazilly-pushed jobs // Important: this must be the last deferred update added (T100085, T154425) - DeferredUpdates::addCallableUpdate( [ 'JobQueueGroup', 'pushLazyJobs' ] ); + DeferredUpdates::addCallableUpdate( [ JobQueueGroup::class, 'pushLazyJobs' ] ); // Run any deferred update tasks; doUpdates() manages transactions itself DeferredUpdates::doUpdates(); } catch ( Exception $e ) { @@ -363,15 +366,13 @@ class JobRunner implements LoggerAwareInterface { * @see $wgJobBackoffThrottling */ private function getBackoffTimeToWait( Job $job ) { - global $wgJobBackoffThrottling; + $throttling = $this->config->get( 'JobBackoffThrottling' ); - if ( !isset( $wgJobBackoffThrottling[$job->getType()] ) || - $job instanceof DuplicateJob // no work was done - ) { + if ( !isset( $throttling[$job->getType()] ) || $job instanceof DuplicateJob ) { return 0; // not throttled } - $itemsPerSecond = $wgJobBackoffThrottling[$job->getType()]; + $itemsPerSecond = $throttling[$job->getType()]; if ( $itemsPerSecond <= 0 ) { return 0; // not throttled } @@ -519,17 +520,17 @@ class JobRunner implements LoggerAwareInterface { * @throws DBError */ private function commitMasterChanges( LBFactory $lbFactory, Job $job, $fnameTrxOwner ) { - global $wgJobSerialCommitThreshold; + $syncThreshold = $this->config->get( 'JobSerialCommitThreshold' ); $time = false; $lb = $lbFactory->getMainLB( wfWikiID() ); - if ( $wgJobSerialCommitThreshold !== false && $lb->getServerCount() > 1 ) { + if ( $syncThreshold !== false && $lb->getServerCount() > 1 ) { // Generally, there is one master connection to the local DB $dbwSerial = $lb->getAnyOpenConnection( $lb->getWriterIndex() ); // We need natively blocking fast locks if ( $dbwSerial && $dbwSerial->namedLocksEnqueue() ) { $time = $dbwSerial->pendingWriteQueryDuration( $dbwSerial::ESTIMATE_DB_APPLY ); - if ( $time < $wgJobSerialCommitThreshold ) { + if ( $time < $syncThreshold ) { $dbwSerial = false; } } else { @@ -541,7 +542,12 @@ class JobRunner implements LoggerAwareInterface { } if ( !$dbwSerial ) { - $lbFactory->commitMasterChanges( $fnameTrxOwner ); + $lbFactory->commitMasterChanges( + $fnameTrxOwner, + // Abort if any transaction was too big + [ 'maxWriteDuration' => $this->config->get( 'MaxJobDBWriteDuration' ) ] + ); + return; } @@ -566,7 +572,11 @@ class JobRunner implements LoggerAwareInterface { } // Actually commit the DB master changes - $lbFactory->commitMasterChanges( $fnameTrxOwner ); + $lbFactory->commitMasterChanges( + $fnameTrxOwner, + // Abort if any transaction was too big + [ 'maxWriteDuration' => $this->config->get( 'MaxJobDBWriteDuration' ) ] + ); ScopedCallback::consume( $unlocker ); } } diff --git a/includes/libs/CryptRand.php b/includes/libs/CryptRand.php index 0d3613ae23..4b4a913569 100644 --- a/includes/libs/CryptRand.php +++ b/includes/libs/CryptRand.php @@ -247,8 +247,11 @@ class CryptRand { // On Linux, getrandom syscall will be used if available. // On Windows CryptGenRandom will always be used // On other platforms, /dev/urandom will be used. + // Avoids polyfills from before php 7.0 // All error situations will throw Exceptions and or Errors - if ( function_exists( 'random_bytes' ) ) { + if ( PHP_VERSION_ID >= 70000 + || ( defined( 'HHVM_VERSION_ID' ) && HHVM_VERSION_ID >= 31101 ) + ) { $rem = $bytes - strlen( $buffer ); $buffer .= random_bytes( $rem ); } diff --git a/includes/libs/filebackend/FileBackendStore.php b/includes/libs/filebackend/FileBackendStore.php index e2f7886c1a..039bd42508 100644 --- a/includes/libs/filebackend/FileBackendStore.php +++ b/includes/libs/filebackend/FileBackendStore.php @@ -1250,7 +1250,7 @@ abstract class FileBackendStore extends FileBackend { * @return array */ protected function sanitizeOpHeaders( array $op ) { - static $longs = [ 'content-disposition', 'x-content-dimensions' ]; + static $longs = [ 'content-disposition' ]; if ( isset( $op['headers'] ) ) { // op sets HTTP headers $newHeaders = []; diff --git a/includes/libs/http/HttpAcceptNegotiator.php b/includes/libs/http/HttpAcceptNegotiator.php new file mode 100644 index 0000000000..5f8d9a69e1 --- /dev/null +++ b/includes/libs/http/HttpAcceptNegotiator.php @@ -0,0 +1,139 @@ +supportedValues = $supported; + $this->defaultValue = reset( $supported ); + } + + /** + * Returns the best supported key from the given weight map. Of the keys from the + * $weights parameter that are also in the list of supported values supplied to + * the constructor, this returns the key that has the highest weight associated + * with it. If two keys have the same weight, the more specific key is preferred, + * as required by RFC2616 section 14. Keys that map to 0 or false are ignored. + * If no matching key is found, $default is returned. + * + * @param float[] $weights An associative array mapping accepted values to their + * respective weights. + * + * @param null|string $default The value to return if non of the keys in $weights + * is supported (null per default). + * + * @return null|string The best supported key from the $weights parameter. + */ + public function getBestSupportedKey( array $weights, $default = null ) { + // Make sure we correctly bias against wildcards and ranges, see RFC2616, section 14. + foreach ( $weights as $name => &$weight ) { + if ( $name === '*' || $name === '*/*' ) { + $weight -= 0.000002; + } elseif ( substr( $name, -2 ) === '/*' ) { + $weight -= 0.000001; + } + } + + // Sort $weights by value and... + asort( $weights ); + + // remove any keys with values equal to 0 or false (HTTP/1.1 section 3.9) + $weights = array_filter( $weights ); + + // ...use the ordered list of keys + $preferences = array_reverse( array_keys( $weights ) ); + + $value = $this->getFirstSupportedValue( $preferences, $default ); + return $value; + } + + /** + * Returns the first supported value from the given preference list. Of the values from + * the $preferences parameter that are also in the list of supported values supplied + * to the constructor, this returns the value that has the lowest index in the list. + * If no such value is found, $default is returned. + * + * @param string[] $preferences A list of acceptable values, in order of preference. + * + * @param null|string $default The value to return if non of the keys in $weights + * is supported (null per default). + * + * @return null|string The best supported key from the $weights parameter. + */ + public function getFirstSupportedValue( array $preferences, $default = null ) { + foreach ( $preferences as $value ) { + foreach ( $this->supportedValues as $supported ) { + if ( $this->valueMatches( $value, $supported ) ) { + return $supported; + } + } + } + + return $default; + } + + /** + * Returns true if the given acceptable value matches the given supported value, + * according to the HTTP specification. The following rules are used: + * + * - comparison is case-insensitive + * - if $accepted and $supported are equal, they match + * - if $accepted is `*` or `*` followed by `/*`, it matches any $supported value. + * - if both $accepted and $supported contain a `/`, and $accepted ends with `/*`, + * they match if the part before the first `/` is equal. + * + * @param string $accepted An accepted value (may contain wildcards) + * @param string $supported A supported value. + * + * @return bool Whether the given supported value matches the given accepted value. + */ + private function valueMatches( $accepted, $supported ) { + // RDF 2045: MIME types are case insensitive. + // full match + if ( strcasecmp( $accepted, $supported ) === 0 ) { + return true; + } + + // wildcard match (HTTP/1.1 section 14.1, 14.2, 14.3) + if ( $accepted === '*' || $accepted === '*/*' ) { + return true; + } + + // wildcard match (HTTP/1.1 section 14.1) + if ( substr( $accepted, -2 ) === '/*' + && strncasecmp( $accepted, $supported, strlen( $accepted ) - 2 ) === 0 + ) { + return true; + } + + return false; + } + +} diff --git a/includes/libs/http/HttpAcceptParser.php b/includes/libs/http/HttpAcceptParser.php new file mode 100644 index 0000000000..bce071e726 --- /dev/null +++ b/includes/libs/http/HttpAcceptParser.php @@ -0,0 +1,78 @@ + 0.8 + $weights = array_combine( $values, $qvalues ); + + return $weights; + } + +} diff --git a/includes/libs/mime/MimeAnalyzer.php b/includes/libs/mime/MimeAnalyzer.php index 565c157331..c361fdfa0d 100644 --- a/includes/libs/mime/MimeAnalyzer.php +++ b/includes/libs/mime/MimeAnalyzer.php @@ -1144,6 +1144,15 @@ EOT; return MEDIATYPE_UNKNOWN; } + /** + * Returns an array of media types (MEDIATYPE_xxx constants) + * + * @return array + */ + public function getMediaTypes() { + return array_keys( $this->mediaTypes ); + } + /** * Get the MIME types that various versions of Internet Explorer would * detect from a chunk of the content. diff --git a/includes/libs/objectcache/MultiWriteBagOStuff.php b/includes/libs/objectcache/MultiWriteBagOStuff.php index 9dcfa7c55e..687c67c356 100644 --- a/includes/libs/objectcache/MultiWriteBagOStuff.php +++ b/includes/libs/objectcache/MultiWriteBagOStuff.php @@ -226,4 +226,12 @@ class MultiWriteBagOStuff extends BagOStuff { return $ret; } + + public function makeKey() { + return call_user_func_array( [ $this->caches[0], __FUNCTION__ ], func_get_args() ); + } + + public function makeGlobalKey() { + return call_user_func_array( [ $this->caches[0], __FUNCTION__ ], func_get_args() ); + } } diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index 423d43eff9..0842e04c2f 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -202,8 +202,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { public static function newEmpty() { return new self( [ 'cache' => new EmptyBagOStuff(), - 'pool' => 'empty', - 'relayer' => new EventRelayerNull( [] ) + 'pool' => 'empty' ] ); } @@ -1109,7 +1108,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { final public function getMultiWithSetCallback( ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = [] ) { - $valueKeys = array_keys( iterator_to_array( $keyedIds, true ) ); + $valueKeys = array_keys( $keyedIds->getArrayCopy() ); $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : []; // Load required keys into process cache in one go @@ -1195,7 +1194,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { final public function getMultiWithUnionSetCallback( ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = [] ) { - $idsByValueKey = iterator_to_array( $keyedIds, true ); + $idsByValueKey = $keyedIds->getArrayCopy(); $valueKeys = array_keys( $idsByValueKey ); $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : []; unset( $opts['lockTSE'] ); // incompatible diff --git a/includes/libs/rdbms/TransactionProfiler.php b/includes/libs/rdbms/TransactionProfiler.php index 5d3534ffaa..823e0dc5cd 100644 --- a/includes/libs/rdbms/TransactionProfiler.php +++ b/includes/libs/rdbms/TransactionProfiler.php @@ -265,8 +265,9 @@ class TransactionProfiler implements LoggerAwareInterface { * @param string $db DB name * @param string $id ID string of transaction * @param float $writeTime Time spent in write queries + * @param integer $affected Number of rows affected by writes */ - public function transactionWritingOut( $server, $db, $id, $writeTime = 0.0 ) { + public function transactionWritingOut( $server, $db, $id, $writeTime = 0.0, $affected = 0 ) { $name = "{$server} ({$db}) (TRX#$id)"; if ( !isset( $this->dbTrxMethodTimes[$name] ) ) { $this->logger->info( "Detected no transaction for '$name' - out of sync." ); @@ -284,6 +285,14 @@ class TransactionProfiler implements LoggerAwareInterface { ); $slow = true; } + // Warn if too many rows were changed... + if ( $affected > $this->expect['maxAffected'] ) { + $this->reportExpectationViolated( + 'maxAffected', + "[transaction $id writes to {$server} ({$db})]", + $affected + ); + } // Fill in the last non-query period... $lastQuery = end( $this->dbTrxMethodTimes[$name] ); if ( $lastQuery ) { diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index b6167aa7ab..fb4122decf 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -124,6 +124,10 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } + public function pendingWriteRowsAffected() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + public function isOpen() { return $this->__call( __FUNCTION__, func_get_args() ); } @@ -243,13 +247,13 @@ class DBConnRef implements IDatabase { } public function selectField( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] + $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = [] ) { return $this->__call( __FUNCTION__, func_get_args() ); } public function selectFieldValues( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] + $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = [] ) { return $this->__call( __FUNCTION__, func_get_args() ); } @@ -407,7 +411,7 @@ class DBConnRef implements IDatabase { public function insertSelect( $destTable, $srcTable, $varMap, $conds, - $fname = __METHOD__, $insertOptions = [], $selectOptions = [] + $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index ee7644febb..9e91592a52 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -200,6 +200,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @var integer Number of write queries for the current transaction */ private $mTrxWriteQueryCount = 0; + /** + * @var integer Number of rows affected by write queries for the current transaction + */ + private $mTrxWriteAffectedRows = 0; /** * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries */ @@ -583,6 +587,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $this->mTrxLevel ? $this->mTrxWriteCallers : []; } + public function pendingWriteRowsAffected() { + return $this->mTrxWriteAffectedRows; + } + /** * Get the list of method names that have pending write queries or callbacks * for this transaction @@ -1011,7 +1019,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $ret !== false ) { $this->lastPing = $startTime; if ( $isWrite && $this->mTrxLevel ) { - $this->updateTrxWriteQueryTime( $sql, $queryRuntime ); + $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() ); $this->mTrxWriteCallers[] = $fname; } } @@ -1042,8 +1050,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @param string $sql A SQL write query * @param float $runtime Total runtime, including RTT + * @param integer $affected Affected row count */ - private function updateTrxWriteQueryTime( $sql, $runtime ) { + private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) { // Whether this is indicative of replica DB runtime (except for RBR or ws_repl) $indicativeOfReplicaRuntime = true; if ( $runtime > self::SLOW_WRITE_SEC ) { @@ -1058,6 +1067,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->mTrxWriteDuration += $runtime; $this->mTrxWriteQueryCount += 1; + $this->mTrxWriteAffectedRows += $affected; if ( $indicativeOfReplicaRuntime ) { $this->mTrxWriteAdjDuration += $runtime; $this->mTrxWriteAdjQueryCount += 1; @@ -1140,7 +1150,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function selectField( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] + $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = [] ) { if ( $var === '*' ) { // sanity throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" ); @@ -1152,7 +1162,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $options['LIMIT'] = 1; - $res = $this->select( $table, $var, $cond, $fname, $options ); + $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds ); if ( $res === false || !$this->numRows( $res ) ) { return false; } @@ -2346,7 +2356,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware public function insertSelect( $destTable, $srcTable, $varMap, $conds, - $fname = __METHOD__, $insertOptions = [], $selectOptions = [] + $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ) { if ( $this->cliMode ) { // For massive migrations with downtime, we don't want to select everything @@ -2358,7 +2368,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $conds, $fname, $insertOptions, - $selectOptions + $selectOptions, + $selectJoinConds ); } @@ -2370,7 +2381,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn ); } $selectOptions[] = 'FOR UPDATE'; - $res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions ); + $res = $this->select( + $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds + ); if ( !$res ) { return false; } @@ -2391,7 +2404,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware */ protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = [], $selectOptions = [] + $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ) { $destTable = $this->tableName( $destTable ); @@ -2401,32 +2414,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $insertOptions = $this->makeInsertOptions( $insertOptions ); - if ( !is_array( $selectOptions ) ) { - $selectOptions = [ $selectOptions ]; - } - - list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions( - $selectOptions ); - - if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) ); - } else { - $srcTable = $this->tableName( $srcTable ); - } + $selectSql = $this->selectSQLText( + $srcTable, + array_values( $varMap ), + $conds, + $fname, + $selectOptions, + $selectJoinConds + ); $sql = "INSERT $insertOptions" . - " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . - " SELECT $startOpts " . implode( ',', $varMap ) . - " FROM $srcTable $useIndex $ignoreIndex "; - - if ( $conds != '*' ) { - if ( is_array( $conds ) ) { - $conds = $this->makeList( $conds, self::LIST_AND ); - } - $sql .= " WHERE $conds"; - } - - $sql .= " $tailOpts"; + " INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' . + $selectSql; return $this->query( $sql, $fname ); } @@ -2805,6 +2804,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) ); $this->mTrxWriteDuration = 0.0; $this->mTrxWriteQueryCount = 0; + $this->mTrxWriteAffectedRows = 0; $this->mTrxWriteAdjDuration = 0.0; $this->mTrxWriteAdjQueryCount = 0; $this->mTrxWriteCallers = []; @@ -2871,7 +2871,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $this->mTrxDoneWrites ) { $this->mLastWriteTime = microtime( true ); $this->trxProfiler->transactionWritingOut( - $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime ); + $this->mServer, + $this->mDBname, + $this->mTrxShortId, + $writeTime, + $this->mTrxWriteAffectedRows + ); } $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT ); @@ -2916,7 +2921,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->mTrxAtomicLevels = []; if ( $this->mTrxDoneWrites ) { $this->trxProfiler->transactionWritingOut( - $this->mServer, $this->mDBname, $this->mTrxShortId ); + $this->mServer, + $this->mDBname, + $this->mTrxShortId + ); } $this->mTrxIdleCallbacks = []; // clear diff --git a/includes/libs/rdbms/database/DatabaseMssql.php b/includes/libs/rdbms/database/DatabaseMssql.php index 782727a2c6..8f3cab8314 100644 --- a/includes/libs/rdbms/database/DatabaseMssql.php +++ b/includes/libs/rdbms/database/DatabaseMssql.php @@ -717,11 +717,12 @@ class DatabaseMssql extends Database { * @param string $fname * @param array $insertOptions * @param array $selectOptions + * @param array $selectJoinConds * @return null|ResultWrapper * @throws Exception */ public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = [], $selectOptions = [] + $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ) { $this->mScrollableCursor = false; try { @@ -732,7 +733,8 @@ class DatabaseMssql extends Database { $conds, $fname, $insertOptions, - $selectOptions + $selectOptions, + $selectJoinConds ); } catch ( Exception $e ) { $this->mScrollableCursor = true; diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index 2fe275b5c1..bdac06c121 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -256,7 +256,10 @@ class DatabasePostgres extends Database { } /* Transaction stays in the ERROR state until rolled back */ if ( $this->mTrxLevel ) { - $this->rollback( __METHOD__ ); + // Throw away the transaction state, then raise the error as normal. + // Note that if this connection is managed by LBFactory, it's already expected + // that the other transactions LBFactory manages will be rolled back. + $this->rollback( __METHOD__, self::FLUSHING_INTERNAL ); } parent::reportQueryError( $error, $errno, $sql, $fname, false ); } @@ -681,14 +684,13 @@ __INDEXATTR__; * @param string $fname * @param array $insertOptions * @param array $selectOptions + * @param array $selectJoinConds * @return bool */ public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = [], $selectOptions = [] + $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ) { - $destTable = $this->tableName( $destTable ); - if ( !is_array( $insertOptions ) ) { $insertOptions = [ $insertOptions ]; } @@ -705,28 +707,9 @@ __INDEXATTR__; $savepoint->savepoint(); } - if ( !is_array( $selectOptions ) ) { - $selectOptions = [ $selectOptions ]; - } - list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = - $this->makeSelectOptions( $selectOptions ); - if ( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( [ $this, 'tableName' ], $srcTable ) ); - } else { - $srcTable = $this->tableName( $srcTable ); - } - - $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . - " SELECT $startOpts " . implode( ',', $varMap ) . - " FROM $srcTable $useIndex $ignoreIndex "; - - if ( $conds != '*' ) { - $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); - } - - $sql .= " $tailOpts"; + $res = parent::nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname, + $insertOptions, $selectOptions, $selectJoinConds ); - $res = (bool)$this->query( $sql, $fname, $savepoint ); if ( $savepoint ) { $bar = pg_result_error( $this->mLastResult ); if ( $bar != false ) { @@ -1059,6 +1042,7 @@ __INDEXATTR__; if ( $schema === false ) { $schema = $this->getCoreSchema(); } + $table = $this->realTableName( $table, 'raw' ); $etable = $this->addQuotes( $table ); $eschema = $this->addQuotes( $schema ); $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " @@ -1386,6 +1370,13 @@ SQL; return false; } + public function serverIsReadOnly() { + $res = $this->query( "SHOW default_transaction_read_only", __METHOD__ ); + $row = $this->fetchObject( $res ); + + return $row ? ( strtolower( $row->default_transaction_read_only ) === 'on' ) : false; + } + /** * @param string $lockName * @return string Integer diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index bec26a617a..7c6413cb3e 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -274,6 +274,14 @@ interface IDatabase { */ public function pendingWriteCallers(); + /** + * Get the number of affected rows from pending write queries + * + * @return integer + * @since 1.30 + */ + public function pendingWriteRowsAffected(); + /** * Is a connection to the database open? * @return bool @@ -560,11 +568,12 @@ interface IDatabase { * @param string|array $cond The condition array. See IDatabase::select() for details. * @param string $fname The function name of the caller. * @param string|array $options The query options. See IDatabase::select() for details. + * @param string|array $join_conds The query join conditions. See IDatabase::select() for details. * * @return bool|mixed The value from the field, or false on failure. */ public function selectField( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] + $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = [] ); /** @@ -581,12 +590,13 @@ interface IDatabase { * @param string|array $cond The condition array. See IDatabase::select() for details. * @param string $fname The function name of the caller. * @param string|array $options The query options. See IDatabase::select() for details. + * @param string|array $join_conds The query join conditions. See IDatabase::select() for details. * * @return bool|array The values from the field, or false on failure * @since 1.25 */ public function selectFieldValues( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] + $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = [] ); /** @@ -1239,12 +1249,14 @@ interface IDatabase { * IDatabase::insert() for details. * @param array $selectOptions Options for the SELECT part of the query, see * IDatabase::select() for details. + * @param array $selectJoinConds Join conditions for the SELECT part of the query, see + * IDatabase::select() for details. * * @return IResultWrapper */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = [], $selectOptions = [] + $insertOptions = [], $selectOptions = [], $selectJoinConds = [] ); /** diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php index ac79acc64f..6e328f4b4c 100644 --- a/includes/libs/rdbms/lbfactory/ILBFactory.php +++ b/includes/libs/rdbms/lbfactory/ILBFactory.php @@ -192,6 +192,13 @@ interface ILBFactory { */ public function rollbackMasterChanges( $fname = __METHOD__ ); + /** + * Check if a transaction round is active + * @return bool + * @since 1.29 + */ + public function hasTransactionRound(); + /** * Determine if any master connection has pending changes * @return bool diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php index 53d5ef4f40..3567204a82 100644 --- a/includes/libs/rdbms/lbfactory/LBFactory.php +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -250,6 +250,10 @@ abstract class LBFactory implements ILBFactory { } ); } + public function hasTransactionRound() { + return ( $this->trxRoundId !== false ); + } + /** * Log query info if multi DB transactions are going to be committed now */ diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitor.php b/includes/libs/rdbms/loadmonitor/LoadMonitor.php index d120b6f3d2..4300e9f1cd 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitor.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitor.php @@ -161,7 +161,10 @@ class LoadMonitor implements ILoadMonitor { if ( !$conn ) { $lagTimes[$i] = false; $host = $this->parent->getServerName( $i ); - $this->replLogger->error( __METHOD__ . ": host $host is unreachable" ); + $this->replLogger->error( + __METHOD__ . ": host {db_server} is unreachable", + [ 'db_server' => $host ] + ); continue; } @@ -171,7 +174,10 @@ class LoadMonitor implements ILoadMonitor { $lagTimes[$i] = $conn->getLag(); if ( $lagTimes[$i] === false ) { $host = $this->parent->getServerName( $i ); - $this->replLogger->error( __METHOD__ . ": host $host is not replicating?" ); + $this->replLogger->error( + __METHOD__ . ": host {db_server} is not replicating?", + [ 'db_server' => $host ] + ); } } diff --git a/includes/linkeddata/PageDataRequestHandler.php b/includes/linkeddata/PageDataRequestHandler.php new file mode 100644 index 0000000000..d26b304d4d --- /dev/null +++ b/includes/linkeddata/PageDataRequestHandler.php @@ -0,0 +1,172 @@ +getText( 'target', '' ) === '' ) { + return false; + } else { + return true; + } + } + + $parts = explode( '/', $subPage, 2 ); + if ( $parts !== 2 ) { + $slot = $parts[0]; + if ( $slot === 'main' or $slot === '' ) { + return true; + } + } + + return false; + } + + /** + * Main method for handling requests. + * + * @param string $subPage + * @param WebRequest $request The request parameters. Known parameters are: + * - title: the page title + * - format: the format + * - oldid|revision: the revision ID + * @param OutputPage $output + * + * @note: Instead of an output page, a WebResponse could be sufficient, but + * redirect logic is currently implemented in OutputPage. + * + * @throws HttpError + */ + public function handleRequest( $subPage, WebRequest $request, OutputPage $output ) { + // No matter what: The response is always public + $output->getRequest()->response()->header( 'Access-Control-Allow-Origin: *' ); + + if ( !$this->canHandleRequest( $subPage, $request ) ) { + throw new HttpError( 400, wfMessage( 'pagedata-bad-title', $subPage ) ); + } + + $revision = 0; + + $parts = explode( '/', $subPage, 2 ); + if ( $subPage !== '' ) { + $title = $parts[1]; + } else { + $title = $request->getText( 'target', '' ); + } + + $revision = $request->getInt( 'oldid', $revision ); + $revision = $request->getInt( 'revision', $revision ); + + if ( $title === null || $title === '' ) { + //TODO: different error message? + throw new HttpError( 400, wfMessage( 'pagedata-bad-title', $title ) ); + } + + try { + $title = Title::newFromTextThrow( $title ); + } catch ( MalformedTitleException $ex ) { + throw new HttpError( 400, wfMessage( 'pagedata-bad-title', $title ) ); + } + + $this->httpContentNegotiation( $request, $output, $title, $revision ); + } + + /** + * Applies HTTP content negotiation. + * If the negotiation is successful, this method will set the appropriate redirect + * in the OutputPage object and return. Otherwise, an HttpError is thrown. + * + * @param WebRequest $request + * @param OutputPage $output + * @param Title $title + * @param int $revision The desired revision + * + * @throws HttpError + */ + public function httpContentNegotiation( + WebRequest $request, + OutputPage $output, + Title $title, + $revision = 0 + ) { + $contentHandler = ContentHandler::getForTitle( $title ); + $mimeTypes = $contentHandler->getSupportedFormats(); + + $headers = $request->getAllHeaders(); + if ( isset( $headers['ACCEPT'] ) ) { + $parser = new HttpAcceptParser(); + $accept = $parser->parseWeights( $headers['ACCEPT'] ); + } else { + // anything goes + $accept = [ + '*' => 0.1 // just to make extra sure + ]; + // prefer the default + $accept[$mimeTypes[0]] = 1; + } + + $negotiator = new HttpAcceptNegotiator( $mimeTypes ); + $format = $negotiator->getBestSupportedKey( $accept, null ); + + if ( $format === null ) { + $format = isset( $accept['text/html'] ) ? 'text/html' : null; + } + + if ( $format === null ) { + $msg = wfMessage( 'pagedata-not-acceptable', implode( ', ', $mimeTypes ) ); + throw new HttpError( 406, $msg ); + } + + $url = $this->getDocUrl( $title, $format, $revision ); + $output->redirect( $url, 303 ); + } + + /** + * Returns a url representing the given title. + * + * @param Title $title + * @param string|null $format The (normalized) format name, or '' + * @param int $revision + * @return string + */ + private function getDocUrl( Title $title, $format = '', $revision = 0 ) { + $params = []; + + if ( $revision > 0 ) { + $params['oldid'] = $revision; + } + + if ( $format === 'text/html' ) { + return $title->getFullURL( $params ); + } + + $params[ 'action' ] = 'raw'; + + return $title->getFullURL( $params ); + } + +} diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php index 317652a3b7..c5501cbf9b 100644 --- a/includes/logging/LogEventsList.php +++ b/includes/logging/LogEventsList.php @@ -390,9 +390,18 @@ class LogEventsList extends ContextSource { [ 'mw-logline-' . $entry->getType() ], $newClasses ); + $attribs = [ + 'data-mw-logid' => $entry->getId(), + 'data-mw-logaction' => $entry->getFullType(), + ]; + $ret = "$del $time $action $comment $revert $tagDisplay"; + + // Let extensions add data + Hooks::run( 'LogEventsListLineEnding', [ $this, &$ret, $entry, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); + $attribs['class'] = implode( ' ', $classes ); - return Html::rawElement( 'li', [ 'class' => $classes ], - "$del $time $action $comment $revert $tagDisplay" ) . "\n"; + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index f260850acc..aae66d37e0 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -461,43 +461,4 @@ class DjVuHandler extends ImageHandler { return false; } } - - /** - * Get useful response headers for GET/HEAD requests for a file with the given metadata - * @param $metadata Array Contains this handler's unserialized getMetadata() for a file - * @param $fallbackWidth int|null Width to fall back to if metadata doesn't have any - * @param $fallbackHeight int|null Height to fall back to if metadata doesn't have any - * @return Array - * @since 1.30 - */ - public function getContentHeaders( $metadata, $fallbackWidth = null, $fallbackHeight = null ) { - if ( !isset( $metadata['xml'] ) ) { - return []; - } - - $trees = $this->extractTreesFromMetadata( $metadata['xml'] ); - $dimensionInfo = $this->getDimensionInfoFromMetaTree( $trees['MetaTree'] ); - - if ( !$dimensionInfo ) { - return []; - } - - $pagesByDimensions = []; - $count = $dimensionInfo['pageCount']; - - for ( $i = 1; $i <= $count; $i++ ) { - $dimensions = $dimensionInfo['dimensionsByPage'][ $i - 1 ]; - $dimensionString = $dimensions['width'] . 'x' . $dimensions['height']; - - if ( isset ( $pagesByDimensions[ $dimensionString ] ) ) { - $pagesByDimensions[ $dimensionString ][] = $i; - } else { - $pagesByDimensions[ $dimensionString ] = [ $i ]; - } - } - - $pageRangesByDimensions = MediaHandler::getPageRangesByDimensions( $pagesByDimensions ); - - return [ 'X-Content-Dimensions' => $pageRangesByDimensions ]; - } } diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php index ec4d372927..88962642e5 100644 --- a/includes/media/MediaHandler.php +++ b/includes/media/MediaHandler.php @@ -916,32 +916,10 @@ abstract class MediaHandler { /** * Get useful response headers for GET/HEAD requests for a file with the given metadata * @param $metadata Array Contains this handler's unserialized getMetadata() for a file - * @param $fallbackWidth int|null Width to fall back to if metadata doesn't have any - * @param $fallbackHeight int|null Height to fall back to if metadata doesn't have any * @return Array * @since 1.30 */ - public function getContentHeaders( $metadata, $fallbackWidth = null, $fallbackHeight = null ) { - if ( !isset( $metadata['width'] ) ) { - if ( is_null( $fallbackWidth ) ) { - return []; - } - - $metadata['width'] = $fallbackWidth; - } - - if ( !isset( $metadata['height'] ) ) { - if ( is_null( $fallbackHeight ) ) { - return []; - } - - $metadata['height'] = $fallbackHeight; - } - - $dimensionString = $metadata['width'] . 'x' . $metadata['height']; - $pagesByDimensions = [ $dimensionString => [ 1 ] ]; - $pageRangesByDimensions = MediaHandler::getPageRangesByDimensions( $pagesByDimensions ); - - return [ 'X-Content-Dimensions' => $pageRangesByDimensions ]; + public function getContentHeaders( $metadata ) { + return []; } } diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index a4a6ba845a..6c103017d0 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -588,7 +588,7 @@ class SqlBagOStuff extends BagOStuff { while ( true ) { $conds = $baseConds; if ( $maxExpTime !== false ) { - $conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime ); + $conds[] = 'exptime >= ' . $db->addQuotes( $maxExpTime ); } $rows = $db->select( $this->getTableNameByShard( $i ), diff --git a/includes/page/CategoryPage.php b/includes/page/CategoryPage.php index ccc50f78dd..7dea27113d 100644 --- a/includes/page/CategoryPage.php +++ b/includes/page/CategoryPage.php @@ -27,7 +27,7 @@ */ class CategoryPage extends Article { # Subclasses can change this to override the viewer class. - protected $mCategoryViewerClass = 'CategoryViewer'; + public $mCategoryViewerClass = 'CategoryViewer'; /** * @var WikiCategoryPage diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 34f62324e0..d8722bac5b 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -4974,6 +4974,7 @@ class Parser { $ig->setContextTitle( $this->mTitle ); $ig->setShowBytes( false ); + $ig->setShowDimensions( false ); $ig->setShowFilename( false ); $ig->setParser( $this ); $ig->setHideBadImages(); diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index 9c6cf93e78..1f0e19eb26 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -327,6 +327,13 @@ class ParserCache { // ...and its pointer $this->mMemc->set( $this->getOptionsKey( $page ), $optionsKey, $expire ); + // Normally, when there was no key change, the above would have + // overwritten the old entry. Delete that old entry to save disk + // space. + $oldParserOutputKey = $this->getParserOutputKey( $page, + $popts->optionsHashPre30( $optionsKey->mUsedOptions, $page->getTitle() ) ); + $this->mMemc->delete( $oldParserOutputKey ); + Hooks::run( 'ParserCacheSaveComplete', [ $this, $parserOutput, $page->getTitle(), $popts, $revId ] diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index f8ed63fc84..5be0321bbe 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -60,7 +60,6 @@ class ParserOptions { */ private static $inCacheKey = [ 'dateformat' => true, - 'editsection' => true, 'numberheadings' => true, 'thumbsize' => true, 'stubthreshold' => true, @@ -82,6 +81,13 @@ class ParserOptions { */ private $mTimestamp; + /** + * The edit section flag is in ParserOptions for historical reasons, but + * doesn't actually affect the parser output since Feb 2015. + * @var bool + */ + private $mEditSection = true; + /** * Stored user object * @var User @@ -244,23 +250,6 @@ class ParserOptions { return $this->setOptionLegacy( 'enableImageWhitelist', $x ); } - /** - * Create "edit section" links? - * @return bool - */ - public function getEditSection() { - return $this->getOption( 'editsection' ); - } - - /** - * Create "edit section" links? - * @param bool|null $x New value (null is no change) - * @return bool Old value - */ - public function setEditSection( $x ) { - return $this->setOptionLegacy( 'editsection', $x ); - } - /** * Automatically number headings? * @return bool @@ -878,6 +867,23 @@ class ParserOptions { return wfSetVar( $this->mTimestamp, $x ); } + /** + * Create "edit section" links? + * @return bool + */ + public function getEditSection() { + return $this->mEditSection; + } + + /** + * Create "edit section" links? + * @param bool|null $x New value (null is no change) + * @return bool Old value + */ + public function setEditSection( $x ) { + return wfSetVar( $this->mEditSection, $x ); + } + /** * Set the redirect target. * @@ -1041,7 +1047,6 @@ class ParserOptions { // *UPDATE* ParserOptions::matches() if any of this changes as needed self::$defaults = [ 'dateformat' => null, - 'editsection' => true, 'tidy' => false, 'interfaceMessage' => false, 'targetLanguage' => null, @@ -1256,16 +1261,32 @@ class ParserOptions { public function optionsHash( $forOptions, $title = null ) { global $wgRenderHashAppend; + $options = $this->options; + $defaults = self::getCanonicalOverrides() + self::getDefaults(); + $inCacheKey = self::$inCacheKey; + + // Historical hack: 'editsection' hasn't been a true parser option since + // Feb 2015 (instead the parser outputs a constant placeholder and post-parse + // processing handles the option). But Wikibase forces it in $forOptions + // and expects the cache key to still vary on it for T85252. + // @todo Deprecate and remove this behavior after optionsHashPre30() is + // removed (Wikibase can use addExtraKey() or something instead). + if ( in_array( 'editsection', $forOptions, true ) ) { + $options['editsection'] = $this->mEditSection; + $defaults['editsection'] = true; + $inCacheKey['editsection'] = true; + ksort( $inCacheKey ); + } + // We only include used options with non-canonical values in the key // so adding a new option doesn't invalidate the entire parser cache. // The drawback to this is that changing the default value of an option // requires manual invalidation of existing cache entries, as mentioned // in the docs on the relevant methods and hooks. - $defaults = self::getCanonicalOverrides() + self::getDefaults(); $values = []; - foreach ( self::$inCacheKey as $option => $include ) { + foreach ( $inCacheKey as $option => $include ) { if ( $include && in_array( $option, $forOptions, true ) ) { - $v = $this->optionToString( $this->options[$option] ); + $v = $this->optionToString( $options[$option] ); $d = $this->optionToString( $defaults[$option] ); if ( $v !== $d ) { $values[] = "$option=$v"; @@ -1364,7 +1385,7 @@ class ParserOptions { // directly. At least Wikibase does at this point in time. if ( !in_array( 'editsection', $forOptions ) ) { $confstr .= '!*'; - } elseif ( !$this->options['editsection'] ) { + } elseif ( !$this->mEditSection ) { $confstr .= '!edit=0'; } diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 767046b302..c2faf48ba7 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -1170,7 +1170,7 @@ MESSAGE; * @param array $templates Keys are name of templates and values are the source of * the template. * @throws MWException - * @return string + * @return string JavaScript code */ protected static function makeLoaderImplementScript( $name, $scripts, $styles, $messages, $templates @@ -1200,7 +1200,7 @@ MESSAGE; * * @param mixed $messages Either an associative array mapping message key to value, or a * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. - * @return string + * @return string JavaScript code */ public static function makeMessageSetScript( $messages ) { return Xml::encodeJsCall( @@ -1256,7 +1256,7 @@ MESSAGE; * * @param string $name * @param string $state - * @return string + * @return string JavaScript code */ public static function makeLoaderStateScript( $name, $state = null ) { if ( is_array( $name ) ) { @@ -1286,7 +1286,7 @@ MESSAGE; * @param string $group Group which the module is in. * @param string $source Source of the module, or 'local' if not foreign. * @param string $script JavaScript code - * @return string + * @return string JavaScript code */ public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script @@ -1358,7 +1358,7 @@ MESSAGE; * @param string $group Group which the module is in * @param string $source Source of the module, or 'local' if not foreign * @param string $skip Script body of the skip function - * @return string + * @return string JavaScript code */ public static function makeLoaderRegisterScript( $name, $version = null, $dependencies = null, $group = null, $source = null, $skip = null @@ -1412,7 +1412,7 @@ MESSAGE; * * @param string $id Source ID * @param string $loadUrl load.php url - * @return string + * @return string JavaScript code */ public static function makeLoaderSourcesScript( $id, $loadUrl = null ) { if ( is_array( $id ) ) { @@ -1436,7 +1436,7 @@ MESSAGE; * * @deprecated since 1.25; use makeInlineScript instead * @param string $script JavaScript code - * @return string + * @return string JavaScript code */ public static function makeLoaderConditionalScript( $script ) { return '(window.RLQ=window.RLQ||[]).push(function(){' . @@ -1466,7 +1466,7 @@ MESSAGE; * the given value. * * @param array $configuration List of configuration values keyed by variable name - * @return string + * @return string JavaScript code */ public static function makeConfigSetScript( array $configuration ) { return Xml::encodeJsCall( diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index 8955b8c2a0..f99114e2b9 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -29,7 +29,7 @@ use MediaWiki\MediaWikiServices; * Object passed around to modules which contains information about the state * of a specific loader request. */ -class ResourceLoaderContext { +class ResourceLoaderContext implements MessageLocalizer { protected $resourceLoader; protected $request; protected $logger; @@ -222,10 +222,12 @@ class ResourceLoaderContext { * Get a Message object with context set. See wfMessage for parameters. * * @since 1.27 + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, + * or a MessageSpecifier. * @param mixed ... * @return Message */ - public function msg() { + public function msg( $key ) { return call_user_func_array( 'wfMessage', func_get_args() ) ->inLanguage( $this->getLanguage() ) // Use a dummy title because there is no real title diff --git a/includes/revisiondelete/RevDelFileItem.php b/includes/revisiondelete/RevDelFileItem.php index 62bafe9485..9beafc9893 100644 --- a/includes/revisiondelete/RevDelFileItem.php +++ b/includes/revisiondelete/RevDelFileItem.php @@ -19,8 +19,6 @@ * @ingroup RevisionDelete */ -use Wikimedia\Rdbms\IDatabase; - /** * Item class for an oldimage table row */ diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index ccb202e65a..e9d2f076b1 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -1485,7 +1485,7 @@ abstract class Skin extends ContextSource { * should fall back to the next notice in its sequence */ private function getCachedNotice( $name ) { - global $wgRenderHashAppend, $parserMemc, $wgContLang; + global $wgRenderHashAppend, $wgContLang; $needParse = false; @@ -1506,9 +1506,10 @@ abstract class Skin extends ContextSource { $notice = $msg->plain(); } + $cache = wfGetCache( CACHE_ANYTHING ); // Use the extra hash appender to let eg SSL variants separately cache. - $key = $parserMemc->makeKey( $name . $wgRenderHashAppend ); - $cachedNotice = $parserMemc->get( $key ); + $key = $cache->makeKey( $name . $wgRenderHashAppend ); + $cachedNotice = $cache->get( $key ); if ( is_array( $cachedNotice ) ) { if ( md5( $notice ) == $cachedNotice['hash'] ) { $notice = $cachedNotice['html']; @@ -1521,7 +1522,7 @@ abstract class Skin extends ContextSource { if ( $needParse ) { $parsed = $this->getOutput()->parse( $notice ); - $parserMemc->set( $key, [ 'html' => $parsed, 'hash' => md5( $notice ) ], 600 ); + $cache->set( $key, [ 'html' => $parsed, 'hash' => md5( $notice ) ], 600 ); $notice = $parsed; } diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index 09ed3c4440..1b561ef643 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -791,16 +791,18 @@ abstract class ChangesListSpecialPage extends SpecialPage { $config = $this->getConfig(); $opts = new FormOptions(); $structuredUI = $this->getUser()->getOption( 'rcenhancedfilters' ); + // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty + $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2; // Add all filters foreach ( $this->filterGroups as $filterGroup ) { // URL parameters can be per-group, like 'userExpLevel', // or per-filter, like 'hideminor'. if ( $filterGroup->isPerGroupRequestParameter() ) { - $opts->add( $filterGroup->getName(), $filterGroup->getDefault() ); + $opts->add( $filterGroup->getName(), $useDefaults ? $filterGroup->getDefault() : '' ); } else { foreach ( $filterGroup->getFilters() as $filter ) { - $opts->add( $filter->getName(), $filter->getDefault( $structuredUI ) ); + $opts->add( $filter->getName(), $useDefaults ? $filter->getDefault( $structuredUI ) : false ); } } } @@ -808,6 +810,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { $opts->add( 'namespace', '', FormOptions::STRING ); $opts->add( 'invert', false ); $opts->add( 'associated', false ); + $opts->add( 'urlversion', 1 ); return $opts; } diff --git a/includes/specialpage/SpecialPage.php b/includes/specialpage/SpecialPage.php index ba58e924ac..9594952d02 100644 --- a/includes/specialpage/SpecialPage.php +++ b/includes/specialpage/SpecialPage.php @@ -33,7 +33,7 @@ use MediaWiki\MediaWikiServices; * * @ingroup SpecialPage */ -class SpecialPage { +class SpecialPage implements MessageLocalizer { // The canonical name of this special page // Also used for the default

    heading, @see getDescription() protected $mName; @@ -743,7 +743,7 @@ class SpecialPage { * @return Message * @see wfMessage */ - public function msg( /* $args */ ) { + public function msg( $key /* $args */ ) { $message = call_user_func_array( [ $this->getContext(), 'msg' ], func_get_args() @@ -783,6 +783,10 @@ class SpecialPage { * @since 1.25 */ public function addHelpLink( $to, $overrideBaseUrl = false ) { + if ( $this->including() ) { + return; + } + global $wgContLang; $msg = $this->msg( $wgContLang->lc( $this->getName() ) . '-helppage' ); diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index 84d3b08095..81e2b7ef2c 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -186,6 +186,7 @@ class SpecialPageFactory { 'Revisiondelete' => 'SpecialRevisionDelete', 'RunJobs' => 'SpecialRunJobs', 'Specialpages' => 'SpecialSpecialpages', + 'PageData' => 'SpecialPageData' ]; private static $list; diff --git a/includes/specials/SpecialChangeContentModel.php b/includes/specials/SpecialChangeContentModel.php index a36b4148bc..8eaae4ca0f 100644 --- a/includes/specials/SpecialChangeContentModel.php +++ b/includes/specials/SpecialChangeContentModel.php @@ -198,7 +198,7 @@ class SpecialChangeContentModel extends FormSpecialPage { $oldContent = $this->oldRevision->getContent(); try { $newContent = ContentHandler::makeContent( - $oldContent->getNativeData(), $this->title, $data['model'] + $oldContent->serialize(), $this->title, $data['model'] ); } catch ( MWException $e ) { return Status::newFatal( diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 52cb30a1bc..7087cff4c0 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -74,6 +74,7 @@ class MIMEsearchPage extends QueryPage { 'img_major_mime' => $this->major, // This is in order to trigger using // the img_media_mime index in "range" mode. + // @todo how is order defined? use MimeAnalyzer::getMediaTypes? 'img_media_type' => [ MEDIATYPE_BITMAP, MEDIATYPE_DRAWING, diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 583d4f9e7c..8528ce26c8 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -25,6 +25,9 @@ class SpecialNewFiles extends IncludableSpecialPage { /** @var FormOptions */ protected $opts; + /** @var string[] */ + protected $mediaTypes; + public function __construct() { parent::__construct( 'Newimages' ); } @@ -32,9 +35,10 @@ class SpecialNewFiles extends IncludableSpecialPage { public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); + $mimeAnalyzer = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); + $this->mediaTypes = $mimeAnalyzer->getMediaTypes(); $out = $this->getOutput(); - $out->addModules( 'mediawiki.special.newFiles' ); $this->addHelpLink( 'Help:New images' ); $opts = new FormOptions(); @@ -42,7 +46,9 @@ class SpecialNewFiles extends IncludableSpecialPage { $opts->add( 'like', '' ); $opts->add( 'user', '' ); $opts->add( 'showbots', false ); + $opts->add( 'newbies', false ); $opts->add( 'hidepatrolled', false ); + $opts->add( 'mediatype', $this->mediaTypes ); $opts->add( 'limit', 50 ); $opts->add( 'offset', '' ); $opts->add( 'start', '' ); @@ -67,6 +73,14 @@ class SpecialNewFiles extends IncludableSpecialPage { $opts->setValue( 'end', $end, true ); } + // if all media types have been selected, wipe out the array to prevent + // the pointless IN(...) query condition (which would have no effect + // because every possible type has been selected) + $missingMediaTypes = array_diff( $this->mediaTypes, $opts->getValue( 'mediatype' ) ); + if ( empty( $missingMediaTypes ) ) { + $opts->setValue( 'mediatype', [] ); + } + $opts->validateIntBounds( 'limit', 0, 500 ); $this->opts = $opts; @@ -85,6 +99,17 @@ class SpecialNewFiles extends IncludableSpecialPage { } protected function buildForm() { + $mediaTypesText = array_map( function ( $type ) { + // mediastatistics-header-unknown, mediastatistics-header-bitmap, + // mediastatistics-header-drawing, mediastatistics-header-audio, + // mediastatistics-header-video, mediastatistics-header-multimedia, + // mediastatistics-header-office, mediastatistics-header-text, + // mediastatistics-header-executable, mediastatistics-header-archive, + return $this->msg( 'mediastatistics-header-' . strtolower( $type ) )->text(); + }, $this->mediaTypes ); + $mediaTypesOptions = array_combine( $mediaTypesText, $this->mediaTypes ); + ksort( $mediaTypesOptions ); + $formDescriptor = [ 'like' => [ 'type' => 'text', @@ -98,6 +123,12 @@ class SpecialNewFiles extends IncludableSpecialPage { 'name' => 'user', ], + 'newbies' => [ + 'type' => 'check', + 'label-message' => 'newimages-newbies', + 'name' => 'newbies', + ], + 'showbots' => [ 'type' => 'check', 'label-message' => 'newimages-showbots', @@ -110,6 +141,16 @@ class SpecialNewFiles extends IncludableSpecialPage { 'name' => 'hidepatrolled', ], + 'mediatype' => [ + 'type' => 'multiselect', + 'dropdown' => true, + 'flatlist' => true, + 'name' => 'mediatype', + 'label-message' => 'newimages-mediatype', + 'options' => $mediaTypesOptions, + 'default' => $this->mediaTypes, + ], + 'limit' => [ 'type' => 'hidden', 'default' => $this->opts->getValue( 'limit' ), @@ -144,11 +185,15 @@ class SpecialNewFiles extends IncludableSpecialPage { } HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ) + // For the 'multiselect' field values to be preserved on submit + ->setFormIdentifier( 'specialnewimages' ) ->setWrapperLegendMsg( 'newimages-legend' ) ->setSubmitTextMsg( 'ilsubmit' ) ->setMethod( 'get' ) ->prepareForm() ->displayForm( false ); + + $this->getOutput()->addModules( 'mediawiki.special.newFiles' ); } protected function getGroupName() { diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index be8ad8fb40..83482f6f2f 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -314,6 +314,7 @@ class SpecialNewpages extends IncludableSpecialPage { $rev->setTitle( $title ); $classes = []; + $attribs = [ 'data-mw-revid' => $result->rev_id ]; $lang = $this->getLanguage(); $dm = $lang->getDirMark(); @@ -378,11 +379,19 @@ class SpecialNewpages extends IncludableSpecialPage { $tagDisplay = ''; } - $css = count( $classes ) ? ' class="' . implode( ' ', $classes ) . '"' : ''; - # Display the old title if the namespace/title has been changed $oldTitleText = ''; $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title ); + $ret = "{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} " + . "{$tagDisplay} {$oldTitleText}"; + + // Let extensions add data + Hooks::run( 'NewPagesLineEnding', [ $this, &$ret, $result, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); + + if ( count( $classes ) ) { + $attribs['class'] = implode( ' ', $classes ); + } if ( !$title->equals( $oldTitle ) ) { $oldTitleText = $oldTitle->getPrefixedText(); @@ -393,8 +402,7 @@ class SpecialNewpages extends IncludableSpecialPage { ); } - return "{$time} {$dm}{$plink} {$hist} {$dm}{$length} " - . "{$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}\n"; + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** diff --git a/includes/specials/SpecialPageData.php b/includes/specials/SpecialPageData.php new file mode 100644 index 0000000000..f7084a870e --- /dev/null +++ b/includes/specials/SpecialPageData.php @@ -0,0 +1,87 @@ +. + * + * @license GPL-2.0+ + */ +class SpecialPageData extends SpecialPage { + + /** + * @var PageDataRequestHandler|null + */ + private $requestHandler = null; + + public function __construct() { + parent::__construct( 'PageData' ); + } + + /** + * Sets the request handler to be used by the special page. + * May be used when a particular instance of PageDataRequestHandler is already + * known, e.g. during testing. + * + * If no request handler is set using this method, a default handler is created + * on demand by initDependencies(). + * + * @param PageDataRequestHandler $requestHandler + */ + public function setRequestHandler( PageDataRequestHandler $requestHandler ) { + $this->requestHandler = $requestHandler; + } + + /** + * Initialize any un-initialized members from global context. + * In particular, this initializes $this->requestHandler + */ + protected function initDependencies() { + if ( $this->requestHandler === null ) { + $this->requestHandler = $this->newDefaultRequestHandler(); + } + } + + /** + * Creates a PageDataRequestHandler based on global defaults. + * + * @return PageDataRequestHandler + */ + private function newDefaultRequestHandler() { + + return new PageDataRequestHandler(); + } + + /** + * @see SpecialWikibasePage::execute + * + * @param string|null $subPage + * + * @throws HttpError + */ + public function execute( $subPage ) { + $this->initDependencies(); + + // If there is no title, show an HTML form + // TODO: Don't do this if HTML is not acceptable according to HTTP headers. + if ( !$this->requestHandler->canHandleRequest( $subPage, $this->getRequest() ) ) { + $this->showForm(); + return; + } + + $this->requestHandler->handleRequest( $subPage, $this->getRequest(), $this->getOutput() ); + } + + /** + * Shows an informative page to the user; Called when there is no page to output. + */ + public function showForm() { + $this->getOutput()->showErrorPage( 'pagedata-title', 'pagedata-text' ); + } + + public function isListed() { + // Do not list this page in Special:SpecialPages + return false; + } + +} diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index acfc1c0e7b..5ec2064fb2 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -138,7 +138,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { * @param string $subpage */ public function execute( $subpage ) { - global $wgStructuredChangeFiltersEnableSaving; + global $wgStructuredChangeFiltersEnableSaving, + $wgStructuredChangeFiltersEnableExperimentalViews; // Backwards-compatibility: redirect to new feed URLs $feedFormat = $this->getRequest()->getVal( 'feed' ); @@ -184,7 +185,58 @@ class SpecialRecentChanges extends ChangesListSpecialPage { 'wgStructuredChangeFiltersEnableSaving', $wgStructuredChangeFiltersEnableSaving ); + $out->addJsConfigVars( + 'wgStructuredChangeFiltersEnableExperimentalViews', + $wgStructuredChangeFiltersEnableExperimentalViews + ); + $out->addJsConfigVars( + 'wgRCFiltersChangeTags', + $this->buildChangeTagList() + ); + } + } + + /** + * Fetch the change tags list for the front end + * + * @return Array Tag data + */ + protected function buildChangeTagList() { + function stripAllHtml( $input ) { + return trim( html_entity_decode( strip_tags( $input ) ) ); } + + $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 ); + $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 ); + $tagStats = ChangeTags::tagUsageStatistics(); + + $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats ); + + // Sort by hits + asort( $tagHitCounts ); + + // Build the list and data + $result = []; + foreach ( $tagHitCounts as $tagName => $hits ) { + if ( + // Only get active tags + isset( $explicitlyDefinedTags[ $tagName ] ) || + isset( $softwareActivatedTags[ $tagName ] ) + ) { + // Parse description + $desc = ChangeTags::tagLongDescriptionMessage( $tagName, $this->getContext() ); + + $result[] = [ + 'name' => $tagName, + 'label' => stripAllHtml( ChangeTags::tagDescription( $tagName, $this->getContext() ) ), + 'description' => $desc ? stripAllHtml( $desc->parse() ) : '', + 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ), + 'hits' => $hits, + ]; + } + } + + return $result; } /** diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index eb4f0cc077..fa385060e8 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -21,7 +21,6 @@ * @ingroup SpecialPage */ -use MediaWiki\MediaWikiServices; use Wikimedia\Rdbms\ResultWrapper; /** diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index f4a4818b32..def639d83b 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -821,6 +821,7 @@ class SpecialUpload extends SpecialPage { $gallery = ImageGalleryBase::factory( false, $this->getContext() ); $gallery->setShowBytes( false ); + $gallery->setShowDimensions( false ); foreach ( $dupes as $file ) { $gallery->add( $file->getTitle() ); } diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index a3880eed50..6bd7eb0e9f 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -365,9 +365,9 @@ class ContribsPager extends RangeChronologicalPager { * @return string */ function formatRow( $row ) { - $ret = ''; $classes = []; + $attribs = []; $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); @@ -388,7 +388,7 @@ class ContribsPager extends RangeChronologicalPager { MediaWiki\restoreWarnings(); if ( $validRevision ) { - $classes = []; + $attribs['data-mw-revid'] = $rev->getId(); $page = Title::newFromRow( $row ); $link = $linkRenderer->makeLink( @@ -535,19 +535,21 @@ class ContribsPager extends RangeChronologicalPager { } // Let extensions add data - Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); + Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); // TODO: Handle exceptions in the catch block above. Do any extensions rely on // receiving empty rows? - if ( $classes === [] && $ret === '' ) { + if ( $classes === [] && $attribs === [] && $ret === '' ) { wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); return "\n"; } + $attribs['class'] = $classes; // FIXME: The signature of the ContributionsLineEnding hook makes it // very awkward to move this LI wrapper into the template. - return Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** diff --git a/includes/specials/pagers/DeletedContribsPager.php b/includes/specials/pagers/DeletedContribsPager.php index 78e1092dc5..43d7ad40c7 100644 --- a/includes/specials/pagers/DeletedContribsPager.php +++ b/includes/specials/pagers/DeletedContribsPager.php @@ -195,6 +195,7 @@ class DeletedContribsPager extends IndexPager { function formatRow( $row ) { $ret = ''; $classes = []; + $attribs = []; /* * There may be more than just revision rows. To make sure that we'll only be processing @@ -213,17 +214,20 @@ class DeletedContribsPager extends IndexPager { MediaWiki\restoreWarnings(); if ( $validRevision ) { + $attribs['data-mw-revid'] = $rev->getId(); $ret = $this->formatRevisionRow( $row ); } // Let extensions add data - Hooks::run( 'DeletedContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); + Hooks::run( 'DeletedContributionsLineEnding', [ $this, &$ret, $row, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); - if ( $classes === [] && $ret === '' ) { + if ( $classes === [] && $attribs === [] && $ret === '' ) { wfDebug( "Dropping Special:DeletedContribution row that could not be formatted\n" ); $ret = "\n"; } else { - $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; + $attribs['class'] = $classes; + $ret = Html::rawElement( 'li', $attribs, $ret ) . "\n"; } return $ret; diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php index cce0323198..001c296d43 100644 --- a/includes/specials/pagers/NewFilesPager.php +++ b/includes/specials/pagers/NewFilesPager.php @@ -74,6 +74,20 @@ class NewFilesPager extends RangeChronologicalPager { } } + if ( $opts->getValue( 'newbies' ) ) { + // newbie = most recent 1% of users + $dbr = wfGetDB( DB_REPLICA ); + $max = $dbr->selectField( 'user', 'max(user_id)', false, __METHOD__ ); + $conds[] = 'img_user >' . (int)( $max - $max / 100 ); + + // there's no point in looking for new user activity in a far past; + // beyond a certain point, we'd just end up scanning the rest of the + // table even though the users we're looking for didn't yet exist... + // see T140537, (for ContribsPages, but similar to this) + $conds[] = 'img_timestamp > ' . + $dbr->addQuotes( $dbr->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) ); + } + if ( !$opts->getValue( 'showbots' ) ) { $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); @@ -112,6 +126,10 @@ class NewFilesPager extends RangeChronologicalPager { $options[] = 'STRAIGHT_JOIN'; } + if ( $opts->getValue( 'mediatype' ) ) { + $conds['img_media_type'] = $opts->getValue( 'mediatype' ); + } + $likeVal = $opts->getValue( 'like' ); if ( !$this->getConfig()->get( 'MiserMode' ) && $likeVal !== '' ) { $dbr = wfGetDB( DB_REPLICA ); diff --git a/includes/templates/EnhancedChangesListGroup.mustache b/includes/templates/EnhancedChangesListGroup.mustache index 352eb17d21..3a37c2ebcc 100644 --- a/includes/templates/EnhancedChangesListGroup.mustache +++ b/includes/templates/EnhancedChangesListGroup.mustache @@ -14,7 +14,7 @@ {{# lines }} - + {{{ recentChangesFlags }}}  diff --git a/includes/tidy/RaggettWrapper.php b/includes/tidy/RaggettWrapper.php index 9f6feb8e55..b793a58af7 100644 --- a/includes/tidy/RaggettWrapper.php +++ b/includes/tidy/RaggettWrapper.php @@ -48,6 +48,12 @@ class RaggettWrapper { // Modify inline Microdata and elements so they say and so // we can trick Tidy into not stripping them out by including them in tidy's new-empty-tags config $wrappedtext = preg_replace( '!<(link|meta)([^>]*?)(/{0,1}>)!', ' tags, but those aren't empty. + $wrappedtext = preg_replace_callback( '!]*)>(.*?)!s', function ( $m ) { + return '' + . $this->replaceCallback( [ $m[2] ] ) + . ''; + }, $wrappedtext ); // Preserve empty li elements (T49673) by abusing Tidy's datafld hack // The whitespace class is as in TY_(InitMap) @@ -78,8 +84,9 @@ class RaggettWrapper { * @return string */ public function postprocess( $text ) { - // Revert back to <{link,meta}> + // Revert back to <{link,meta,style}> $text = preg_replace( '!]*?)(/{0,1}>)!', '<$1$2$3', $text ); + $text = preg_replace( '!<(/?)html-(style)([^>]*)>!', '<$1$2$3>', $text ); // Remove datafld $text = str_replace( '
  • and used in the body for Microdata new-empty-tags: html-meta, html-link, wbr, source, track new-inline-tags: video, audio, bdi, data, time, mark +# html-style is a hack we use to prevent pre-HTML5 versions of Tidy from stripping + + +!! html+tidy +
    + + +
    +!! end diff --git a/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php b/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php new file mode 100644 index 0000000000..afd80ff7a2 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php @@ -0,0 +1,37 @@ + 1, 'b' => 2, 'c' => 3 ]; + $filtered = wfArrayFilter( $arr, function( $val, $key ) { + return $key !== 'b'; + } ); + $this->assertSame( [ 'a' => 1, 'c' => 3 ], $filtered ); + + $arr = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + $filtered = wfArrayFilter( $arr, function( $val, $key ) { + return $val !== 2; + } ); + $this->assertSame( [ 'a' => 1, 'c' => 3 ], $filtered ); + + $arr = [ 'a', 'b', 'c' ]; + $filtered = wfArrayFilter( $arr, function( $val, $key ) { + return $key !== 0; + } ); + $this->assertSame( [ 1 => 'b', 2 => 'c' ], $filtered ); + } + + public function testWfArrayFilterByKey() { + $arr = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + $filtered = wfArrayFilterByKey( $arr, function( $key ) { + return $key !== 'b'; + } ); + $this->assertSame( [ 'a' => 1, 'c' => 3 ], $filtered ); + + $arr = [ 'a', 'b', 'c' ]; + $filtered = wfArrayFilterByKey( $arr, function( $key ) { + return $key !== 0; + } ); + $this->assertSame( [ 1 => 'b', 2 => 'c' ], $filtered ); + } +} diff --git a/tests/phpunit/includes/ReadOnlyModeTest.php b/tests/phpunit/includes/ReadOnlyModeTest.php index 9c02bbd9d4..b14424fb3a 100644 --- a/tests/phpunit/includes/ReadOnlyModeTest.php +++ b/tests/phpunit/includes/ReadOnlyModeTest.php @@ -1,7 +1,5 @@ assertSame( $expected, Sanitizer::isReservedDataAttribute( $attr ) ); + } + + public static function provideIsReservedDataAttribute() { + return [ + [ 'foo', false ], + [ 'data', false ], + [ 'data-foo', false ], + [ 'data-mw', true ], + [ 'data-ooui', true ], + [ 'data-parsoid', true ], + [ 'data-mw-foo', true ], + [ 'data-ooui-foo', true ], + [ 'data-mwfoo', true ], // could be false but this is how it's implemented currently + ]; + } } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index 238b65f429..6c4499948d 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -716,28 +716,33 @@ class TitleTest extends MediaWikiTestCase { return [ // ns = 0 [ - Title::makeTitle( NS_MAIN, 'Foobar' ), - 'Foobar' + Title::makeTitle( NS_MAIN, 'Foo bar' ), + 'Foo bar' ], // ns = 2 [ - Title::makeTitle( NS_USER, 'Foobar' ), - 'User:Foobar' + Title::makeTitle( NS_USER, 'Foo bar' ), + 'User:Foo bar' + ], + // ns = 3 + [ + Title::makeTitle( NS_USER_TALK, 'Foo bar' ), + 'User talk:Foo bar' ], // fragment not included [ - Title::makeTitle( NS_MAIN, 'Foobar', 'fragment' ), - 'Foobar' + Title::makeTitle( NS_MAIN, 'Foo bar', 'fragment' ), + 'Foo bar' ], // ns = -2 [ - Title::makeTitle( NS_MEDIA, 'Foobar' ), - 'Media:Foobar' + Title::makeTitle( NS_MEDIA, 'Foo bar' ), + 'Media:Foo bar' ], // non-existent namespace [ - Title::makeTitle( 100000, 'Foobar' ), - ':Foobar' + Title::makeTitle( 100777, 'Foo bar' ), + 'Special:Badtitle/NS100777:Foo bar' ], ]; } @@ -749,4 +754,47 @@ class TitleTest extends MediaWikiTestCase { public function testGetPrefixedText( Title $title, $expected ) { $this->assertEquals( $expected, $title->getPrefixedText() ); } + + public function provideGetPrefixedDBKey() { + return [ + // ns = 0 + [ + Title::makeTitle( NS_MAIN, 'Foo_bar' ), + 'Foo_bar' + ], + // ns = 2 + [ + Title::makeTitle( NS_USER, 'Foo_bar' ), + 'User:Foo_bar' + ], + // ns = 3 + [ + Title::makeTitle( NS_USER_TALK, 'Foo_bar' ), + 'User_talk:Foo_bar' + ], + // fragment not included + [ + Title::makeTitle( NS_MAIN, 'Foo_bar', 'fragment' ), + 'Foo_bar' + ], + // ns = -2 + [ + Title::makeTitle( NS_MEDIA, 'Foo_bar' ), + 'Media:Foo_bar' + ], + // non-existent namespace + [ + Title::makeTitle( 100777, 'Foo_bar' ), + 'Special:Badtitle/NS100777:Foo_bar' + ], + ]; + } + + /** + * @covers Title::getPrefixedDBKey + * @dataProvider provideGetPrefixedDBKey + */ + public function testGetPrefixedDBKey( Title $title, $expected ) { + $this->assertEquals( $expected, $title->getPrefixedDBkey() ); + } } diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php index 253ac959ff..ee0ad946bd 100644 --- a/tests/phpunit/includes/api/ApiBaseTest.php +++ b/tests/phpunit/includes/api/ApiBaseTest.php @@ -174,4 +174,43 @@ class ApiBaseTest extends ApiTestCase { ], $user ) ); } + /** + * @covers ApiBase::dieStatus + */ + public function testDieStatus() { + $mock = new MockApi(); + + $status = StatusValue::newGood(); + $status->error( 'foo' ); + $status->warning( 'bar' ); + try { + $mock->dieStatus( $status ); + $this->fail( 'Expected exception not thrown' ); + } catch ( ApiUsageException $ex ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' ); + $this->assertFalse( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' ); + } + + $status = StatusValue::newGood(); + $status->warning( 'foo' ); + $status->warning( 'bar' ); + try { + $mock->dieStatus( $status ); + $this->fail( 'Expected exception not thrown' ); + } catch ( ApiUsageException $ex ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'foo' ), 'Exception has "foo"' ); + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'bar' ), 'Exception has "bar"' ); + } + + $status = StatusValue::newGood(); + $status->setOk( false ); + try { + $mock->dieStatus( $status ); + $this->fail( 'Expected exception not thrown' ); + } catch ( ApiUsageException $ex ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'unknownerror-nocode' ), + 'Exception has "unknownerror-nocode"' ); + } + } + } diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php index f01a670b71..028d3b4135 100644 --- a/tests/phpunit/includes/api/ApiParseTest.php +++ b/tests/phpunit/includes/api/ApiParseTest.php @@ -9,18 +9,117 @@ */ class ApiParseTest extends ApiTestCase { - protected function setUp() { - parent::setUp(); - $this->doLogin(); + protected static $pageId; + protected static $revIds = []; + + public function addDBDataOnce() { + $user = static::getTestSysop()->getUser(); + $title = Title::newFromText( __CLASS__ ); + $page = WikiPage::factory( $title ); + + $status = $page->doEditContent( + ContentHandler::makeContent( 'Test for revdel', $title, CONTENT_MODEL_WIKITEXT ), + __METHOD__ . ' Test for revdel', 0, false, $user + ); + if ( !$status->isOk() ) { + $this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) ); + } + self::$pageId = $status->value['revision']->getPage(); + self::$revIds['revdel'] = $status->value['revision']->getId(); + + $status = $page->doEditContent( + ContentHandler::makeContent( 'Test for oldid', $title, CONTENT_MODEL_WIKITEXT ), + __METHOD__ . ' Test for oldid', 0, false, $user + ); + if ( !$status->isOk() ) { + $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); + } + self::$revIds['oldid'] = $status->value['revision']->getId(); + + $status = $page->doEditContent( + ContentHandler::makeContent( 'Test for latest', $title, CONTENT_MODEL_WIKITEXT ), + __METHOD__ . ' Test for latest', 0, false, $user + ); + if ( !$status->isOk() ) { + $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) ); + } + self::$revIds['latest'] = $status->value['revision']->getId(); + + RevisionDeleter::createList( + 'revision', RequestContext::getMain(), $title, [ self::$revIds['revdel'] ] + )->setVisibility( [ + 'value' => [ + Revision::DELETED_TEXT => 1, + ], + 'comment' => 'Test for revdel', + ] ); + + Title::clearCaches(); // Otherwise it has the wrong latest revision for some reason } - public function testParseNonexistentPage() { - $somePage = mt_rand(); + public function testParseByName() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => __CLASS__, + ] ); + $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'page' => __CLASS__, + 'disablelimitreport' => 1, + ] ); + $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + } + + public function testParseById() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'pageid' => self::$pageId, + ] ); + $this->assertContains( 'Test for latest', $res[0]['parse']['text'] ); + } + + public function testParseByOldId() { + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => self::$revIds['oldid'], + ] ); + $this->assertContains( 'Test for oldid', $res[0]['parse']['text'] ); + $this->assertArrayNotHasKey( 'textdeleted', $res[0]['parse'] ); + $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] ); + } + + public function testParseRevDel() { + $user = static::getTestUser()->getUser(); + $sysop = static::getTestSysop()->getUser(); try { $this->doApiRequest( [ 'action' => 'parse', - 'page' => $somePage ] ); + 'oldid' => self::$revIds['revdel'], + ], null, null, $user ); + $this->fail( "API did not return an error as expected" ); + } catch ( ApiUsageException $ex ) { + $this->assertTrue( ApiTestCase::apiExceptionHasCode( $ex, 'permissiondenied' ), + "API failed with error 'permissiondenied'" ); + } + + $res = $this->doApiRequest( [ + 'action' => 'parse', + 'oldid' => self::$revIds['revdel'], + ], null, null, $sysop ); + $this->assertContains( 'Test for revdel', $res[0]['parse']['text'] ); + $this->assertArrayHasKey( 'textdeleted', $res[0]['parse'] ); + $this->assertArrayNotHasKey( 'textsuppressed', $res[0]['parse'] ); + } + + public function testParseNonexistentPage() { + try { + $this->doApiRequest( [ + 'action' => 'parse', + 'page' => 'DoesNotExist', + ] ); $this->fail( "API did not return an error when parsing a nonexistent page" ); } catch ( ApiUsageException $ex ) { diff --git a/tests/phpunit/includes/auth/AuthManagerTest.php b/tests/phpunit/includes/auth/AuthManagerTest.php index 015fb3e633..a8405992fe 100644 --- a/tests/phpunit/includes/auth/AuthManagerTest.php +++ b/tests/phpunit/includes/auth/AuthManagerTest.php @@ -611,7 +611,7 @@ class AuthManagerTest extends \MediaWikiTestCase { $this->assertSame( 'de', $user->getOption( 'language' ) ); $this->assertSame( 'zh', $user->getOption( 'variant' ) ); - $this->setMwGlobals( 'wgContLang', \Language::factory( 'en' ) ); + $this->setMwGlobals( 'wgContLang', \Language::factory( 'fr' ) ); $user = \User::newFromName( self::usernameForCreation() ); $user->addToDatabase(); diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php index 308e6de11e..029d1fe386 100644 --- a/tests/phpunit/includes/changes/EnhancedChangesListTest.php +++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php @@ -98,6 +98,9 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $recentChange = $this->getEditChange( '20131103092153' ); $enhancedChangesList->recentChangesLine( $recentChange, false ); + $html = $enhancedChangesList->endRecentChangesList(); + $this->assertContains( 'data-mw-revid="5"', $html ); + $recentChange2 = $this->getEditChange( '20131103092253' ); $enhancedChangesList->recentChangesLine( $recentChange2, false ); @@ -105,6 +108,13 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { preg_match_all( '/td class="mw-enhanced-rc-nested"/', $html, $matches ); $this->assertCount( 2, $matches[0] ); + + $recentChange3 = $this->getLogChange(); + $enhancedChangesList->recentChangesLine( $recentChange3, false ); + + $html = $enhancedChangesList->endRecentChangesList(); + $this->assertContains( 'data-mw-logaction="foo/bar"', $html ); + $this->assertContains( 'data-mw-logid="25"', $html ); } /** @@ -129,6 +139,15 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { return $recentChange; } + private function getLogChange() { + $user = $this->getMutableTestUser()->getUser(); + $recentChange = $this->testRecentChangesHelper->makeLogRecentChange( 'foo', 'bar', $user, + 'Title', '20131103092153', 0, 0 + ); + + return $recentChange; + } + /** * @return RecentChange */ diff --git a/tests/phpunit/includes/changes/OldChangesListTest.php b/tests/phpunit/includes/changes/OldChangesListTest.php index 51cfadcb6f..f892eb70ed 100644 --- a/tests/phpunit/includes/changes/OldChangesListTest.php +++ b/tests/phpunit/includes/changes/OldChangesListTest.php @@ -119,15 +119,17 @@ class OldChangesListTest extends MediaWikiLangTestCase { ); } - public function testRecentChangesLine_Tags() { + public function testRecentChangesLine_Attribs() { $recentChange = $this->getEditChange(); $recentChange->mAttribs['ts_tags'] = 'vandalism,newbie'; $oldChangesList = $this->getOldChangesList(); $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 ); - $this->assertRegExp( '/
  • /', $line ); - $this->assertRegExp( '/
  • /', $line ); + $this->assertRegExp( '/
  • /', + $line ); + $this->assertRegExp( '/
  • /', + $line ); } public function testRecentChangesLine_numberOfWatchingUsers() { diff --git a/tests/phpunit/includes/db/DatabaseSQLTest.php b/tests/phpunit/includes/db/DatabaseSQLTest.php index b6088ff676..206655c05e 100644 --- a/tests/phpunit/includes/db/DatabaseSQLTest.php +++ b/tests/phpunit/includes/db/DatabaseSQLTest.php @@ -17,13 +17,13 @@ class DatabaseSQLTest extends MediaWikiTestCase { protected function assertLastSql( $sqlText ) { $this->assertEquals( - $this->database->getLastSqls(), - $sqlText + $sqlText, + $this->database->getLastSqls() ); } protected function assertLastSqlDb( $sqlText, $db ) { - $this->assertEquals( $db->getLastSqls(), $sqlText ); + $this->assertEquals( $sqlText, $db->getLastSqls() ); } /** @@ -365,7 +365,8 @@ class DatabaseSQLTest extends MediaWikiTestCase { $sql['conds'], __METHOD__, isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [], - isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [] + isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [], + isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : [] ); $this->assertLastSql( $sqlTextNative ); @@ -380,7 +381,8 @@ class DatabaseSQLTest extends MediaWikiTestCase { $sql['conds'], __METHOD__, isset( $sql['insertOptions'] ) ? $sql['insertOptions'] : [], - isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [] + isset( $sql['selectOptions'] ) ? $sql['selectOptions'] : [], + isset( $sql['selectJoinConds'] ) ? $sql['selectJoinConds'] : [] ); $this->assertLastSqlDb( implode( '; ', [ $sqlSelect, $sqlInsert ] ), $dbWeb ); } @@ -397,7 +399,7 @@ class DatabaseSQLTest extends MediaWikiTestCase { "INSERT INTO insert_table " . "(field_insert,field) " . "SELECT field_select,field2 " . - "FROM select_table", + "FROM select_table WHERE *", "SELECT field_select AS field_insert,field2 AS field " . "FROM select_table WHERE * FOR UPDATE", "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')" @@ -437,6 +439,28 @@ class DatabaseSQLTest extends MediaWikiTestCase { "FROM select_table WHERE field = '2' ORDER BY field FOR UPDATE", "INSERT IGNORE INTO insert_table (field_insert,field) VALUES ('0','1')" ], + [ + [ + 'destTable' => 'insert_table', + 'srcTable' => [ 'select_table1', 'select_table2' ], + 'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ], + 'conds' => [ 'field' => 2 ], + 'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ], + 'selectJoinConds' => [ + 'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ], + ], + ], + "INSERT INTO insert_table " . + "(field_insert,field) " . + "SELECT field_select,field2 " . + "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " . + "WHERE field = '2' " . + "ORDER BY field", + "SELECT field_select AS field_insert,field2 AS field " . + "FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " . + "WHERE field = '2' ORDER BY field FOR UPDATE", + "INSERT INTO insert_table (field_insert,field) VALUES ('0','1')" + ], ]; } diff --git a/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php b/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php new file mode 100644 index 0000000000..4415bc9717 --- /dev/null +++ b/tests/phpunit/includes/libs/http/HttpAcceptNegotiatorTest.php @@ -0,0 +1,151 @@ +getFirstSupportedValue( $accepted, $default ); + + $this->assertEquals( $expected, $actual ); + } + + public function provideGetBestSupportedKey() { + return [ + [ // #0: empty + [], // supported + [], // accepted + null, // default + null, // expected + ], + [ // #1: simple + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xzy' => 1, 'text/bar' => 0.5 ], // accepted + null, // default + 'text/BAR', // expected + ], + [ // #2: default + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xzy' => 1, 'text/xoo' => 0.5 ], // accepted + 'X', // default + 'X', // expected + ], + [ // #3: weighted + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/foo' => 0.3, 'text/BAR' => 0.8, 'application/zuul' => 0.5 ], // accepted + null, // default + 'text/BAR', // expected + ], + [ // #4: zero weight + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/foo' => 0, 'text/xoo' => 1 ], // accepted + null, // default + null, // expected + ], + [ // #5: * wildcard + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xoo' => 0.5, '*' => 0.1 ], // accepted + null, // default + 'text/foo', // expected + ], + [ // #6: */* wildcard + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xoo' => 0.5, '*/*' => 0.1 ], // accepted + null, // default + 'text/foo', // expected + ], + [ // #7: text/* wildcard + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/foo' => 0.3, 'application/*' => 0.8 ], // accepted + null, // default + 'application/zuul', // expected + ], + [ // #8: Test specific format preferred over wildcard (T133314) + [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported + [ '*/*' => 1, 'text/html' => 1 ], // accepted + null, // default + 'text/html', // expected + ], + [ // #9: Test specific format preferred over range (T133314) + [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported + [ 'text/*' => 1, 'text/html' => 1 ], // accepted + null, // default + 'text/html', // expected + ], + [ // #10: Test range preferred over wildcard (T133314) + [ 'application/rdf+xml', 'text/html' ], // supported + [ '*/*' => 1, 'text/*' => 1 ], // accepted + null, // default + 'text/html', // expected + ], + ]; + } + + /** + * @dataProvider provideGetBestSupportedKey + */ + public function testGetBestSupportedKey( $supported, $accepted, $default, $expected ) { + $negotiator = new HttpAcceptNegotiator( $supported ); + $actual = $negotiator->getBestSupportedKey( $accepted, $default ); + + $this->assertEquals( $expected, $actual ); + } + +} diff --git a/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php b/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php new file mode 100644 index 0000000000..5bd9425305 --- /dev/null +++ b/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php @@ -0,0 +1,57 @@ + 1 ] + ], + [ // #2 + 'Accept: text/plain', + [ 'text/plain' => 1 ] + ], + [ // #3 + 'Accept: application/vnd.php.serialized, application/rdf+xml', + [ 'application/vnd.php.serialized' => 1, 'application/rdf+xml' => 1 ] + ], + [ // #4 + 'foo; q=0.2, xoo; q=0,text/n3', + [ 'text/n3' => 1, 'foo' => 0.2 ] + ], + [ // #5 + '*; q=0.2, */*; q=0.1,text/*', + [ 'text/*' => 1, '*' => 0.2, '*/*' => 0.1 ] + ], + // TODO: nicely ignore additional type paramerters + //[ // #6 + // 'Foo; q=0.2, Xoo; level=3, Bar; charset=xyz; q=0.4', + // [ 'xoo' => 1, 'bar' => 0.4, 'foo' => 0.1 ] + //], + ]; + } + + /** + * @dataProvider provideParseWeights + */ + public function testParseWeights( $header, $expected ) { + $parser = new HttpAcceptParser(); + $actual = $parser->parseWeights( $header ); + + $this->assertEquals( $expected, $actual ); // shouldn't be sensitive to order + } + +} diff --git a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php index 38d63e341c..775709f241 100644 --- a/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php @@ -98,4 +98,39 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase { // Set in tier 2 $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' ); } + + /** + * @covers MultiWriteBagOStuff::makeKey + */ + public function testMakeKey() { + $cache1 = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'makeKey' ] )->getMock(); + $cache1->expects( $this->once() )->method( 'makeKey' ) + ->willReturn( 'special' ); + + $cache2 = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'makeKey' ] )->getMock(); + $cache2->expects( $this->never() )->method( 'makeKey' ); + + $cache = new MultiWriteBagOStuff( [ 'caches' => [ $cache1, $cache2 ] ] ); + $this->assertSame( 'special', $cache->makeKey( 'a', 'b' ) ); + } + + /** + * @covers MultiWriteBagOStuff::makeGlobalKey + */ + public function testMakeGlobalKey() { + $cache1 = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'makeGlobalKey' ] )->getMock(); + $cache1->expects( $this->once() )->method( 'makeGlobalKey' ) + ->willReturn( 'special' ); + + $cache2 = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'makeGlobalKey' ] )->getMock(); + $cache2->expects( $this->never() )->method( 'makeGlobalKey' ); + + $cache = new MultiWriteBagOStuff( [ 'caches' => [ $cache1, $cache2 ] ] ); + + $this->assertSame( 'special', $cache->makeGlobalKey( 'a', 'b' ) ); + } } diff --git a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php index 728e6717d2..2b0436614e 100644 --- a/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php +++ b/tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php @@ -1191,4 +1191,40 @@ class WANObjectCacheTest extends PHPUnit_Framework_TestCase { [ null, 86400, 800, .2, 800 ] ]; } + + /** + * @covers WANObjectCache::makeKey + */ + public function testMakeKey() { + $backend = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'makeKey' ] )->getMock(); + $backend->expects( $this->once() )->method( 'makeKey' ) + ->willReturn( 'special' ); + + $wanCache = new WANObjectCache( [ + 'cache' => $backend, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ) + ] ); + + $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) ); + } + + /** + * @covers WANObjectCache::makeGlobalKey + */ + public function testMakeGlobalKey() { + $backend = $this->getMockBuilder( HashBagOStuff::class ) + ->setMethods( [ 'makeGlobalKey' ] )->getMock(); + $backend->expects( $this->once() )->method( 'makeGlobalKey' ) + ->willReturn( 'special' ); + + $wanCache = new WANObjectCache( [ + 'cache' => $backend, + 'pool' => 'testcache-hash', + 'relayer' => new EventRelayerNull( [] ) + ] ); + + $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) ); + } } diff --git a/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php b/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php new file mode 100644 index 0000000000..666dcf2a10 --- /dev/null +++ b/tests/phpunit/includes/linkeddata/PageDataRequestHandlerTest.php @@ -0,0 +1,288 @@ +interfaceTitle = Title::newFromText( "Special:PageDataRequestHandlerTest" ); + + $this->obLevel = ob_get_level(); + } + + protected function tearDown() { + $obLevel = ob_get_level(); + + while ( ob_get_level() > $this->obLevel ) { + ob_end_clean(); + } + + if ( $obLevel !== $this->obLevel ) { + $this->fail( "Test changed output buffer level: was {$this->obLevel}" . + "before test, but $obLevel after test." + ); + } + + parent::tearDown(); + } + + /** + * @return PageDataRequestHandler + */ + protected function newHandler() { + return new PageDataRequestHandler( 'json' ); + } + + /** + * @param array $params + * @param string[] $headers + * + * @return OutputPage + */ + protected function makeOutputPage( array $params, array $headers ) { + // construct request + $request = new FauxRequest( $params ); + $request->response()->header( 'Status: 200 OK', true, 200 ); // init/reset + + foreach ( $headers as $name => $value ) { + $request->setHeader( strtoupper( $name ), $value ); + } + + // construct Context and OutputPage + $context = new DerivativeContext( RequestContext::getMain() ); + $context->setRequest( $request ); + + $output = new OutputPage( $context ); + $output->setTitle( $this->interfaceTitle ); + $context->setOutput( $output ); + + return $output; + } + + public function handleRequestProvider() { + + $cases = []; + + $cases[] = [ '', [], [], '!!', 400 ]; + + $cases[] = [ '', [ 'target' => 'Helsinki' ], [], '!!', 303, [ 'Location' => '!.+!' ] ]; + + $subpageCases = []; + foreach ( $cases as $c ) { + $case = $c; + $case[0] = 'main/'; + + if ( isset( $case[1]['target'] ) ) { + $case[0] .= $case[1]['target']; + unset( $case[1]['target'] ); + } + + $subpageCases[] = $case; + } + + $cases = array_merge( $cases, $subpageCases ); + + $cases[] = [ + '', + [ 'target' => 'Helsinki' ], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki$!' ] + ]; + + $cases[] = [ + '', + [ + 'target' => 'Helsinki', + 'revision' => '4242', + ], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki(\?|&)oldid=4242!' ] + ]; + + $cases[] = [ + '/Helsinki', + [], + [], + '!!', + 303, + [ 'Location' => '!Helsinki&action=raw!' ] + ]; + + // #31: /Q5 with "Accept: text/foobar" triggers a 406 + $cases[] = [ + 'main/Helsinki', + [], + [ 'Accept' => 'text/foobar' ], + '!!', + 406, + [], + ]; + + $cases[] = [ + 'main/Helsinki', + [], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki$!' ] + ]; + + $cases[] = [ + '/Helsinki', + [], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki$!' ] + ]; + + $cases[] = [ + 'main/AC/DC', + [], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!AC/DC$!' ] + ]; + + return $cases; + } + + /** + * @dataProvider handleRequestProvider + * + * @param string $subpage The subpage to request (or '') + * @param array $params Request parameters + * @param array $headers Request headers + * @param string $expectedOutput Regex to match the output against. + * @param int $expectedStatusCode Expected HTTP status code. + * @param string[] $expectedHeaders Expected HTTP response headers. + */ + public function testHandleRequest( + $subpage, + array $params, + array $headers, + $expectedOutput, + $expectedStatusCode = 200, + array $expectedHeaders = [] + ) { + $output = $this->makeOutputPage( $params, $headers ); + $request = $output->getRequest(); + + /* @var FauxResponse $response */ + $response = $request->response(); + + // construct handler + $handler = $this->newHandler(); + + try { + ob_start(); + $handler->handleRequest( $subpage, $request, $output ); + + if ( $output->getRedirect() !== '' ) { + // hack to apply redirect to web response + $output->output(); + } + + $text = ob_get_contents(); + ob_end_clean(); + + $this->assertEquals( $expectedStatusCode, $response->getStatusCode(), 'status code' ); + $this->assertRegExp( $expectedOutput, $text, 'output' ); + + foreach ( $expectedHeaders as $name => $exp ) { + $value = $response->getHeader( $name ); + $this->assertNotNull( $value, "header: $name" ); + $this->assertInternalType( 'string', $value, "header: $name" ); + $this->assertRegExp( $exp, $value, "header: $name" ); + } + } catch ( HttpError $e ) { + ob_end_clean(); + $this->assertEquals( $expectedStatusCode, $e->getStatusCode(), 'status code' ); + $this->assertRegExp( $expectedOutput, $e->getHTML(), 'error output' ); + } + + // We always set "Access-Control-Allow-Origin: *" + $this->assertSame( '*', $response->getHeader( 'Access-Control-Allow-Origin' ) ); + } + + public function provideHttpContentNegotiation() { + $helsinki = Title::newFromText( 'Helsinki' ); + return [ + 'Accept Header of HTML' => [ + $helsinki, + [ 'ACCEPT' => 'text/html' ], // headers + 'Helsinki' + ], + 'Accept Header without weights' => [ + $helsinki, + [ 'ACCEPT' => '*/*, text/html, text/x-wiki' ], + 'Helsinki&action=raw' + ], + 'Accept Header with weights' => [ + $helsinki, + [ 'ACCEPT' => 'text/*; q=0.5, text/json; q=0.7, application/rdf+xml; q=0.8' ], + 'Helsinki&action=raw' + ], + 'Accept Header accepting evertyhing and HTML' => [ + $helsinki, + [ 'ACCEPT' => 'text/html, */*' ], + 'Helsinki&action=raw' + ], + 'No Accept Header' => [ + $helsinki, + [], + 'Helsinki&action=raw' + ], + ]; + } + + /** + * @dataProvider provideHttpContentNegotiation + * + * @param Title $title + * @param array $headers Request headers + * @param string $expectedRedirectSuffix Expected suffix of the HTTP Location header. + * + * @throws HttpError + */ + public function testHttpContentNegotiation( + Title $title, + array $headers, + $expectedRedirectSuffix + ) { + /* @var FauxResponse $response */ + $output = $this->makeOutputPage( [], $headers ); + $request = $output->getRequest(); + + $handler = $this->newHandler(); + $handler->httpContentNegotiation( $request, $output, $title ); + + $this->assertStringEndsWith( + $expectedRedirectSuffix, + $output->getRedirect(), + 'redirect target' + ); + } +} diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php index 530afa0ce9..7a052f6035 100644 --- a/tests/phpunit/includes/media/MediaHandlerTest.php +++ b/tests/phpunit/includes/media/MediaHandlerTest.php @@ -65,27 +65,4 @@ class MediaHandlerTest extends MediaWikiTestCase { } return $result; } - - /** - * @covers MediaHandler::getPageRangesByDimensions - * - * @dataProvider provideTestGetPageRangesByDimensions - */ - public function testGetPageRangesByDimensions( $pagesByDimensions, $expected ) { - $this->assertEquals( $expected, MediaHandler::getPageRangesByDimensions( $pagesByDimensions ) ); - } - - public static function provideTestGetPageRangesByDimensions() { - return [ - [ [ '123x456' => [ 1 ] ], '123x456:1' ], - [ [ '123x456' => [ 1, 2 ] ], '123x456:1-2' ], - [ [ '123x456' => [ 1, 2, 3 ] ], '123x456:1-3' ], - [ [ '123x456' => [ 1, 2, 3, 5 ] ], '123x456:1-3,5' ], - [ [ '123x456' => [ 1, 3 ] ], '123x456:1,3' ], - [ [ '123x456' => [ 1, 2, 3, 5, 6, 7 ] ], '123x456:1-3,5-7' ], - [ [ '123x456' => [ 1, 2, 3, 5, 6, 7 ], - '789x789' => [ 4, 8, 9 ] ], '123x456:1-3,5-7/789x789:4,8-9' - ], - ]; - } } diff --git a/tests/phpunit/includes/media/XContentDimensionsTest.php b/tests/phpunit/includes/media/XContentDimensionsTest.php deleted file mode 100644 index dddcc98829..0000000000 --- a/tests/phpunit/includes/media/XContentDimensionsTest.php +++ /dev/null @@ -1,31 +0,0 @@ -dataFile( $filename ); - $headers = $file->getContentHeaders(); - $this->assertEquals( true, isset( $headers['X-Content-Dimensions'] ) ); - $this->assertEquals( $headers['X-Content-Dimensions'], $expectedXContentDimensions ); - } - - public static function provideGetContentHeaders() { - return [ - [ '80x60-2layers.xcf', '80x60:1' ], - [ 'animated.gif', '45x30:1' ], - [ 'landscape-plain.jpg', '1024x768:1' ], - [ 'portrait-rotated.jpg', '768x1024:1' ], - [ 'Wikimedia-logo.svg', '1024x1024:1' ], - [ 'webp_animated.webp', '300x225:1' ], - [ 'test.tiff', '20x20:1' ], - ]; - } -} diff --git a/tests/phpunit/includes/parser/ParserOptionsTest.php b/tests/phpunit/includes/parser/ParserOptionsTest.php index 81f0564024..d6061420e7 100644 --- a/tests/phpunit/includes/parser/ParserOptionsTest.php +++ b/tests/phpunit/includes/parser/ParserOptionsTest.php @@ -22,7 +22,6 @@ class ParserOptionsTest extends MediaWikiTestCase { return [ 'No overrides' => [ true, [] ], 'In-key options are ok' => [ true, [ - 'editsection' => false, 'thumbsize' => 1e100, 'wrapclass' => false, ] ], @@ -65,14 +64,14 @@ class ParserOptionsTest extends MediaWikiTestCase { } public static function provideOptionsHashPre30() { - $used = [ 'wrapclass', 'editsection', 'printable' ]; + $used = [ 'wrapclass', 'printable' ]; return [ 'Canonical options, nothing used' => [ [], '*!*!*!*!*!*', [] ], - 'Canonical options, used some options' => [ $used, '*!*!*!*!*', [] ], + 'Canonical options, used some options' => [ $used, '*!*!*!*!*!*', [] ], 'Used some options, non-default values' => [ $used, - '*!*!*!*!*!printable=1!wrapclass=foobar', + '*!*!*!*!*!*!printable=1!wrapclass=foobar', [ 'setWrapOutputClass' => 'foobar', 'setIsPrintable' => true, @@ -87,6 +86,14 @@ class ParserOptionsTest extends MediaWikiTestCase { 'wgHooks' => [ 'PageRenderingHash' => [ [ __CLASS__ . '::onPageRenderingHash' ] ] ], ] ], + + // Test weird historical behavior is still weird + 'Canonical options, editsection=true used' => [ [ 'editsection' ], '*!*!*!*!*', [ + 'setEditSection' => true, + ] ], + 'Canonical options, editsection=false used' => [ [ 'editsection' ], '*!*!*!*!*!edit=0', [ + 'setEditSection' => false, + ] ], ]; } @@ -117,7 +124,7 @@ class ParserOptionsTest extends MediaWikiTestCase { } public static function provideOptionsHash() { - $used = [ 'wrapclass', 'editsection', 'printable' ]; + $used = [ 'wrapclass', 'printable' ]; $classWrapper = TestingAccessWrapper::newFromClass( ParserOptions::class ); $classWrapper->getDefaults(); @@ -154,6 +161,30 @@ class ParserOptionsTest extends MediaWikiTestCase { $confstr .= '!onPageRenderingHash'; } + // Test weird historical behavior is still weird + public function testOptionsHashEditSection() { + global $wgHooks; + + $this->setMwGlobals( [ + 'wgRenderHashAppend' => '', + 'wgHooks' => [ 'PageRenderingHash' => [] ] + $wgHooks, + ] ); + + $popt = ParserOptions::newCanonical(); + $popt->registerWatcher( function ( $name ) { + $this->assertNotEquals( 'editsection', $name ); + } ); + + $this->assertTrue( $popt->getEditSection() ); + $this->assertSame( 'canonical', $popt->optionsHash( [] ) ); + $this->assertSame( 'canonical', $popt->optionsHash( [ 'editsection' ] ) ); + + $popt->setEditSection( false ); + $this->assertFalse( $popt->getEditSection() ); + $this->assertSame( 'canonical', $popt->optionsHash( [] ) ); + $this->assertSame( 'editsection=0', $popt->optionsHash( [ 'editsection' ] ) ); + } + /** * @expectedException InvalidArgumentException * @expectedExceptionMessage Unknown parser option bogus diff --git a/tests/phpunit/includes/specials/SpecialPageDataTest.php b/tests/phpunit/includes/specials/SpecialPageDataTest.php new file mode 100644 index 0000000000..c93fe479c9 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialPageDataTest.php @@ -0,0 +1,149 @@ +getContext()->setOutput( new OutputPage( $page->getContext() ) ); + + $page->setRequestHandler( new PageDataRequestHandler() ); + + return $page; + } + + public function provideExecute() { + $cases = []; + + $cases['Empty request'] = [ '', [], [], '!!', 200 ]; + + $cases['Only title specified'] = [ + '', + [ 'target' => 'Helsinki' ], + [], + '!!', + 303, + [ 'Location' => '!.+!' ] + ]; + + $cases['Accept only HTML'] = [ + '', + [ 'target' => 'Helsinki' ], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki$!' ] + ]; + + $cases['Accept only HTML with revid'] = [ + '', + [ + 'target' => 'Helsinki', + 'revision' => '4242', + ], + [ 'Accept' => 'text/HTML' ], + '!!', + 303, + [ 'Location' => '!Helsinki(\?|&)oldid=4242!' ] + ]; + + $cases['Nothing specified'] = [ + 'main/Helsinki', + [], + [], + '!!', + 303, + [ 'Location' => '!Helsinki&action=raw!' ] + ]; + + $cases['Nothing specified'] = [ + '/Helsinki', + [], + [], + '!!', + 303, + [ 'Location' => '!Helsinki&action=raw!' ] + ]; + + $cases['Invalid Accept header'] = [ + 'main/Helsinki', + [], + [ 'Accept' => 'text/foobar' ], + '!!', + 406, + [], + ]; + + return $cases; + } + + /** + * @dataProvider provideExecute + * + * @param string $subpage The subpage to request (or '') + * @param array $params Request parameters + * @param array $headers Request headers + * @param string $expRegExp Regex to match the output against. + * @param int $expCode Expected HTTP status code + * @param array $expHeaders Expected HTTP response headers + */ + public function testExecute( + $subpage, + array $params, + array $headers, + $expRegExp, + $expCode = 200, + array $expHeaders = [] + ) { + $request = new FauxRequest( $params ); + $request->response()->header( 'Status: 200 OK', true, 200 ); // init/reset + + foreach ( $headers as $name => $value ) { + $request->setHeader( strtoupper( $name ), $value ); + } + + try { + /* @var FauxResponse $response */ + list( $output, $response ) = $this->executeSpecialPage( $subpage, $request ); + + $this->assertEquals( $expCode, $response->getStatusCode(), "status code" ); + $this->assertRegExp( $expRegExp, $output, "output" ); + + foreach ( $expHeaders as $name => $exp ) { + $value = $response->getHeader( $name ); + $this->assertNotNull( $value, "header: $name" ); + $this->assertInternalType( 'string', $value, "header: $name" ); + $this->assertRegExp( $exp, $value, "header: $name" ); + } + } catch ( HttpError $e ) { + $this->assertEquals( $expCode, $e->getStatusCode(), "status code" ); + $this->assertRegExp( $expRegExp, $e->getHTML(), "error output" ); + } + } + + public function testSpecialPageWithoutParameters() { + $this->setContentLang( Language::factory( 'en' ) ); + $request = new FauxRequest(); + $request->response()->header( 'Status: 200 OK', true, 200 ); // init/reset + + list( $output, ) = $this->executeSpecialPage( '', $request ); + + $this->assertContains( + "Content negotiation applies based on you client's Accept header.", + $output, + "output" + ); + } + +} diff --git a/tests/phpunit/includes/user/PasswordResetTest.php b/tests/phpunit/includes/user/PasswordResetTest.php index 3363bca524..53f02df69c 100644 --- a/tests/phpunit/includes/user/PasswordResetTest.php +++ b/tests/phpunit/includes/user/PasswordResetTest.php @@ -10,8 +10,7 @@ class PasswordResetTest extends PHPUnit_Framework_TestCase { * @dataProvider provideIsAllowed */ public function testIsAllowed( $passwordResetRoutes, $enableEmail, - $allowsAuthenticationDataChange, $canEditPrivate, $canSeePassword, - $userIsBlocked, $isAllowed + $allowsAuthenticationDataChange, $canEditPrivate, $block, $globalBlock, $isAllowed ) { $config = new HashConfig( [ 'PasswordResetRoutes' => $passwordResetRoutes, @@ -25,13 +24,12 @@ class PasswordResetTest extends PHPUnit_Framework_TestCase { $user = $this->getMockBuilder( User::class )->getMock(); $user->expects( $this->any() )->method( 'getName' )->willReturn( 'Foo' ); - $user->expects( $this->any() )->method( 'isBlocked' )->willReturn( $userIsBlocked ); + $user->expects( $this->any() )->method( 'getBlock' )->willReturn( $block ); + $user->expects( $this->any() )->method( 'getGlobalBlock' )->willReturn( $globalBlock ); $user->expects( $this->any() )->method( 'isAllowed' ) - ->will( $this->returnCallback( function ( $perm ) use ( $canEditPrivate, $canSeePassword ) { + ->will( $this->returnCallback( function ( $perm ) use ( $canEditPrivate ) { if ( $perm === 'editmyprivateinfo' ) { return $canEditPrivate; - } elseif ( $perm === 'passwordreset' ) { - return $canSeePassword; } else { $this->fail( 'Unexpected permission check' ); } @@ -44,67 +42,103 @@ class PasswordResetTest extends PHPUnit_Framework_TestCase { public function provideIsAllowed() { return [ - [ + 'no routes' => [ 'passwordResetRoutes' => [], 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'canSeePassword' => true, - 'userIsBlocked' => false, + 'block' => null, + 'globalBlock' => null, 'isAllowed' => false, ], - [ + 'email disabled' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => false, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'canSeePassword' => true, - 'userIsBlocked' => false, + 'block' => null, + 'globalBlock' => null, 'isAllowed' => false, ], - [ + 'auth data change disabled' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, 'allowsAuthenticationDataChange' => false, 'canEditPrivate' => true, - 'canSeePassword' => true, - 'userIsBlocked' => false, + 'block' => null, + 'globalBlock' => null, 'isAllowed' => false, ], - [ + 'cannot edit private data' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => false, - 'canSeePassword' => true, - 'userIsBlocked' => false, + 'block' => null, + 'globalBlock' => null, 'isAllowed' => false, ], - [ + 'blocked with account creation disabled' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'canSeePassword' => true, - 'userIsBlocked' => true, + 'block' => new Block( [ 'createAccount' => true ] ), + 'globalBlock' => null, 'isAllowed' => false, ], - [ + 'blocked w/o account creation disabled' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'canSeePassword' => false, - 'userIsBlocked' => false, + 'block' => new Block( [] ), + 'globalBlock' => null, 'isAllowed' => true, ], - [ + 'using blocked proxy' => [ 'passwordResetRoutes' => [ 'username' => true ], 'enableEmail' => true, 'allowsAuthenticationDataChange' => true, 'canEditPrivate' => true, - 'canSeePassword' => true, - 'userIsBlocked' => false, + 'block' => new Block( [ 'systemBlock' => 'proxy' ] ), + 'globalBlock' => null, + 'isAllowed' => false, + ], + 'globally blocked with account creation disabled' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => null, + 'globalBlock' => new Block( [ 'systemBlock' => 'global-block', 'createAccount' => true ] ), + 'isAllowed' => false, + ], + 'globally blocked with account creation not disabled' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => null, + 'globalBlock' => new Block( [ 'systemBlock' => 'global-block', 'createAccount' => false ] ), + 'isAllowed' => true, + ], + 'blocked via wgSoftBlockRanges' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => new Block( [ 'systemBlock' => 'wgSoftBlockRanges', 'anonOnly' => true ] ), + 'globalBlock' => null, + 'isAllowed' => true, + ], + 'all OK' => [ + 'passwordResetRoutes' => [ 'username' => true ], + 'enableEmail' => true, + 'allowsAuthenticationDataChange' => true, + 'canEditPrivate' => true, + 'block' => null, + 'globalBlock' => null, 'isAllowed' => true, ], ]; diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php index 22fd7b8510..a474f20c6f 100644 --- a/tests/phpunit/languages/LanguageTest.php +++ b/tests/phpunit/languages/LanguageTest.php @@ -1739,9 +1739,8 @@ class LanguageTest extends LanguageClassesTestCase { [ 'zh', 'zh', 'zh is defined as the parent language of zh, ' . 'because zh converter can convert zh-cn to zh' ], [ 'zh-invalid', null, 'do not be fooled by arbitrarily composed language codes' ], - [ 'en-gb', null, 'en does not have converter' ], - [ 'en', null, 'en does not have converter. Although FakeConverter ' - . 'handles en -> en conversion but it is useless' ], + [ 'de-formal', null, 'de does not have converter' ], + [ 'de', null, 'de does not have converter' ], ]; } diff --git a/tests/phpunit/mocks/filebackend/MockFSFile.php b/tests/phpunit/mocks/filebackend/MockFSFile.php index 8ee45a87cf..047c03ad1f 100644 --- a/tests/phpunit/mocks/filebackend/MockFSFile.php +++ b/tests/phpunit/mocks/filebackend/MockFSFile.php @@ -22,8 +22,8 @@ */ /** - * Class representing an in memory fake file. - * This is intended for unit testing / developement when you do not want + * Class representing an in-memory fake file. + * This is intended for unit testing / development when you do not want * to hit the filesystem. * * It reimplements abstract methods with some hardcoded values. Might diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 53362c4665..ee3cd5bbfb 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -94,6 +94,7 @@ return [ 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js', 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js', 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js', + 'tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js', diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js new file mode 100644 index 0000000000..edaaa39f6e --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js @@ -0,0 +1,262 @@ +/* eslint-disable camelcase */ +/* eslint no-underscore-dangle: "off" */ +( function ( mw, $ ) { + var mockFilterStructure = [ { + name: 'group1', + title: 'Group 1', + type: 'send_unselected_if_any', + filters: [ + { name: 'filter1', default: true }, + { name: 'filter2' } + ] + }, { + name: 'group2', + title: 'Group 2', + type: 'send_unselected_if_any', + filters: [ + { name: 'filter3' }, + { name: 'filter4', default: true } + ] + }, { + name: 'group3', + title: 'Group 3', + type: 'string_options', + filters: [ + { name: 'filter5' }, + { name: 'filter6' } + ] + } ], + minimalDefaultParams = { + filter1: '1', + filter4: '1' + }; + + QUnit.module( 'mediawiki.rcfilters - UriProcessor' ); + + QUnit.test( 'getVersion', function ( assert ) { + var uriProcessor = new mw.rcfilters.UriProcessor( new mw.rcfilters.dm.FiltersViewModel() ); + + assert.ok( + uriProcessor.getVersion( { param1: 'foo', urlversion: '2' } ), + 2, + 'Retrieving the version from the URI query' + ); + + assert.ok( + uriProcessor.getVersion( { param1: 'foo' } ), + 1, + 'Getting version 1 if no version is specified' + ); + } ); + + QUnit.test( 'updateModelBasedOnQuery & getUriParametersFromModel', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + baseParams = { + filter1: '0', + filter2: '0', + filter3: '0', + filter4: '0', + group3: '', + highlight: '0', + invert: '0', + group1__filter1_color: null, + group1__filter2_color: null, + group2__filter3_color: null, + group2__filter4_color: null, + group3__filter5_color: null, + group3__filter6_color: null + }; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + uriProcessor.updateModelBasedOnQuery( {} ); + assert.deepEqual( + uriProcessor.getUriParametersFromModel(), + $.extend( true, {}, baseParams, minimalDefaultParams ), + 'Version 1: Empty url query sets model to defaults' + ); + + uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } ); + assert.deepEqual( + uriProcessor.getUriParametersFromModel(), + baseParams, + 'Version 2: Empty url query sets model to all-false' + ); + + uriProcessor.updateModelBasedOnQuery( { filter1: '1', urlversion: '2' } ); + assert.deepEqual( + uriProcessor.getUriParametersFromModel(), + $.extend( true, {}, baseParams, { filter1: '1' } ), + 'Parameters in Uri query set parameter value in the model' + ); + + uriProcessor.updateModelBasedOnQuery( { highlight: '1', group1__filter1_color: 'c1', urlversion: '2' } ); + assert.deepEqual( + uriProcessor.getUriParametersFromModel(), + $.extend( true, {}, baseParams, { + highlight: '1', + group1__filter1_color: 'c1' + } ), + 'Highlight parameters in Uri query set highlight state in the model' + ); + + uriProcessor.updateModelBasedOnQuery( { invert: '1', urlversion: '2' } ); + assert.deepEqual( + uriProcessor.getUriParametersFromModel(), + $.extend( true, {}, baseParams, { + invert: '1' + } ), + 'Invert parameter in Uri query set invert state in the model' + ); + } ); + + QUnit.test( 'isNewState', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + states: { + curr: {}, + new: {} + }, + result: false, + message: 'Empty objects are not new state.' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '0' } + }, + result: true, + message: 'Nulified parameter is a new state' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '1', filter2: '1' } + }, + result: true, + message: 'Added parameters are a new state' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '1', filter2: '0' } + }, + result: false, + message: 'Added null parameters are not a new state (normalizing equals old state)' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '1', foo: 'bar' } + }, + result: true, + message: 'Added unrecognized parameters are a new state' + }, + { + states: { + curr: { filter1: '1', foo: 'bar' }, + new: { filter1: '1', foo: 'baz' } + }, + result: true, + message: 'Changed unrecognized parameters are a new state' + } + ]; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + cases.forEach( function ( testCase ) { + assert.equal( + uriProcessor.isNewState( testCase.states.curr, testCase.states.new ), + testCase.result, + testCase.message + ); + } ); + } ); + + QUnit.test( 'doesQueryContainRecognizedParams', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + query: {}, + result: false, + message: 'Empty query is not valid for load.' + }, + { + query: { highlight: '1' }, + result: false, + message: 'Highlight state alone is not valid for load' + }, + { + query: { urlversion: '2' }, + result: true, + message: 'urlversion=2 state alone is valid for load as an empty state' + }, + { + query: { filter1: '1', foo: 'bar' }, + result: true, + message: 'Existence of recognized parameters makes the query valid for load' + }, + { + query: { foo: 'bar', debug: true }, + result: false, + message: 'Only unrecognized parameters makes the query invalid for load' + } + ]; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + cases.forEach( function ( testCase ) { + assert.equal( + uriProcessor.doesQueryContainRecognizedParams( testCase.query ), + testCase.result, + testCase.message + ); + } ); + } ); + + QUnit.test( '_getNormalizedQueryParams', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + query: {}, + result: $.extend( true, { urlversion: '2' }, minimalDefaultParams ), + message: 'Empty query returns defaults (urlversion 1).' + }, + { + query: { urlversion: '2' }, + result: { urlversion: '2' }, + message: 'Empty query returns empty (urlversion 2)' + }, + { + query: { filter1: '0' }, + result: { urlversion: '2', filter4: '1' }, + message: 'urlversion 1 returns query that overrides defaults' + }, + { + query: { filter3: '1' }, + result: { urlversion: '2', filter1: '1', filter4: '1', filter3: '1' }, + message: 'urlversion 1 with an extra param value returns query that is joined with defaults' + } + ]; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + cases.forEach( function ( testCase ) { + assert.deepEqual( + uriProcessor._getNormalizedQueryParams( testCase.query ), + testCase.result, + testCase.message + ); + } ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js index 714739b680..233ec761ee 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -10,6 +10,9 @@ 'group2filter1-desc': 'Description of Filter 1 in Group 2', 'group2filter2-label': 'xGroup 2: Filter 2', 'group2filter2-desc': 'Description of Filter 2 in Group 2' + }, + config: { + wgStructuredChangeFiltersEnableExperimentalViews: true } } ) ); @@ -63,9 +66,15 @@ } ] } ], + namespaces = { + 0: 'Main', + 1: 'Talk', + 2: 'User', + 3: 'User talk' + }, model = new mw.rcfilters.dm.FiltersViewModel(); - model.initializeFilters( definition ); + model.initializeFilters( definition, namespaces ); assert.ok( model.getItemByName( 'group1__filter1' ) instanceof mw.rcfilters.dm.FilterItem && @@ -74,6 +83,10 @@ model.getItemByName( 'group2__filter2' ) instanceof mw.rcfilters.dm.FilterItem && model.getItemByName( 'group3__filter1' ) instanceof mw.rcfilters.dm.FilterItem && model.getItemByName( 'group3__filter2' ) instanceof mw.rcfilters.dm.FilterItem, + model.getItemByName( 'namespace__0' ) instanceof mw.rcfilters.dm.FilterItem, + model.getItemByName( 'namespace__1' ) instanceof mw.rcfilters.dm.FilterItem, + model.getItemByName( 'namespace__2' ) instanceof mw.rcfilters.dm.FilterItem, + model.getItemByName( 'namespace__3' ) instanceof mw.rcfilters.dm.FilterItem, 'Filters instantiated and stored correctly' ); @@ -85,7 +98,11 @@ group2__filter1: false, group2__filter2: false, group3__filter1: false, - group3__filter2: false + group3__filter2: false, + namespace__0: false, + namespace__1: false, + namespace__2: false, + namespace__3: false }, 'Initial state of filters' ); @@ -103,7 +120,11 @@ group2__filter1: false, group2__filter2: true, group3__filter1: true, - group3__filter2: false + group3__filter2: false, + namespace__0: false, + namespace__1: false, + namespace__2: false, + namespace__3: false }, 'Updating filter states correctly' ); @@ -188,16 +209,6 @@ assert.deepEqual( model.getDefaultParams(), { - group1__hidefilter1_color: null, - group1__hidefilter2_color: null, - group1__hidefilter3_color: null, - group2__hidefilter4_color: null, - group2__hidefilter5_color: null, - group2__hidefilter6_color: null, - group3__filter7_color: null, - group3__filter8_color: null, - group3__filter9_color: null, - highlight: '0', hidefilter1: '1', hidefilter2: '0', hidefilter3: '1', @@ -245,6 +256,12 @@ } ] } ], + namespaces = { + 0: 'Main', + 1: 'Talk', + 2: 'User', + 3: 'User talk' + }, testCases = [ { query: 'group', @@ -269,6 +286,18 @@ group2: [ 'group2__filter1', 'group2__filter2' ] }, reason: 'Finds filters containing the query string in their group title' + }, + { + query: ':Main', + expectedMatches: { + namespace: [ 'namespace__0' ] + }, + reason: 'Finds namespaces when using : prefix' + }, + { + query: ':group', + expectedMatches: {}, + reason: 'Finds no results if using namespaces prefix (:) to search for filter title' } ], model = new mw.rcfilters.dm.FiltersViewModel(), @@ -282,7 +311,7 @@ return result; }; - model.initializeFilters( definition ); + model.initializeFilters( definition, namespaces ); testCases.forEach( function ( testCase ) { matches = model.findMatches( testCase.query ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js index 8786993869..477550bd4b 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js @@ -12,7 +12,7 @@ assert.strictEqual( $( '.toc' ).length, 0, 'There is no table of contents on the page at the beginning' ); tocHtml = '
    ' + - '
    ' + + '
    ' + '

    Contents

    ' + '
    ' + '
    ' +