Merge "Callers of ResourceLoader::getModule should check for null return"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 4 Apr 2014 00:54:54 +0000 (00:54 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 4 Apr 2014 00:54:54 +0000 (00:54 +0000)
48 files changed:
RELEASE-NOTES-1.23
includes/AutoLoader.php
includes/PrefixSearch.php
includes/api/ApiFeedRecentChanges.php
includes/api/ApiOpenSearch.php
includes/api/ApiQuery.php
includes/api/ApiQueryPrefixSearch.php [new file with mode: 0644]
includes/cache/LocalisationCache.php
includes/debug/Debug.php
includes/jobqueue/JobQueue.php
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobQueueRedis.php
includes/objectcache/BagOStuff.php
includes/objectcache/MemcachedPeclBagOStuff.php
includes/objectcache/MultiWriteBagOStuff.php
includes/objectcache/RedisBagOStuff.php
includes/objectcache/SqlBagOStuff.php
includes/profiler/Profiler.php
includes/profiler/ProfilerStub.php
maintenance/jsduck/categories.json
maintenance/language/languages.inc
resources/Resources.php
resources/jquery.tipsy/jquery.tipsy.js
resources/mediawiki.api/mediawiki.api.js
resources/mediawiki.special/mediawiki.special.userlogin.signup.css
resources/mediawiki.ui/components/default/forms.less
resources/mediawiki.ui/components/vector/forms.less
resources/mediawiki/mediawiki.debug.js
resources/mediawiki/mediawiki.debug.profile.css [new file with mode: 0644]
resources/mediawiki/mediawiki.debug.profile.js [new file with mode: 0644]
resources/mediawiki/mediawiki.js
resources/oojs-ui/i18n/bg.json
resources/oojs-ui/i18n/dsb.json
resources/oojs-ui/i18n/el.json
resources/oojs-ui/i18n/es.json
resources/oojs-ui/i18n/et.json
resources/oojs-ui/i18n/hi.json
resources/oojs-ui/i18n/kk-cyrl.json
resources/oojs-ui/i18n/ms.json
resources/oojs-ui/i18n/pl.json
resources/oojs-ui/i18n/qu.json
resources/oojs-ui/i18n/zh-hans.json
resources/oojs-ui/oojs-ui-apex.css
resources/oojs-ui/oojs-ui.js
resources/oojs-ui/oojs-ui.svg.css
resources/oojs/oojs.js
skins/common/shared.css
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js

index d430a4d..d833018 100644 (file)
@@ -240,6 +240,8 @@ production.
 * (bug 42026) Deprecated uctoponly in favor of ucshow=top.
 * list=search no longer has a "srredirects" parameter. Redirects are now
   included in all searches.
+* Added list=prefixsearch that works like action=opensearch but can be used as
+  a generator.
 
 === Languages updated in 1.23 ===
 
index b7d7d8c..b4a97a3 100644 (file)
@@ -199,6 +199,7 @@ $wgAutoloadLocalClasses = array(
        'StatCounter' => 'includes/StatCounter.php',
        'Status' => 'includes/Status.php',
        'StreamFile' => 'includes/StreamFile.php',
+       'StringPrefixSearch' => 'includes/PrefixSearch.php',
        'StubContLang' => 'includes/StubObject.php',
        'StubObject' => 'includes/StubObject.php',
        'StubUserLang' => 'includes/StubObject.php',
@@ -208,6 +209,7 @@ $wgAutoloadLocalClasses = array(
        'Title' => 'includes/Title.php',
        'TitleArray' => 'includes/TitleArray.php',
        'TitleArrayFromResult' => 'includes/TitleArrayFromResult.php',
+       'TitlePrefixSearch' => 'includes/PrefixSearch.php',
        'UnlistedSpecialPage' => 'includes/specialpage/UnlistedSpecialPage.php',
        'UploadSourceAdapter' => 'includes/Import.php',
        'UppercaseCollation' => 'includes/Collation.php',
@@ -343,6 +345,7 @@ $wgAutoloadLocalClasses = array(
        'ApiQueryPageProps' => 'includes/api/ApiQueryPageProps.php',
        'ApiQueryPagesWithProp' => 'includes/api/ApiQueryPagesWithProp.php',
        'ApiQueryPagePropNames' => 'includes/api/ApiQueryPagePropNames.php',
+       'ApiQueryPrefixSearch' => 'includes/api/ApiQueryPrefixSearch.php',
        'ApiQueryProtectedTitles' => 'includes/api/ApiQueryProtectedTitles.php',
        'ApiQueryQueryPage' => 'includes/api/ApiQueryQueryPage.php',
        'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php',
index 780cae5..957a119 100644 (file)
  *
  * @ingroup Search
  */
-class PrefixSearch {
+abstract class PrefixSearch {
        /**
         * Do a prefix search of titles and return a list of matching page names.
+        * @deprecated: Since 1.23, use TitlePrefixSearch or StringPrefixSearch classes
         *
         * @param $search String
         * @param $limit Integer
@@ -36,11 +37,24 @@ class PrefixSearch {
         * @return Array of strings
         */
        public static function titleSearch( $search, $limit, $namespaces = array() ) {
+               $search = new StringPrefixSearch;
+               return $search->search( $search, $limit, $namespaces );
+       }
+
+       /**
+        * Do a prefix search of titles and return a list of matching page names.
+        *
+        * @param $search String
+        * @param $limit Integer
+        * @param array $namespaces used if query is not explicitly prefixed
+        * @return Array of strings or Title objects
+        */
+       public function search( $search, $limit, $namespaces = array() ) {
                $search = trim( $search );
                if ( $search == '' ) {
                        return array(); // Return empty result
                }
-               $namespaces = self::validateNamespaces( $namespaces );
+               $namespaces = $this->validateNamespaces( $namespaces );
 
                // Find a Title which is not an interwiki and is in NS_MAIN
                $title = Title::newFromText( $search );
@@ -49,7 +63,7 @@ class PrefixSearch {
                        if ( $ns[0] == NS_MAIN ) {
                                $ns = $namespaces; // no explicit prefix, use default namespaces
                        }
-                       return self::searchBackend(
+                       return $this->searchBackend(
                                $ns, $title->getText(), $limit );
                }
 
@@ -57,14 +71,68 @@ class PrefixSearch {
                $title = Title::newFromText( $search . 'Dummy' );
                if ( $title && $title->getText() == 'Dummy'
                        && $title->getNamespace() != NS_MAIN
-                       && !$title->isExternal() ) {
-                       return self::searchBackend(
-                               array( $title->getNamespace() ), '', $limit );
+                       && !$title->isExternal() )
+               {
+                       $namespaces = array( $title->getNamespace() );
+                       $search = '';
                }
 
-               return self::searchBackend( $namespaces, $search, $limit );
+               return $this->searchBackend( $namespaces, $search, $limit );
+       }
+
+       /**
+        * Do a prefix search for all possible variants of the prefix
+        * @param $search String
+        * @param $limit Integer
+        * @param array $namespaces
+        *
+        * @return array
+        */
+       public function searchWithVariants( $search, $limit, array $namespaces ) {
+               wfProfileIn( __METHOD__ );
+               $searches = $this->search( $search, $limit, $namespaces );
+
+               // if the content language has variants, try to retrieve fallback results
+               $fallbackLimit = $limit - count( $searches );
+               if ( $fallbackLimit > 0 ) {
+                       global $wgContLang;
+
+                       $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
+                       $fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) );
+
+                       foreach ( $fallbackSearches as $fbs ) {
+                               $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
+                               $searches = array_merge( $searches, $fallbackSearchResult );
+                               $fallbackLimit -= count( $fallbackSearchResult );
+
+                               if ( $fallbackLimit == 0 ) {
+                                       break;
+                               }
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               return $searches;
        }
 
+       /**
+        * When implemented in a descendant class, receives an array of Title objects and returns
+        * either an unmodified array or an array of strings corresponding to titles passed to it.
+        *
+        * @param array $titles
+        * @return array
+        */
+       protected abstract function titles( array $titles );
+
+       /**
+        * When implemented in a descendant class, receives an array of titles as strings and returns
+        * either an unmodified array or an array of Title objects corresponding to strings received.
+        *
+        * @param array $strings
+        *
+        * @return array
+        */
+       protected abstract function strings( array $strings );
+
        /**
         * Do a prefix search of titles and return a list of matching page names.
         * @param $namespaces Array
@@ -72,20 +140,20 @@ class PrefixSearch {
         * @param $limit Integer
         * @return Array of strings
         */
-       protected static function searchBackend( $namespaces, $search, $limit ) {
+       protected function searchBackend( $namespaces, $search, $limit ) {
                if ( count( $namespaces ) == 1 ) {
                        $ns = $namespaces[0];
                        if ( $ns == NS_MEDIA ) {
                                $namespaces = array( NS_FILE );
                        } elseif ( $ns == NS_SPECIAL ) {
-                               return self::specialSearch( $search, $limit );
+                               return $this->titles( $this->specialSearch( $search, $limit ) );
                        }
                }
                $srchres = array();
                if ( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
-                       return self::defaultSearchBackend( $namespaces, $search, $limit );
+                       return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit ) );
                }
-               return $srchres;
+               return $this->strings( $srchres );
        }
 
        /**
@@ -95,7 +163,7 @@ class PrefixSearch {
         * @param $limit Integer: max number of items to return
         * @return Array
         */
-       protected static function specialSearch( $search, $limit ) {
+       protected function specialSearch( $search, $limit ) {
                global $wgContLang;
 
                # normalize searchKey, so aliases with spaces can be found - bug 25675
@@ -129,7 +197,7 @@ class PrefixSearch {
                                // localizes its input leading to searches for e.g. Special:All
                                // returning Spezial:MediaWiki-Systemnachrichten and returning
                                // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
-                               $srchres[] = Title::makeTitleSafe( NS_SPECIAL, $page )->getPrefixedText();
+                               $srchres[] = Title::makeTitleSafe( NS_SPECIAL, $page );
                                wfRestoreWarnings();
                        }
 
@@ -150,38 +218,30 @@ class PrefixSearch {
         * @param array $namespaces namespaces to search in
         * @param string $search term
         * @param $limit Integer: max number of items to return
-        * @return Array of title strings
+        * @return Array of Title objects
         */
-       protected static function defaultSearchBackend( $namespaces, $search, $limit ) {
+       protected function defaultSearchBackend( $namespaces, $search, $limit ) {
                $ns = array_shift( $namespaces ); // support only one namespace
                if ( in_array( NS_MAIN, $namespaces ) ) {
                        $ns = NS_MAIN; // if searching on many always default to main
                }
 
-               // Prepare nested request
-               $req = new FauxRequest( array(
-                       'action' => 'query',
-                       'list' => 'allpages',
-                       'apnamespace' => $ns,
-                       'aplimit' => $limit,
-                       'apprefix' => $search
-               ));
-
-               // Execute
-               $module = new ApiMain( $req );
-               $module->execute();
-
-               // Get resulting data
-               $data = $module->getResultData();
-
-               // Reformat useful data for future printing by JSON engine
+               $t = Title::newFromText( $search, $ns );
+               $prefix = $t ? $t->getDBkey() : '';
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'page',
+                       array( 'page_id', 'page_namespace', 'page_title' ),
+                       array(
+                               'page_namespace' => $ns,
+                               'page_title ' . $dbr->buildLike( $prefix, $dbr->anyString() )
+                       ),
+                       __METHOD__,
+                       array( 'LIMIT' => $limit, 'ORDER BY' => 'page_title' )
+               );
                $srchres = array();
-               foreach ( (array)$data['query']['allpages'] as $pageinfo ) {
-                       // Note: this data will no be printable by the xml engine
-                       // because it does not support lists of unnamed items
-                       $srchres[] = $pageinfo['title'];
+               foreach ( $res as $row ) {
+                       $srchres[] = Title::newFromRow( $row );
                }
-
                return $srchres;
        }
 
@@ -191,7 +251,7 @@ class PrefixSearch {
         * @param $namespaces Array
         * @return Array (default: contains only NS_MAIN)
         */
-       protected static function validateNamespaces( $namespaces ) {
+       protected function validateNamespaces( $namespaces ) {
                global $wgContLang;
 
                // We will look at each given namespace against wgContLang namespaces
@@ -211,3 +271,37 @@ class PrefixSearch {
                return array( NS_MAIN );
        }
 }
+
+/**
+ * Performs prefix search, returning Title objects
+ * @ingroup Search
+ */
+class TitlePrefixSearch extends PrefixSearch {
+
+       protected function titles( array $titles ) {
+               return $titles;
+       }
+
+       protected function strings( array $strings ) {
+               $titles = array_map( 'Title::newFromText', $strings );
+               $lb = new LinkBatch( $titles );
+               $lb->setCaller( __METHOD__ );
+               $lb->execute();
+               return $titles;
+       }
+}
+
+/**
+ * Performs prefix search, returning strings
+ * @ingroup Search
+ */
+class StringPrefixSearch extends PrefixSearch {
+
+       protected function titles( array $titles ) {
+               return array_map( function( Title $t ) { return $t->getPrefixedText(); }, $titles );
+       }
+
+       protected function strings( array $strings ) {
+               return $strings;
+       }
+}
index f1c1bf3..9062ad9 100644 (file)
@@ -80,6 +80,10 @@ class ApiFeedRecentChanges extends ApiBase {
        public function getFeedObject( $feedFormat, $specialClass ) {
                if ( $specialClass === 'SpecialRecentchangeslinked' ) {
                        $title = Title::newFromText( $this->params['target'] );
+                       if ( !$title ) {
+                               $this->dieUsageMsg( array( 'invalidtitle', $this->params['target'] ) );
+                       }
+
                        $feed = new ChangesFeed( $feedFormat, false );
                        $feedObj = $feed->getFeedObject(
                                $this->msg( 'recentchangeslinked-title', $title->getPrefixedText() )
index f2bf754..68b62af 100644 (file)
@@ -60,28 +60,8 @@ class ApiOpenSearch extends ApiBase {
                        $this->getMain()->setCacheMaxAge( $wgSearchSuggestCacheExpiry );
                        $this->getMain()->setCacheMode( 'public' );
 
-                       $searches = PrefixSearch::titleSearch( $search, $limit,
-                               $namespaces );
-
-                       // if the content language has variants, try to retrieve fallback results
-                       $fallbackLimit = $limit - count( $searches );
-                       if ( $fallbackLimit > 0 ) {
-                               global $wgContLang;
-
-                               $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
-                               $fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) );
-
-                               foreach ( $fallbackSearches as $fbs ) {
-                                       $fallbackSearchResult = PrefixSearch::titleSearch( $fbs, $fallbackLimit,
-                                               $namespaces );
-                                       $searches = array_merge( $searches, $fallbackSearchResult );
-                                       $fallbackLimit -= count( $fallbackSearchResult );
-
-                                       if ( $fallbackLimit == 0 ) {
-                                               break;
-                                       }
-                               }
-                       }
+                       $searcher = new StringPrefixSearch;
+                       $searches = $searcher->searchWithVariants( $search, $limit, $namespaces );
                }
                // Set top level elements
                $result = $this->getResult();
index c6ae611..0a95fba 100644 (file)
@@ -86,6 +86,7 @@ class ApiQuery extends ApiBase {
                'logevents' => 'ApiQueryLogEvents',
                'pageswithprop' => 'ApiQueryPagesWithProp',
                'pagepropnames' => 'ApiQueryPagePropNames',
+               'prefixsearch' => 'ApiQueryPrefixSearch',
                'protectedtitles' => 'ApiQueryProtectedTitles',
                'querypage' => 'ApiQueryQueryPage',
                'random' => 'ApiQueryRandom',
diff --git a/includes/api/ApiQueryPrefixSearch.php b/includes/api/ApiQueryPrefixSearch.php
new file mode 100644 (file)
index 0000000..dfe0eec
--- /dev/null
@@ -0,0 +1,124 @@
+<?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.23
+ */
+
+/**
+ * @ingroup API
+ */
+class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
+       public function __construct( $query, $moduleName ) {
+               parent::__construct( $query, $moduleName, 'ps' );
+       }
+
+       public function execute() {
+               $this->run();
+       }
+
+       public function executeGenerator( $resultPageSet ) {
+               $this->run( $resultPageSet );
+       }
+
+       /**
+        * @param $resultPageSet ApiPageSet
+        */
+       private function run( $resultPageSet = null ) {
+               $params = $this->extractRequestParams();
+               $search = $params['search'];
+               $limit = $params['limit'];
+               $namespaces = $params['namespace'];
+
+               $searcher = new TitlePrefixSearch;
+               $titles = $searcher->searchWithVariants( $search, $limit, $namespaces );
+               if ( $resultPageSet ) {
+                       $resultPageSet->populateFromTitles( $titles );
+               } else {
+                       $result = $this->getResult();
+                       foreach ( $titles as $title ) {
+                               if ( !$limit-- ) {
+                                       break;
+                               }
+                               $vals = array(
+                                       'ns' => intval( $title->getNamespace() ),
+                                       'title' => $title->getPrefixedText(),
+                               );
+                               if ( $title->isSpecialPage() ) {
+                                       $vals['special'] = '';
+                               } else {
+                                       $vals['pageid'] = intval( $title->getArticleId() );
+                               }
+                               $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+                               if ( !$fit ) {
+                                       break;
+                               }
+                       }
+                       $result->setIndexedTagName_internal(
+                               array( 'query', $this->getModuleName() ), $this->getModulePrefix()
+                       );
+               }
+       }
+
+       public function getCacheMode( $params ) {
+               return 'public';
+       }
+
+       public function getAllowedParams() {
+                       return array(
+                               'search' => array(
+                                       ApiBase::PARAM_TYPE => 'string',
+                                       ApiBase::PARAM_REQUIRED => true,
+                               ),
+                               'namespace' => array(
+                                       ApiBase::PARAM_DFLT => NS_MAIN,
+                                       ApiBase::PARAM_TYPE => 'namespace',
+                                       ApiBase::PARAM_ISMULTI => true,
+                               ),
+                               'limit' => array(
+                                       ApiBase::PARAM_DFLT => 10,
+                                       ApiBase::PARAM_TYPE => 'limit',
+                                       ApiBase::PARAM_MIN => 1,
+                                       ApiBase::PARAM_MAX => 100, // Non-standard value for compatibility
+                                                                  // with action=opensearch
+                                       ApiBase::PARAM_MAX2 => 200,
+                               ),
+                       );
+       }
+
+       public function getParamDescription() {
+               return array(
+                       'search' => 'Search string',
+                       'limit' => 'Maximum amount of results to return',
+                       'namespace' => 'Namespaces to search',
+               );
+       }
+
+       public function getDescription() {
+               return 'Perform a prefix search for page titles';
+       }
+
+       public function getExamples() {
+               return array(
+                       'api.php?action=query&list=prefixsearch&pssearch=meaning',
+               );
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Prefixsearch';
+       }
+}
index 8827219..b89316b 100644 (file)
@@ -539,7 +539,7 @@ class LocalisationCache {
         * @throws MWException if there is a syntax error in the JSON file
         * @return array with a 'messages' key, or empty array if the file doesn't exist
         */
-       protected function readJSONFile( $fileName ) {
+       public function readJSONFile( $fileName ) {
                wfProfileIn( __METHOD__ );
 
                if ( !is_readable( $fileName ) ) {
index d105bd0..86f3dda 100644 (file)
@@ -530,6 +530,12 @@ class MWDebug {
                global $wgVersion, $wgRequestTime;
                $request = $context->getRequest();
 
+               // HHVM's reported memory usage from memory_get_peak_usage()
+               // is not useful when passing false, but we continue passing
+               // false for consistency of historical data in zend.
+               // see: https://github.com/facebook/hhvm/issues/2257#issuecomment-39362246
+               $realMemoryUsage = wfIsHHVM();
+
                return array(
                        'mwVersion' => $wgVersion,
                        'phpVersion' => PHP_VERSION,
@@ -546,9 +552,10 @@ class MWDebug {
                                'headers' => $request->getAllHeaders(),
                                'params' => $request->getValues(),
                        ),
-                       'memory' => $context->getLanguage()->formatSize( memory_get_usage() ),
-                       'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ),
+                       'memory' => $context->getLanguage()->formatSize( memory_get_usage( $realMemoryUsage ) ),
+                       'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage( $realMemoryUsage ) ),
                        'includes' => self::getFilesIncluded( $context ),
+                       'profile' => Profiler::instance()->getRawData(),
                );
        }
 }
index a537861..2d7103c 100644 (file)
@@ -376,7 +376,7 @@ abstract class JobQueue {
                // Flag this job as an old duplicate based on its "root" job...
                try {
                        if ( $job && $this->isRootJobOldDuplicate( $job ) ) {
-                               JobQueue::incrStats( 'job-pop-duplicate', $this->type );
+                               JobQueue::incrStats( 'job-pop-duplicate', $this->type, 1, $this->wiki );
                                $job = DuplicateJob::newFromJob( $job ); // convert to a no-op
                        }
                } catch ( MWException $e ) {
@@ -715,11 +715,15 @@ abstract class JobQueue {
         * @param string $key Event type
         * @param string $type Job type
         * @param int $delta
+        * @param string $wiki Wiki ID (added in 1.23)
         * @since 1.22
         */
-       public static function incrStats( $key, $type, $delta = 1 ) {
+       public static function incrStats( $key, $type, $delta = 1, $wiki = null ) {
                wfIncrStats( $key, $delta );
                wfIncrStats( "{$key}-{$type}", $delta );
+               if ( $wiki !== null ) {
+                       wfIncrStats( "{$key}-{$type}-{$wiki}", $delta );
+               }
        }
 
        /**
index e83c26d..5f1ca14 100644 (file)
@@ -258,11 +258,12 @@ class JobQueueDB extends JobQueue {
                        foreach ( array_chunk( $rows, 50 ) as $rowBatch ) {
                                $dbw->insert( 'job', $rowBatch, $method );
                        }
-                       JobQueue::incrStats( 'job-insert', $this->type, count( $rows ) );
+                       JobQueue::incrStats( 'job-insert', $this->type, count( $rows ), $this->wiki );
                        JobQueue::incrStats(
                                'job-insert-duplicate',
                                $this->type,
-                               count( $rowSet ) + count( $rowList ) - count( $rows )
+                               count( $rowSet ) + count( $rowList ) - count( $rows ),
+                               $this->wiki
                        );
                } catch ( DBError $e ) {
                        if ( $flags & self::QOS_ATOMIC ) {
@@ -313,7 +314,7 @@ class JobQueueDB extends JobQueue {
                                        $this->cache->set( $this->getCacheKey( 'empty' ), 'true', self::CACHE_TTL_LONG );
                                        break; // nothing to do
                                }
-                               JobQueue::incrStats( 'job-pop', $this->type );
+                               JobQueue::incrStats( 'job-pop', $this->type, 1, $this->wiki );
                                // Get the job object from the row...
                                $title = Title::makeTitleSafe( $row->job_namespace, $row->job_title );
                                if ( !$title ) {
@@ -684,8 +685,9 @@ class JobQueueDB extends JobQueue {
                                                        'job_id' => $ids ),
                                                __METHOD__
                                        );
-                                       $count += $dbw->affectedRows();
-                                       JobQueue::incrStats( 'job-recycle', $this->type, $dbw->affectedRows() );
+                                       $affected = $dbw->affectedRows();
+                                       $count += $affected;
+                                       JobQueue::incrStats( 'job-recycle', $this->type, $affected, $this->wiki );
                                        $this->cache->set( $this->getCacheKey( 'empty' ), 'false', self::CACHE_TTL_LONG );
                                }
                        }
@@ -710,8 +712,9 @@ class JobQueueDB extends JobQueue {
                        );
                        if ( count( $ids ) ) {
                                $dbw->delete( 'job', array( 'job_id' => $ids ), __METHOD__ );
-                               $count += $dbw->affectedRows();
-                               JobQueue::incrStats( 'job-abandon', $this->type, $dbw->affectedRows() );
+                               $affected = $dbw->affectedRows();
+                               $count += $affected;
+                               JobQueue::incrStats( 'job-abandon', $this->type, $affected, $this->wiki );
                        }
 
                        $dbw->unlock( "jobqueue-recycle-{$this->type}", __METHOD__ );
index c785cb2..135a61d 100644 (file)
@@ -237,9 +237,9 @@ class JobQueueRedis extends JobQueue {
 
                                return false;
                        }
-                       JobQueue::incrStats( 'job-insert', $this->type, count( $items ) );
+                       JobQueue::incrStats( 'job-insert', $this->type, count( $items ), $this->wiki );
                        JobQueue::incrStats( 'job-insert-duplicate', $this->type,
-                               count( $items ) - $failed - $pushed );
+                               count( $items ) - $failed - $pushed, $this->wiki );
                } catch ( RedisException $e ) {
                        $this->throwRedisException( $conn, $e );
                }
@@ -331,7 +331,7 @@ LUA;
                                        break; // no jobs; nothing to do
                                }
 
-                               JobQueue::incrStats( 'job-pop', $this->type );
+                               JobQueue::incrStats( 'job-pop', $this->type, 1, $this->wiki );
                                $item = $this->unserialize( $blob );
                                if ( $item === false ) {
                                        wfDebugLog( 'JobQueueRedis', "Could not unserialize {$this->type} job." );
@@ -715,8 +715,8 @@ LUA;
                        if ( $res ) {
                                list( $released, $abandoned, $pruned, $undelayed ) = $res;
                                $count += $released + $pruned + $undelayed;
-                               JobQueue::incrStats( 'job-recycle', $this->type, $released );
-                               JobQueue::incrStats( 'job-abandon', $this->type, $abandoned );
+                               JobQueue::incrStats( 'job-recycle', $this->type, $released, $this->wiki );
+                               JobQueue::incrStats( 'job-abandon', $this->type, $abandoned, $this->wiki );
                        }
                } catch ( RedisException $e ) {
                        $this->throwRedisException( $conn, $e );
index 217142c..ad3aa96 100644 (file)
 abstract class BagOStuff {
        private $debugMode = false;
 
+       protected $lastError = self::ERR_NONE;
+
+       /** Possible values for getLastError() */
+       const ERR_NONE        = 0; // no error
+       const ERR_NO_RESPONSE = 1; // no response
+       const ERR_UNREACHABLE = 2; // can't connect
+       const ERR_UNEXPECTED  = 3; // response gave some error
+
        /**
         * @param $bool bool
         */
@@ -169,9 +177,12 @@ abstract class BagOStuff {
         * @return bool success
         */
        public function lock( $key, $timeout = 6 ) {
+               $this->clearLastError();
                $timestamp = microtime( true ); // starting UNIX timestamp
                if ( $this->add( "{$key}:lock", 1, $timeout ) ) {
                        return true;
+               } elseif ( $this->getLastError() ) {
+                       return false;
                }
 
                $uRTT = ceil( 1e6 * ( microtime( true ) - $timestamp ) ); // estimate RTT (us)
@@ -186,7 +197,11 @@ abstract class BagOStuff {
                                $sleep *= 2;
                        }
                        usleep( $sleep ); // back off
+                       $this->clearLastError();
                        $locked = $this->add( "{$key}:lock", 1, $timeout );
+                       if ( $this->getLastError() ) {
+                               return false;
+                       }
                } while ( !$locked );
 
                return $locked;
@@ -292,6 +307,32 @@ abstract class BagOStuff {
                return $this->incr( $key, - $value );
        }
 
+       /**
+        * Get the "last error" registered; clearLastError() should be called manually
+        * @return integer ERR_* constant for the "last error" registry
+        * @since 1.23
+        */
+       public function getLastError() {
+               return $this->lastError;
+       }
+
+       /**
+        * Clear the "last error" registry
+        * @since 1.23
+        */
+       public function clearLastError() {
+               $this->lastError = self::ERR_NONE;
+       }
+
+       /**
+        * Set the "last error" registry
+        * @param $err integer ERR_* constant
+        * @since 1.23
+        */
+       protected function setLastError( $err ) {
+               $this->lastError = $err;
+       }
+
        /**
         * @param $text string
         */
index 18546d4..1c780b4 100644 (file)
@@ -231,6 +231,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                                        $msg = "Memcached error: $msg";
                                }
                                wfDebugLog( 'memcached-serious', $msg );
+                               $this->setLastError( BagOStuff::ERR_UNEXPECTED );
                }
                return $result;
        }
index e550c0d..b97410a 100644 (file)
@@ -179,6 +179,16 @@ class MultiWriteBagOStuff extends BagOStuff {
                return $this->doWrite( 'merge', $key, $callback, $exptime );
        }
 
+       public function getLastError() {
+               return isset( $this->caches[0] ) ? $this->caches[0]->getLastError() : self::ERR_NONE;
+       }
+
+       public function clearLastError() {
+               if ( isset( $this->caches[0] ) ) {
+                       $this->caches[0]->clearLastError();
+               }
+       }
+
        /**
         * @param $method string
         * @return bool
index f54726f..872af63 100644 (file)
@@ -305,6 +305,7 @@ class RedisBagOStuff extends BagOStuff {
                                return array( $server, $conn );
                        }
                }
+               $this->setLastError( BagOStuff::ERR_UNREACHABLE );
                return array( false, false );
        }
 
@@ -322,6 +323,7 @@ class RedisBagOStuff extends BagOStuff {
         * object and let it be reopened during the next request.
         */
        protected function handleException( RedisConnRef $conn, $e ) {
+               $this->setLastError( BagOStuff::ERR_UNEXPECTED );
                $this->redisPool->handleError( $conn, $e );
        }
 
index 2c90339..74b5de2 100644 (file)
@@ -626,8 +626,10 @@ class SqlBagOStuff extends BagOStuff {
                }
                wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" );
                if ( $exception instanceof DBConnectionError ) {
+                       $this->setLastError( BagOStuff::ERR_UNREACHABLE );
                        wfDebug( __METHOD__ . ": ignoring connection error\n" );
                } else {
+                       $this->setLastError( BagOStuff::ERR_UNEXPECTED );
                        wfDebug( __METHOD__ . ": ignoring query error\n" );
                }
        }
@@ -646,8 +648,10 @@ class SqlBagOStuff extends BagOStuff {
                }
                wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" );
                if ( $exception instanceof DBConnectionError ) {
+                       $this->setLastError( BagOStuff::ERR_UNREACHABLE );
                        wfDebug( __METHOD__ . ": ignoring connection error\n" );
                } else {
+                       $this->setLastError( BagOStuff::ERR_UNEXPECTED );
                        wfDebug( __METHOD__ . ": ignoring query error\n" );
                }
        }
index a26ef68..f992d83 100644 (file)
@@ -96,7 +96,7 @@ class ProfileSection {
  */
 class Profiler {
        protected $mStack = array(), $mWorkStack = array(), $mCollated = array(),
-               $mCalls = array(), $mTotals = array();
+               $mCalls = array(), $mTotals = array(), $mPeriods = array();
        protected $mTimeMetric = 'wall';
        protected $mProfileID = false, $mCollateDone = false, $mTemplated = false;
 
@@ -558,6 +558,7 @@ class Profiler {
                                $this->mMin[$fname] = 1 << 24;
                                $this->mMax[$fname] = 0;
                                $this->mOverhead[$fname] = 0;
+                               $this->mPeriods[$fname] = array();
                        }
 
                        $this->mCollated[$fname] += $elapsed;
@@ -566,6 +567,7 @@ class Profiler {
                        $this->mMin[$fname] = min( $this->mMin[$fname], $elapsed );
                        $this->mMax[$fname] = max( $this->mMax[$fname], $elapsed );
                        $this->mOverhead[$fname] += $subcalls;
+                       $this->mPeriods[$fname][] = compact( 'start', 'end', 'memory', 'subcalls' );
                }
 
                $this->mCalls['-overhead-total'] = $profileCount;
@@ -610,6 +612,37 @@ class Profiler {
                return $prof;
        }
 
+       /**
+        * @return array
+        */
+       public function getRawData() {
+               $this->collateData();
+
+               $profile = array();
+               $total = isset( $this->mCollated['-total'] ) ? $this->mCollated['-total'] : 0;
+               foreach ( $this->mCollated as $fname => $elapsed ) {
+                       $periods = array();
+                       foreach ( $this->mPeriods[$fname] as $period ) {
+                               $period['start'] *= 1000;
+                               $period['end'] *= 1000;
+                               $periods[] = $period;
+                       }
+                       $profile[] = array(
+                               'name' => $fname,
+                               'calls' => $this->mCalls[$fname],
+                               'elapsed' => $elapsed * 1000,
+                               'percent' => $total ? 100. * $elapsed / $total : 0,
+                               'memory' => $this->mMemory[$fname],
+                               'min' => $this->mMin[$fname] * 1000,
+                               'max' => $this->mMax[$fname] * 1000,
+                               'overhead' => $this->mOverhead[$fname],
+                               'periods' => $periods,
+                       );
+               }
+
+               return $profile;
+       }
+
        /**
         * Dummy calls to wfProfileIn/wfProfileOut to calculate its overhead
         */
index 3697f35..7b310c1 100644 (file)
@@ -41,4 +41,5 @@ class ProfilerStub extends Profiler {
        public function getCurrentSection() { return ''; }
        public function transactionWritingIn( $server, $db ) {}
        public function transactionWritingOut( $server, $db ) {}
+       public function getRawData() { return array(); }
 }
index adaf114..eaf0a38 100644 (file)
@@ -62,7 +62,8 @@
                                        "mw.log",
                                        "mw.inspect",
                                        "mw.inspect.reports",
-                                       "mw.Debug"
+                                       "mw.Debug",
+                                       "mw.Debug.profile"
                                ]
                        }
                ]
index 61ee424..0483aea 100644 (file)
@@ -125,12 +125,16 @@ class Languages {
                $this->mNamespaceAliases[$code] = array();
                $this->mMagicWords[$code] = array();
                $this->mSpecialPageAliases[$code] = array();
+
+               $jsonfilename = Language::getJsonMessagesFileName( $code );
+               if ( file_exists( $jsonfilename ) ) {
+                       $json = Language::getLocalisationCache()->readJSONFile( $jsonfilename );
+                       $this->mRawMessages[$code] = $json['messages'];
+               }
+
                $filename = Language::getMessagesFileName( $code );
                if ( file_exists( $filename ) ) {
                        require $filename;
-                       if ( isset( $messages ) ) {
-                               $this->mRawMessages[$code] = $messages;
-                       }
                        if ( isset( $fallback ) ) {
                                $this->mFallback[$code] = $fallback;
                        }
index 2f09de6..6e0d717 100644 (file)
@@ -738,6 +738,7 @@ return array(
                'dependencies' => array(
                        'mediawiki.api',
                        'mediawiki.Title',
+                       'user.tokens',
                ),
        ),
        'mediawiki.api.login' => array(
@@ -758,9 +759,18 @@ return array(
                ),
        ),
        'mediawiki.debug' => array(
-               'scripts' => 'resources/mediawiki/mediawiki.debug.js',
-               'styles' => 'resources/mediawiki/mediawiki.debug.less',
-               'dependencies' => 'jquery.footHovzer',
+               'scripts' => array(
+                       'resources/mediawiki/mediawiki.debug.js',
+                       'resources/mediawiki/mediawiki.debug.profile.js'
+               ),
+               'styles' => array(
+                       'resources/mediawiki/mediawiki.debug.less',
+                       'resources/mediawiki/mediawiki.debug.profile.css'
+               ),
+               'dependencies' => array(
+                       'jquery.footHovzer',
+                       'jquery.tipsy',
+               ),
                'position' => 'bottom',
        ),
        'mediawiki.debug.init' => array(
index 6e47c60..f920e8b 100644 (file)
@@ -8,46 +8,53 @@
 
 (function($) {
 
-       function maybeCall(thing, ctx) {
-               return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
-       };
-       
+    function maybeCall(thing, ctx) {
+        return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
+    }
+
     function fixTitle($ele) {
         if ($ele.attr('title') || typeof($ele.attr('original-title')) != 'string') {
             $ele.attr('original-title', $ele.attr('title') || '').removeAttr('title');
         }
     }
-    
+
     function Tipsy(element, options) {
         this.$element = $(element);
         this.options = options;
         this.enabled = true;
         fixTitle(this.$element);
     }
-    
+
     Tipsy.prototype = {
         show: function() {
             var title = this.getTitle();
             if (title && this.enabled) {
                 var $tip = this.tip();
-                
+
                 $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
                 $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
                 if (this.options.className) {
                     $tip.addClass(maybeCall(this.options.className, this.$element[0]));
                 }
                 $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).appendTo(document.body);
-                
+
                 var pos = $.extend({}, this.$element.offset(), {
                     width: this.$element[0].offsetWidth,
                     height: this.$element[0].offsetHeight
                 });
-                
-                var actualWidth = $tip[0].offsetWidth, actualHeight = $tip[0].offsetHeight;
+
                 var gravity = (typeof this.options.gravity == 'function')
                                 ? this.options.gravity.call(this.$element[0])
                                 : this.options.gravity;
-                
+
+                // Attach css classes before checking height/width so they
+                // can be applied.
+                $tip.addClass('tipsy-' + gravity);
+                if (this.options.className) {
+                    $tip.addClass(maybeCall(this.options.className, this.$element[0]));
+                }
+
+                var actualWidth = $tip[0].offsetWidth, actualHeight = $tip[0].offsetHeight;
                 var tp;
                 switch (gravity.charAt(0)) {
                     case 'n':
@@ -63,7 +70,7 @@
                         tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
                         break;
                 }
-                
+
                 if (gravity.length == 2) {
                     if (gravity.charAt(1) == 'w') {
                         if (this.options.center) {
@@ -79,9 +86,8 @@
                         }
                     }
                 }
-                
-                $tip.css(tp).addClass('tipsy-' + gravity);
-                
+                $tip.css(tp);
+
                 if (this.options.fade) {
                     $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}, 100);
                 } else {
@@ -89,7 +95,7 @@
                 }
             }
         },
-        
+
         hide: function() {
             if (this.options.fade) {
                 this.tip().stop().fadeOut(100, function() { $(this).remove(); });
                 this.tip().remove();
             }
         },
-        
+
         getTitle: function() {
             var title, $e = this.$element, o = this.options;
             fixTitle($e);
             title = ('' + title).replace(/(^\s*|\s*$)/, "");
             return title || o.fallback;
         },
-        
+
         tip: function() {
             if (!this.$tip) {
                 this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"/></div>');
             }
             return this.$tip;
         },
-        
+
         validate: function() {
             if (!this.$element[0].parentNode) {
                 this.hide();
                 this.options = null;
             }
         },
-        
+
         enable: function() { this.enabled = true; },
         disable: function() { this.enabled = false; },
         toggleEnabled: function() { this.enabled = !this.enabled; }
     };
-    
+
     $.fn.tipsy = function(options) {
-        
+
         if (options === true) {
             return this.data('tipsy');
         } else if (typeof options == 'string') {
             return this.data('tipsy')[options]();
         }
-        
+
         options = $.extend({}, $.fn.tipsy.defaults, options);
-        
+
         function get(ele) {
             var tipsy = $.data(ele, 'tipsy');
             if (!tipsy) {
             }
             return tipsy;
         }
-        
+
         function enter() {
             var tipsy = get(this);
             tipsy.hoverState = 'in';
                 setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn);
             }
         };
-        
+
         function leave() {
             var tipsy = get(this);
             tipsy.hoverState = 'out';
                 setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut);
             }
         };
-        
+
         if (!options.live) this.each(function() { get(this); });
-        
+
         if (options.trigger != 'manual') {
             var binder   = options.live ? 'live' : 'bind',
                 eventIn  = options.trigger == 'hover' ? 'mouseenter' : 'focus',
                 eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
             this[binder](eventIn, enter)[binder](eventOut, leave);
         }
-        
+
         return this;
-        
+
     };
-    
+
     $.fn.tipsy.defaults = {
         className: null,
         delayIn: 0,
         title: 'title',
         trigger: 'hover'
     };
-    
+
     // Overwrite this method to provide options on a per-element basis.
     // For example, you could store the gravity in a 'tipsy-gravity' attribute:
     // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
     $.fn.tipsy.elementOptions = function(ele, options) {
         return $.metadata ? $.extend({}, options, $(ele).metadata()) : options;
     };
-    
+
     $.fn.tipsy.autoNS = function() {
         return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n';
     };
-    
+
     $.fn.tipsy.autoWE = function() {
         return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w';
     };
-    
+
 })(jQuery);
index 04919d3..f300672 100644 (file)
                                dataType: 'json'
                        }
                },
-               tokenCache = {};
+               // Keyed by ajax url and symbolic name for the individual request
+               deferreds = {};
+
+       // Pre-populate with fake ajax deferreds to save http requests for tokens
+       // we already have on the page via the user.tokens module (bug 34733).
+       deferreds[ defaultOptions.ajax.url ] = {};
+       $.each( mw.user.tokens.get(), function ( key, value ) {
+               // This requires #getToken to use the same key as user.tokens.
+               // Format: token-type + "Token" (eg. editToken, patrolToken, watchToken).
+               deferreds[ defaultOptions.ajax.url ][ key ] = $.Deferred()
+                       .resolve( value )
+                       .promise( { abort: function () {} } );
+       } );
 
        /**
         * Constructor to create an object to interact with the API of a particular MediaWiki server.
                 * @since 1.22
                 */
                postWithToken: function ( tokenType, params ) {
-                       var api = this, hasOwn = tokenCache.hasOwnProperty;
-                       if ( hasOwn.call( tokenCache, tokenType ) && tokenCache[tokenType] !== undefined ) {
-                               params.token = tokenCache[tokenType];
+                       var api = this;
+
+                       return api.getToken( tokenType ).then( function ( token ) {
+                               params.token = token;
                                return api.post( params ).then(
+                                       // If no error, return to caller as-is
                                        null,
+                                       // Error handler
                                        function ( code ) {
                                                if ( code === 'badtoken' ) {
-                                                       // force a new token, clear any old one
-                                                       tokenCache[tokenType] = params.token = undefined;
-                                                       return api.post( params );
+                                                       // Clear from cache
+                                                       deferreds[ this.defaults.ajax.url ][ tokenType + 'Token' ] =
+                                                               params.token = undefined;
+
+                                                       // Try again, once
+                                                       return api.getToken( tokenType ).then( function ( token ) {
+                                                               params.token = token;
+                                                               return api.post( params );
+                                                       } );
                                                }
-                                               // Pass the promise forward, so the caller gets error codes
+
+                                               // Different error, pass on to let caller handle the error code
                                                return this;
                                        }
                                );
-                       } else {
-                               return api.getToken( tokenType ).then( function ( token ) {
-                                       tokenCache[tokenType] = params.token = token;
-                                       return api.post( params );
-                               } );
-                       }
+                       } );
                },
 
                /**
-                * Api helper to grab any token.
+                * Get a token for a certain action from the API.
                 *
-                * @param {string} type Token type.
+                * @param {string} type Token type
                 * @return {jQuery.Promise}
                 * @return {Function} return.done
                 * @return {string} return.done.token Received token.
                 */
                getToken: function ( type ) {
                        var apiPromise,
+                               deferredGroup = deferreds[ this.defaults.ajax.url ],
+                               d = deferredGroup && deferredGroup[ type + 'Token' ];
+
+                       if ( !d ) {
                                d = $.Deferred();
 
-                       apiPromise = this.get( {
-                                       action: 'tokens',
-                                       type: type
-                               } )
-                               .done( function ( data ) {
-                                       // If token type is not available for this user,
-                                       // key '...token' is missing or can contain Boolean false
-                                       if ( data.tokens && data.tokens[type + 'token'] ) {
-                                               d.resolve( data.tokens[type + 'token'] );
-                                       } else {
-                                               d.reject( 'token-missing', data );
-                                       }
-                               } )
-                               .fail( d.reject );
+                               apiPromise = this.get( { action: 'tokens', type: type } )
+                                       .done( function ( data ) {
+                                               // If token type is not available for this user,
+                                               // key '...token' is missing or can contain Boolean false
+                                               if ( data.tokens && data.tokens[type + 'token'] ) {
+                                                       d.resolve( data.tokens[type + 'token'] );
+                                               } else {
+                                                       d.reject( 'token-missing', data );
+                                               }
+                                       } )
+                                       .fail( d.reject );
+
+                               // Attach abort handler
+                               d.abort = apiPromise.abort;
+
+                               // Store deferred now so that we can use this again even if it isn't ready yet
+                               if ( !deferredGroup ) {
+                                       deferredGroup = deferreds[ this.defaults.ajax.url ] = {};
+                               }
+                               deferredGroup[ type + 'Token' ] = d;
+                       }
 
-                       return d.promise( { abort: apiPromise.abort } );
+                       return d.promise( { abort: d.abort } );
                }
        };
 
index 1564712..0998d4c 100644 (file)
@@ -36,13 +36,16 @@ div.mw-createacct-benefits-container h2 {
        background: url(images/icon-contributors.png) no-repeat left center;
 }
 
-/* Special font for numbers in benefits*/
-div.mw-number-text h3 {
+/*
+ * Special font for numbers in benefits, same as Vector's @content-heading-font-family.
+ * Needs an ID so that it's more specific than Vector's div#content h3.
+ */
+#bodyContent div.mw-number-text h3 {
        top: 0;
        margin: 0;
        padding: 0;
        color: #252525;
-       font-family: 'Georgia', serif;
+       font-family: "Linux Libertine", Georgia, Times, serif;
        font-weight: normal;
        font-size: 2.2em;
        line-height: 1.2;
index 5c6aa6a..6157fa2 100644 (file)
        width: @defaultFormWidth;
 
        // Immediate divs in a vform are block and spaced-out.
+       // XXX: We shouldn't depend on the tag name here...
        & > div {
                display: block;
                margin: 0 0 15px 0;
                padding: 0;
                width: 100%;
+       }
 
-               // MW currently doesn't use the type attribute everywhere on inputs.
-               input,
-               .mw-ui-button {
-                       display: block;
-                       .box-sizing(border-box);
-                       margin: 0;
-                       width: 100%;
-               }
-
-               // We exclude these because they'll generally use mw-ui-button.
-               // Otherwise, we'll unintentionally override that.
-               input:not([type=button]):not([type=submit]):not([type=file]), {
-                       .agora-field-styling(); // mixins/forms.less
-               }
-
-               label {
-                       display: block;
-                       .box-sizing(border-box);
-                       .agora-label-styling();
-                       width: auto;
-                       margin: 0 0 0.2em;
-                       padding: 0;
-               }
-
-               // Override input styling just for checkboxes and radio inputs.
-               input[type="checkbox"],
-               input[type="radio"] {
-                       display: inline;
-                       .box-sizing(content-box);
-                       width: auto;
-               }
+       // MW currently doesn't use the type attribute everywhere on inputs.
+       input,
+       .mw-ui-button {
+               display: block;
+               .box-sizing(border-box);
+               margin: 0;
+               width: 100%;
+       }
 
+       // We exclude these because they'll generally use mw-ui-button.
+       // Otherwise, we'll unintentionally override that.
+       input:not([type=button]):not([type=submit]):not([type=file]), {
+               .agora-field-styling(); // mixins/forms.less
        }
 
+       label {
+               display: block;
+               .box-sizing(border-box);
+               .agora-label-styling();
+               width: auto;
+               margin: 0 0 0.2em;
+               padding: 0;
+       }
+
+       // Override input styling just for checkboxes and radio inputs.
+       input[type="checkbox"],
+       input[type="radio"] {
+               display: inline;
+               .box-sizing(content-box);
+               width: auto;
+       }
+
+
        // Styles for information boxes
        //
        // Regular HTMLForm uses .error class, some special pages like
 // You generally don't need to use this class on divs within an Agora
 // form container such as mw-ui-vform
 // XXX DRY: This repeats earlier styling, use an @include agora-div-styling ?
+// XXX: What is this even for?
 .mw-ui-vform-div {
        display: block;
        margin: 0 0 15px;
index ebb175b..2bbd8f0 100644 (file)
@@ -2,7 +2,7 @@
 @import "../../mixins/type";
 
 .mw-ui-vform,
-.mw-ui-vform > div input,
+.mw-ui-vform input,
 .mw-ui-input {
        .vector-type();
 }
index 1b183d9..f56f0d9 100644 (file)
 
                        paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length );
 
+                       paneTriggerBitDiv( 'profile', 'Profile', this.data.profile.length );
+
                        gitInfo = '';
                        if ( this.data.gitRevision !== false ) {
                                gitInfo = '(' + this.data.gitRevision.substring( 0, 7 ) + ')';
                                querylist: this.buildQueryTable(),
                                debuglog: this.buildDebugLogTable(),
                                request: this.buildRequestPane(),
-                               includes: this.buildIncludesPane()
+                               includes: this.buildIncludesPane(),
+                               profile: this.buildProfilePane()
                        };
 
                        for ( id in panes ) {
                        }
 
                        return $table;
+               },
+
+               buildProfilePane: function () {
+                       return mw.Debug.profile.init();
                }
        };
 
diff --git a/resources/mediawiki/mediawiki.debug.profile.css b/resources/mediawiki/mediawiki.debug.profile.css
new file mode 100644 (file)
index 0000000..bf49d1a
--- /dev/null
@@ -0,0 +1,46 @@
+
+.mw-debug-profile-tipsy .tipsy-inner {
+       /* undo max-width from vector on .tipsy-inner */
+       max-width: none;
+       /* needed for some browsers to provide space for the scrollbar without wrapping text */
+       min-width: 100%;
+       max-height: 150px;
+       overflow-y: auto;
+}
+
+.mw-debug-profile-underline {
+       stroke-width: 1;
+       stroke: #dfdfdf;
+}
+
+.mw-debug-profile-period {
+       fill: red;
+}
+
+/* connecting line between endpoints on long events */
+.mw-debug-profile-period line {
+       stroke: red;
+       stroke-width: 2;
+}
+
+.mw-debug-profile-tipsy,
+.mw-debug-profile-timeline text {
+       color: #444;
+       fill: #444;
+       /* using em's causes the two locations to have different sizes */
+       font-size: 12px;
+       font-family: sans-serif;
+}
+
+.mw-debug-profile-meta,
+.mw-debug-profile-timeline tspan {
+       /* using em's causes the two locations to have different sizes */
+       font-size: 10px;
+}
+
+.mw-debug-profile-no-data {
+       text-align: center;
+       padding-top: 5em;
+       font-weight: bold;
+       font-size: 1.2em;
+}
diff --git a/resources/mediawiki/mediawiki.debug.profile.js b/resources/mediawiki/mediawiki.debug.profile.js
new file mode 100644 (file)
index 0000000..f6bc81d
--- /dev/null
@@ -0,0 +1,546 @@
+/*!
+ * JavaScript for the debug toolbar profiler, enabled through $wgDebugToolbar
+ * and StartProfiler.php.
+ *
+ * @author Erik Bernhardson
+ * @since 1.23
+ */
+
+( function ( mw, $ ) {
+       'use strict';
+
+       /**
+        * @singleton
+        * @class mw.Debug.profile
+        */
+       var profile = mw.Debug.profile = {
+               /**
+                * Object containing data for the debug toolbar
+                *
+                * @property ProfileData
+                */
+               data: null,
+
+               /**
+                * @property DOMElement
+                */
+               container: null,
+
+               /**
+                * Initializes the profiling pane.
+                */
+               init: function ( data, width, mergeThresholdPx, dropThresholdPx ) {
+                       data = data || mw.config.get( 'debugInfo' ).profile;
+                       profile.width = width || $(window).width() - 20;
+                       // merge events from same pixel(some events are very granular)
+                       mergeThresholdPx = mergeThresholdPx || 2;
+                       // only drop events if requested
+                       dropThresholdPx = dropThresholdPx || 0;
+
+                       if ( !Array.prototype.map || !Array.prototype.reduce || !Array.prototype.filter ) {
+                               profile.container = profile.buildRequiresES5();
+                       } else if ( data.length === 0 ) {
+                               profile.container = profile.buildNoData();
+                       } else {
+                               // generate a flyout
+                               profile.data = new ProfileData( data, profile.width, mergeThresholdPx, dropThresholdPx );
+                               // draw it
+                               profile.container = profile.buildSvg( profile.container );
+                               profile.attachFlyout();
+                       }
+
+                       return profile.container;
+               },
+
+               buildRequiresES5: function () {
+                       return $( '<div>' )
+                               .text( 'An ES5 compatible javascript engine is required for the profile visualization.' )
+                               .get( 0 );
+               },
+
+               buildNoData: function () {
+                       return $( '<div>' ).addClass( 'mw-debug-profile-no-data' )
+                               .text( 'No events recorded, ensure profiling is enabled in StartProfiler.php.' )
+                               .get( 0 );
+               },
+
+               /**
+                * Creates DOM nodes appropriately namespaced for SVG.
+                *
+                * @param string tag to create
+                * @return DOMElement
+                */
+               createSvgElement: document.createElementNS.bind( document, 'http://www.w3.org/2000/svg' ),
+
+               /**
+                * @param DOMElement|undefined
+                */
+               buildSvg: function ( node ) {
+                       var container, group, i, g,
+                               timespan = profile.data.timespan,
+                               gapPerEvent = 38,
+                               space = 10.5,
+                               currentHeight = space,
+                               totalHeight = 0;
+
+                       profile.ratio = ( profile.width - space * 2 ) / ( timespan.end - timespan.start );
+                       totalHeight += gapPerEvent * profile.data.groups.length;
+
+                       if ( node ) {
+                               $( node ).empty();
+                       } else {
+                               node = profile.createSvgElement( 'svg' );
+                               node.setAttribute( 'version', '1.2' );
+                               node.setAttribute( 'baseProfile', 'tiny' );
+                       }
+                       node.style.height = totalHeight;
+                       node.style.width = profile.width;
+
+                       // use a container that can be transformed
+                       container = profile.createSvgElement( 'g' );
+                       node.appendChild( container );
+
+                       for ( i = 0; i < profile.data.groups.length; i++ ) {
+                               group = profile.data.groups[i];
+                               g = profile.buildTimeline( group );
+
+                               g.setAttribute( 'transform', 'translate( 0 ' + currentHeight + ' )' );
+                               container.appendChild( g );
+
+                               currentHeight += gapPerEvent;
+                       }
+
+                       return node;
+               },
+
+               /**
+                * @param Object group of periods to transform into graphics
+                */
+               buildTimeline: function ( group ) {
+                       var text, tspan, line, i,
+                               sum = group.timespan.sum,
+                               ms = ' ~ ' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms',
+                               timeline = profile.createSvgElement( 'g' );
+
+                       timeline.setAttribute( 'class', 'mw-debug-profile-timeline' );
+
+                       // draw label
+                       text = profile.createSvgElement( 'text' );
+                       text.setAttribute( 'x', profile.xCoord( group.timespan.start ) );
+                       text.setAttribute( 'y', 0 );
+                       text.textContent = group.name;
+                       timeline.appendChild( text );
+
+                       // draw metadata
+                       tspan = profile.createSvgElement( 'tspan' );
+                       tspan.textContent = ms;
+                       text.appendChild( tspan );
+
+                       // draw timeline periods
+                       for ( i = 0; i < group.periods.length; i++ ) {
+                               timeline.appendChild( profile.buildPeriod( group.periods[i] ) );
+                       }
+
+                       // full-width line under each timeline
+                       line = profile.createSvgElement( 'line' );
+                       line.setAttribute( 'class', 'mw-debug-profile-underline' );
+                       line.setAttribute( 'x1', 0 );
+                       line.setAttribute( 'y1', 28 );
+                       line.setAttribute( 'x2', profile.width );
+                       line.setAttribute( 'y2', 28 );
+                       timeline.appendChild( line );
+
+                       return timeline;
+               },
+
+               /**
+                * @param Object period to transform into graphics
+                */
+               buildPeriod: function ( period ) {
+                       var node,
+                               head = profile.xCoord( period.start ),
+                               tail = profile.xCoord( period.end ),
+                               g = profile.createSvgElement( 'g' );
+
+                       g.setAttribute( 'class', 'mw-debug-profile-period' );
+                       $( g ).data( 'period', period );
+
+                       if ( head + 16 > tail ) {
+                               node = profile.createSvgElement( 'rect' );
+                               node.setAttribute( 'x', head );
+                               node.setAttribute( 'y', 8 );
+                               node.setAttribute( 'width', 2 );
+                               node.setAttribute( 'height', 9 );
+                               g.appendChild( node );
+
+                               node = profile.createSvgElement( 'rect' );
+                               node.setAttribute( 'x', head );
+                               node.setAttribute( 'y', 8 );
+                               node.setAttribute( 'width', ( period.end - period.start ) * profile.ratio || 2 );
+                               node.setAttribute( 'height', 6 );
+                               g.appendChild( node );
+                       } else {
+                               node = profile.createSvgElement( 'polygon' );
+                               node.setAttribute( 'points', pointList( [
+                                       [ head, 8 ],
+                                       [ head, 19 ],
+                                       [ head + 8, 8 ],
+                                       [ head, 8]
+                               ] ) );
+                               g.appendChild( node );
+
+                               node = profile.createSvgElement( 'polygon' );
+                               node.setAttribute( 'points', pointList( [
+                                       [ tail, 8 ],
+                                       [ tail, 19 ],
+                                       [ tail - 8, 8 ],
+                                       [ tail, 8 ],
+                               ] ) );
+                               g.appendChild( node );
+
+                               node = profile.createSvgElement( 'line' );
+                               node.setAttribute( 'x1', head );
+                               node.setAttribute( 'y1', 9 );
+                               node.setAttribute( 'x2', tail );
+                               node.setAttribute( 'y2', 9 );
+                               g.appendChild( node );
+                       }
+
+                       return g;
+               },
+
+               /**
+                * @param Object
+                */
+               buildFlyout: function ( period ) {
+                       var contained, sum, ms, mem, i,
+                               node = $( '<div>' );
+
+                       for ( i = 0; i < period.contained.length; i++ ) {
+                               contained = period.contained[i];
+                               sum = contained.end - contained.start;
+                               ms = '' + ( sum < 1 ? sum.toFixed( 2 ) : sum.toFixed( 0 ) ) + ' ms';
+                               mem = formatBytes( contained.memory );
+
+                               $( '<div>' ).text( contained.source.name )
+                                       .append( $( '<span>' ).text( ' ~ ' + ms + ' / ' + mem ).addClass( 'mw-debug-profile-meta' ) )
+                                       .appendTo( node );
+                       }
+
+                       return node;
+               },
+
+               /**
+                * Attach a hover flyout to all .mw-debug-profile-period groups.
+                */
+               attachFlyout: function () {
+                       // for some reason addClass and removeClass from jQuery
+                       // arn't working on svg elements in chrome <= 33.0 (possibly more)
+                       var $container = $( profile.container ),
+                               addClass = function ( node, value ) {
+                                       var current = node.getAttribute( 'class' ),
+                                               list = current ? current.split( ' ' ) : false,
+                                               idx = list ? list.indexOf( value ) : -1;
+
+                                       if ( idx === -1 ) {
+                                               node.setAttribute( 'class', current ? ( current + ' ' + value ) : value );
+                                       }
+                               },
+                               removeClass = function ( node, value ) {
+                                       var current = node.getAttribute( 'class' ),
+                                               list = current ? current.split( ' ' ) : false,
+                                               idx = list ? list.indexOf( value ) : -1;
+
+                                       if ( idx !== -1 ) {
+                                               list.splice( idx, 1 );
+                                               node.setAttribute( 'class', list.join( ' ' ) );
+                                       }
+                               },
+                               // hide all tipsy flyouts
+                               hide = function () {
+                                       $container.find( '.mw-debug-profile-period.tipsy-visible' )
+                                               .each( function () {
+                                                       removeClass( this, 'tipsy-visible' );
+                                                       $( this ).tipsy( 'hide' );
+                                               } );
+                               };
+
+                       $container.find( '.mw-debug-profile-period' ).tipsy( {
+                               fade: true,
+                               gravity: function () {
+                                       return $.fn.tipsy.autoNS.call( this )
+                                               + $.fn.tipsy.autoWE.call( this );
+                               },
+                               className: 'mw-debug-profile-tipsy',
+                               center: false,
+                               html: true,
+                               trigger: 'manual',
+                               title: function () {
+                                       return profile.buildFlyout( $( this ).data( 'period' ) ).html();
+                               },
+                       } ).on( 'mouseenter', function () {
+                               hide();
+                               addClass( this, 'tipsy-visible' );
+                               $( this ).tipsy( 'show' );
+                       } );
+
+                       $container.on( 'mouseleave', function ( event ) {
+                               var $from = $( event.relatedTarget ),
+                                       $to = $( event.target );
+                               // only close the tipsy if we are not
+                               if ( $from.closest( '.tipsy' ).length === 0 &&
+                                       $to.closest( '.tipsy' ).length === 0 &&
+                                       $to.get( 0 ).namespaceURI !== 'http://www.w4.org/2000/svg'
+                               ) {
+                                       hide();
+                               }
+                       } ).on( 'click', function () {
+                               // convenience method for closing
+                               hide();
+                       } );
+               },
+
+               /**
+                * @return number the x co-ordinate for the specified timestamp
+                */
+               xCoord: function ( msTimestamp ) {
+                       return ( msTimestamp - profile.data.timespan.start ) * profile.ratio;
+               },
+       };
+
+       function ProfileData( data, width, mergeThresholdPx, dropThresholdPx ) {
+               // validate input data
+               this.data = data.map( function ( event ) {
+                       event.periods = event.periods.filter( function ( period ) {
+                               return period.start && period.end
+                                       && period.start < period.end
+                                       // period start must be a reasonable ms timestamp
+                                       && period.start > 1000000;
+                       } );
+                       return event;
+               } ).filter( function ( event ) {
+                       return event.name && event.periods.length > 0;
+               } );
+
+               // start and end time of the data
+               this.timespan = this.data.reduce( function ( result, event ) {
+                       return event.periods.reduce( periodMinMax, result );
+               }, periodMinMax.initial() );
+
+               // transform input data
+               this.groups = this.collate( width, mergeThresholdPx, dropThresholdPx );
+
+               return this;
+       }
+
+       /**
+        * There are too many unique events to display a line for each,
+        * so this does a basic grouping.
+        */
+       ProfileData.groupOf = function ( label ) {
+               var pos, prefix = 'Profile section ended by close(): ';
+               if ( label.indexOf( prefix ) === 0 ) {
+                       label = label.substring( prefix.length );
+               }
+
+               pos = [ '::', ':', '-' ].reduce( function ( result, separator ) {
+                       var pos = label.indexOf( separator );
+                       if ( pos === -1 ) {
+                               return result;
+                       } else if ( result === -1 ) {
+                               return pos;
+                       } else {
+                               return Math.min( result, pos );
+                       }
+               }, -1 );
+
+               if ( pos === -1 ) {
+                       return label;
+               } else {
+                       return label.substring( 0, pos );
+               }
+       };
+
+       /**
+        * @return Array list of objects with `name` and `events` keys
+        */
+       ProfileData.groupEvents = function ( events ) {
+               var group, i,
+                       groups = {};
+
+               // Group events together
+               for ( i = events.length - 1; i >= 0; i-- ) {
+                       group = ProfileData.groupOf( events[i].name );
+                       if ( groups[group] ) {
+                               groups[group].push( events[i] );
+                       } else {
+                               groups[group] = [events[i]];
+                       }
+               }
+
+               // Return an array of groups
+               return Object.keys( groups ).map( function ( group ) {
+                       return {
+                               name: group,
+                               events: groups[group],
+                       };
+               } );
+       };
+
+       ProfileData.periodSorter = function ( a, b ) {
+               if ( a.start === b.start ) {
+                       return a.end - b.end;
+               }
+               return a.start - b.start;
+       };
+
+       ProfileData.genMergePeriodReducer = function ( mergeThresholdMs ) {
+               return function ( result, period ) {
+                       if ( result.length === 0 ) {
+                               // period is first result
+                               return [{
+                                       start: period.start,
+                                       end: period.end,
+                                       contained: [period],
+                               }];
+                       }
+                       var last = result[result.length - 1];
+                       if ( period.end < last.end ) {
+                               // end is contained within previous
+                               result[result.length - 1].contained.push( period );
+                       } else if ( period.start - mergeThresholdMs < last.end ) {
+                               // neighbors within merging distance
+                               result[result.length - 1].end = period.end;
+                               result[result.length - 1].contained.push( period );
+                       } else {
+                               // period is next result
+                               result.push({
+                                       start: period.start,
+                                       end: period.end,
+                                       contained: [period],
+                               });
+                       }
+                       return result;
+               };
+       };
+
+       /**
+        * Collect all periods from the grouped events and apply merge and
+        * drop transformations
+        */
+       ProfileData.extractPeriods = function ( events, mergeThresholdMs, dropThresholdMs ) {
+               // collect the periods from all events
+               return events.reduce( function ( result, event ) {
+                               if ( !event.periods.length ) {
+                                       return result;
+                               }
+                               result.push.apply( result, event.periods.map( function ( period ) {
+                                       // maintain link from period to event
+                                       period.source = event;
+                                       return period;
+                               } ) );
+                               return result;
+                       }, [] )
+                       // sort combined periods
+                       .sort( ProfileData.periodSorter )
+                       // Apply merge threshold. Original periods
+                       // are maintained in the `contained` property
+                       .reduce( ProfileData.genMergePeriodReducer( mergeThresholdMs ), [] )
+                       // Apply drop threshold
+                       .filter( function ( period ) {
+                               return period.end - period.start > dropThresholdMs;
+                       } );
+       };
+
+       /**
+        * runs a callback on all periods in the group.  Only valid after
+        * groups.periods[0..n].contained are populated. This runs against
+        * un-transformed data and is better suited to summing or other
+        * stat collection
+        */
+       ProfileData.reducePeriods = function ( group, callback, result ) {
+               return group.periods.reduce( function ( result, period ) {
+                       return period.contained.reduce( callback, result );
+               }, result );
+       };
+
+       /**
+        * Transforms this.data grouping by labels, merging neighboring
+        * events in the groups, and drops events and groups below the
+        * display threshold. Groups are returned sorted by starting time.
+        */
+       ProfileData.prototype.collate = function ( width, mergeThresholdPx, dropThresholdPx ) {
+               // ms to pixel ratio
+               var ratio = ( this.timespan.end - this.timespan.start ) / width,
+                       // transform thresholds to ms
+                       mergeThresholdMs = mergeThresholdPx * ratio,
+                       dropThresholdMs = dropThresholdPx * ratio;
+
+               return ProfileData.groupEvents( this.data )
+                       // generate data about the grouped events
+                       .map( function ( group ) {
+                               // Cleaned periods from all events
+                               group.periods = ProfileData.extractPeriods( group.events, mergeThresholdMs, dropThresholdMs );
+                               // min and max timestamp per group
+                               group.timespan = ProfileData.reducePeriods( group, periodMinMax, periodMinMax.initial() );
+                               // ms from first call to end of last call
+                               group.timespan.length = group.timespan.end - group.timespan.start;
+                               // collect the un-transformed periods
+                               group.timespan.sum = ProfileData.reducePeriods( group, function ( result, period ) {
+                                               result.push( period );
+                                               return result;
+                                       }, [] )
+                                       // sort by start time
+                                       .sort( ProfileData.periodSorter )
+                                       // merge overlapping
+                                       .reduce( ProfileData.genMergePeriodReducer( 0 ), [] )
+                                       // sum
+                                       .reduce( function ( result, period ) {
+                                               return result + period.end - period.start;
+                                       }, 0 );
+
+                               return group;
+                       }, this )
+                       // remove groups that have had all their periods filtered
+                       .filter( function ( group ) {
+                               return group.periods.length > 0;
+                       } )
+                       // sort events by first start
+                       .sort( function ( a, b ) {
+                               return ProfileData.periodSorter( a.timespan, b.timespan );
+                       } );
+       };
+
+       // reducer to find edges of period array
+       function periodMinMax( result, period ) {
+               if ( period.start < result.start ) {
+                       result.start = period.start;
+               }
+               if ( period.end > result.end ) {
+                       result.end = period.end;
+               }
+               return result;
+       }
+
+       periodMinMax.initial = function () {
+               return { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY };
+       };
+
+       function formatBytes( bytes ) {
+               var i, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+               if ( bytes === 0 ) {
+                       return '0 Bytes';
+               }
+               i = parseInt( Math.floor( Math.log( bytes ) / Math.log( 1024 ) ), 10 );
+               return Math.round( bytes / Math.pow( 1024, i ), 2 ) + ' ' + sizes[i];
+       }
+
+       // turns a 2d array into a point list for svg
+       // polygon points attribute
+       // ex: [[1,2],[3,4],[4,2]] = '1,2 3,4 4,2'
+       function pointList( pairs ) {
+               return pairs.map( function ( pair ) {
+                       return pair.join( ',' );
+               } ).join( ' ' );
+       }
+}( mediaWiki, jQuery ) );
index d8a17f6..885fd3d 100644 (file)
@@ -2218,12 +2218,15 @@ var mw = ( function ( $, undefined ) {
 
                        return {
                                /**
-                                * Escape a string for HTML. Converts special characters to HTML entities.
+                                * Escape a string for HTML.
+                                *
+                                * Converts special characters to HTML entities.
                                 *
                                 *     mw.html.escape( '< > \' & "' );
                                 *     // Returns &lt; &gt; &#039; &amp; &quot;
                                 *
                                 * @param {string} s The string to escape
+                                * @return {string} HTML
                                 */
                                escape: function ( s ) {
                                        return s.replace( /['"<>&]/g, escapeCallback );
@@ -2242,6 +2245,7 @@ var mw = ( function ( $, undefined ) {
                                 *  - this.Cdata: The value attribute is included, and an exception is
                                 *   thrown if it contains an illegal ETAGO delimiter.
                                 *   See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
+                                * @return {string} HTML
                                 */
                                element: function ( name, attrs, contents ) {
                                        var v, attrName, s = '<' + name;
index 939719b..6006244 100644 (file)
@@ -8,6 +8,6 @@
         ]
     },
     "ooui-dialog-action-close": "Затваряне",
-    "ooui-outline-control-remove": "Ð\9fÑ\80емаÑ\85ване Ð½Ð° Ð¾Ð±ÐµÐºÑ\82и",
+    "ooui-outline-control-remove": "Ð\9fÑ\80емаÑ\85ване Ð½Ð° Ð¾Ð±ÐµÐºÑ\82а",
     "ooui-toolbar-more": "Още"
-}
\ No newline at end of file
+}
index 0f47587..ef60093 100644 (file)
@@ -7,5 +7,6 @@
     "ooui-dialog-action-close": "Zacyniś",
     "ooui-outline-control-move-down": "Element dołoj pśesunuś",
     "ooui-outline-control-move-up": "Element górjej pśesunuś",
+    "ooui-outline-control-remove": "Zapisk wótpóraś",
     "ooui-toolbar-more": "Wěcej"
-}
\ No newline at end of file
+}
index 66051f1..d1ef8b2 100644 (file)
@@ -12,7 +12,8 @@
         ]
     },
     "ooui-dialog-action-close": "Κλείσιμο",
-    "ooui-outline-control-move-down": "Μετακίνηση προς τα κάτω",
-    "ooui-outline-control-move-up": "Μετακίνηση προς τα πάνω",
+    "ooui-outline-control-move-down": "Μετακίνηση στοιχείου προς τα κάτω",
+    "ooui-outline-control-move-up": "Μετακίνηση στοιχείου προς τα επάνω",
+    "ooui-outline-control-remove": "Αφαίρεση στοιχείου",
     "ooui-toolbar-more": "Περισσότερα"
-}
\ No newline at end of file
+}
index eb53138..a3be749 100644 (file)
@@ -21,4 +21,4 @@
     "ooui-outline-control-move-up": "Mover arriba",
     "ooui-outline-control-remove": "Eliminar elemento",
     "ooui-toolbar-more": "Más"
-}
\ No newline at end of file
+}
index aa780e4..f0ddc39 100644 (file)
@@ -10,4 +10,4 @@
     "ooui-outline-control-move-up": "Liiguta üksust ülespoole",
     "ooui-outline-control-remove": "Eemalda üksus",
     "ooui-toolbar-more": "Veel"
-}
\ No newline at end of file
+}
index 3c41b3e..ae895d1 100644 (file)
@@ -13,4 +13,4 @@
     "ooui-outline-control-move-up": "प्रविष्टि ऊपर ले जाएँ",
     "ooui-outline-control-remove": "आइटम हटाएँ",
     "ooui-toolbar-more": "अधिक"
-}
\ No newline at end of file
+}
index df37fe3..7213dad 100644 (file)
@@ -9,4 +9,4 @@
     "ooui-outline-control-move-up": "Элементті жоғары жылжыту",
     "ooui-outline-control-remove": "Элементті алып тастау",
     "ooui-toolbar-more": "толығырақ"
-}
\ No newline at end of file
+}
index 21aef50..4f78aaa 100644 (file)
@@ -8,5 +8,6 @@
     "ooui-dialog-action-close": "Tutup",
     "ooui-outline-control-move-down": "Alihkan perkara ke bawah",
     "ooui-outline-control-move-up": "Alihkan perkara ke atas",
