Merge "Link to mediawiki.org's SkinDistributor from the fallback skin"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 28 Nov 2014 23:25:47 +0000 (23:25 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 28 Nov 2014 23:25:47 +0000 (23:25 +0000)
56 files changed:
RELEASE-NOTES-1.25
autoload.php
docs/hooks.txt
includes/DefaultSettings.php
includes/Defines.php
includes/User.php
includes/api/ApiBase.php
includes/api/ApiOpenSearch.php
includes/api/ApiQueryTags.php
includes/api/i18n/be-tarask.json
includes/api/i18n/en.json
includes/api/i18n/mk.json
includes/api/i18n/qqq.json
includes/api/i18n/sv.json
includes/api/i18n/zh-hans.json
includes/api/i18n/zh-hant.json
includes/parser/CoreParserFunctions.php
includes/parser/MWTidy.php
includes/parser/Parser.php
includes/search/SearchEngine.php
includes/site/SiteListFileCache.php [new file with mode: 0644]
includes/site/SiteListFileCacheBuilder.php [new file with mode: 0644]
includes/site/SiteSQLStore.php
includes/skins/SkinFallbackTemplate.php
includes/skins/SkinTemplate.php
includes/specials/SpecialBlock.php
includes/specials/SpecialBlockList.php
includes/specials/SpecialRandomInCategory.php
includes/specials/SpecialUnblock.php
includes/utils/IP.php
languages/i18n/bg.json
languages/i18n/ca.json
languages/i18n/egl.json
languages/i18n/ka.json
languages/i18n/kk-cyrl.json
languages/i18n/lt.json
languages/i18n/ml.json
languages/i18n/nap.json
languages/i18n/sr-ec.json
languages/i18n/yue.json
languages/i18n/zh-hant.json
maintenance/postgres/update-keys.sql [new file with mode: 0644]
maintenance/rebuildSitesCache.php [new file with mode: 0644]
opensearch_desc.php
resources/src/mediawiki.less/mediawiki.ui/mixins.less
resources/src/mediawiki.ui/components/checkbox.less
resources/src/mediawiki.ui/components/forms.less
resources/src/mediawiki.ui/components/icons.less
resources/src/mediawiki.ui/components/inputs.less
resources/src/mediawiki.ui/components/radio.less
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/cache/GenderCacheTest.php
tests/phpunit/includes/parser/TagHooksTest.php
tests/phpunit/includes/site/SiteListFileCacheBuilderTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteListFileCacheTest.php [new file with mode: 0644]
tests/phpunit/includes/title/MediaWikiTitleCodecTest.php

index bc35e5c..caf29f0 100644 (file)
@@ -23,6 +23,10 @@ production.
   See StartProfiler.sample for details.
 * $wgMangleFlashPolicy was added to make MediaWiki's mangling of anything that
   might be a flash policy directive configurable.
+* ApiOpenSearch now supports XML output. The OpenSearchXml extension should no
+  longer be used. If extracts and page images are desired, the TextExtracts and
+  PageImages extensions are required.
+* $wgOpenSearchTemplate is deprecated in favor of $wgOpenSearchTemplates.
 
 === New features in 1.25 ===
 * (bug 62861) Updated plural rules to CLDR 26. Includes incompatible changes
@@ -52,6 +56,9 @@ production.
 * The debug logging internals have been overhauled, and are now using the
   PSR-3 interfaces.
 * Update CSSJanus to v1.1.1.
+* Added a hook, "ApiOpenSearchSuggest", to allow extensions to provide extracts
+  and images for ApiOpenSearch output. The semantics are identical to the
+  "OpenSearchXml" hook provided by the OpenSearchXml extension.
 
 === Bug fixes in 1.25 ===
 * (bug 71003) No additional code will be generated to try to load CSS-embedded
@@ -106,6 +113,11 @@ production.
 * Query page data for generator=search and generator=prefixsearch will now
   include an "index" field, which may be used by the client for sorting the
   search results.
+* ApiOpenSearch now supports XML output.
+* ApiOpenSearch will now output descriptions and URLs as array indexes 2 and 3
+  in JSON format.
+* (bug T76051) list=tags will now continue correctly.
+* (bug T76052) list=tags can now indicate whether a tag is defined.
 
 === Action API internal changes in 1.25 ===
 * ApiHelp has been rewritten to support i18n and paginated HTML output.
index 472d17e..58e62b9 100644 (file)
@@ -917,6 +917,7 @@ $wgAutoloadLocalClasses = array(
        'RebuildLocalisationCache' => __DIR__ . '/maintenance/rebuildLocalisationCache.php',
        'RebuildMessages' => __DIR__ . '/maintenance/rebuildmessages.php',
        'RebuildRecentchanges' => __DIR__ . '/maintenance/rebuildrecentchanges.php',
+       'RebuildSitesCache' => __DIR__ . '/maintenance/rebuildSitesCache.php',
        'RebuildTextIndex' => __DIR__ . '/maintenance/rebuildtextindex.php',
        'RecentChange' => __DIR__ . '/includes/changes/RecentChange.php',
        'RecompressTracked' => __DIR__ . '/maintenance/storage/recompressTracked.php',
@@ -1023,6 +1024,8 @@ $wgAutoloadLocalClasses = array(
        'SiteArray' => __DIR__ . '/includes/site/SiteList.php',
        'SiteConfiguration' => __DIR__ . '/includes/SiteConfiguration.php',
        'SiteList' => __DIR__ . '/includes/site/SiteList.php',
+       'SiteListFileCache' => __DIR__ . '/includes/site/SiteListFileCache.php',
+       'SiteListFileCacheBuilder' => __DIR__ . '/includes/site/SiteListFileCacheBuilder.php',
        'SiteObject' => __DIR__ . '/includes/site/Site.php',
        'SiteSQLStore' => __DIR__ . '/includes/site/SiteSQLStore.php',
        'SiteStats' => __DIR__ . '/includes/SiteStats.php',
index 1e4be55..0146b86 100644 (file)
@@ -407,6 +407,18 @@ $module: ApiBase Module object
 &$help: Array of HTML strings to be joined for the output.
 $options: Array Options passed to ApiHelp::getHelp
 
+'ApiOpenSearchSuggest': Called when constructing the OpenSearch results. Hooks
+can alter or append to the array.
+&$results: array of associative arrays. Keys are:
+  - title: Title object.
+  - redirect from: Title or null.
+  - extract: Description for this result.
+  - extract trimmed: If truthy, the extract will not be trimmed to
+    $wgOpenSearchDescriptionLength.
+  - image: Thumbnail for this result. Value is an array with subkeys 'source'
+    (url), 'width', 'height', 'alt', 'align'.
+  - url: Url for the given title.
+
 'APIQueryAfterExecute': After calling the execute() method of an
 action=query submodule. Use this to extend core API modules.
 &$module: Module object
index f83c402..7523193 100644 (file)
@@ -3754,6 +3754,18 @@ $wgInterwikiFallbackSite = 'wiki';
 
 /** @} */ # end of Interwiki caching settings.
 
+/**
+ * @name SiteStore caching settings.
+ * @{
+ */
+
+/**
+ * Specify the file location for the SiteStore json cache file.
+ */
+$wgSitesCacheFile = false;
+
+/** @} */ # end of SiteStore caching settings.
+
 /**
  * If local interwikis are set up which allow redirects,
  * set this regexp to restrict URLs which will be displayed
@@ -5528,9 +5540,24 @@ $wgSearchHighlightBoundaries = '[\p{Z}\p{P}\p{C}]';
  * PHP wrapper to avoid firing up mediawiki for every keystroke
  *
  * Placeholders: {searchTerms}
+ *
+ * @deprecated since 1.25 Use $wgOpenSearchTemplates['application/x-suggestions+json'] instead
  */
 $wgOpenSearchTemplate = false;
 
+/**
+ * Templates for OpenSearch suggestions, defaults to API action=opensearch
+ *
+ * Sites with heavy load would typically have these point to a custom
+ * PHP wrapper to avoid firing up mediawiki for every keystroke
+ *
+ * Placeholders: {searchTerms}
+ */
+$wgOpenSearchTemplates = array(
+       'application/x-suggestions+json' => false,
+       'application/x-suggestions+xml' => false,
+);
+
 /**
  * Enable OpenSearch suggestions requested by MediaWiki. Set this to
  * false if you've disabled scripts that use api?action=opensearch and
@@ -5545,6 +5572,11 @@ $wgEnableOpenSearchSuggest = true;
  */
 $wgOpenSearchDefaultLimit = 10;
 
+/**
+ * Minimum length of extract in <Description>. Actual extracts will last until the end of sentence.
+ */
+$wgOpenSearchDescriptionLength = 100;
+
 /**
  * Expiry time for search suggestion responses
  */
index d9e3aab..d0c2226 100644 (file)
@@ -224,6 +224,12 @@ define( 'MW_SUPPORTS_RESOURCE_MODULES', 1 );
 /**@{
  * Allowed values for Parser::$mOutputType
  * Parameter to Parser::startExternalParse().
+ * Use of Parser consts is preferred:
+ * - Parser::OT_HTML
+ * - Parser::OT_WIKI
+ * - Parser::OT_PREPROCESS
+ * - Parser::OT_MSG
+ * - Parser::OT_PLAIN
  */
 define( 'OT_HTML', 1 );
 define( 'OT_WIKI', 2 );
@@ -234,6 +240,9 @@ define( 'OT_PLAIN', 4 );
 
 /**@{
  * Flags for Parser::setFunctionHook
+ * Use of Parser consts is preferred:
+ * - Parser::SFH_NO_HASH
+ * - Parser::SFH_OBJECT_ARGS
  */
 define( 'SFH_NO_HASH', 1 );
 define( 'SFH_OBJECT_ARGS', 2 );
index 16a78f6..5348020 100644 (file)
@@ -4476,7 +4476,7 @@ class User implements IDBAccessObject {
                }
                $title = self::getGroupPage( $group );
                if ( $title ) {
-                       $page = $title->getPrefixedText();
+                       $page = $title->getFullText();
                        return "[[$page|$text]]";
                } else {
                        return $text;
index ee5861d..f9960d8 100644 (file)
@@ -1042,7 +1042,7 @@ abstract class ApiBase extends ContextSource {
         * @param array $params All supplied parameters for the module
         * @return bool
         */
-       public final function validateToken( $token, array $params ) {
+       final public function validateToken( $token, array $params ) {
                $tokenType = $this->needsToken();
                $salts = ApiQueryTokens::getTokenTypeSalts();
                if ( !isset( $salts[$tokenType] ) ) {
index 8fa495c..4a9e216 100644 (file)
@@ -3,6 +3,8 @@
  * Created on Oct 13, 2006
  *
  * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ * Copyright © 2008 Brion Vibber <brion@wikimedia.org>
+ * Copyright © 2014 Brad Jorsch <bjorsch@wikimedia.org>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  */
 class ApiOpenSearch extends ApiBase {
 
+       private $format = null;
+       private $fm = null;
+
        /**
-        * Override built-in handling of format parameter.
-        * Only JSON is supported.
+        * Get the output format
         *
-        * @return ApiFormatBase
+        * @return string
         */
-       public function getCustomPrinter() {
-               $params = $this->extractRequestParams();
-               $format = $params['format'];
-               $allowed = array( 'json', 'jsonfm' );
-               if ( in_array( $format, $allowed ) ) {
-                       return $this->getMain()->createPrinterByName( $format );
+       protected function getFormat() {
+               if ( $this->format === null ) {
+                       $params = $this->extractRequestParams();
+                       $format = $params['format'];
+
+                       $allowedParams = $this->getAllowedParams();
+                       if ( !in_array( $format, $allowedParams['format'][ApiBase::PARAM_TYPE] ) ) {
+                               $format = $allowedParams['format'][ApiBase::PARAM_DFLT];
+                       }
+
+                       if ( substr( $format, -2 ) === 'fm' ) {
+                               $this->format = substr( $format, 0, -2 );
+                               $this->fm = 'fm';
+                       } else {
+                               $this->format = $format;
+                               $this->fm = '';
+                       }
                }
+               return $this->format;
+       }
+
+       public function getCustomPrinter() {
+               switch( $this->getFormat() ) {
+                       case 'json':
+                               return $this->getMain()->createPrinterByName( 'json' . $this->fm );
 
-               return $this->getMain()->createPrinterByName( $allowed[0] );
+                       case 'xml':
+                               $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm );
+                               $printer->setRootElement( 'SearchSuggestion' );
+                               return $printer;
+
+                       default:
+                               ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
+               }
        }
 
        public function execute() {
@@ -51,21 +80,166 @@ class ApiOpenSearch extends ApiBase {
                $namespaces = $params['namespace'];
                $suggest = $params['suggest'];
 
-               // Some script that was loaded regardless of wgEnableOpenSearchSuggest, likely cached.
-               if ( $suggest && !$this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) {
-                       $searches = array();
+               if ( $params['redirects'] === null ) {
+                       // Backwards compatibility, don't resolve for JSON.
+                       $resolveRedir = $this->getFormat() !== 'json';
                } else {
+                       $resolveRedir = $params['redirects'] === 'resolve';
+               }
+
+               $results = array();
+
+               if ( !$suggest || $this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) {
                        // Open search results may be stored for a very long time
                        $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) );
                        $this->getMain()->setCacheMode( 'public' );
+                       $this->search( $search, $limit, $namespaces, $resolveRedir, $results );
+
+                       // Allow hooks to populate extracts and images
+                       wfRunHooks( 'ApiOpenSearchSuggest', array( &$results ) );
 
-                       $searcher = new StringPrefixSearch;
-                       $searches = $searcher->searchWithVariants( $search, $limit, $namespaces );
+                       // Trim extracts, if necessary
+                       $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' );
+                       foreach ( $results as &$r ) {
+                               if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) {
+                                       $r['extract'] = self::trimExtract( $r['extract'], $length );
+                               }
+                       }
                }
-               // Set top level elements
+
+               // Populate result object
+               $this->populateResult( $search, $results );
+       }
+
+       /**
+        * Perform the search
+        *
+        * @param string $search Text to search
+        * @param int $limit Maximum items to return
+        * @param array $namespaces Namespaces to search
+        * @param bool $resolveRedir Whether to resolve redirects
+        * @param array &$results Put results here
+        */
+       protected function search( $search, $limit, $namespaces, $resolveRedir, &$results ) {
+               // Find matching titles as Title objects
+               $searcher = new TitlePrefixSearch;
+               $titles = $searcher->searchWithVariants( $search, $limit, $namespaces );
+
+               if ( $resolveRedir ) {
+                       // Query for redirects
+                       $db = $this->getDb();
+                       $lb = new LinkBatch( $titles );
+                       $res = $db->select(
+                               array( 'page', 'redirect' ),
+                               array( 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ),
+                               array(
+                                       'rd_from = page_id',
+                                       'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ),
+                                       $lb->constructSet( 'page', $db ),
+                               ),
+                               __METHOD__
+                       );
+                       $redirects = array();
+                       foreach ( $res as $row ) {
+                               $redirects[$row->page_namespace][$row->page_title] =
+                                       array( $row->rd_namespace, $row->rd_title );
+                       }
+
+                       // Bypass any redirects
+                       $seen = array();
+                       foreach ( $titles as $title ) {
+                               $ns = $title->getNamespace();
+                               $dbkey = $title->getDBkey();
+                               $from = null;
+                               if ( isset( $redirects[$ns][$dbkey] ) ) {
+                                       list( $ns, $dbkey ) = $redirects[$ns][$dbkey];
+                                       $from = $title;
+                                       $title = Title::makeTitle( $ns, $dbkey );
+                               }
+                               if ( !isset( $seen[$ns][$dbkey] ) ) {
+                                       $seen[$ns][$dbkey] = true;
+                                       $results[$title->getArticleId()] = array(
+                                               'title' => $title,
+                                               'redirect from' => $from,
+                                               'extract' => false,
+                                               'extract trimmed' => false,
+                                               'image' => false,
+                                               'url' => wfExpandUrl( $title->getFullUrl(), PROTO_CURRENT ),
+                                       );
+                               }
+                       }
+               } else {
+                       foreach ( $titles as $title ) {
+                               $results[$title->getArticleId()] = array(
+                                       'title' => $title,
+                                       'redirect from' => null,
+                                       'extract' => false,
+                                       'extract trimmed' => false,
+                                       'image' => false,
+                                       'url' => wfExpandUrl( $title->getFullUrl(), PROTO_CURRENT ),
+                               );
+                       }
+               }
+       }
+
+       /**
+        * @param string $search
+        * @param array &$results
+        */
+       protected function populateResult( $search, &$results ) {
                $result = $this->getResult();
-               $result->addValue( null, 0, $search );
-               $result->addValue( null, 1, $searches );
+
+               switch ( $this->getFormat() ) {
+                       case 'json':
+                               // http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1
+                               $result->addValue( null, 0, strval( $search ) );
+                               $terms = array();
+                               $descriptions = array();
+                               $urls = array();
+                               foreach ( $results as $r ) {
+                                       $terms[] = $r['title']->getPrefixedText();
+                                       $descriptions[] = strval( $r['extract'] );
+                                       $urls[] = $r['url'];
+                               }
+                               $result->addValue( null, 1, $terms );
+                               $result->addValue( null, 2, $descriptions );
+                               $result->addValue( null, 3, $urls );
+                               break;
+
+                       case 'xml':
+                               // http://msdn.microsoft.com/en-us/library/cc891508%28v=vs.85%29.aspx
+                               $imageKeys = array(
+                                       'source' => true,
+                                       'alt' => true,
+                                       'width' => true,
+                                       'height' => true,
+                                       'align' => true,
+                               );
+                               $items = array();
+                               foreach ( $results as $r ) {
+                                       $item = array();
+                                       $result->setContent( $item, $r['title']->getPrefixedText(), 'Text' );
+                                       $result->setContent( $item, $r['url'], 'Url' );
+                                       if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
+                                               $result->setContent( $item, $r['extract'], 'Description' );
+                                       }
+                                       if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
+                                               $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
+                                       }
+                                       $items[] = $item;
+                               }
+                               $result->setIndexedTagName( $items, 'Item' );
+                               $result->addValue( null, 'version', '2.0' );
+                               $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
+                               $query = array();
+                               $result->setContent( $query, strval( $search ) );
+                               $result->addValue( null, 'Query', $query );
+                               $result->addValue( null, 'Section', $items );
+                               break;
+
+                       default:
+                               ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
+               }
        }
 
        public function getAllowedParams() {
@@ -84,9 +258,12 @@ class ApiOpenSearch extends ApiBase {
                                ApiBase::PARAM_ISMULTI => true
                        ),
                        'suggest' => false,
+                       'redirects' => array(
+                               ApiBase::PARAM_TYPE => array( 'return', 'resolve' ),
+                       ),
                        'format' => array(
                                ApiBase::PARAM_DFLT => 'json',
-                               ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ),
+                               ApiBase::PARAM_TYPE => array( 'json', 'jsonfm', 'xml', 'xmlfm' ),
                        )
                );
        }
@@ -101,4 +278,72 @@ class ApiOpenSearch extends ApiBase {
        public function getHelpUrls() {
                return 'https://www.mediawiki.org/wiki/API:Opensearch';
        }
+
+       /**
+        * Trim an extract to a sensible length.
+        *
+        * Adapted from Extension:OpenSearchXml, which adapted it from
+        * Extension:ActiveAbstract.
+        *
+        * @param string $text
+        * @param int $len Target length; actual result will continue to the end of a sentence.
+        * @return string
+        */
+       public static function trimExtract( $text, $length ) {
+               static $regex = null;
+
+               if ( $regex === null ) {
+                       $endchars = array(
+                               '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII
+                               '。', // full-width ideographic full-stop
+                               '.', '!', '?', // double-width roman forms
+                               '。', // half-width ideographic full stop
+                       );
+                       $endgroup = implode( '|', $endchars );
+                       $end = "(?:$endgroup)";
+                       $sentence = ".{{$length},}?$end+";
+                       $regex = "/^($sentence)/u";
+               }
+
+               $matches = array();
+               if ( preg_match( $regex, $text, $matches ) ) {
+                       return trim( $matches[1] );
+               } else {
+                       // Just return the first line
+                       $lines = explode( "\n", $text );
+                       return trim( $lines[0] );
+               }
+       }
+
+       /**
+        * Fetch the template for a type.
+        *
+        * @param string $type MIME type
+        * @return string
+        */
+       public static function getOpenSearchTemplate( $type ) {
+               global $wgOpenSearchTemplate, $wgCanonicalServer;
+
+               if ( $wgOpenSearchTemplate && $type === 'application/x-suggestions+json' ) {
+                       return $wgOpenSearchTemplate;
+               }
+
+               $ns = implode( '|', SearchEngine::defaultNamespaces() );
+               if ( !$ns ) {
+                       $ns = "0";
+               }
+
+               switch ( $type ) {
+                       case 'application/x-suggestions+json':
+                               return $wgCanonicalServer . wfScript( 'api' )
+                                       . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
+
+                       case 'application/x-suggestions+xml':
+                               return $wgCanonicalServer . wfScript( 'api' )
+                                       . '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns;
+
+                       default:
+                               throw new MWException( __METHOD__ . ": Unknown type '$type'" );
+               }
+       }
 }
index f3b2652..7f2dc85 100644 (file)
  */
 class ApiQueryTags extends ApiQueryBase {
 
-       /**
-        * @var ApiResult
-        */
-       private $result;
-
-       private $limit;
-       private $fld_displayname = false, $fld_description = false,
-               $fld_hitcount = false;
-
        public function __construct( ApiQuery $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'tg' );
        }
@@ -49,84 +40,77 @@ class ApiQueryTags extends ApiQueryBase {
 
                $prop = array_flip( $params['prop'] );
 
-               $this->fld_displayname = isset( $prop['displayname'] );
-               $this->fld_description = isset( $prop['description'] );
-               $this->fld_hitcount = isset( $prop['hitcount'] );
-
-               $this->limit = $params['limit'];
-               $this->result = $this->getResult();
+               $fld_displayname = isset( $prop['displayname'] );
+               $fld_description = isset( $prop['description'] );
+               $fld_hitcount = isset( $prop['hitcount'] );
+               $fld_defined = isset( $prop['defined'] );
+
+               $limit = $params['limit'];
+               $result = $this->getResult();
+
+               $definedTags = array_fill_keys( ChangeTags::listDefinedTags(), 0 );
+
+               # Fetch defined tags that aren't past the continuation
+               if ( $params['continue'] !== null ) {
+                       $cont = $params['continue'];
+                       $tags = array_filter( array_keys( $definedTags ), function ( $v ) use ( $cont ) {
+                               return $v >= $cont;
+                       } );
+                       $tags = array_fill_keys( $tags, 0 );
+               } else {
+                       $tags = $definedTags;
+               }
 
+               # Merge in all used tags
                $this->addTables( 'change_tag' );
                $this->addFields( 'ct_tag' );
-
-               $this->addFieldsIf( array( 'hitcount' => 'COUNT(*)' ), $this->fld_hitcount );
-
-               $this->addOption( 'LIMIT', $this->limit + 1 );
+               $this->addFields( array( 'hitcount' => $fld_hitcount ? 'COUNT(*)' : '0' ) );
+               $this->addOption( 'LIMIT', $limit + 1 );
                $this->addOption( 'GROUP BY', 'ct_tag' );
                $this->addWhereRange( 'ct_tag', 'newer', $params['continue'], null );
-
                $res = $this->select( __METHOD__ );
-
-               $ok = true;
-
                foreach ( $res as $row ) {
-                       if ( !$ok ) {
-                               break;
-                       }
-                       $ok = $this->doTag( $row->ct_tag, $this->fld_hitcount ? $row->hitcount : 0 );
+                       $tags[$row->ct_tag] = (int)$row->hitcount;
                }
 
-               // include tags with no hits yet
-               foreach ( ChangeTags::listDefinedTags() as $tag ) {
-                       if ( !$ok ) {
+               # Now make sure the array is sorted for proper continuation
+               ksort( $tags );
+
+               $count = 0;
+               foreach ( $tags as $tagName => $hitcount ) {
+                       if ( ++$count > $limit ) {
+                               $this->setContinueEnumParameter( 'continue', $tagName );
                                break;
                        }
-                       $ok = $this->doTag( $tag, 0 );
-               }
-
-               $this->result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'tag' );
-       }
 
-       private function doTag( $tagName, $hitcount ) {
-               static $count = 0;
-               static $doneTags = array();
+                       $tag = array();
+                       $tag['name'] = $tagName;
 
-               if ( in_array( $tagName, $doneTags ) ) {
-                       return true;
-               }
-
-               if ( ++$count > $this->limit ) {
-                       $this->setContinueEnumParameter( 'continue', $tagName );
-
-                       return false;
-               }
-
-               $tag = array();
-               $tag['name'] = $tagName;
-
-               if ( $this->fld_displayname ) {
-                       $tag['displayname'] = ChangeTags::tagDescription( $tagName );
-               }
-
-               if ( $this->fld_description ) {
-                       $msg = wfMessage( "tag-$tagName-description" );
-                       $tag['description'] = $msg->exists() ? $msg->text() : '';
-               }
+                       if ( $fld_displayname ) {
+                               $tag['displayname'] = ChangeTags::tagDescription( $tagName );
+                       }
 
-               if ( $this->fld_hitcount ) {
-                       $tag['hitcount'] = $hitcount;
-               }
+                       if ( $fld_description ) {
+                               $msg = $this->msg( "tag-$tagName-description" );
+                               $tag['description'] = $msg->exists() ? $msg->text() : '';
+                       }
 
-               $doneTags[] = $tagName;
+                       if ( $fld_hitcount ) {
+                               $tag['hitcount'] = $hitcount;
+                       }
 
-               $fit = $this->result->addValue( array( 'query', $this->getModuleName() ), null, $tag );
-               if ( !$fit ) {
-                       $this->setContinueEnumParameter( 'continue', $tagName );
+                       if ( $fld_defined && isset( $definedTags[$tagName] ) ) {
+                               $tag['defined'] = '';
+                       }
 
-                       return false;
+                       $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $tag );
+                       if ( !$fit ) {
+                               $this->setContinueEnumParameter( 'continue', $tagName );
+                               break;
+                       }
                }
 
-               return true;
+               $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'tag' );
        }
 
        public function getCacheMode( $params ) {
@@ -151,7 +135,8 @@ class ApiQueryTags extends ApiQueryBase {
                                        'name',
                                        'displayname',
                                        'description',
-                                       'hitcount'
+                                       'hitcount',
+                                       'defined',
                                ),
                                ApiBase::PARAM_ISMULTI => true
                        )
@@ -160,7 +145,7 @@ class ApiQueryTags extends ApiQueryBase {
 
        protected function getExamplesMessages() {
                return array(
-                       'action=query&list=tags&tgprop=displayname|description|hitcount'
+                       'action=query&list=tags&tgprop=displayname|description|hitcount|defined'
                                => 'apihelp-query+tags-example-simple',
                );
        }
index 5ddadc5..35f4eaa 100644 (file)
@@ -18,5 +18,6 @@
        "apihelp-main-param-uselang": "Мова для выкарыстаньня ў перакладах паведамленьняў. Сьпіс кодаў можа быць атрыманы з [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] з siprop=languages, або трэба вызначыць «user», каб ужываць наладкі мовы цяперашняга карыстальніка, або вызначыць «content», каб ужываць мову зьместу гэтай вікі.",
        "apihelp-block-description": "Блякаваньне ўдзельніка.",
        "apihelp-block-param-user": "Імя ўдзельніка, IP-адрас або IP-дыяпазон, якія вы хочаце заблякаваць.",
-       "apihelp-block-param-expiry": "Час заканчэньня. Можа быць адносным (напрыклад, «5 months» або «2 weeks») ці аблсалютным (напрыклад, «2014-09-18T12:34:56Z»). Калі выстаўлены на «infinite», «indefinite» ці «never», блякаваньне будзе бестэрміновым."
+       "apihelp-block-param-expiry": "Час заканчэньня. Можа быць адносным (напрыклад, «5 months» або «2 weeks») ці аблсалютным (напрыклад, «2014-09-18T12:34:56Z»). Калі выстаўлены на «infinite», «indefinite» ці «never», блякаваньне будзе бестэрміновым.",
+       "apihelp-block-param-reason": "Прычына блякаваньня."
 }
index 17b79ff..e941f1a 100644 (file)
        "apihelp-opensearch-param-limit": "Maximum number of results to return.",
        "apihelp-opensearch-param-namespace": "Namespaces to search.",
        "apihelp-opensearch-param-suggest": "Do nothing if [https://www.mediawiki.org/wiki/Manual:$wgEnableOpenSearchSuggest $wgEnableOpenSearchSuggest] is false.",
+       "apihelp-opensearch-param-redirects": "How to handle redirects:\n;return:Return the redirect itself.\n;resolve:Return the target page. May return fewer than $1limit results.\nFor historical reasons, the default is \"return\" for $1format=json and \"resolve\" for other formats.",
        "apihelp-opensearch-param-format": "The format of the output.",
        "apihelp-opensearch-example-te": "Find pages beginning with \"Te\"",
 
 
        "apihelp-query+tags-description": "List change tags.",
        "apihelp-query+tags-param-limit": "The maximum number of tags to list.",
-       "apihelp-query+tags-param-prop": "Which properties to get:\n;name:Adds name of tag.\n;displayname:Adds system message for the tag.\n;description:Adds description of the tag.\n;hitcount:Adds the amount of revisions that have this tag.",
+       "apihelp-query+tags-param-prop": "Which properties to get:\n;name:Adds name of tag.\n;displayname:Adds system message for the tag.\n;description:Adds description of the tag.\n;hitcount:Adds the amount of revisions that have this tag.\n;defined:Indicate whether the tag is defined.",
        "apihelp-query+tags-example-simple": "List available tags",
 
        "apihelp-query+templates-description": "Returns all pages transcluded on the given pages.",
index a3fc0db..d98b6e2 100644 (file)
        "apihelp-query+alldeletedrevisions-param-to": "Запри го исписот на овој наслов.",
        "apihelp-query+alldeletedrevisions-example-user": "Список на последните 50 избришани придонеси на Корисник:Пример",
        "apihelp-query+alldeletedrevisions-example-ns-main": "Список на последните 50 избришани преработки во главниот именски простор",
+       "apihelp-query+allimages-example-B": "Прикажи список на податотеки што почнуваат со буквата „Б“",
+       "apihelp-query+allimages-example-recent": "Прикажи список на неодамна подигнати податотеки сличен на [[Special:NewFiles]]",
+       "apihelp-query+allimages-example-generator": "Прикажи информации за околу 4 податотеки што почнуваат со буквата „Т“",
+       "apihelp-query+alllinks-description": "Наброј ги сите врски што водат кон даден именски простор.",
+       "apihelp-query+alllinks-param-from": "Наслов на врската од која ќе почне набројувањето.",
+       "apihelp-query+alllinks-param-to": "Наслов на врската на која ќе запре набројувањето.",
+       "apihelp-query+alllinks-param-prefix": "Пребарај ги сите сврзани наслови што почнуваат со оваа вредност.",
+       "apihelp-query+alllinks-param-unique": "Прикажувај само различни поврзани наслови. Не може да се користи со $1prop=ids.\nКога се користи како создавач, дава целни страници наместо изворни.",
        "apihelp-query+alllinks-param-namespace": "Именскиот простор што се набројува.",
        "apihelp-query+alllinks-param-limit": "Колку вкупно ставки да се дадат.",
        "apihelp-query+alllinks-param-dir": "Насока на исписот.",
index fb368e3..8382edc 100644 (file)
        "apihelp-opensearch-param-limit": "{{doc-apihelp-param|opensearch|limit}}",
        "apihelp-opensearch-param-namespace": "{{doc-apihelp-param|opensearch|namespace}}",
        "apihelp-opensearch-param-suggest": "{{doc-apihelp-param|opensearch|suggest}}",
+       "apihelp-opensearch-param-redirects": "{{doc-apihelp-param|opensearch|redirects}}",
        "apihelp-opensearch-param-format": "{{doc-apihelp-param|opensearch|format}}",
        "apihelp-opensearch-example-te": "{{doc-apihelp-example|opensearch}}",
        "apihelp-options-description": "{{doc-apihelp-description|options}}",
index 73b56ba..da27da9 100644 (file)
@@ -3,7 +3,8 @@
                "authors": [
                        "Jopparn",
                        "Lokal Profil",
-                       "WikiPhoenix"
+                       "WikiPhoenix",
+                       "Victorsa"
                ]
        },
        "apihelp-main-param-action": "Vilken åtgärd som ska utföras.",
@@ -28,6 +29,7 @@
        "apihelp-createaccount-description": "Skapa ett nytt användarkonto.",
        "apihelp-createaccount-param-name": "Användarnamn.",
        "apihelp-createaccount-param-password": "Lösenord (ignoreras om $1mailpassword angetts).",
+       "apihelp-createaccount-param-domain": "Domän för extern autentisering (frivillig).",
        "apihelp-createaccount-param-email": "Användarens e-postadress (valfritt).",
        "apihelp-createaccount-param-realname": "Användarens riktiga namn (valfritt).",
        "apihelp-createaccount-example-pass": "Skapa användaren \"testuser\" med lösenordet \"test123\"",
@@ -58,7 +60,7 @@
        "apihelp-expandtemplates-param-text": "Wikitext att konvertera.",
        "apihelp-feedcontributions-param-year": "Från år (och tidigare).",
        "apihelp-feedcontributions-param-month": "Från månad (och tidigare).",
-       "apihelp-feedcontributions-example-simple": "Returnerar bidrag för [[User:Example]]",
+       "apihelp-feedcontributions-example-simple": "Returnera bidrag för [[User:Example]]",
        "apihelp-feedrecentchanges-param-days": "Dagar att begränsa resultaten till.",
        "apihelp-feedrecentchanges-param-limit": "Maximalt antal resultat att returnera.",
        "apihelp-feedrecentchanges-param-hideminor": "Dölj mindre ändringar.",
        "apihelp-feedrecentchanges-param-target": "Visa endast ändringarna av sidor som den här sidan länkar till.",
        "apihelp-feedrecentchanges-example-simple": "Visa senaste ändringar",
        "apihelp-feedrecentchanges-example-30days": "Visa senaste ändringar för 30 dygn",
+       "apihelp-filerevert-description": "Återställ en fil till en äldre version.",
        "apihelp-filerevert-param-comment": "Ladda upp kommentar.",
        "apihelp-filerevert-example-revert": "Återställ Wiki.png till versionen från 2011-03-05T15:27:40Z",
+       "apihelp-help-example-main": "Hjälp för huvudmodul",
        "apihelp-help-example-recursive": "All hjälp på en sida",
        "apihelp-help-example-help": "Hjälp för själva hjälpmodulen",
        "apihelp-imagerotate-description": "Rotera en eller flera bilder.",
-       "apihelp-imagerotate-param-rotation": "Grader att rotera medurs.",
+       "apihelp-imagerotate-param-rotation": "Grader att rotera bild medurs.",
        "apihelp-imagerotate-example-simple": "Rotera [[:File:Example.png]] med 90 grader",
        "apihelp-imagerotate-example-generator": "Rotera alla bilder i [[:Category:Flip]] med 180 grader",
+       "apihelp-import-param-summary": "Importera sammanfattning.",
        "apihelp-import-param-xml": "Uppladdad XML-fil.",
+       "apihelp-import-param-interwikisource": "För interwiki-importer: wiki som du vill importera från.",
+       "apihelp-import-param-interwikipage": "För interwiki-importer: sidan som du vill importera.",
+       "apihelp-import-param-fullhistory": "För interwiki-importer: importera hela historiken, inte bara den aktuella versionen.",
+       "apihelp-import-param-templates": "För interwiki-importer: importera även alla mallar som ingår.",
+       "apihelp-import-param-namespace": "För interwiki-importer: importera till denna namnrymd.",
+       "apihelp-import-param-rootpage": "Importera som undersida till denna sida.",
+       "apihelp-import-example-import": "Importera [[meta:Help:Parserfunktioner]] till namnrymd 100 med full historik.",
        "apihelp-login-param-name": "Användarnamn.",
        "apihelp-login-param-password": "Lösenord.",
        "apihelp-login-param-domain": "Domän (valfritt).",
        "apihelp-move-param-unwatch": "Ta bort sidan och omdirigeringen från din bevakningslista.",
        "apihelp-move-param-ignorewarnings": "Ignorera alla varningar.",
        "apihelp-opensearch-param-search": "Söksträng.",
+       "apihelp-opensearch-param-limit": "Maximalt antal resultat att returnera.",
        "apihelp-opensearch-param-namespace": "Namnrymder att genomsöka.",
        "apihelp-opensearch-param-suggest": "Gör ingenting om [https://www.mediawiki.org/wiki/Manual:$wgEnableOpenSearchSuggest $wgEnableOpenSearchSuggest] är falskt.",
+       "apihelp-opensearch-param-format": "Formatet för utdata.",
+       "apihelp-opensearch-example-te": "Hitta sidor som börjar med \"Te\"",
        "apihelp-options-example-reset": "Återställ alla inställningar",
        "apihelp-paraminfo-param-helpformat": "Format för hjälpsträngar.",
        "apihelp-patrol-example-revid": "Patrullera en sidversion",
        "apihelp-query+alldeletedrevisions-paraminfo-nonuseronly": "Kan inte användas med $3user.",
        "apihelp-query+allfileusages-example-unique": "Lista unika filtitlar",
        "apihelp-query+allimages-param-sort": "Egenskap att sortera efter.",
-       "apihelp-query+allmessages-param-lang": "Returnerar meddelanden på detta språk.",
-       "apihelp-query+allmessages-example-ipb": "Visa meddelande som börjar med \"ipb-\"",
+       "apihelp-query+allmessages-param-lang": "Returnera meddelanden på detta språk.",
+       "apihelp-query+allmessages-example-ipb": "Visa meddelanden som börjar med \"ipb-\"",
        "apihelp-query+allmessages-example-de": "Visa meddelandena \"august\" och \"mainpage\" på tyska",
        "apihelp-query+allpages-param-filterredir": "Vilka sidor att lista.",
        "apihelp-query+stashimageinfo-description": "Returnerar filinformation för temporära filer.",
index 4fe99be..357c6c1 100644 (file)
        "apihelp-query+iwlinks-param-title": "用于搜索的跨wiki链接。必须与$1prefix一起使用。",
        "apihelp-query+iwlinks-example-simple": "从[[首页]]获取跨wiki链接",
        "apihelp-query+langbacklinks-param-lang": "用于语言链接的语言。",
+       "apihelp-query+langbacklinks-param-title": "要搜索的语言链接。必须与$1lang一起使用。",
        "apihelp-query+langbacklinks-example-simple": "获取链接至[[:fr:Test]]的页面",
        "apihelp-query+langbacklinks-example-generator": "获取链接至[[:fr:Test]]的页面的信息",
        "apihelp-query+langlinks-param-limit": "返回多少语言链接。",
        "apihelp-userrights-param-remove": "将用户从这些组中移除。",
        "apihelp-userrights-param-reason": "更改原因。",
        "apihelp-userrights-example-user": "将用户FooBot添加至“机器人”用户组,并从“管理员”和“行政员”组移除",
+       "apihelp-userrights-example-userid": "将ID为123的用户加入至“机器人”组,并将其从“管理员”和“行政员”组移除",
        "apihelp-watch-example-watch": "监视页面“首页”",
        "apihelp-watch-example-unwatch": "取消监视页面“首页”",
        "apihelp-dbg-description": "输出数据为PHP的var_export()格式。",
index 7af61ae..05847db 100644 (file)
@@ -2,7 +2,8 @@
        "@metadata": {
                "authors": [
                        "Cwlin0416",
-                       "Liuxinyu970226"
+                       "Liuxinyu970226",
+                       "LNDDYL"
                ]
        },
        "apihelp-main-param-action": "要執行的動作。",
@@ -53,6 +54,8 @@
        "apihelp-edit-param-sectiontitle": "新章節的標題。",
        "apihelp-edit-param-text": "頁面內容。",
        "apihelp-edit-param-summary": "編輯摘要。 當未設定 $1section=new 與 $1sectiontitle 時也會當做章節標題。",
+       "apihelp-edit-param-minor": "小編輯。",
+       "apihelp-edit-param-notminor": "非小編輯。",
        "apihelp-edit-param-createonly": "若頁面已存在,則不編輯頁面。",
        "apihelp-edit-param-nocreate": "若頁面不存在,則產生錯誤。",
        "apihelp-edit-param-watch": "加入頁面至您的監視清單。",
index d9f1761..6f19a23 100644 (file)
@@ -36,7 +36,7 @@ class CoreParserFunctions {
                # Syntax for arguments (see Parser::setFunctionHook):
                #  "name for lookup in localized magic words array",
                #  function callback,
-               #  optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}}
+               #  optional Parser::SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}}
                #    instead of {{#int:...}})
                $noHashFunctions = array(
                        'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc',
@@ -57,24 +57,24 @@ class CoreParserFunctions {
                        'revisiontimestamp', 'revisionuser', 'cascadingsources',
                );
                foreach ( $noHashFunctions as $func ) {
-                       $parser->setFunctionHook( $func, array( __CLASS__, $func ), SFH_NO_HASH );
+                       $parser->setFunctionHook( $func, array( __CLASS__, $func ), Parser::SFH_NO_HASH );
                }
 
-               $parser->setFunctionHook( 'namespace', array( __CLASS__, 'mwnamespace' ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'namespace', array( __CLASS__, 'mwnamespace' ), Parser::SFH_NO_HASH );
+               $parser->setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), Parser::SFH_NO_HASH );
                $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) );
                $parser->setFunctionHook( 'speciale', array( __CLASS__, 'speciale' ) );
-               $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS );
+               $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), Parser::SFH_OBJECT_ARGS );
                $parser->setFunctionHook( 'formatdate', array( __CLASS__, 'formatDate' ) );
 
                if ( $wgAllowDisplayTitle ) {
-                       $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
+                       $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), Parser::SFH_NO_HASH );
                }
                if ( $wgAllowSlowParserFunctions ) {
                        $parser->setFunctionHook(
                                'pagesinnamespace',
                                array( __CLASS__, 'pagesinnamespace' ),
-                               SFH_NO_HASH
+                               Parser::SFH_NO_HASH
                        );
                }
        }