-    "ooui-toolbar-more": "Lagi"
-}
\ No newline at end of file
+    "ooui-outline-control-remove": "Buang perkara",
+    "ooui-toolbar-more": "Selebihnya"
+}
index 33ff886..2cb7330 100644 (file)
@@ -21,4 +21,4 @@
     "ooui-outline-control-move-up": "Przenieś wyżej",
     "ooui-outline-control-remove": "Usuń element",
     "ooui-toolbar-more": "Więcej"
-}
\ No newline at end of file
+}
index d98b0e4..4442528 100644 (file)
@@ -10,4 +10,4 @@
     "ooui-outline-control-move-up": "Qallawata huqariy",
     "ooui-outline-control-remove": "P'anqa sutikunata qichuy",
     "ooui-toolbar-more": "Aswan"
-}
\ No newline at end of file
+}
index 58e8ebe..19d9c7e 100644 (file)
@@ -21,6 +21,6 @@
     "ooui-dialog-action-close": "关闭",
     "ooui-outline-control-move-down": "下移项",
     "ooui-outline-control-move-up": "上移项",
-    "ooui-outline-control-remove": "除项",
+    "ooui-outline-control-remove": "除项",
     "ooui-toolbar-more": "更多"
-}
\ No newline at end of file
+}
index 091f4fc..00cd433 100644 (file)
 .oo-ui-buttonedElement-framed.oo-ui-widget-disabled .oo-ui-buttonedElement-button.oo-ui-buttonedElement-pressed {
   color: #333;
   background: #eee;
+  border-color: #ccc;
   opacity: 0.5;
   box-shadow: none;
 }
   background-color: #e1f3ff;
 }
 
-.oo-ui-optionWidget-selected {
+.oo-ui-selectWidget-depressed .oo-ui-optionWidget-selected {
+  background-color: #a7dcff;
+}
+
+.oo-ui-selectWidget-pressed .oo-ui-optionWidget-pressed {
   background-color: #a7dcff;
 }
 
 }
 
 .oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected,
+.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
 .oo-ui-buttonOptionWidget.oo-ui-optionWidget-highlighted {
   background-color: transparent;
 }
   left: 4em;
 }
 
-.oo-ui-outlineItemWidget.oo-ui-optionWidget-selected {
+.oo-ui-selectWidget-depressed .oo-ui-outlineItemWidget.oo-ui-optionWidget-selected {
   text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
   background-color: #a7dcff;
 }
index 937ff33..1565cbb 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (23fb1b6144)
+ * OOjs UI v0.1.0-pre (eaa1b7f06d)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: Thu Mar 27 2014 14:49:30 GMT-0700 (PDT)
+ * Date: Thu Apr 03 2014 16:56:21 GMT-0700 (PDT)
  */
 ( function ( OO ) {
 
@@ -237,13 +237,11 @@ OO.ui.Element.getJQuery = function ( context, frame ) {
  *
  * @static
  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
- * @return {HTMLDocument} Document object
- * @throws {Error} If context is invalid
+ * @return {HTMLDocument|null} Document object
  */
 OO.ui.Element.getDocument = function ( obj ) {
-       var doc =
-               // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
-               ( obj[0] && obj[0].ownerDocument ) ||
+       // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
+       return ( obj[0] && obj[0].ownerDocument ) ||
                // Empty jQuery selections might have a context
                obj.context ||
                // HTMLElement
@@ -251,13 +249,8 @@ OO.ui.Element.getDocument = function ( obj ) {
                // Window
                obj.document ||
                // HTMLDocument
-               ( obj.nodeType === 9 && obj );
-
-       if ( doc ) {
-               return doc;
-       }
-
-       throw new Error( 'Invalid context' );
+               ( obj.nodeType === 9 && obj ) ||
+               null;
 };
 
 /**
@@ -533,6 +526,14 @@ OO.ui.Element.prototype.getTagName = function () {
        return this.constructor.static.tagName;
 };
 
+/**
+ * Check if the element is attached to the DOM
+ * @return {boolean} The element is attached to the DOM
+ */
+OO.ui.Element.prototype.isElementAttached = function () {
+       return $.contains( this.getElementDocument(), this.$element[0] );
+};
+
 /**
  * Get the DOM document.
  *
@@ -1787,7 +1788,7 @@ OO.ui.Widget.prototype.updateDisabled = function () {
 /**
  * Set the disabled state of the widget.
  *
- * This should probably change the widgets's appearance and prevent it from being used.
+ * This should probably change the widgets' appearance and prevent it from being used.
  *
  * @param {boolean} disabled Disable widget
  * @chainable
@@ -3231,9 +3232,8 @@ OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
                                                        }
                                                }
                                        }
-                               }
                                // Include tools with matching name and exclude already used tools
-                               else if ( item.name && ( !used || !used[item.name] ) ) {
+                               else if ( item.name && ( !used || !used[item.name] ) ) {
                                        names.push( item.name );
                                        if ( used ) {
                                                used[item.name] = true;
@@ -3268,7 +3268,9 @@ OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
  */
 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
        // Configuration initialization
-       config = config || {};
+       config = $.extend( true, {
+               'aggregations': { 'disable': 'itemDisable' }
+       }, config );
 
        // Parent constructor
        OO.ui.ToolGroup.super.call( this, config );
@@ -3280,6 +3282,7 @@ OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
        this.toolbar = toolbar;
        this.tools = {};
        this.pressed = null;
+       this.autoDisabled = false;
        this.include = config.include || [];
        this.exclude = config.exclude || [];
        this.promote = config.promote || [];
@@ -3294,6 +3297,7 @@ OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
                'mouseout': OO.ui.bind( this.onMouseOut, this )
        } );
        this.toolbar.getToolFactory().connect( this, { 'register': 'onToolFactoryRegister' } );
+       this.connect( this, { 'itemDisable': 'updateDisabled' } );
 
        // Initialization
        this.$group.addClass( 'oo-ui-toolGroup-tools' );
@@ -3335,8 +3339,43 @@ OO.ui.ToolGroup.static.titleTooltips = false;
  */
 OO.ui.ToolGroup.static.accelTooltips = false;
 
+/**
+ * Automatically disable the toolgroup when all tools are disabled
+ *
+ * @static
+ * @property {boolean}
+ * @inheritable
+ */
+OO.ui.ToolGroup.static.autoDisable = true;
+
 /* Methods */
 
+/**
+ * @inheritdoc
+ */
+OO.ui.ToolGroup.prototype.isDisabled = function () {
+       return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToolGroup.prototype.updateDisabled = function () {
+       var i, item, allDisabled = true;
+
+       if ( this.constructor.static.autoDisable ) {
+               for ( i = this.items.length - 1; i >= 0; i-- ) {
+                       item = this.items[i];
+                       if ( !item.isDisabled() ) {
+                               allDisabled = false;
+                               break;
+                       }
+               }
+               this.autoDisabled = allDisabled;
+       }
+       OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
+};
+
 /**
  * Handle mouse down events.
  *
@@ -3506,6 +3545,8 @@ OO.ui.ToolGroup.prototype.populate = function () {
        }
        // Re-add tools (moving existing ones to new locations)
        this.addItems( add );
+       // Disabled state may depend on items
+       this.updateDisabled();
 };
 
 /**
@@ -4668,6 +4709,18 @@ OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
 
 /* Methods */
 
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupToolGroup.prototype.setDisabled = function () {
+       // Parent method
+       OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
+
+       if ( this.isDisabled() && this.isElementAttached() ) {
+               this.setActive( false );
+       }
+};
+
 /**
  * Handle focus being lost.
  *
@@ -5766,6 +5819,7 @@ OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
        this.data = data;
        this.selected = false;
        this.highlighted = false;
+       this.pressed = false;
 
        // Initialization
        this.$element
@@ -5796,6 +5850,8 @@ OO.ui.OptionWidget.static.selectable = true;
 
 OO.ui.OptionWidget.static.highlightable = true;
 
+OO.ui.OptionWidget.static.pressable = true;
+
 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
 
 /* Methods */
@@ -5820,6 +5876,16 @@ OO.ui.OptionWidget.prototype.isHighlightable = function () {
        return this.constructor.static.highlightable && !this.disabled;
 };
 
+/**
+ * Check if option can be pressed.
+ *
+ * @method
+ * @returns {boolean} Item is pressable
+ */
+OO.ui.OptionWidget.prototype.isPressable = function () {
+       return this.constructor.static.pressable && !this.disabled;
+};
+
 /**
  * Check if option is selected.
  *
@@ -5840,6 +5906,16 @@ OO.ui.OptionWidget.prototype.isHighlighted = function () {
        return this.highlighted;
 };
 
+/**
+ * Check if option is pressed.
+ *
+ * @method
+ * @returns {boolean} Item is pressed
+ */
+OO.ui.OptionWidget.prototype.isPressed = function () {
+       return this.pressed;
+};
+
 /**
  * Set selected state.
  *
@@ -5881,20 +5957,45 @@ OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
        return this;
 };
 
+/**
+ * Set pressed state.
+ *
+ * @method
+ * @param {boolean} [state=false] Press option
+ * @chainable
+ */
+OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
+       if ( !this.disabled && this.constructor.static.pressable ) {
+               this.pressed = !!state;
+               if ( this.pressed ) {
+                       this.$element.addClass( 'oo-ui-optionWidget-pressed' );
+               } else {
+                       this.$element.removeClass( 'oo-ui-optionWidget-pressed' );
+               }
+       }
+       return this;
+};
+
 /**
  * Make the option's highlight flash.
  *
+ * While flashing, the visual style of the pressed state is removed if present.
+ *
  * @method
  * @param {Function} [done] Callback to execute when flash effect is complete.
  */
 OO.ui.OptionWidget.prototype.flash = function ( done ) {
        var $this = this.$element;
 
-       if ( !this.disabled && this.constructor.static.highlightable ) {
-               $this.removeClass( 'oo-ui-optionWidget-highlighted' );
+       if ( !this.disabled && this.constructor.static.pressable ) {
+               $this.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
                setTimeout( OO.ui.bind( function () {
                        $this.addClass( 'oo-ui-optionWidget-highlighted' );
                        if ( done ) {
+                               // Restore original classes
+                               $this
+                                       .toggleClass( 'oo-ui-optionWidget-highlighted', this.highlighted )
+                                       .toggleClass( 'oo-ui-optionWidget-pressed', this.pressed );
                                setTimeout( done, 100 );
                        }
                }, this ), 100 );
@@ -5947,7 +6048,7 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-selectWidget' );
+       this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
        if ( $.isArray( config.items ) ) {
                this.addItems( config.items );
        }
@@ -5969,6 +6070,11 @@ OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
  * @param {OO.ui.OptionWidget|null} item Highlighted item
  */
 
+/**
+ * @event press
+ * @param {OO.ui.OptionWidget|null} item Pressed item
+ */
+
 /**
  * @event select
  * @param {OO.ui.OptionWidget|null} item Selected item
@@ -6002,10 +6108,10 @@ OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
        var item;
 
        if ( !this.disabled && e.which === 1 ) {
-               this.pressed = true;
+               this.togglePressed( true );
                item = this.getTargetItem( e );
                if ( item && item.isSelectable() ) {
-                       this.intializeSelection( item );
+                       this.pressItem( item );
                        this.selecting = item;
                        this.$( this.$.context ).one( 'mouseup', OO.ui.bind( this.onMouseUp, this ) );
                }
@@ -6022,7 +6128,8 @@ OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
  */
 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
        var item;
-       this.pressed = false;
+
+       this.togglePressed( false );
        if ( !this.selecting ) {
                item = this.getTargetItem( e );
                if ( item && item.isSelectable() ) {
@@ -6030,9 +6137,11 @@ OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
                }
        }
        if ( !this.disabled && e.which === 1 && this.selecting ) {
+               this.pressItem( null );
                this.selectItem( this.selecting );
                this.selecting = null;
        }
+
        return false;
 };
 
@@ -6049,7 +6158,7 @@ OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
        if ( !this.disabled && this.pressed ) {
                item = this.getTargetItem( e );
                if ( item && item !== this.selecting && item.isSelectable() ) {
-                       this.intializeSelection( item );
+                       this.pressItem( item );
                        this.selecting = item;
                }
        }
@@ -6084,7 +6193,7 @@ OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
  */
 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
        if ( !this.disabled ) {
-               this.highlightItem();
+               this.highlightItem( null );
        }
        return false;
 };
@@ -6156,6 +6265,22 @@ OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
        return null;
 };
 
+/**
+ * Toggle pressed state.
+ *
+ * @param {boolean} pressed An option is being pressed
+ */
+OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
+       if ( pressed === undefined ) {
+               pressed = !this.pressed;
+       }
+       if ( pressed !== this.pressed ) {
+               this.$element.toggleClass( 'oo-ui-selectWidget-pressed', pressed );
+               this.$element.toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
+               this.pressed = pressed;
+       }
+};
+
 /**
  * Highlight an item.
  *
@@ -6167,12 +6292,19 @@ OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
  * @chainable
  */
 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
-       var i, len;
+       var i, len, highlighted,
+               changed = false;
 
        for ( i = 0, len = this.items.length; i < len; i++ ) {
-               this.items[i].setHighlighted( this.items[i] === item );
+               highlighted = this.items[i] === item;
+               if ( this.items[i].isHighlighted() !== highlighted ) {
+                       this.items[i].setHighlighted( highlighted );
+                       changed = true;
+               }
+       }
+       if ( changed ) {
+               this.emit( 'highlight', item );
        }
-       this.emit( 'highlight', item );
 
        return this;
 };
@@ -6186,12 +6318,45 @@ OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
  * @chainable
  */
 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
-       var i, len;
+       var i, len, selected,
+               changed = false;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               selected = this.items[i] === item;
+               if ( this.items[i].isSelected() !== selected ) {
+                       this.items[i].setSelected( selected );
+                       changed = true;
+               }
+       }
+       if ( changed ) {
+               this.emit( 'select', item );
+       }
+
+       return this;
+};
+
+/**
+ * Press an item.
+ *
+ * @method
+ * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
+ * @fires press
+ * @chainable
+ */
+OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
+       var i, len, pressed,
+               changed = false;
 
        for ( i = 0, len = this.items.length; i < len; i++ ) {
-               this.items[i].setSelected( this.items[i] === item );
+               pressed = this.items[i] === item;
+               if ( this.items[i].isPressed() !== pressed ) {
+                       this.items[i].setPressed( pressed );
+                       changed = true;
+               }
+       }
+       if ( changed ) {
+               this.emit( 'press', item );
        }