index b310862..ff62e9b 100644 (file)
@@ -127,17 +127,11 @@ class MWTidy {
         * @return string Corrected HTML output
         */
        public static function tidy( $text ) {
-               global $wgTidyInternal;
-
                $wrapper = new MWTidyWrapper;
                $wrappedtext = $wrapper->getWrapped( $text );
 
                $retVal = null;
-               if ( $wgTidyInternal ) {
-                       $correctedtext = self::execInternalTidy( $wrappedtext, false, $retVal );
-               } else {
-                       $correctedtext = self::execExternalTidy( $wrappedtext, false, $retVal );
-               }
+               $correctedtext = self::clean( $wrappedtext, false, $retVal );
 
                if ( $retVal < 0 ) {
                        wfDebug( "Possible tidy configuration error!\n" );
@@ -160,16 +154,33 @@ class MWTidy {
         * @return bool Whether the HTML is valid
         */
        public static function checkErrors( $text, &$errorStr = null ) {
+               $retval = 0;
+               $errorStr = self::clean( $text, true, $retval );
+               return ( $retval < 0 && $errorStr == '' ) || $retval == 0;
+       }
+
+       /**
+        * Perform a clean/repair operation
+        * @param string $text HTML to check
+        * @param bool $stderr Whether to read result from STDERR rather than STDOUT
+        * @param int &$retval Exit code (-1 on internal error)
+        * @return string|null
+        */
+       private static function clean( $text, $stderr = false, &$retval = null ) {
                global $wgTidyInternal;
 
-               $retval = 0;
                if ( $wgTidyInternal ) {
-                       $errorStr = self::execInternalTidy( $text, true, $retval );
+                       if ( wfIsHHVM() ) {
+                               if ( $stderr ) {
+                                       throw new MWException( __METHOD__.": error text return from HHVM tidy is not supported" );
+                               }
+                               return self::hhvmClean( $text, $retval );
+                       } else {
+                               return self::phpClean( $text, $stderr, $retval );
+                       }
                } else {
-                       $errorStr = self::execExternalTidy( $text, true, $retval );
+                       return self::externalClean( $text, $stderr, $retval );
                }
-
-               return ( $retval < 0 && $errorStr == '' ) || $retval == 0;
        }
 
        /**
@@ -181,7 +192,7 @@ class MWTidy {
         * @param int &$retval Exit code (-1 on internal error)
         * @return string|null
         */
-       private static function execExternalTidy( $text, $stderr = false, &$retval = null ) {
+       private static function externalClean( $text, $stderr = false, &$retval = null ) {
                global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
                wfProfileIn( __METHOD__ );
 
@@ -248,7 +259,7 @@ class MWTidy {
         * @param int &$retval Exit code (-1 on internal error)
         * @return string|null
         */
-       private static function execInternalTidy( $text, $stderr = false, &$retval = null ) {
+       private static function phpClean( $text, $stderr = false, &$retval = null ) {
                global $wgTidyConf, $wgDebugTidy;
                wfProfileIn( __METHOD__ );
 
@@ -288,4 +299,29 @@ class MWTidy {
                wfProfileOut( __METHOD__ );
                return $cleansource;
        }
+
+       /**
+        * Use the tidy extension for HHVM from
+        * https://github.com/wikimedia/mediawiki-php-tidy
+        *
+        * This currently does not support the object-oriented interface, but
+        * tidy_repair_string() can be used for the most common tasks.
+        *
+        * @param string $text HTML to check
+        * @param int &$retval Exit code (-1 on internal error)
+        * @return string|null
+        */
+       private static function hhvmClean( $text, &$retval ) {
+               global $wgTidyConf;
+               wfProfileIn( __METHOD__ );
+               $cleansource = tidy_repair_string( $text, $wgTidyConf, 'utf8' );
+               if ( $cleansource === false ) {
+                       $cleansource = null;
+                       $retval = -1;
+               } else {
+                       $retval = 0;
+               }
+               wfProfileOut( __METHOD__ );
+               return $cleansource;
+       }
 }
index 07eb340..1b926c4 100644 (file)
@@ -79,7 +79,6 @@ class Parser {
        const HALF_PARSED_VERSION = 2;
 
        # Flags for Parser::setFunctionHook
-       # Also available as global constants from Defines.php
        const SFH_NO_HASH = 1;
        const SFH_OBJECT_ARGS = 2;
 
@@ -3816,7 +3815,7 @@ class Parser {
                }
 
                $allArgs = array( &$this );
-               if ( $flags & SFH_OBJECT_ARGS ) {
+               if ( $flags & self::SFH_OBJECT_ARGS ) {
                        # Convert arguments to PPNodes and collect for appending to $allArgs
                        $funcArgs = array();
                        foreach ( $args as $k => $v ) {
@@ -5218,7 +5217,7 @@ class Parser {
         * The callback function should have the form:
         *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
         *
-        * Or with SFH_OBJECT_ARGS:
+        * Or with Parser::SFH_OBJECT_ARGS:
         *    function myParserFunction( $parser, $frame, $args ) { ... }
         *
         * The callback may either return the text result of the function, or an array with the text
@@ -5232,10 +5231,10 @@ class Parser {
         * @param string $id The magic word ID
         * @param callable $callback The callback function (and object) to use
         * @param int $flags A combination of the following flags:
-        *     SFH_NO_HASH   No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
+        *     Parser::SFH_NO_HASH      No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
         *
-        *     SFH_OBJECT_ARGS   Pass the template arguments as PPNode objects instead of text. This
-        *     allows for conditional expansion of the parse tree, allowing you to eliminate dead
+        *     Parser::SFH_OBJECT_ARGS  Pass the template arguments as PPNode objects instead of text.
+        *     This allows for conditional expansion of the parse tree, allowing you to eliminate dead
         *     branches and thus speed up parsing. It is also possible to analyse the parse tree of
         *     the arguments, and to control the way they are expanded.
         *
@@ -5277,7 +5276,7 @@ class Parser {
                                $syn = $wgContLang->lc( $syn );
                        }
                        # Add leading hash
-                       if ( !( $flags & SFH_NO_HASH ) ) {
+                       if ( !( $flags & self::SFH_NO_HASH ) ) {
                                $syn = '#' . $syn;
                        }
                        # Remove trailing colon
index 0eb87e4..c6cbfbe 100644 (file)
@@ -500,22 +500,12 @@ class SearchEngine {
        /**
         * Get OpenSearch suggestion template
         *
+        * @deprecated since 1.25
         * @return string
         */
        public static function getOpenSearchTemplate() {
-               global $wgOpenSearchTemplate, $wgCanonicalServer;
-
-               if ( $wgOpenSearchTemplate ) {
-                       return $wgOpenSearchTemplate;
-               } else {
-                       $ns = implode( '|', SearchEngine::defaultNamespaces() );
-                       if ( !$ns ) {
-                               $ns = "0";
-                       }
-
-                       return $wgCanonicalServer . wfScript( 'api' )
-                               . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
-               }
+               wfDeprecated( __METHOD__, '1.25' );
+               return ApiOpenSearch::getOpenSearchTemplate( 'application/x-suggestions+json' );
        }
 
        /**
diff --git a/includes/site/SiteListFileCache.php b/includes/site/SiteListFileCache.php
new file mode 100644 (file)
index 0000000..c0ecab1
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.25
+ *
+ * @file
+ *
+ * @license GNU GPL v2+
+ */
+class SiteListFileCache {
+
+       /**
+        * @var SiteList
+        */
+       private $sites = null;
+
+       /**
+        * @var string
+        */
+       private $cacheFile;
+
+       /**
+        * @param string $cacheFile
+        */
+       public function __construct( $cacheFile ) {
+               $this->cacheFile = $cacheFile;
+       }
+
+       /**
+        * @since 1.25
+        *
+        * @return SiteList
+        */
+       public function getSites() {
+               if ( $this->sites === null ) {
+                       $this->sites = $this->loadSitesFromCache();
+               }
+
+               return $this->sites;
+       }
+
+       /**
+        * @since 1.25
+        */
+       public function getSite( $globalId ) {
+               $sites = $this->getSites();
+
+               return $sites->hasSite( $globalId ) ? $sites->getSite( $globalId ) : null;
+       }
+
+       /**
+        * @return SiteList
+        */
+       private function loadSitesFromCache() {
+               $data = $this->loadJsonFile();
+
+               $sites = new SiteList();
+
+               // @todo lazy initialize the site objects in the site list (e.g. only when needed to access)
+               foreach( $data['sites'] as $siteArray ) {
+                       $sites[] = $this->newSiteFromArray( $siteArray );
+               }
+
+               return $sites;
+       }
+
+       /**
+        * @throws MWException
+        * @return array
+        */
+       private function loadJsonFile() {
+               if ( !is_readable( $this->cacheFile ) ) {
+                       throw new MWException( 'SiteList cache file not found.' );
+               }
+
+               $contents = file_get_contents( $this->cacheFile );
+               $data = json_decode( $contents, true );
+
+               if ( !is_array( $data ) || !array_key_exists( 'sites', $data ) ) {
+                       throw new MWException( 'SiteStore json cache data is invalid.' );
+               }
+
+               return $data;
+       }
+
+       /**
+        * @param array $data
+        *
+        * @return Site
+        */
+       private function newSiteFromArray( array $data ) {
+               $siteType = array_key_exists( 'type', $data ) ? $data['type'] : Site::TYPE_UNKNOWN;
+               $site = Site::newForType( $siteType );
+
+               $site->setGlobalId( $data['globalid'] );
+               $site->setInternalId( $data['internalid'] );
+               $site->setForward( $data['forward'] );
+               $site->setGroup( $data['group'] );
+               $site->setLanguageCode( $data['language'] );
+               $site->setSource( $data['source'] );
+               $site->setExtraData( $data['data'] );
+               $site->setExtraConfig( $data['config'] );
+
+               foreach( $data['identifiers'] as $identifier ) {
+                       $site->addLocalId( $identifier['type'], $identifier['key'] );
+               }
+
+               return $site;
+       }
+
+}
diff --git a/includes/site/SiteListFileCacheBuilder.php b/includes/site/SiteListFileCacheBuilder.php
new file mode 100644 (file)
index 0000000..0a71aae
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.25
+ *
+ * @file
+ *
+ * @license GNU GPL v2+
+ */
+class SiteListFileCacheBuilder {
+
+       /**
+        * @var SiteStore
+        */
+       private $siteStore;
+
+       /**
+        * @var string
+        */
+       private $cacheFile;
+
+       /**
+        * @param SiteStore $siteStore
+        * @param string $cacheFile
+        */
+       public function __construct( SiteStore $siteStore, $cacheFile ) {
+               $this->siteStore = $siteStore;
+               $this->cacheFile = $cacheFile;
+       }
+
+       public function build() {
+               $this->sites = $this->siteStore->getSites( 'recache' );
+               $this->cacheSites( $this->sites->getArrayCopy() );
+       }
+
+       /**
+        * @param Site[] $sites
+        *
+        * @throws MWException if in manualRecache mode
+        * @return bool
+        */
+       private function cacheSites( array $sites ) {
+               $sitesArray = array();
+
+               foreach ( $sites as $site ) {
+                       $globalId = $site->getGlobalId();
+                       $sitesArray[$globalId] = $this->getSiteAsArray( $site );
+               }
+
+               $json = json_encode( array(
+                       'sites' => $sitesArray
+               ) );
+
+               $result = file_put_contents( $this->cacheFile, $json );
+
+               return $result !== false;
+       }
+
+       /**
+        * @param Site $site
+        *
+        * @return array
+        */
+       private function getSiteAsArray( Site $site ) {
+               $siteEntry = unserialize( $site->serialize() );
+               $siteIdentifiers = $this->buildLocalIdentifiers( $site );
+               $identifiersArray = array();
+
+               foreach( $siteIdentifiers as $identifier ) {
+                       $identifiersArray[] = $identifier;
+               }
+
+               $siteEntry['identifiers'] = $identifiersArray;
+
+               return $siteEntry;
+       }
+
+       /**
+        * @param Site $site
+        *
+        * @return array Site local identifiers
+        */
+       private function buildLocalIdentifiers( Site $site ) {
+               $localIds = array();
+
+               foreach ( $site->getLocalIds() as $idType => $ids ) {
+                       foreach ( $ids as $id ) {
+                               $localIds[] = array(
+                                       'type' => $idType,
+                                       'key' => $id
+                               );
+                       }
+               }
+
+               return $localIds;
+       }
+
+}
index fde22f1..c1a350d 100644 (file)
@@ -51,15 +51,25 @@ class SiteSQLStore implements SiteStore {
         */
        private $cacheTimeout = 3600;
 
+       /**
+        * @var BagOStuff
+        */
+       private $cache;
+
        /**
         * @since 1.21
         *
         * @param ORMTable|null $sitesTable
+        * @param BagOStuff|null $cache
         *
         * @return SiteStore
         */
-       public static function newInstance( ORMTable $sitesTable = null ) {
-               return new static( $sitesTable );
+       public static function newInstance( ORMTable $sitesTable = null, BagOStuff $cache = null ) {
+               if ( $cache === null ) {
+                       $cache = wfGetMainCache();
+               }
+
+               return new static( $cache, $sitesTable );
        }
 
        /**
@@ -67,13 +77,15 @@ class SiteSQLStore implements SiteStore {
         *
         * @since 1.21
         *
+        * @param BagOStuff $cache
         * @param ORMTable|null $sitesTable
         */
-       protected function __construct( ORMTable $sitesTable = null ) {
+       protected function __construct( BagOStuff $cache, ORMTable $sitesTable = null ) {
                if ( $sitesTable === null ) {
                        $sitesTable = $this->newSitesTable();
                }
 
+               $this->cache = $cache;
                $this->sitesTable = $sitesTable;
        }
 
@@ -123,8 +135,7 @@ class SiteSQLStore implements SiteStore {
 
                if ( $source === 'cache' ) {
                        if ( $this->sites === null ) {
-                               $cache = wfGetMainCache();
-                               $sites = $cache->get( $this->getCacheKey() );
+                               $sites = $this->cache->get( $this->getCacheKey() );
 
                                if ( is_object( $sites ) ) {
                                        $this->sites = $sites;
@@ -257,8 +268,7 @@ class SiteSQLStore implements SiteStore {
                        }
                }
 
-               $cache = wfGetMainCache();
-               $cache->set( $this->getCacheKey(), $this->sites, $this->cacheTimeout );
+               $this->cache->set( $this->getCacheKey(), $this->sites, $this->cacheTimeout );
 
                wfProfileOut( __METHOD__ );
        }
@@ -374,8 +384,7 @@ class SiteSQLStore implements SiteStore {
        public function reset() {
                wfProfileIn( __METHOD__ );
                // purge cache
-               $cache = wfGetMainCache();
-               $cache->delete( $this->getCacheKey() );
+               $this->cache->delete( $this->getCacheKey() );
                $this->sites = null;
 
                wfProfileOut( __METHOD__ );
index ea94fad..0d072ab 100644 (file)
@@ -17,7 +17,7 @@ class SkinFallbackTemplate extends BaseTemplate {
         * @return array
         */
        private function findInstalledSkins() {
-               $styleDirectory = $this->config->get( 'StyleDirectory' ); // @todo we should inject this directly?
+               $styleDirectory = $this->config->get( 'StyleDirectory' );
                // Get all subdirectories which might contains skins
                $possibleSkins = scandir( $styleDirectory );
                $possibleSkins = array_filter( $possibleSkins, function ( $maybeDir ) use ( $styleDirectory ) {
index 6c72703..3a39fee 100644 (file)
@@ -1004,7 +1004,7 @@ class SkinTemplate extends Skin {
                                        if ( $user->isAllowed( 'deletedhistory' ) ) {
                                                $n = $title->isDeleted();
                                                if ( $n ) {
-                                                       $undelTitle = SpecialPage::getTitleFor( 'Undelete' );
+                                                       $undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() );
                                                        // If the user can't undelete but can view deleted
                                                        // history show them a "View .. deleted" tab instead.
                                                        $msgKey = $user->isAllowed( 'undelete' ) ? 'undelete' : 'viewdeleted';
@@ -1012,7 +1012,7 @@ class SkinTemplate extends Skin {
                                                                'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false,
                                                                'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" )
                                                                        ->setContext( $this->getContext() )->numParams( $n )->text(),
-                                                               'href' => $undelTitle->getLocalURL( array( 'target' => $title->getPrefixedDBkey() ) )
+                                                               'href' => $undelTitle->getLocalURL()
                                                        );
                                                }
                                        }
index 084336d..a4269d1 100644 (file)
@@ -136,6 +136,7 @@ class SpecialBlock extends FormSpecialPage {
                                'autofocus' => true,
                                'required' => true,
                                'validation-callback' => array( __CLASS__, 'validateTargetField' ),
+                               'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
                        ),
                        'Expiry' => array(
                                'type' => !count( $suggestedDurations ) ? 'text' : 'selectorother',
@@ -309,7 +310,7 @@ class SpecialBlock extends FormSpecialPage {
         * @return string
         */
        protected function preText() {
-               $this->getOutput()->addModules( 'mediawiki.special.block' );
+               $this->getOutput()->addModules( array( 'mediawiki.special.block', 'mediawiki.userSuggest' ) );
 
                $text = $this->msg( 'blockiptext' )->parse();
 
index aefd99a..02b2626 100644 (file)
@@ -47,6 +47,7 @@ class SpecialBlockList extends SpecialPage {
                $lang = $this->getLanguage();
                $out->setPageTitle( $this->msg( 'ipblocklist' ) );
                $out->addModuleStyles( 'mediawiki.special' );
+               $out->addModules( 'mediawiki.userSuggest' );
 
                $request = $this->getRequest();
                $par = $request->getVal( 'ip', $par );
@@ -72,6 +73,7 @@ class SpecialBlockList extends SpecialPage {
                                'tabindex' => '1',
                                'size' => '45',
                                'default' => $this->target,
+                               'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
                        ),
                        'Options' => array(
                                'type' => 'multiselect',
index 570ab3b..f9e03ab 100644 (file)
@@ -117,7 +117,7 @@ class SpecialRandomInCategory extends FormSpecialPage {
                        return Status::newFatal( $msg );
 
                } elseif ( !$this->category ) {
-                       return; // no data sent
+                       return false; // no data sent
                }
 
                $title = $this->getRandomTitle();
index 244b889..192dc96 100644 (file)
@@ -53,7 +53,7 @@ class SpecialUnblock extends SpecialPage {
 
                $out = $this->getOutput();
                $out->setPageTitle( $this->msg( 'unblockip' ) );
-               $out->addModules( 'mediawiki.special' );
+               $out->addModules( array( 'mediawiki.special', 'mediawiki.userSuggest' ) );
 
                $form = new HTMLForm( $this->getFields(), $this->getContext() );
                $form->setWrapperLegendMsg( 'unblockip' );
@@ -88,6 +88,7 @@ class SpecialUnblock extends SpecialPage {
                                'autofocus' => true,
                                'size' => '45',
                                'required' => true,
+                               'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
                        ),
                        'Name' => array(
                                'type' => 'info',
index 0e2db8c..77f9cd1 100644 (file)
@@ -628,6 +628,25 @@ class IP {
                        strcmp( $hexIP, $end ) <= 0 );
        }
 
+       /**
+        * Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
+        *
+        * @since 1.25
+        *
+        * @param string $ip the IP to check
+        * @param array $ranges the IP ranges, each element a range
+        *
+        * @return bool true if the specified adress belongs to the specified range; otherwise, false.
+        */
+       public static function isInRanges( $ip, $ranges ) {
+               foreach ( $ranges as $range ) {
+                       if ( self::isInRange( $ip, $range ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
        /**
         * Convert some unusual representations of IPv4 addresses to their
         * canonical dotted quad representation.
index fc86b50..78e6b1a 100644 (file)
@@ -24,7 +24,8 @@
                        "Gazimagomedov",
                        "StanProg",
                        "Bjankuloski06",
-                       "Vodnokon4e"
+                       "Vodnokon4e",
+                       "ShadeOfGrey"
                ]
        },
        "tog-underline": "Подчертаване на препратките:",
        "specialpages-group-wiki": "Данни и инструменти",
        "specialpages-group-redirects": "Пренасочващи специални страници",
        "specialpages-group-spam": "Инструменти против спам",
+       "specialpages-group-developer": "Инструменти за разработчици",
        "blankpage": "Празна страница",
        "intentionallyblankpage": "Тази страница умишлено е оставена празна",
        "external_image_whitelist": " #Оставете този ред така, както го виждате. <pre>\n#Поставете долу фрагменти от регулярни изрази (само частта между //).\n#Тези фрагменти ще се съпоставят с интернет адресите на външните (hotlinked) картинки.\n#Картинките, чиито адреси отговарят на вписаните регулярни изрази, ще се визуализират, за останалите ще се появява само връзка.\n#Редовете, започващи с # се възприемат като коментари.\n#Командите са чувствителни на малки и главни букви.\n\n#Слагайте всички фрагменти от регулярни изрази НАД този ред. Оставете този ред така, както го виждате. </pre>",
        "logentry-move-move": "$1 {{GENDER:$2|премести}} страница „$3“ като „$4“",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|премести}} страницата „$3“ като „$4“ без пренасочване",
        "logentry-move-move_redir": "$1 {{GENDER:$2|премести}} страницата $3 като $4 (върху пренасочване)",
-       "logentry-move-move_redir-noredirect": "$1 {GENDER:$2|премести}} върху пренасочване $3 като $4 без пренасочване",
+       "logentry-move-move_redir-noredirect": "$1 {{GENDER:$2|премести}} върху пренасочване $3 като $4 без пренасочване",
        "logentry-patrol-patrol": "$1 {{GENDER:$2|отбеляза}} като патрулирана версия $4 на страницата „$3“",
        "logentry-patrol-patrol-auto": "$1 автоматично {{GENDER:$2|отбеляза}} като патрулирана версия $4 на страницата $3",
        "logentry-newusers-newusers": "Потребителската сметка $1 беше {{GENDER:$2|създадена}}",
index 2353b8d..976e9d3 100644 (file)
        "mainpage-description": "Pàgina principal",
        "policy-url": "Project:Polítiques",
        "portal": "Portal de la comunitat",
-       "portal-url": "Project:Portal de la comunitat",
+       "portal-url": "Project:Portal",
        "privacy": "Política de privadesa",
        "privacypage": "Project:Política de privadesa",
        "badaccess": "Error de permisos",
index 057709c..0f4bca7 100644 (file)
        "filerenameerror": "An n'é mìa pusébil cambiêr al nòm ed \"$1\" in \"$2\".",
        "filedeleteerror": "An n'é mìa pusébil scanşlêr al file \"$1\".",
        "directorycreateerror": "An n'é mìa pusébil fêr la directory \"$1\".",
+       "directoryreadonlyerror": "La cartèla \"$1\" la 's pōl sōl lēşer.",
+       "directorynotreadableerror": "La cartèla \"$1\" la 's pōl mìa lēşer.",
        "filenotfound": "An n'é mìa pusébil catêr al file \"$1\".",
        "unexpected": "Valōr mìa pervést \"$1\"=\"$2\".",
        "formerror": "Erōr: an n'é ma pusébil spidîr al môdul.",
        "right-editprotected": "Mudéfica 'l pàgini prutèti cun \"{{int:protect-level-sysop}}\"",
        "right-editsemiprotected": "Mudéfica 'l pàgini prutèti cun \"{{int:protect-level-autoconfirmed}}\"",
        "right-editinterface": "Mudéfica al colegamèint tra sistēma e utèint",
+       "right-editusercssjs": "Mudéfica i file CSS e JS 'd êter utèint",
+       "right-editusercss": "Mudéfica i file CSS 'd êter utèint",
+       "right-edituserjs": "Mudéfica i file JS 'd êter utèint",
+       "right-editmyusercss": "Mudéfica i file CSS dal só utèint",
+       "right-editmyuserjs": "Mudéfica i file JavaScript dal só utèint",
+       "right-viewmywatchlist": "Guêrda la lésta di tō tgnû 'd ôc specêl",
+       "right-editmywatchlist": "Mudéfica i tō tgnu 'd ôc. Da nutêr che soquânti asiòun a prân incòra zuntêr dal pàgini ânca sèinsa avèiren al dirét.",
+       "right-viewmyprivateinfo": "Guêrda al tō infurmasiòun personêli (per eşèimpi: indirés ed pôsta eletrônica, nòm vèira)",
+       "right-editmyprivateinfo": "Câmbia 'l tō infurmasiòun personêli (per eşèimpi: indirés ed pôsta eletrônica, nòm vèira)",
+       "right-editmyoptions": "Câmbia al tō preferèinsi",
+       "right-rollback": "Scanşèla a la şvêlta al mudéfichi ed l'ûltèint ch'l'à mudifichê 'na pàgina pariculêra",
        "newuserlogpage": "Utèint nōv",
        "action-read": "lēzer cla pàgina ché",
        "action-edit": "Mudifichêr cla pàgina ché",
index 46544dc..d0572c6 100644 (file)
        "content-model-text": "ჩვეულებრივი ტექსტი",
        "content-model-javascript": "ჯავასკრიპტი",
        "content-model-css": "CSS",
+       "duplicate-args-category": "გვერდები, რომლებიც იყენებენ დუბლიკატ არგუმენტებს თარგების გამოძახებისას",
+       "duplicate-args-category-desc": "გვერდები, რომლებიც იყენებენ დუბლიკატ არგუმენტებს თარგების გამოძახებისას, როგორებიც არის <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> ან <code><nowiki>{{foo|bar|1=bar}}</nowiki></code>.",
        "expensive-parserfunction-warning": "ყურადღება. მოცემული გვერდი შეიცავს ძალიან ბევრ მძიმე ფუნქციას.\n\nგამოძახებათა რაოდენობა შეზღუდულია $2 დონეზე.ამ შემთხვევაში უნდა გაკეთდეს  $1 გამოძახება.",
        "expensive-parserfunction-category": "გვერდი ძალიან ბევრი მძიმე ფუნქციის მოთხოვნით",
        "post-expand-template-inclusion-warning": "ყურადღება. ჩართული თარგების ზომა ძალიან დიდია. ზოგი თარგი შეიძლება დარჩეს გათიშული.",
index 6a49891..cf1caab 100644 (file)
        "license": "Лицензияландыруы:",
        "license-header": "Лицензияландыруы",
        "nolicense": "Ештеңе таңдалмаған",
+       "licenses-edit": "Лицензия талғауларын өңдеу",
        "license-nopreview": "(Қарап шығу жетімді емес)",
        "upload_source_url": "(жарамды, баршаға қатынаулы URL)",
        "upload_source_file": "(компьютеріңіздегі файл)",
        "logentry-newusers-autocreate": "$1 қатысушы аккаунтын автоматты түрде {{GENDER:$2|тіркеді}}",
        "logentry-rights-rights": "$1 $3 үшін топ мүшелігін $4 дегеннен $5 дегенге {{GENDER:$2|өзгерті}}",
        "logentry-rights-rights-legacy": "$1 $3 үшін топ мүшелігін {{GENDER:$2|өзгерті}}",
+       "logentry-upload-upload": "$1 $3 файлын {{GENDER:$2|жүктеді}}",
        "rightsnone": "(ешқандай)",
        "revdelete-summary": "өңдеменің қысқаша мазмұндамасы",
        "feedback-subject": "Тақырып:",
index d9758a0..59dd3bb 100644 (file)
        "download": "parsisiųsti",
        "unwatchedpages": "Nestebimi puslapiai",
        "listredirects": "Peradresavimų sąrašas",
+       "listduplicatedfiles": "Pasikartojančių rinkmenų sąrašas",
        "unusedtemplates": "Nenaudojami šablonai",
        "unusedtemplatestext": "Šis puslapis rodo sąrašą puslapių, esančių {{ns:template}} vardų srityje, kurie nėra įterpti į jokį kitą puslapį. Nepamirškite patikrinti kitų nuorodų prieš juos ištrinant.",
        "unusedtemplateswlh": "kitos nuorodos",
        "movepagetalktext": "Susietas aptarimo puslapis bus automatiškai perkeltas kartu su juo, '''išskyrus:''':\n*Puslapis nauju pavadinimu jau turi netuščią aptarimo puslapį, arba\n*Paliksite žemiau esančia varnelę nepažymėtą.\n\nŠiais atvejais jūs savo nuožiūra turite perkelti arba apjungti aptarimo puslapį.",
        "movearticle": "Pervardinti puslapį:",
        "moveuserpage-warning": "'''Dėmesio:''' Jūs ruošiatės perkelti naudotojo puslapį. Atkreipkite dėmesį, kad bus perkeltas tik puslapis, naudotojas ''nebus'' pervadintas.",
+       "movecategorypage-warning": "<strong>Dėmesio:</strong> Jūs ketinate pervadinti kategorijos puslapį. Atminkite, kad tik pats puslapis bus pervadintas, tačiau kategorijai priskirti puslapiai <em>nebus</em> perkelti naujon kategorijon.",
        "movenologintext": "Norėdami pervadinti puslapį, turite būti užsiregistravęs naudotojas ir būti  [[Special:UserLogin|prisijungęs]].",
        "movenotallowed": "Jūs neturite teisių pervadinti puslapių.",
        "movenotallowedfile": "Jūs neturite teisės perkelti failus.",
        "fileduplicatesearch-result-n": "Šis failas „$1“ turi $2 {{PLURAL:$2|identišką dublikatą|identiškus dublikatus|identiškų dublikatų}}.",
        "fileduplicatesearch-noresults": "Nėra failo pavadinimu \"$1\".",
        "specialpages": "Specialieji puslapiai",
-       "specialpages-note": "* Įprastą specialius puslapius.\n* <span class=\"mw-specialpagerestricted\">Tik specialius puslapius.</span>",
+       "specialpages-note": "* Įprasti specialieji puslapiai.\n* <span class=\"mw-specialpagerestricted\">Apriboto pasiekiamumo specialieji puslapiai.</span>",
        "specialpages-group-maintenance": "Sistemos palaikymo pranešimai",
        "specialpages-group-other": "Kiti specialieji puslapiai",
        "specialpages-group-login": "Prisijungti / sukurti paskyrą",
        "specialpages-group-wiki": "Wiki duomenys ir priemonės",
        "specialpages-group-redirects": "Specialieji nukreipimo puslapiai",
        "specialpages-group-spam": "Šlamšto valdymo priemonės",
+       "specialpages-group-developer": "Kūrėjo įrankai",
        "blankpage": "Tuščias puslapis",
        "intentionallyblankpage": "Šis puslapis specialiai paliktas tuščias",
        "external_image_whitelist": " #Palikite šią eilutę, tokią kokia yra <pre>\n#Įrašykite standartinių išraiškų fragmentus (tik dalį tarp //)\n#Juos bus bandoma sutapdinti su išorinių paveikslėlių adresais\n#Tie, kurie sutaps, bus rodomi kaip paveikslėliai, o kiti bus rodomi tik kaip nuorodos\n#Raidžių dydis nėra svarbus\n#Eilutės, prasidedančios # yra komentarai\n\n#Įterpkite visus standartinių išraiškų fragmentus prieš šią eilutę. Palikite šią eilutę, tokią kokia yra </pre>",
        "duration-decades": "$1 {{PLURAL:$1|dešimtmetis|dešimtmečiai|dešimtmečių}}",
        "duration-centuries": "$1 {{PLURAL:$1|amžius|amžiai|amžių}}",
        "duration-millennia": "$1 {{PLURAL:$1|tūkstantmetis|tūkstantmečiai|tūkstantmečių}}",
+       "limitreport-title": "Analizatoriaus duomenys:",
        "expand_templates_output": "Rezultatas",
        "expand_templates_ok": "Gerai",
        "expand_templates_remove_comments": "Pašalinti komentarus",
index 1faf2c7..8f27068 100644 (file)
        "specialpages-group-wiki": "വിവരങ്ങളും ഉപകരണങ്ങളും",
        "specialpages-group-redirects": "തിരിച്ചുവിടൽ സംബന്ധിച്ച പ്രത്യേക താളുകൾ",
        "specialpages-group-spam": "പാഴെഴുത്ത് ഉപകരണങ്ങൾ",
+       "specialpages-group-developer": "വികസന ഉപകരണങ്ങൾ",
        "blankpage": "ശൂന്യതാൾ",
        "intentionallyblankpage": "ഈ താൾ മനഃപൂർവ്വം ശൂന്യമായി ഇട്ടിരിക്കുന്നതാണ്‌.",
        "external_image_whitelist": "#ഈ വരി ഇതേ പോലെ സൂക്ഷിക്കുക <pre>\n#റെഗുലർ എക്സ്പ്രെഷൻ ഘടകങ്ങൾ (ഭാഗം // എന്നതിന്റെയുള്ളിൽ ആയിരിക്കുന്ന വിധത്തിൽ) താഴെ ചേർക്കുക\n#ഇത് പുറത്തുനിന്നുള്ള (ഹോട്ട്ലിങ്ക്ഡ്) ചിത്രങ്ങളുടെ യൂ.ആർ.എല്ലുമായി ഒത്തുനോക്കുന്നതാണ്.\n#പൊരുത്തപ്പെട്ടു പോകുന്ന യൂ.ആർ.എല്ലുകൾ മാത്രം പ്രദർശിപ്പിക്കും, അല്ലാത്തവ ചിത്രത്തിലേയ്ക്കുള്ള കണ്ണിയായി കാണും\n#കുറിപ്പുകളായി കണക്കാക്കാൻ വരികളുടെയാദ്യം # ചേർക്കുക\n#ഇത് കേസ് സെൻസിറ്റീവ് ആണ്\n\n#എല്ലാ റെജെക്സ് ഘടകങ്ങളും ഈ വരിക്ക് മേലേയായി ചേർക്കുക. ഈ വരി ഇതേ പോലെ നിലനിർത്തുക </pre>",
        "revdelete-uname-unhid": "ഉപയോക്തൃനാമം മറച്ചത് ഒഴിവാക്കിയിരിക്കുന്നു",
        "revdelete-restricted": "കാര്യനിർവാഹകർക്ക് പ്രവർത്തന അതിരുകൾ ഏർപ്പെടുത്തിയിരിക്കുന്നു",
        "revdelete-unrestricted": "കാര്യനിർവാഹകർക്ക് ഏർപ്പെടുത്തിയ പ്രവർത്തന അതിരുകൾ നീക്കം ചെയ്തിരിക്കുന്നു",
+       "logentry-merge-merge": "$3 എന്ന താൾ $4 എന്നതിലേക്ക് ($5 നാൾപ്പതിപ്പ് വരെ), $1 {{GENDER:$2|ലയിപ്പിച്ചു}}",
        "logentry-move-move": "$1 എന്ന ഉപയോക്താവ് $3 എന്ന താൾ $4 എന്നാക്കി {{GENDER:$2|മാറ്റിയിരിക്കുന്നു}}",
        "logentry-move-move-noredirect": "$3 എന്ന താൾ $4 എന്ന തലക്കെട്ടിലേയ്ക്ക് തിരിച്ചുവിടലില്ലാതെ $1 {{GENDER:$2|മാറ്റി}}",
        "logentry-move-move_redir": "$3 എന്ന താൾ $4 എന്ന താളിനു മുകളിലേയ്ക്ക്, $1 {{GENDER:$2|മാറ്റിയിരിക്കുന്നു}}",
index f762469..1703ebf 100644 (file)
        "specialpages-group-wiki": "Data e strumiente",
        "specialpages-group-redirects": "Redirezionamiente d' 'e paggene speciale",
        "specialpages-group-spam": "Strumiente p' 'o spam",
+       "specialpages-group-developer": "Strumiente p' 'e sviluppature",
        "blankpage": "Paggene abbacante",
        "intentionallyblankpage": "Sta paggena s'è lassata abbacante apposta",
        "external_image_whitelist": "  #Lassate sta linea accussì accussì comme sta<pre>\n#Mettete piezze 'espressione regolare (chilla parta nfra 'e //) sotto\n#Chille s'azzeccano ch' 'e ndirizze URL 'e l'immaggine 'e fore (collegamiente cavere)\n#Chille cu nu cunfronto positivo sarranno mmustate comme immaggene, o pure comme a nu link a l'immaggine ca mmustano\n#Linee c'accumenciano pe' # songo trattate comme commente\n#Chist'è insenzitivo p' 'e maiuscole e minuscole\n\n#Mettete tutt' 'e piezze regex ncopp' 'a stalinea. Lassate sta linea eguale eguale comme 'a verite</pre>",
index 39de585..ba492b2 100644 (file)
        "error": "Грешка",
        "databaseerror": "Грешка у бази података",
        "databaseerror-text": "Дошло је до грешке у упиту базе података. Можда је у питању програмска грешка.",
+       "databaseerror-textcl": "Дошло је до грешке у упиту базе података.",
        "databaseerror-query": "Упит: $1",
        "databaseerror-function": "Функција: $1",
        "databaseerror-error": "Грешка: $1",
        "specialpages-group-wiki": "Подаци и алати",
        "specialpages-group-redirects": "Преусмеравање посебних страница",
        "specialpages-group-spam": "Алатке против непожељних порука",
+       "specialpages-group-developer": "Програмерски алати",
        "blankpage": "Празна страница",
        "intentionallyblankpage": "Ова страница је намерно остављена празном.",
        "external_image_whitelist": " #Оставите овај ред онаквим какав јесте<pre>\n#Испод додајте одломке регуларних израза (само део који се налази између //)\n#Они ће бити упоређени с адресама спољашњих слика\n#Оне које се поклапају биће приказане као слике, а преостале као везе до слика\n#Редови који почињу с тарабом се сматрају коментарима\n#Сви уноси су осетљиви на мала и велика слова\n\n#Додајте све одломке регуларних израза изнад овог реда. Овај ред не дирајте</pre>",
index efba070..4d53855 100644 (file)
@@ -20,7 +20,8 @@
                        "Ktchankt",
                        "Kc kennylau",
                        "Mywood",
-                       "Impersonator 1"
+                       "Impersonator 1",
+                       "Cedric tsan cantonais"
                ]
        },
        "tog-underline": "連結加底線:",
@@ -63,7 +64,7 @@
        "tog-prefershttps": "簽到後繼續用加密連線",
        "underline-always": "全部",
        "underline-never": "永不",
-       "underline-default": "瀏覽器預設",
+       "underline-default": "瀏覽器或瀏覽器膚色預設",
        "editfont-style": "編輯區字型樣式:",
        "editfont-default": "瀏覽器預設",
        "editfont-monospace": "固定間距字型",
        "listingcontinuesabbrev": "續",
        "index-category": "做咗索引嘅版",
        "noindex-category": "未做索引嘅版",
+       "broken-file-category": "有失效文件鏈接嘅版",
        "about": "關於",
        "article": "內容頁",
        "newwindow": "(響新視窗度打開)",
        "cancel": "取消",
        "moredotdotdot": "更多...",
+       "morenotlisted": "爾張清單重未完成。",
        "mypage": "版",
        "mytalk": "傾偈",
        "anontalk": "同呢個 IP 傾偈",
        "otherlanguages": "第啲語言",
        "redirectedfrom": "(由$1跳轉過來)",
        "redirectpagesub": "跳轉頁",
+       "redirectto": "跳轉去:",
        "lastmodifiedat": "呢一頁嘅最後修改係響$1 $2。",
        "viewcount": "呢一頁已經有$1人次睇過。",
        "protectedpage": "受保護頁",
        "hidetoc": "收埋",
        "collapsible-collapse": "摺埋",
        "collapsible-expand": "展開",
+       "confirmable-confirm": "�確唔確定?",
+       "confirmable-yes": "確定。",
+       "confirmable-no": "唔確定。",
        "thisisdeleted": "睇下定係還原$1?",
        "viewdeleted": "去睇$1?",
        "restorelink": "$1次已刪除嘅編輯",
        "filerenameerror": "檔案 \"$1\" 唔改得做 \"$2\"。",
        "filedeleteerror": "檔案 \"$1\" 唔刪得。",
        "directorycreateerror": "目錄 \"$1\" 開唔到。",
+       "directoryreadonlyerror": "\"$1\"係唯讀文件,無得編輯。",
+       "directorynotreadableerror": "讀唔到\"$1\"。",
        "filenotfound": "檔案 \"$1\" 搵唔到。",
        "unexpected": "意外數值。 \"$1\"=\"$2\"。",
        "formerror": "錯誤:表格交唔到",
        "viewsourcetext": "你可以睇吓或者複製呢一頁嘅原始碼:",
        "viewyourtext": "你可以睇同複製呢版入面<strong>由你改</strong>嘅原碼:",
        "protectedinterface": "呢一頁提供軟件嘅介面文字,呢一頁已經保護咗以預防濫用。\n要加或者改所有維基站嘅翻譯,請去 [//translatewiki.net/ translatewiki.net]嘅  MediaWiki 本地化項目。",
-       "editinginterface": "'''警告:'''你而家編輯緊嘅呢一個用嚟提供介面文字嘅頁面。響呢一頁嘅更改會影響到其他用戶使用中嘅介面外觀。要加或者改所有維基站嘅翻譯,請去 [//translatewiki.net/ translatewiki.net]嘅  MediaWiki 本地化項目。",
+       "editinginterface": "'''警告''':閣下而家編輯緊嘅係為爾隻軟件提供介面文字嘅版。\n改爾一版會自動改埋爾個維基嘅其他用戶用緊嘅介面嘅文字。",
+       "translateinterface": "要加或者改所有維基項目嘅翻譯,請去MediaWiki嘅本地化項目:[//translatewiki.net/ translatewiki.net]。",
        "cascadeprotected": "呢一版已經保護咗唔能夠編輯,因為佢係響以下嘅{{PLURAL:$1|一|幾}}頁度包含咗,當中啟用咗\"連串\"保護選項來保護嗰一版: $2",
        "namespaceprotected": "你無權編輯響'''$1'''空間名裏面嘅呢一版。",
        "customcssprotected": "你無權改呢版CSS,因為佢包含其他用戶嘅個人設定。",
        "invalidtitle-knownnamespace": "名域 \"$2\" 同版名 \"$3\" 無效嘅標題",
        "invalidtitle-unknownnamespace": "未知名域號碼 \"$1\" 同版名 \"$2\" 無效嘅標題",
        "exception-nologin": "未簽到",
-       "exception-nologin-text": "請[[Special:Userlogin|簽到]]之後先至睇或者改版。",
+       "exception-nologin-text": "請[[Special:Userlogin|簽到]]之後先至睇或者改版。",
        "exception-nologin-text-manual": "請$1之後先至睇或者改呢版。",
        "virus-badscanner": "壞設定: 未知嘅病毒掃瞄器: ''$1''",
        "virus-scanfailed": "掃瞄失敗 (代碼 $1)",
        "createaccount-text": "有人響{{SITENAME}}度用咗你個電郵開咗個名叫 \"$2\" 嘅新戶口 ($4),密碼係 \"$3\" 。你應該而家登入,改埋個密碼。\n\n如果個戶口係開錯咗嘅話,你可以唔埋呢篇信。",
        "login-throttled": "你已經試咗太多次簽到動作。\n請等$1再試過。",
        "login-abort-generic": "你簽到失敗",
+       "login-migrated-generic": "由於閣下嘅用戶已經搬走徂,因此閣下嘅爾個用戶名已經唔存在。",
        "loginlanguagelabel": "語言:$1",
        "suspicious-userlogout": "你去登出嘅要求已經拒絕咗,因為佢可能由壞咗嘅瀏覽器或者快取代理傳送。",
        "createacct-another-realname-tip": "真名可以唔填。\n如果你畀埋佢,有需要嘅時候會用佢來標示你嘅工夫。",
        "passwordreset-capture-help": "如果揀呢度,電郵連臨時密碼金向你顯示,同時會送畀用戶。",
        "passwordreset-email": "電郵地址:",
        "passwordreset-emailtitle": "{{SITENAME}}嘅戶口資料",
+       "passwordreset-emailtext-ip": "有人(可能係閣下自己,來自IP地址$1)請求更改閣下喺{{SITENAME}}($4)嘅密碼。同爾個電子郵件有關聯嘅用戶包括:\n\n$2\n\n{{PLURAL:$3|爾個|爾啲}}臨時密碼會喺{{$5}}日之後失效。\n\n如果係閣下自己請求改密碼嘅,請馬上登錄{{SITENAME}}並且更改密碼。如果閣下諗返起自己個密碼,或者根本無申請過改密碼嘅話,請忽略爾條訊息,繼續用返舊密碼。",
+       "passwordreset-emailtext-user": "{{SITENAME}}用戶$1請求更改閣下喺{{SITENAME}}道嘅密碼$4。同爾個電子郵件有關聯嘅用戶包括:\n\n$2\n\n{{PLURAL:$3|爾個|爾啲}}臨時密碼會喺{{$5}}日之後失效。\n\n如果係閣下自己請求改密碼嘅,請馬上登錄{{SITENAME}}並且更改密碼。如果閣下諗返起自己個密碼,或者根本無申請過改密碼嘅話,請忽略爾條訊息,繼續用返舊密碼。",
        "passwordreset-emailelement": "用戶名:$1\n臨時密碼:$2",
        "passwordreset-emailsent": "密碼重設電郵經已送出。",
        "passwordreset-emailsent-capture": "密碼重設電郵經已送出,下面有顯示。",
        "preview": "預覽",
        "showpreview": "顯示預覽",
        "showdiff": "顯示差異",
-       "anoneditwarning": "'''警告:'''你重未登入。你嘅 IP 位址會喺呢個頁面嘅修訂歷史中記錄落嚟。",
+       "blankarticle": "<strong>警告</strong>:閣下開緊爾版係空白嘅,撳多次「{{int:savearticle}}」就會開一個乜都無嘅版(可能被視為破壞)。",
+       "anoneditwarning": "'''警告:'''閣下重未登入。閣下嘅 IP 地址會喺爾一版嘅修訂歷史裡邊記錄落嚟。",
        "anonpreviewwarning": "''你重未登入,你嘅 IP 位址會喺呢個頁面嘅修訂歷史中記錄落嚟。''",
        "missingsummary": "'''提醒:''' 你未提供編輯摘要。如果你再撳多一下「{{int:savearticle}}」嘅話,咁你儲存嘅編輯就會無摘要。",
        "missingcommenttext": "請輸入一個註解。",
        "anontalkpagetext": "----''呢度係匿名用戶嘅討論頁,佢可能係重未開戶口,或者佢重唔識開戶口。我哋會用數字表示嘅IP地址嚟代表佢。一個IP地址係可以由幾個用戶夾來用。如果你係匿名用戶,同覺得呢啲留言係同你冇關係嘅話,唔該去[[Special:UserLogin/signup|開一個新戶口]]或[[Special:UserLogin|登入]],避免喺以後嘅留言會同埋其它用戶混淆。''",
        "noarticletext": "喺呢一頁而家並冇任何嘅文字,你可以喺其它嘅頁面中[[Special:Search/{{PAGENAME}}|搵呢一頁嘅標題]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搵有關嘅日誌],\n或者[{{fullurl:{{FULLPAGENAME}}|action=edit}} 編輯呢一版]</span>。",
        "noarticletext-nopermission": "呢一頁而家冇任何文字,你可以喺其它嘅頁面中[[Special:Search/{{PAGENAME}}|搵呢一頁嘅標題]],或者<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搵有關嘅日誌]</span>。",
+       "missing-revision": "The revision #$1 of the page named \"{{FULLPAGENAME}}\" does not exist.\n\nThis is usually caused by following an outdated history link to a page that has been deleted.\nDetails can be found in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].\n\n《{{FULLPAGENAME}}》嘅編輯#$1唔存在。\n\n恁通常係因為一條過徂時嘅鏈接帶徂閣下去一個已經刪除徂嘅版。\n詳情請查閱[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 刪文紀錄]。",
        "userpage-userdoesnotexist": "用戶戶口\"<nowiki>$1</nowiki>\"重未開。請響䦒/編輯呢版之前先檢查一下。",
        "userpage-userdoesnotexist-view": "用戶戶口\"$1\"重未開。",
        "blocked-notice-logextract": "呢位用戶而家被封鎖緊。\n下面有最近嘅封鎖紀錄以供參考:",
        "postedit-confirmation-saved": "呢版經已儲存咗。",
        "edit-already-exists": "唔可以開一新版。\n佢已經存在。",
        "defaultmessagetext": "預設訊息文字",
-       "editwarning-warning": "離開呢一版會令到你嘅修改唔見咗。\n你可以響你嘅喜好設定嘅\"{{int:prefs-editing}}\"小節度停用呢個警告。",
+       "content-not-allowed-here": "「$1」唔可以輸入[[$2]]。",
+       "editwarning-warning": "離開爾一版會令到閣下嘅修改唔見咗。\n閣下可以喺喜好設定嘅\"{{int:prefs-editing}}\"小節度停用爾個警告。",
+       "editpage-notsupportedcontentformat-title": "唔支持爾種內容格式。",
+       "content-model-wikitext": "維基文字",
+       "content-model-text": "純文字",
+       "content-model-javascript": "JavaScript程式語言",
+       "content-model-css": "層疊樣式表",
+       "duplicate-args-category": "爾版用徂幾個重複加類嘅模。",
        "expensive-parserfunction-warning": "警告: 呢一版有太多耗費嘅語法功能呼叫。\n\n佢應該少過$2次呼叫,佢而家係$1次呼叫。",
        "expensive-parserfunction-category": "響版度有太多嘅耗費嘅語法功能呼叫",
        "post-expand-template-inclusion-warning": "警告: 包含模大細太大。\n有啲模將唔會包含。",
        "specialpages-group-wiki": "Wiki資料同工具",
        "specialpages-group-redirects": "跳轉特別頁",
        "specialpages-group-spam": "反垃圾工具",
+       "specialpages-group-developer": "開發者工具",
        "blankpage": "空白頁",
        "intentionallyblankpage": "呢一版係留空咗嘅,用來作測速等用嘅。",
        "external_image_whitelist": " #留番呢行一樣嘅字<pre>\n#響下面(//嘅中間部份)入正規表達式\n#呢啲將會同外面(已超連結嘅)圖像配合\n#嗰啲晒對到出來嘅會顯示做圖像,唔係嘅話就只係會顯示連結\n#有 # 開頭嘅行會當做註解\n#無分大細楷\n\n#響呢行上面入晒全部嘅regex。留番呢行一樣嘅字</pre>",
index 6694d2c..2a735a8 100644 (file)
        "tog-prefershttps": "登入時始終使用安全連線",
        "underline-always": "永遠使用",
        "underline-never": "永不使用",
-       "underline-default": "外觀或瀏覽器預設值",
+       "underline-default": "外觀或瀏覽器預設值",
        "editfont-style": "編輯區字型樣式:",
        "editfont-default": "瀏覽器預設值",
        "editfont-monospace": "等距字型",
        "newarticle": "(新)",
        "newarticletext": "您正連結至一頁不存在頁面。\n要建立該頁面,請在下方的編輯框中輸入內容 (詳情請參考 [$1 説明頁面])。\n如果您是不小心來到此頁面,請點選瀏覽器的 <strong>返回</strong> 按鈕。",
        "anontalkpagetext": "----\n<em>此討論頁面是給尚未建立帳號的匿名使用者使用</em>\n因此我們必須使用 IP 位址來辨識身份,但相同的 IP 位址可能由許多不同的使用者所共用。\n如果您是匿名使用者並且覺得評論的內容與您無關,請 [[Special:UserLogin/signup|建立新帳號]] 或 [[Special:UserLogin|登入]] 避免與其他匿名使用者混淆。",
-       "noarticletext": "此頁面目前沒有內容,\n您可以在其它頁面中 [[Special:Search/{{PAGENAME}}|搜尋此頁面標題]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜尋相關日誌],\n或 [{{fullurl:{{FULLPAGENAME}}|action=edit}} 編輯此頁]</span>。",
+       "noarticletext": "此頁面目前沒有內容,您可以在其它頁面中[[Special:Search/{{PAGENAME}}|搜尋此頁面標題]]、<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜尋相關日誌]或[{{fullurl:{{FULLPAGENAME}}|action=edit}} 編輯此頁]</span>。",
        "noarticletext-nopermission": "此頁面目前沒有內容,\n您可以在其它頁面中 [[Special:Search/{{PAGENAME}}|搜尋此頁面標題]],或 <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搜尋相關日誌]</span>,但您沒有權限建立此頁面。",
        "missing-revision": "頁面名稱 \"{{FULLPAGENAME}}\" 的 #$1 修訂版本不存在。\n\n通常是因連結到過期的歷史頁面,該頁面已被刪除。\n詳情請參考 [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 刪除日誌]。",
        "userpage-userdoesnotexist": "使用者帳號 \"$1\" 尚未註冊。\n若您要建立/編輯此頁面,請先檢查是否正確。",
        "searchrelated": "相關",
        "searchall": "全部",
        "showingresults": "以下顯示從第 <strong>$2</strong> 筆開始,共 {{PLURAL:$1|<strong>1</strong> 筆結果|<strong>$1</strong> 筆結果}}:",
-       "showingresultsinrange": "以下顯示從第 <strong>$2</strong> 筆至第 <strong>$3</strong> 筆中的 {{PLURAL:$1|<strong>1</strong> 筆結果|<strong>$1</strong> 筆結果}}:",
+       "showingresultsinrange": "以下顯示從第 <strong>$2</strong> 筆至第 <strong>$3</strong> 筆中的 {{PLURAL:$1|<strong>$1</strong> 筆結果}}:",
        "search-showingresults": "{{PLURAL:$4|第 <strong>$1</strong> 筆結果,共 <strong>$3</strong> 筆|第 <strong>$1 - $2</strong> 筆結果,共 <strong>$3</strong> 筆}}",
        "search-nonefound": "無符合查詢條件的結果。",
        "powersearch-legend": "進階搜尋",
diff --git a/maintenance/postgres/update-keys.sql b/maintenance/postgres/update-keys.sql
new file mode 100644 (file)
index 0000000..7761d0c
--- /dev/null
@@ -0,0 +1,29 @@
+-- SQL to insert update keys into the initial tables after a
+-- fresh installation of MediaWiki's database.
+-- This is read and executed by the install script; you should
+-- not have to run it by itself unless doing a manual install.
+-- Insert keys here if either the unnecessary would cause heavy
+-- processing or could potentially cause trouble by lowering field
+-- sizes, adding constraints, etc.
+-- When adjusting field sizes, it is recommended removing old
+-- patches but to play safe, update keys should also inserted here.
+
+-- The /*_*/ comments in this and other files are
+-- replaced with the defined table prefix by the installer
+-- and updater scripts. If you are installing or running
+-- updates manually, you will need to manually insert the
+-- table prefix if any when running these scripts.
+--
+
+INSERT INTO /*_*/updatelog (ul_key, ul_value)
+       VALUES( 'filearchive-fa_major_mime-patch-fa_major_mime-chemical.sql', null );
+INSERT INTO /*_*/updatelog (ul_key, ul_value)
+       VALUES( 'image-img_major_mime-patch-img_major_mime-chemical.sql', null );
+INSERT INTO /*_*/updatelog (ul_key, ul_value)
+       VALUES( 'oldimage-oi_major_mime-patch-oi_major_mime-chemical.sql', null );
+INSERT INTO /*_*/updatelog (ul_key, ul_value)
+       VALUES( 'user_groups-ug_group-patch-ug_group-length-increase-255.sql', null );
+INSERT INTO /*_*/updatelog (ul_key, ul_value)
+       VALUES( 'user_former_groups-ufg_group-patch-ufg_group-length-increase-255.sql', null );
+INSERT INTO /*_*/updatelog (ul_key, ul_value)
+       VALUES( 'user_properties-up_property-patch-up_property.sql', null );
diff --git a/maintenance/rebuildSitesCache.php b/maintenance/rebuildSitesCache.php
new file mode 100644 (file)
index 0000000..862a983
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to dump the SiteStore as a static json file.
+ *
+ * @ingroup Maintenance
+ */
+class RebuildSitesCache extends Maintenance {
+
+       public function __construct() {
+               parent::__construct();
+
+               $this->mDescription = "Dumps site store as json";
+               $this->addOption( 'file', 'File to output the json to', false, true );
+       }
+
+       public function execute() {
+               $siteListFileCacheBuilder = new SiteListFileCacheBuilder(
+                       SiteSQLStore::newInstance(),
+                       $this->getCacheFile()
+               );
+
+               $siteListFileCacheBuilder->build();
+       }
+
+       /**
+        * @return string
+        */
+       private function getCacheFile() {
+               if ( $this->hasOption( 'file' ) ) {
+                       $jsonFile = $this->getOption( 'file' );
+               } else {
+                       $jsonFile = $this->getConfig()->get( 'SitesCacheFile' );
+
+                       if ( $jsonFile === false ) {
+                               $this->error( 'Error: No sites cache file is set in configuration.', 1 );
+                       }
+               }
+
+               return $jsonFile;
+       }
+
+}
+
+$maintClass = "RebuildSitesCache";
+require_once RUN_MAINTENANCE_IF_MAIN;
index ecd5051..5e5e35d 100644 (file)
@@ -78,13 +78,18 @@ $urls[] = array(
        'method' => 'get',
        'template' => $searchPage->getCanonicalURL( 'search={searchTerms}' ) );
 
-if ( $wgEnableAPI ) {
-       // JSON interface for search suggestions.
-       // Supported in Firefox 2 and later.
-       $urls[] = array(
-               'type' => 'application/x-suggestions+json',
-               'method' => 'get',
-               'template' => SearchEngine::getOpenSearchTemplate() );
+foreach ( $wgOpenSearchTemplates as $type => $template ) {
+       if ( !$template && $wgEnableAPI ) {
+               $template = ApiOpenSearch::getOpenSearchTemplate( $type );
+       }
+
+       if ( $template ) {
+               $urls[] = array(
+                       'type' => $type,
+                       'method' => 'get',
+                       'template' => $template,
+               );
+       }
 }
 
 // Allow hooks to override the suggestion URL settings in a more
index 1f21b41..40a5c41 100644 (file)
@@ -63,7 +63,8 @@
        border: 1px solid @colorGray12;
 
        &:hover,
-       &:active {
+       &:active,
+       &:visited {
                // make sure that is isn't inheriting from a general rule
                color: @colorButtonText;
        }
index b479020..ff454e1 100644 (file)
 //
 // Markup:
 // <div class="mw-ui-checkbox">
-//   <input type="checkbox" id="kss-example-5"><label for="kss-example-5">Standard checkbox</label>
+//   <input type="checkbox" id="kss-example-3">
+//   <label for="kss-example-3">Standard checkbox</label>
 // </div>
 // <div class="mw-ui-checkbox">
-//   <input type="checkbox" id="kss-example-5-checked" checked><label for="kss-example-5-checked">Standard checked checkbox</label>
+//   <input type="checkbox" id="kss-example-3-checked" checked>
+//   <label for="kss-example-3-checked">Standard checked checkbox</label>
 // </div>
 // <div class="mw-ui-checkbox">
-//   <input type="checkbox" id="kss-example-5-disabled" disabled><label for="kss-example-5-disabled">Disabled checkbox</label>
+//   <input type="checkbox" id="kss-example-3-disabled" disabled>
+//   <label for="kss-example-3-disabled">Disabled checkbox</label>
 // </div>
 // <div class="mw-ui-checkbox">
-//   <input type="checkbox" id="kss-example-5-disabled-checked" disabled checked><label for="kss-example-5-disabled-checked">Disabled checked checkbox</label>
+//   <input type="checkbox" id="kss-example-3-disabled-checked" disabled checked>
+//   <label for="kss-example-3-disabled-checked">Disabled checked checkbox</label>
 // </div>
 //
-// Styleguide 5.
+// Styleguide 3.
 .mw-ui-checkbox {
        display: inline-block;
        vertical-align: middle;
                height: @checkboxSize;
                // This is needed for Firefox mobile (See bug 71750 to workaround default Firefox stylesheet)
                max-width: none;
-               margin-right: .4em;
+               margin-right: 0.4em;
 
                // the pseudo before element of the label after the checkbox now looks like a checkbox
                & + label::before {
                        content: '';
                        cursor: pointer;
+                       .box-sizing(border-box);
                        position: absolute;
                        left: 0;
                        border-radius: @borderRadius;
@@ -65,7 +70,6 @@
                        height: @checkboxSize;
                        background-color: #fff;
                        border: 1px solid @colorGray7;
-                       .box-sizing(border-box);
                }
 
                // when the input is checked, style the label pseudo before element that followed as a checked checkbox
@@ -86,6 +90,7 @@
                        border-width: 2px;
                }
 
+               &:focus:hover + label::before,
                &:hover + label::before {
                        border-bottom-width: 3px;
                }
index 499b91a..dc49e20 100644 (file)
@@ -15,7 +15,7 @@
 
 // Forms
 //
-// Styleguide 3.
+// Styleguide 5.
 
 // VForm
 //
@@ -34,7 +34,7 @@
 //   </div>
 // </form>
 //
-// Styleguide 3.1.
+// Styleguide 5.1.
 .mw-ui-vform {
        .box-sizing(border-box);
 
        //   </div>
        // </form>
        //
-       // Styleguide 3.2.
+       // Styleguide 5.2.
        .error,
        .errorbox,
        .warningbox,
index dc2ca52..0912554 100644 (file)
@@ -26,7 +26,7 @@
 // However, icon-only elements do not yet degrade to text-only elements in these
 // browsers.
 //
-// Styleguide 4.
+// Styleguide 6.
 
 .mw-ui-icon {
        position: relative;
@@ -39,7 +39,7 @@
        // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok">OK</div>
        // <div class="mw-ui-icon mw-ui-icon-element mw-ui-icon-ok mw-ui-button mw-ui-progressive">OK</div>
        //
-       // Styleguide 4.1.1.
+       // Styleguide 6.1.1.
        &.mw-ui-icon-element {
                @width: @iconSize + ( 2 * @gutterWidth );
 
@@ -75,7 +75,7 @@
        // <div class="mw-ui-icon mw-ui-icon-before mw-ui-icon-ok">OK</div>
        // <div class="mw-ui-icon mw-ui-icon-before mw-ui-icon-ok mw-ui-progressive mw-ui-button">OK</div>
        //
-       // Styleguide 4.1.2
+       // Styleguide 6.1.2
        &.mw-ui-icon-before {
                &:before {
                        position: relative;
@@ -89,7 +89,7 @@
        // Markup:
        // <div class="mw-ui-icon mw-ui-icon-after mw-ui-icon-ok mw-ui-progressive mw-ui-button">OK</div>
        //
-       // Styleguide 4.1.3
+       // Styleguide 6.1.3
        &.mw-ui-icon-after {
                &:after {
                        position: relative;
index 9f3a77d..28d597e 100644 (file)
@@ -9,7 +9,7 @@
        font-style: italic;
        font-weight: normal;
 }
-// Inputs
+// Text inputs
 //
 // Apply the mw-ui-input class to input and textarea fields.
 //
index 6d8978e..425ec1b 100644 (file)
 //
 // Markup:
 // <div class="mw-ui-radio">
-//   <input type="radio" id="kss-example-7" name="kss-example-7">
-//   <label for="kss-example-7">Standard radio</label>
+//   <input type="radio" id="kss-example-4" name="kss-example-4">
+//   <label for="kss-example-4">Standard radio</label>
 // </div>
 // <div class="mw-ui-radio">
-//   <input type="radio" id="kss-example-7-checked" name="kss-example-7" checked>
-//   <label for="kss-example-7-checked">Standard checked radio</label>
+//   <input type="radio" id="kss-example-4-checked" name="kss-example-4" checked>
+//   <label for="kss-example-4-checked">Standard checked radio</label>
 // </div>
 // <div class="mw-ui-radio">
-//   <input type="radio" id="kss-example-7-disabled" name="kss-example-7-disabled" disabled>
-//   <label for="kss-example-7-disabled">Disabled radio</label>
+//   <input type="radio" id="kss-example-4-disabled" name="kss-example-4-disabled" disabled>
+//   <label for="kss-example-4-disabled">Disabled radio</label>
 // </div>
 // <div class="mw-ui-radio">
-//   <input type="radio" id="kss-example-7-disabled-checked" name="kss-example-7-disabled" disabled checked>
-//   <label for="kss-example-7-disabled-checked">Disabled checked radio</label>
+//   <input type="radio" id="kss-example-4-disabled-checked" name="kss-example-4-disabled" disabled checked>
+//   <label for="kss-example-4-disabled-checked">Disabled checked radio</label>
 // </div>
 //
-// Styleguide 7.
+// Styleguide 4.
 .mw-ui-radio {
        display: inline-block;
        vertical-align: middle;
@@ -42,6 +42,7 @@
        line-height: @radioSize;
 
        * {
+               // reset font sizes (see bug 72727)
                font: inherit;
                vertical-align: middle;
        }
@@ -59,8 +60,8 @@
 
                // the pseudo before element of the label after the radio now looks like a radio
                & + label::before {
-                       cursor: pointer;
                        content: '';
+                       cursor: pointer;
                        .box-sizing(border-box);
                        position: absolute;
                        left: 0;
                        background-origin: border-box;
                }
 
+               &:active + label::before {
+                       background-color: @colorGray13;
+                       border-color: @colorGray13;
+               }
+
                &:focus + label::before {
                        border-width: 2px;
                }
                        border-bottom-width: 3px;
                }
 
-               &:active + label::before {
-                       background-color: @colorGray13;
-                       border-color: @colorGray13;
-               }
-
-               // disabled checked boxes have a gray background
+               // disabled radios have a gray background
                &:disabled + label::before {
                        cursor: default;
-                       border-color: @colorGray14;
                        background-color: @colorGray14;
+                       border-color: @colorGray14;
                }
 
-               // disabled and checked boxes have a white circle
+               // disabled and checked radios have a white circle
                &:disabled:checked + label::before {
                        .background-image-svg('images/radio_disabled.svg', 'images/radio_disabled.png');
                }
index a4bc427..01c2578 100644 (file)
@@ -14,6 +14,7 @@ class TitleTest extends MediaWikiTestCase {
                        'wgLang' => Language::factory( 'en' ),
                        'wgAllowUserJs' => false,
                        'wgDefaultLanguageVariant' => false,
+                       'wgMetaNamespace' => 'Project',
                ) );
        }
 
index ce2db5d..04fb00d 100644 (file)
@@ -6,14 +6,10 @@
  */
 class GenderCacheTest extends MediaWikiLangTestCase {
 
-       protected function setUp() {
-               global $wgDefaultUserOptions;
-               parent::setUp();
+       function addDBData() {
                //ensure the correct default gender
-               $wgDefaultUserOptions['gender'] = 'unknown';
-       }
+               $this->mergeMwGlobalArrayValue( 'wgDefaultUserOptions', array( 'gender' => 'unknown' ) );
 
-       function addDBData() {
                $user = User::newFromName( 'UTMale' );
                if ( $user->getID() == 0 ) {
                        $user->addToDatabase();
index e3c4cc8..251da47 100644 (file)
@@ -89,7 +89,7 @@ class TagHookTest extends MediaWikiTestCase {
                global $wgParserConf, $wgContLang;
                $parser = new Parser( $wgParserConf );
 
-               $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS );
+               $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), Parser::SFH_OBJECT_ARGS );
                $parser->parse(
                        "Foo<$tag>Bar</$tag>Baz",
                        Title::newFromText( 'Test' ),
diff --git a/tests/phpunit/includes/site/SiteListFileCacheBuilderTest.php b/tests/phpunit/includes/site/SiteListFileCacheBuilderTest.php
new file mode 100644 (file)
index 0000000..af02429
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.25
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @covers SiteListFileCacheBuilder
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SiteListFileCacheBuilderTest extends PHPUnit_Framework_TestCase {
+
+       public function testBuild() {
+               $cacheFile = $this->getCacheFile();
+
+               $cacheBuilder = $this->newSiteListFileCacheBuilder( $this->getSites(), $cacheFile );
+               $cacheBuilder->build();
+
+               $contents = file_get_contents( $cacheFile );
+               $this->assertEquals( json_encode( $this->getExpectedData() ), $contents );
+       }
+
+       private function getExpectedData() {
+               return array(
+                       'sites' => array(
+                               'foobar' => array(
+                                       'globalid' => 'foobar',
+                                       'type' => 'unknown',
+                                       'group' => 'none',
+                                       'source' => 'local',
+                                       'language' => null,
+                                       'localids' => array(),
+                                       'config' => array(),
+                                       'data' => array(),
+                                       'forward' => false,
+                                       'internalid' => null,
+                                       'identifiers' => array()
+                               ),
+                               'enwiktionary' => array(
+                                       'globalid' => 'enwiktionary',
+                                       'type' => 'mediawiki',
+                                       'group' => 'wiktionary',
+                                       'source' => 'local',
+                                       'language' => 'en',
+                                       'localids' => array(
+                                               'equivalent' => array( 'enwiktionary' )
+                                       ),
+                                       'config' => array(),
+                                       'data' => array(
+                                               'paths' => array(
+                                                       'page_path' => 'https://en.wiktionary.org/wiki/$1',
+                                                       'file_path' => 'https://en.wiktionary.org/w/$1'
+                                               )
+                                       ),
+                                       'forward' => false,
+                                       'internalid' => null,
+                                       'identifiers' => array(
+                                               array(
+                                                       'type' => 'equivalent',
+                                                       'key' => 'enwiktionary'
+                                               )
+                                       )
+                               )
+                       )
+               );
+       }
+
+       private function newSiteListFileCacheBuilder( SiteList $sites, $cacheFile ) {
+               return new SiteListFileCacheBuilder(
+                       $this->getSiteSQLStore( $sites ),
+                       $cacheFile
+               );
+       }
+
+       private function getSiteSQLStore( SiteList $sites ) {
+               $siteSQLStore = $this->getMockBuilder( 'SiteSQLStore' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $siteSQLStore->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( $sites ) );
+
+               return $siteSQLStore;
+       }
+
+       private function getSites() {
+               $sites = array();
+
+               $site = new Site();
+               $site->setGlobalId( 'foobar' );
+               $sites[] = $site;
+
+               $site = new MediaWikiSite();
+               $site->setGlobalId( 'enwiktionary' );
+               $site->setGroup( 'wiktionary' );
+               $site->setLanguageCode( 'en' );
+               $site->addNavigationId( 'enwiktionary' );
+               $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
+               $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
+               $sites[] = $site;
+
+               return new SiteList( $sites );
+       }
+
+       private function getCacheFile() {
+               return sys_get_temp_dir() . '/sites-' . time() . '.json';
+       }
+
+}
diff --git a/tests/phpunit/includes/site/SiteListFileCacheTest.php b/tests/phpunit/includes/site/SiteListFileCacheTest.php
new file mode 100644 (file)
index 0000000..b598eed
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.25
+ *
+ * @ingroup Site
+ * @ingroup Test
+ *
+ * @covers SiteListFileCache
+ * @group Site
+ *
+ * @licence GNU GPL v2+
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class SiteListFileCacheTest extends PHPUnit_Framework_TestCase {
+
+       public function testGetSites() {
+               $cacheFile = $this->getCacheFile();
+
+               $sites = $this->getSites();
+               $cacheBuilder = $this->newSiteListFileCacheBuilder( $sites, $cacheFile );
+               $cacheBuilder->build();
+
+               $cache = new SiteListFileCache( $cacheFile );
+               $this->assertEquals( $sites, $cache->getSites() );
+       }
+
+       public function testGetSite() {
+               $cacheFile = $this->getCacheFile();
+
+               $sites = $this->getSites();
+               $cacheBuilder = $this->newSiteListFileCacheBuilder( $sites, $cacheFile );
+               $cacheBuilder->build();
+
+               $cache = new SiteListFileCache( $cacheFile );
+
+               $this->assertEquals( $sites->getSite( 'enwiktionary' ), $cache->getSite( 'enwiktionary' ) );
+       }
+
+       private function newSiteListFileCacheBuilder( SiteList $sites, $cacheFile ) {
+               return new SiteListFileCacheBuilder(
+                       $this->getSiteSQLStore( $sites ),
+                       $cacheFile
+               );
+       }
+
+       private function getSiteSQLStore( SiteList $sites ) {
+               $siteSQLStore = $this->getMockBuilder( 'SiteSQLStore' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $siteSQLStore->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( $sites ) );
+
+               return $siteSQLStore;
+       }
+
+       private function getSites() {
+               $sites = array();
+
+               $site = new Site();
+               $site->setGlobalId( 'foobar' );
+               $sites[] = $site;
+
+               $site = new MediaWikiSite();
+               $site->setGlobalId( 'enwiktionary' );
+               $site->setGroup( 'wiktionary' );
+               $site->setLanguageCode( 'en' );
+               $site->addNavigationId( 'enwiktionary' );
+               $site->setPath( MediaWikiSite::PATH_PAGE, "https://en.wiktionary.org/wiki/$1" );
+               $site->setPath( MediaWikiSite::PATH_FILE, "https://en.wiktionary.org/w/$1" );
+               $sites[] = $site;
+
+               return new SiteList( $sites );
+       }
+
+       private function getCacheFile() {
+               return sys_get_temp_dir() . '/sites-' . time() . '.json';
+       }
+
+}
index f95b305..f1146a7 100644 (file)
@@ -39,6 +39,7 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
                        'wgLang' => Language::factory( 'en' ),
                        'wgAllowUserJs' => false,
                        'wgDefaultLanguageVariant' => false,
+                       'wgMetaNamespace' => 'Project',
                        'wgLocalInterwikis' => array( 'localtestiw' ),
                        'wgCapitalLinks' => true,
 
@@ -82,6 +83,8 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
        protected function makeCodec( $lang ) {
                $gender = $this->getGenderCache();
                $lang = Language::factory( $lang );
+               // language object can came from cache, which does not respect test settings
+               $lang->resetNamespaces();
                return new MediaWikiTitleCodec( $lang, $gender );
        }