-       this.emit( 'select', item );
 
        return this;
 };
@@ -6205,7 +6370,7 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
  * @param {OO.ui.OptionWidget} [item] Item to select
  * @chainable
  */
-OO.ui.SelectWidget.prototype.intializeSelection = function( item ) {
+OO.ui.SelectWidget.prototype.intializeSelection = function ( item ) {
        var i, len, selected;
 
        for ( i = 0, len = this.items.length; i < len; i++ ) {
@@ -7659,7 +7824,7 @@ OO.ui.TextInputWidget.prototype.onEdit = function () {
  *
  * @chainable
  */
-OO.ui.TextInputWidget.prototype.adjustSize = function() {
+OO.ui.TextInputWidget.prototype.adjustSize = function () {
        var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
 
        if ( this.multiline && this.autosize ) {
index 887da4d..a7eecc1 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (23fb1b6144)
+ * OOjs UI v0.1.0-pre (eaa1b7f06d)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: Thu Mar 27 2014 14:49:30 GMT-0700 (PDT)
+ * Date: Thu Apr 03 2014 16:56:21 GMT-0700 (PDT)
  */
 
 /* Textures */
index cd966b9..f953878 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs v1.0.8
+ * OOjs v1.0.9
  * https://www.mediawiki.org/wiki/OOjs
  *
  * Copyright 2011-2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: Tue Mar 11 2014 19:27:31 GMT+0100 (CET)
+ * Date: Wed Apr 02 2014 14:29:36 GMT-0700 (PDT)
  */
 ( function ( global ) {
 
@@ -50,6 +50,17 @@ oo.isPlainObject = function ( obj ) {
        return true;
 };
 
+/**
+ * Utility to initialize a class for OO inheritance.
+ *
+ * Currently this just initializes an empty static object.
+ *
+ * @param {Function} fn
+ */
+oo.initClass = function ( fn ) {
+       fn.static = fn.static || {};
+};
+
 /**
  * Utility for common usage of Object#create for inheriting from one
  * prototype to another.
@@ -107,7 +118,7 @@ oo.inheritClass = function ( targetFn, originFn ) {
        } );
 
        // Extend static properties - always initialize both sides
-       originFn.static = originFn.static || {};
+       oo.initClass( originFn );
        targetFn.static = Object.create( originFn.static );
 };
 
@@ -151,7 +162,7 @@ oo.mixinClass = function ( targetFn, originFn ) {
        }
 
        // Copy static properties - always initialize both sides
-       targetFn.static = targetFn.static || {};
+       oo.initClass( targetFn );
        if ( originFn.static ) {
                for ( key in originFn.static ) {
                        if ( hasOwn.call( originFn.static, key ) ) {
@@ -159,7 +170,7 @@ oo.mixinClass = function ( targetFn, originFn ) {
                        }
                }
        } else {
-               originFn.static = {};
+               oo.initClass( originFn );
        }
 };
 
@@ -768,8 +779,9 @@ oo.inheritClass( oo.Factory, oo.Registry );
  * Classes must have a static `name` property to be registered.
  *
  *     function MyClass() {};
+ *     OO.initClass( MyClass );
  *     // Adds a static property to the class defining a symbolic name
- *     MyClass.static = { 'name': 'mine' };
+ *     MyClass.static.name = 'mine';
  *     // Registers class with factory, available via symbolic name 'mine'
  *     factory.register( MyClass );
  *
index 942cbac..00bd26b 100644 (file)
@@ -881,10 +881,8 @@ ul.mw-gallery-packed-hover li.gallerybox:hover div.gallerytextwrapper,
 ul.mw-gallery-packed-overlay li.gallerybox div.gallerytextwrapper,
 ul.mw-gallery-packed-hover li.gallerybox.mw-gallery-focused div.gallerytextwrapper {
        position:absolute;
-       opacity:.8;
-       filter:alpha(opacity=80);
-       zoom: 1;
-       background-color:white;
+       background: white;
+       background: rgba(255, 255, 255, 0.8);
        padding: 5px 10px;
        bottom: 0;
        left: 0; /* Needed for IE */
@@ -899,13 +897,6 @@ ul.mw-gallery-packed {
        text-align: center;
 }
 
-ul.mw-gallery-packed-hover div.gallerytext,
-ul.mw-gallery-packed-overlay div.gallerytext {
-       opacity: 1;
-       position: relative; /* Resets opacity in old IE */
-}
-
-
 .mw-ajax-loader {
        /* @embed */
        background-image: url(images/ajax-loader.gif);
index 3be3642..e5066e0 100644 (file)
                this.server.respond();
        } );
 
+       QUnit.test( 'getToken( cached )', function ( assert ) {
+               QUnit.expect( 2 );
+
+               var api = new mw.Api();
+
+               // Get editToken for local wiki, this should not make
+               // a request as it should be retrieved from user.tokens.
+               api.getToken( 'edit' )
+                       .done( function ( token ) {
+                               assert.ok( token.length, 'Got a token' );
+                       } )
+                       .fail( function ( err ) {
+                               assert.equal( '', err, 'API error' );
+                       } );
+
+               assert.equal( this.server.requests.length, 0, 'Requests made' );
+       } );
+
+       QUnit.test( 'getToken( uncached )', function ( assert ) {
+               QUnit.expect( 2 );
+
+               var api = new mw.Api();
+
+               // Get a token of a type that isn't prepopulated by user.tokens.
+               // Could use "block" or "delete" here, but those could in theory
+               // be added to user.tokens, use a fake one instead.
+               api.getToken( 'testaction' )
+                       .done( function ( token ) {
+                               assert.ok( token.length, 'Got a token' );
+                       } )
+                       .fail( function ( err ) {
+                               assert.equal( '', err, 'API error' );
+                       } );
+
+               assert.equal( this.server.requests.length, 1, 'Requests made' );
+
+               this.server.respond( function ( request ) {
+                       request.respond( 200, { 'Content-Type': 'application/json' },
+                               '{ "tokens": { "testactiontoken": "0123abc" } }'
+                       );
+               } );
+       } );
+
 }( mediaWiki ) );