Merge "Remove includepath stuff from MediaWikiPHPUnitCommand"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 6 May 2014 10:00:16 +0000 (10:00 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 6 May 2014 10:00:16 +0000 (10:00 +0000)
56 files changed:
includes/DefaultSettings.php
includes/GitInfo.php
includes/Revision.php
includes/api/ApiQuerySiteinfo.php
includes/cache/LocalisationCache.php
includes/clientpool/RedisConnectionPool.php
includes/content/Content.php
includes/content/TextContent.php
includes/content/WikitextContent.php
includes/filerepo/FileRepo.php
includes/filerepo/file/LocalFile.php
includes/installer/PostgresUpdater.php
includes/installer/i18n/es.json
includes/installer/i18n/ko.json
includes/search/SearchMssql.php
includes/search/SearchMySQL.php
includes/search/SearchOracle.php
includes/search/SearchSqlite.php
includes/specials/SpecialChangePassword.php
languages/i18n/ar.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/bn.json
languages/i18n/cs.json
languages/i18n/cy.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/hr.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/mk.json
languages/i18n/nb.json
languages/i18n/pl.json
languages/i18n/pt.json
languages/i18n/ro.json
languages/i18n/sl.json
languages/i18n/yi.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/postgres/tables.sql
resources/lib/oojs-ui/i18n/ne.json
resources/lib/oojs-ui/oojs-ui-apex.css
resources/lib/oojs-ui/oojs-ui.js
resources/lib/oojs-ui/oojs-ui.svg.css
resources/src/mediawiki.api/mediawiki.api.js
resources/src/mediawiki.less/mediawiki.mixins.less
resources/src/mediawiki.ui/mixins/utilities.less
skins/vector/components/navigation.less
skins/vector/components/watchstar.less
skins/vector/variables.less
skins/vector/vector.js
tests/phpunit/data/gitinfo/info-testValidJsonData.json [new file with mode: 0644]
tests/phpunit/includes/GitInfoTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js

index 0ca35f0..2b8aec3 100644 (file)
@@ -400,8 +400,6 @@ $wgImgAuthUrlPathMap = array();
  *                          url        : base URL to the root of the zone
  *                          urlsByExt  : map of file extension types to base URLs
  *                                       (useful for using a different cache for videos)
- *                          handlerUrl : base script-handled URL to the root of the zone
- *                                       (see FileRepo::getZoneHandlerUrl() function)
  *                      Zones default to using "<repo name>-<zone name>" as the container name
  *                      and default to using the container root as the zone's root directory.
  *                      Nesting of zone locations within other zones should be avoided.
index 6b092d9..dc2fff1 100644 (file)
@@ -35,33 +35,91 @@ class GitInfo {
         */
        protected $basedir;
 
+       /**
+        * Path to JSON cache file for pre-computed git information.
+        */
+       protected $cacheFile;
+
+       /**
+        * Cached git information.
+        */
+       protected $cache = array();
+
        /**
         * Map of repo URLs to viewer URLs. Access via static method getViewers().
         */
        private static $viewers = false;
 
        /**
-        * @param string $dir The root directory of the repo where the .git dir can be found
+        * @param string $repoDir The root directory of the repo where .git can be found
+        * @param bool $usePrecomputed Use precomputed information if available
+        * @see precomputeValues
+        */
+       public function __construct( $repoDir, $usePrecomputed = true ) {
+               $this->cacheFile = self::getCacheFilePath( $repoDir );
+               if ( $usePrecomputed &&
+                       $this->cacheFile !== null &&
+                       is_readable( $this->cacheFile )
+               ) {
+                       $this->cache = FormatJson::decode(
+                               file_get_contents( $this->cacheFile ),
+                               true
+                       );
+               }
+
+               if ( !$this->cacheIsComplete() ) {
+                       $this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
+                       if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
+                               $GITfile = file_get_contents( $this->basedir );
+                               if ( strlen( $GITfile ) > 8 &&
+                                       substr( $GITfile, 0, 8 ) === 'gitdir: '
+                               ) {
+                                       $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
+                                       if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
+                                               // Path from GITfile is absolute
+                                               $this->basedir = $path;
+                                       } else {
+                                               $this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Compute the path to the cache file for a given directory.
+        *
+        * @param string $repoDir The root directory of the repo where .git can be found
+        * @return string Path to GitInfo cache file in $wgCacheDirectory or null if
+        * $wgCacheDirectory is false (cache disabled).
         */
-       public function __construct( $dir ) {
-               $this->basedir = $dir . DIRECTORY_SEPARATOR . '.git';
-               if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
-                       $GITfile = file_get_contents( $this->basedir );
-                       if ( strlen( $GITfile ) > 8 && substr( $GITfile, 0, 8 ) === 'gitdir: ' ) {
-                               $path = rtrim( substr( $GITfile, 8 ), "\r\n" );
-                               $isAbsolute = $path[0] === '/' || substr( $path, 1, 1 ) === ':';
-                               $this->basedir = $isAbsolute ? $path : $dir . DIRECTORY_SEPARATOR . $path;
+       protected static function getCacheFilePath( $repoDir ) {
+               global $IP, $wgCacheDirectory;
+               if ( $wgCacheDirectory ) {
+                       // Transform path to git repo to something we can safely embed in a filename
+                       $repoName = $repoDir;
+                       if ( strpos( $repoName, $IP ) === 0 ) {
+                               // Strip $IP from path
+                               $repoName = substr( $repoName, strlen( $IP ) );
                        }
+                       $repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
+                       $fileName = 'info' . $repoName . '.json';
+                       return implode(
+                               DIRECTORY_SEPARATOR,
+                               array( $wgCacheDirectory, 'gitinfo', $fileName )
+                       );
                }
+               return null;
        }
 
        /**
-        * Return a singleton for the repo at $IP
+        * Get the singleton for the repo at $IP
+        *
         * @return GitInfo
         */
        public static function repo() {
-               global $IP;
                if ( is_null( self::$repo ) ) {
+                       global $IP;
                        self::$repo = new self( $IP );
                }
                return self::$repo;
@@ -78,50 +136,56 @@ class GitInfo {
        }
 
        /**
-        * Return the HEAD of the repo (without any opening "ref: ")
-        * @return string The HEAD
+        * Get the HEAD of the repo (without any opening "ref: ")
+        *
+        * @return string|bool The HEAD (git reference or SHA1) or false
         */
        public function getHead() {
-               $headFile = "{$this->basedir}/HEAD";
+               if ( !isset( $this->cache['head'] ) ) {
+                       $headFile = "{$this->basedir}/HEAD";
+                       $head = false;
 
-               if ( !is_readable( $headFile ) ) {
-                       return false;
-               }
+                       if ( is_readable( $headFile ) ) {
+                               $head = file_get_contents( $headFile );
 
-               $head = file_get_contents( $headFile );
-
-               if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
-                       return rtrim( $m[1] );
-               } else {
-                       return rtrim( $head );
+                               if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
+                                       $head = rtrim( $m[1] );
+                               } else {
+                                       $head = rtrim( $head );
+                               }
+                       }
+                       $this->cache['head'] = $head;
                }
+               return $this->cache['head'];
        }
 
        /**
-        * Return the SHA1 for the current HEAD of the repo
-        * @return string A SHA1 or false
+        * Get the SHA1 for the current HEAD of the repo
+        *
+        * @return string|bool A SHA1 or false
         */
        public function getHeadSHA1() {
-               $head = $this->getHead();
-
-               // If detached HEAD may be a SHA1
-               if ( self::isSHA1( $head ) ) {
-                       return $head;
-               }
-
-               // If not a SHA1 it may be a ref:
-               $refFile = "{$this->basedir}/{$head}";
-               if ( !is_readable( $refFile ) ) {
-                       return false;
+               if ( !isset( $this->cache['headSHA1'] ) ) {
+                       $head = $this->getHead();
+                       $sha1 = false;
+
+                       // If detached HEAD may be a SHA1
+                       if ( self::isSHA1( $head ) ) {
+                               $sha1 = $head;
+                       } else {
+                               // If not a SHA1 it may be a ref:
+                               $refFile = "{$this->basedir}/{$head}";
+                               if ( is_readable( $refFile ) ) {
+                                       $sha1 = rtrim( file_get_contents( $refFile ) );
+                               }
+                       }
+                       $this->cache['headSHA1'] = $sha1;
                }
-
-               $sha1 = rtrim( file_get_contents( $refFile ) );
-
-               return $sha1;
+               return $this->cache['headSHA1'];
        }
 
        /**
-        * Return the commit date of HEAD entry of the git code repository
+        * Get the commit date of HEAD entry of the git code repository
         *
         * @since 1.22
         * @return int|bool Commit date (UNIX timestamp) or false
@@ -129,67 +193,51 @@ class GitInfo {
        public function getHeadCommitDate() {
                global $wgGitBin;
 
-               if ( !is_file( $wgGitBin ) || !is_executable( $wgGitBin ) ) {
-                       return false;
-               }
-
-               $environment = array( "GIT_DIR" => $this->basedir );
-               $cmd = wfEscapeShellArg( $wgGitBin ) . " show -s --format=format:%ct HEAD";
-               $retc = false;
-               $commitDate = wfShellExec( $cmd, $retc, $environment );
-
-               if ( $retc !== 0 ) {
-                       return false;
-               } else {
-                       return (int)$commitDate;
+               if ( !isset( $this->cache['headCommitDate'] ) ) {
+                       $date = false;
+                       if ( is_file( $wgGitBin ) && is_executable( $wgGitBin ) ) {
+                               $environment = array( "GIT_DIR" => $this->basedir );
+                               $cmd = wfEscapeShellArg( $wgGitBin ) .
+                                       " show -s --format=format:%ct HEAD";
+                               $retc = false;
+                               $commitDate = wfShellExec( $cmd, $retc, $environment );
+                               if ( $retc === 0 ) {
+                                       $date = (int)$commitDate;
+                               }
+                       }
+                       $this->cache['headCommitDate'] = $date;
                }
+               return $this->cache['headCommitDate'];
        }
 
        /**
-        * Return the name of the current branch, or HEAD if not found
-        * @return string The branch name, HEAD, or false
+        * Get the name of the current branch, or HEAD if not found
+        *
+        * @return string|bool The branch name, HEAD, or false
         */
        public function getCurrentBranch() {
-               $head = $this->getHead();
-               if ( $head && preg_match( "#^refs/heads/(.*)$#", $head, $m ) ) {
-                       return $m[1];
-               } else {
-                       return $head;
+               if ( !isset( $this->cache['branch'] ) ) {
+                       $branch = $this->getHead();
+                       if ( $branch &&
+                               preg_match( "#^refs/heads/(.*)$#", $branch, $m )
+                       ) {
+                               $branch = $m[1];
+                       }
+                       $this->cache['branch'] = $branch;
                }
+               return $this->cache['branch'];
        }
 
        /**
         * Get an URL to a web viewer link to the HEAD revision.
         *
-        * @return string|bool string if a URL is available or false otherwise.
+        * @return string|bool String if a URL is available or false otherwise
         */
        public function getHeadViewUrl() {
-               $config = "{$this->basedir}/config";
-               if ( !is_readable( $config ) ) {
-                       return false;
-               }
-
-               wfSuppressWarnings();
-               $configArray = parse_ini_file( $config, true );
-               wfRestoreWarnings();
-               $remote = false;
-
-               // Use the "origin" remote repo if available or any other repo if not.
-               if ( isset( $configArray['remote origin'] ) ) {
-                       $remote = $configArray['remote origin'];
-               } elseif ( is_array( $configArray ) ) {
-                       foreach ( $configArray as $sectionName => $sectionConf ) {
-                               if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
-                                       $remote = $sectionConf;
-                               }
-                       }
-               }
-
-               if ( $remote === false || !isset( $remote['url'] ) ) {
+               $url = $this->getRemoteUrl();
+               if ( $url === false ) {
                        return false;
                }
-
-               $url = $remote['url'];
                if ( substr( $url, -4 ) !== '.git' ) {
                        $url .= '.git';
                }
@@ -209,6 +257,91 @@ class GitInfo {
                return false;
        }
 
+       /**
+        * Get the URL of the remote origin.
+        * @return string|bool string if a URL is available or false otherwise.
+        */
+       protected function getRemoteUrl() {
+               if ( !isset( $this->cache['remoteURL'] ) ) {
+                       $config = "{$this->basedir}/config";
+                       $url = false;
+                       if ( is_readable( $config ) ) {
+                               wfSuppressWarnings();
+                               $configArray = parse_ini_file( $config, true );
+                               wfRestoreWarnings();
+                               $remote = false;
+
+                               // Use the "origin" remote repo if available or any other repo if not.
+                               if ( isset( $configArray['remote origin'] ) ) {
+                                       $remote = $configArray['remote origin'];
+                               } elseif ( is_array( $configArray ) ) {
+                                       foreach ( $configArray as $sectionName => $sectionConf ) {
+                                               if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
+                                                       $remote = $sectionConf;
+                                               }
+                                       }
+                               }
+
+                               if ( $remote !== false && isset( $remote['url'] ) ) {
+                                       $url = $remote['url'];
+                               }
+                       }
+                       $this->cache['remoteURL'] = $url;
+               }
+               return $this->cache['remoteURL'];
+       }
+
+       /**
+        * Check to see if the current cache is fully populated.
+        *
+        * Note: This method is public only to make unit testing easier. There's
+        * really no strong reason that anything other than a test should want to
+        * call this method.
+        *
+        * @return bool True if all expected cache keys exist, false otherwise
+        */
+       public function cacheIsComplete() {
+               return isset( $this->cache['head'] ) &&
+                       isset( $this->cache['headSHA1'] ) &&
+                       isset( $this->cache['headCommitDate'] ) &&
+                       isset( $this->cache['branch'] ) &&
+                       isset( $this->cache['remoteURL'] );
+       }
+
+       /**
+        * Precompute and cache git information.
+        *
+        * Creates a JSON file in the cache directory associated with this
+        * GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing
+        * the same directory to avoid needing to examine the .git directory again.
+        *
+        * @since 1.24
+        */
+       public function precomputeValues() {
+               if ( $this->cacheFile !== null ) {
+                       // Try to completely populate the cache
+                       $this->getHead();
+                       $this->getHeadSHA1();
+                       $this->getHeadCommitDate();
+                       $this->getCurrentBranch();
+                       $this->getRemoteUrl();
+
+                       if ( !$this->cacheIsComplete() ) {
+                               wfDebugLog( "Failed to compute GitInfo for \"{$this->basedir}\"" );
+                               return;
+                       }
+
+                       $cacheDir = dirname( $this->cacheFile );
+                       if ( !file_exists( $cacheDir ) &&
+                               !wfMkdirParents( $cacheDir, null, __METHOD__ )
+                       ) {
+                               throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
+                       }
+
+                       file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
+               }
+       }
+
        /**
         * @see self::getHeadSHA1
         * @return string
index 5a83d38..b0423fb 100644 (file)
@@ -134,8 +134,8 @@ class Revision implements IDBAccessObject {
         *      Revision::READ_LATEST  : Select the data from the master (since 1.20)
         *      Revision::READ_LOCKING : Select & lock the data from the master
         *
-        * @param int $revId
-        * @param int $pageId (optional)
+        * @param int $pageId
+        * @param int $revId (optional)
         * @param int $flags Bitfield (optional)
         * @return Revision|null
         */
index b7796ee..b0e9bd2 100644 (file)
@@ -567,7 +567,10 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                                                $ret['vcs-system'] = 'git';
                                                $ret['vcs-version'] = $vcsVersion;
                                                $ret['vcs-url'] = $gitInfo->getHeadViewUrl();
-                                               $ret['vcs-date'] = wfTimestamp( TS_ISO_8601, $gitInfo->getHeadCommitDate() );
+                                               $vcsDate = $gitInfo->getHeadCommitDate();
+                                               if ( $vcsDate !== false ) {
+                                                       $ret['vcs-date'] = wfTimestamp( TS_ISO_8601, $vcsDate );
+                                               }
                                        } else {
                                                $svnInfo = SpecialVersion::getSvnInfo( $extensionPath );
                                                if ( $svnInfo !== false ) {
index 3bbf1bb..1153fd2 100644 (file)
@@ -1171,7 +1171,7 @@ class LCStoreDB implements LCStore {
                $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
                        array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
                if ( $row ) {
-                       return unserialize( $row->lc_value );
+                       return unserialize( $db->decodeBlob( $row->lc_value ) );
                } else {
                        return null;
                }
@@ -1233,7 +1233,7 @@ class LCStoreDB implements LCStore {
                $this->batch[] = array(
                        'lc_lang' => $this->currentLang,
                        'lc_key' => $key,
-                       'lc_value' => serialize( $value ) );
+                       'lc_value' => $this->dbw->encodeBlob( serialize( $value ) ) );
 
                if ( count( $this->batch ) >= 100 ) {
                        $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
index 15f0a47..36d2731 100644 (file)
@@ -44,6 +44,8 @@ class RedisConnectionPool {
         */
        /** @var string Connection timeout in seconds */
        protected $connectTimeout;
+       /** @var string Read timeout in seconds */
+       protected $readTimeout;
        /** @var string Plaintext auth password */
        protected $password;
        /** @var bool Whether connections persist */
@@ -76,6 +78,7 @@ class RedisConnectionPool {
                                'See https://www.mediawiki.org/wiki/Redis#Setup' );
                }
                $this->connectTimeout = $options['connectTimeout'];
+               $this->readTimeout = $options['readTimeout'];
                $this->persistent = $options['persistent'];
                $this->password = $options['password'];
                if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
@@ -97,6 +100,9 @@ class RedisConnectionPool {
                if ( !isset( $options['connectTimeout'] ) ) {
                        $options['connectTimeout'] = 1;
                }
+               if ( !isset( $options['readTimeout'] ) ) {
+                       $options['readTimeout'] = 31; // handles up to 30 second blocking commands
+               }
                if ( !isset( $options['persistent'] ) ) {
                        $options['persistent'] = false;
                }
@@ -112,6 +118,9 @@ class RedisConnectionPool {
         * $options include:
         *   - connectTimeout : The timeout for new connections, in seconds.
         *                      Optional, default is 1 second.
+        *   - readTimeout    : The timeout for operation reads, in seconds.
+        *                      Commands like BLPOP can fail if told to wait longer than this.
+        *                      Optional, default is 60 seconds.
         *   - persistent     : Set this to true to allow connections to persist across
         *                      multiple web requests. False by default.
         *   - password       : The authentication password, will be sent to Redis in clear text.
@@ -216,6 +225,7 @@ class RedisConnectionPool {
                }
 
                if ( $conn ) {
+                       $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
                        $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
                        $this->connections[$server][] = array( 'conn' => $conn, 'free' => false );
 
index 210c117..3286c0a 100644 (file)
@@ -86,7 +86,7 @@ interface Content {
        public function getNativeData();
 
        /**
-        * Returns the content's nominal size in bogo-bytes.
+        * Returns the content's nominal size in "bogo-bytes".
         *
         * @return int
         */
index b13b5fa..0c6b06f 100644 (file)
@@ -77,9 +77,9 @@ class TextContent extends AbstractContent {
        }
 
        /**
-        * returns the text's size in bytes.
+        * Returns the text's size in bytes.
         *
-        * @return int The size
+        * @return int
         */
        public function getSize() {
                $text = $this->getNativeData();
@@ -170,8 +170,7 @@ class TextContent extends AbstractContent {
         *
         * @since 1.21
         *
-        * @param Content $that The other content object to compare this content
-        * object to.
+        * @param Content $that The other content object to compare this content object to.
         * @param Language $lang The language object to use for text segmentation.
         *    If not given, $wgContentLang is used.
         *
@@ -269,10 +268,12 @@ class TextContent extends AbstractContent {
         * This implementation provides lossless conversion between content models based
         * on TextContent.
         *
-        * @param string $toModel
-        * @param string $lossy
+        * @param string $toModel The desired content model, use the CONTENT_MODEL_XXX flags.
+        * @param string $lossy Flag, set to "lossy" to allow lossy conversion. If lossy conversion is not
+        *     allowed, full round-trip conversion is expected to work without losing information.
         *
-        * @return Content|bool
+        * @return Content|bool A content object with the content model $toModel, or false if that
+        *     conversion is not supported.
         *
         * @see Content::convert()
         */
@@ -286,7 +287,7 @@ class TextContent extends AbstractContent {
                $toHandler = ContentHandler::getForModelID( $toModel );
 
                if ( $toHandler instanceof TextContentHandler ) {
-                       //NOTE: ignore content serialization format - it's just text anyway.
+                       // NOTE: ignore content serialization format - it's just text anyway.
                        $text = $this->getNativeData();
                        $converted = $toHandler->unserializeContent( $text );
                }
index 9b1a3c7..dba0205 100644 (file)
@@ -258,8 +258,6 @@ class WikitextContent extends TextContent {
         *    find out (default: null).
         * @param Title $title Optional title, defaults to the title from the current main request.
         *
-        * @internal param \IContextSource $context context for parsing if necessary
-        *
         * @return bool
         */
        public function isCountable( $hasLinks = null, Title $title = null ) {
@@ -319,7 +317,6 @@ class WikitextContent extends TextContent {
         * @param int $revId Revision to pass to the parser (default: null)
         * @param ParserOptions $options (default: null)
         * @param bool $generateHtml (default: true)
-        * @internal param \IContextSource|null $context
         *
         * @return ParserOutput Representing the HTML form of the text
         */
index 7d4e8cc..7a30ccc 100644 (file)
@@ -295,29 +295,6 @@ class FileRepo {
                }
        }
 
-       /**
-        * Get the thumb zone URL configured to be handled by scripts like thumb_handler.php.
-        * This is probably only useful for internal requests, such as from a fast frontend server
-        * to a slower backend server.
-        *
-        * Large sites may use a different host name for uploads than for wikis. In any case, the
-        * wiki configuration is needed in order to use thumb.php. To avoid extracting the wiki ID
-        * from the URL path, one can configure thumb_handler.php to recognize a special path on the
-        * same host name as the wiki that is used for viewing thumbnails.
-        *
-        * @param string $zone One of: public, deleted, temp, thumb
-        * @return string|bool String or false
-        */
-       public function getZoneHandlerUrl( $zone ) {
-               if ( isset( $this->zones[$zone]['handlerUrl'] )
-                       && in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) )
-               ) {
-                       return $this->zones[$zone]['handlerUrl'];
-               }
-
-               return false;
-       }
-
        /**
         * Get the backend storage path corresponding to a virtual URL.
         * Use this function wisely.
index 9b9f0a9..b3d5d5d 100644 (file)
@@ -496,6 +496,8 @@ class LocalFile extends File {
 
                $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
 
+               $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
+
                if ( empty( $decoded['major_mime'] ) ) {
                        $decoded['mime'] = 'unknown/unknown';
                } else {
@@ -2114,7 +2116,7 @@ class LocalFileDeleteBatch {
                        $dbw->insertSelect( 'filearchive', 'image',
                                array(
                                        'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
+                                       'fa_storage_key' => $dbw->conditional( array( 'img_sha1' => '' ), $dbw->addQuotes( '' ), $concat ),
                                        'fa_deleted_user' => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
                                        'fa_deleted_reason' => $encReason,
@@ -2146,7 +2148,7 @@ class LocalFileDeleteBatch {
                        $dbw->insertSelect( 'filearchive', 'oldimage',
                                array(
                                        'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
+                                       'fa_storage_key' => $dbw->conditional( array( 'oi_sha1' => '' ), $dbw->addQuotes( '' ), $concat ),
                                        'fa_deleted_user' => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
                                        'fa_deleted_reason' => $encReason,
index a6d7cb2..e7aab8a 100644 (file)
@@ -406,9 +406,12 @@ class PostgresUpdater extends DatabaseUpdater {
                        array( 'addPgField', 'recentchanges', 'rc_source', "TEXT NOT NULL DEFAULT ''" ),
                        array( 'addPgField', 'page', 'page_links_updated', "TIMESTAMPTZ NULL" ),
                        array( 'addPgField', 'mwuser', 'user_password_expires', 'TIMESTAMPTZ NULL' ),
+                       array( 'changeFieldPurgeTable', 'l10n_cache', 'lc_value', 'bytea', "replace(lc_value,'\','\\\\')::bytea" ),
+
+                       // 1.24
                        array( 'addPgField', 'page_props', 'pp_sortkey', 'float NULL' ),
                        array( 'addPgIndex', 'page_props', 'pp_propname_sortkey_page',
-                                       '( pp_propname, pp_sortkey, pp_page ) WHERE ( pp_sortkey NOT NULL )' ),
+                                       '( pp_propname, pp_sortkey, pp_page ) WHERE ( pp_sortkey IS NOT NULL )' ),
                );
        }
 
@@ -675,6 +678,35 @@ END;
                }
        }
 
+       protected function changeFieldPurgeTable( $table, $field, $newtype, $default ) {
+               ## For a cache table, empty it if the field needs to be changed, because the old contents
+               ## may be corrupted.  If the column is already the desired type, refrain from purging.
+               $fi = $this->db->fieldInfo( $table, $field );
+               if ( is_null( $fi ) ) {
+                       $this->output( "...ERROR: expected column $table.$field to exist\n" );
+                       exit( 1 );
+               }
+
+               if ( $fi->type() === $newtype ) {
+                       $this->output( "...column '$table.$field' is already of type '$newtype'\n" );
+               } else {
+                       $this->output( "Purging data from cache table '$table'\n" );
+                       $this->db->query("DELETE from $table" );
+                       $this->output( "Changing column type of '$table.$field' from '{$fi->type()}' to '$newtype'\n" );
+                       $sql = "ALTER TABLE $table ALTER $field TYPE $newtype";
+                       if ( strlen( $default ) ) {
+                               $res = array();
+                               if ( preg_match( '/DEFAULT (.+)/', $default, $res ) ) {
+                                       $sqldef = "ALTER TABLE $table ALTER $field SET DEFAULT $res[1]";
+                                       $this->db->query( $sqldef );
+                                       $default = preg_replace( '/\s*DEFAULT .+/', '', $default );
+                               }
+                               $sql .= " USING $default";
+                       }
+                       $this->db->query( $sql );
+               }
+       }
+
        protected function setDefault( $table, $field, $default ) {
 
                $info = $this->db->fieldInfo( $table, $field );
index 44493e7..518f7aa 100644 (file)
        "config-download-localsettings": "Descargar archivo <code>LocalSettings.php</code>",
        "config-help": "Ayuda",
        "config-nofile": "El archivo \"$1\" no se pudo encontrar. ¿Se ha eliminado?",
-       "config-extension-link": "¿Sabías que tu wiki admite [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensiones]?\n\nPuedes navegar por las [//www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category categorías] o visitar la [/www.mediawiki.org/wiki/Extension_Matrix central] para ver una lista completa.",
+       "config-extension-link": "¿Sabías que tu wiki admite [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensiones]?\n\nPuedes navegar por las [//www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category categorías] o visitar la [//www.mediawiki.org/wiki/Extension_Matrix matriz de extensiones] para ver una lista completa.",
        "mainpagetext": "'''MediaWiki ha sido instalado con éxito.'''",
        "mainpagedocfooter": "Consulta la [//meta.wikimedia.org/wiki/Ayuda:Guía del usuario de contenidos] para obtener información sobre el uso del software wiki.\n\n== Empezando ==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista de ajustes de configuración]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/es FAQ de MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista de correo de anuncios de distribución de MediaWiki]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Regionalizar MediaWiki para tu idioma]"
 }
index d2d8101..eb7cc5b 100644 (file)
@@ -82,7 +82,7 @@
        "config-gd": "내장된 GD 그래픽 라이브러리를 찾았습니다.\n올리기를 활성화할 경우 그림 섬네일이 활성화됩니다.",
        "config-no-scaling": "GD 라이브러리나 ImageMagick를 찾을 수 없습니다.\n그림 섬네일이 비활성화됩니다.",
        "config-no-uri": "'''오류:''' 현재 URI를 확인할 수 없습니다.\n설치가 중단되었습니다.",
-       "config-no-cli-uri": "'''경고''': 기본 값을 사용하여 <code>--scriptpath</code>를 지정하지 않았습니다: <code>$1</code>.",
+       "config-no-cli-uri": "'''경고''': 기본값을 사용하여 <code>--scriptpath</code>를 지정하지 않았습니다: <code>$1</code>.",
        "config-using-server": "\"<nowiki>$1</nowiki>\"(을)를 서버 이름으로 사용합니다.",
        "config-using-uri": "\"<nowiki>$1$2</nowiki>\"(을)를 서버 URL로 사용합니다.",
        "config-uploads-not-safe": "'''경고:''' 올리기에 대한 기본 디렉터리(<code>$1</code>)는 임의의 스크립트 실행에 취약합니다.\n미디어위키는 보안 위협 때문에 모든 올려진 파일을 검사하지만, 올리기를 활성화하기 전에 [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security 이 보안 취약점을 해결할 것]을 매우 권장합니다.",
index 3eef498..ed76ff8 100644 (file)
@@ -132,7 +132,7 @@ class SearchMssql extends SearchDatabase {
         */
        function parseQuery( $filteredText, $fulltext ) {
                global $wgContLang;
-               $lc = SearchEngine::legalSearchChars();
+               $lc = $this->legalSearchChars();
                $this->searchTerms = array();
 
                # @todo FIXME: This doesn't handle parenthetical expressions.
index 345ced5..cc20d02 100644 (file)
@@ -43,7 +43,7 @@ class SearchMySQL extends SearchDatabase {
         */
        function parseQuery( $filteredText, $fulltext ) {
                global $wgContLang;
-               $lc = SearchEngine::legalSearchChars(); // Minus format chars
+               $lc = $this->legalSearchChars(); // Minus format chars
                $searchon = '';
                $this->searchTerms = array();
 
index 93427d1..c944152 100644 (file)
@@ -171,7 +171,7 @@ class SearchOracle extends SearchDatabase {
         */
        function parseQuery( $filteredText, $fulltext ) {
                global $wgContLang;
-               $lc = SearchEngine::legalSearchChars();
+               $lc = $this->legalSearchChars();
                $this->searchTerms = array();
 
                # @todo FIXME: This doesn't handle parenthetical expressions.
index 1ac4946..6b1a6b2 100644 (file)
@@ -42,7 +42,7 @@ class SearchSqlite extends SearchDatabase {
         */
        function parseQuery( $filteredText, $fulltext ) {
                global $wgContLang;
-               $lc = SearchEngine::legalSearchChars(); // Minus format chars
+               $lc = $this->legalSearchChars(); // Minus format chars
                $searchon = '';
                $this->searchTerms = array();
 
index 91d0404..8afbf4b 100644 (file)
@@ -296,7 +296,8 @@ class SpecialChangePassword extends FormSpecialPage {
                if ( $isSelf ) {
                        // This is needed to keep the user connected since
                        // changing the password also modifies the user's token.
-                       $user->setCookies();
+                       $remember = $this->getRequest()->getCookie( 'Token' ) !== null;
+                       $user->setCookies( null, null, $remember );
                }
                $user->resetPasswordExpiration();
                $user->saveSettings();
index 134d5a9..d97103a 100644 (file)
@@ -43,7 +43,8 @@
                        "نصوح",
                        "وهراني",
                        "아라",
-                       "Test Create account"
+                       "Test Create account",
+                       "Kuwaity26"
                ]
        },
        "tog-underline": "سطر تحت الوصلات:",
        "permalink": "رابط دائم",
        "print": "اطبع",
        "view": "مطالعة",
+       "view-foreign": "اعرض على $1",
        "edit": "عدل",
        "edit-local": "تعديل الوصف المحلي",
        "create": "أنشئ",
        "search-file-match": "(يطابق محتوى الملف)",
        "search-suggest": "أتقصد: $1",
        "search-interwiki-caption": "المشاريع الشقيقة",
-       "search-interwiki-default": "$1 نتيجة:",
+       "search-interwiki-default": "نتائح من $1:",
        "search-interwiki-more": "(المزيد)",
        "search-relatedarticle": "مرتبطة",
        "searcheverything-enable": "ابحث في جميع النطاقات",
        "action-createpage": "إنشاء الصفحات",
        "action-createtalk": "إنشاء صفحات النقاش",
        "action-createaccount": "إنشاء حساب المستخدم هذا",
+       "action-history": "اعرض تاريخ هذه الصفحة",
        "action-minoredit": "التعليم على هذا التعديل كطفيف",
        "action-move": "نقل هذه الصفحة",
        "action-move-subpages": "نقل هذه الصفحة، وصفحاتها الفرعية",
        "listgrouprights-removegroup-self": "يمكنه إزالة {{PLURAL:$2|المجموعة|المجموعات}} من حسابه الخاص: $1",
        "listgrouprights-addgroup-self-all": "يمكنه إضافة كل المجموعات إلى حسابه الخاص",
        "listgrouprights-removegroup-self-all": "يمكنه إزالة كل المجموعات من حسابه الخاص",
+       "listgrouprights-namespaceprotection-namespace": "النطاق",
+       "trackingcategories-name": "اسم الرسالة",
+       "trackingcategories-disabled": "التصنيف غير مفعل",
        "mailnologin": "لا يوجد عنوان للإرسال",
        "mailnologintext": "يجب أن تقوم [[Special:UserLogin|بتسجيل الدخول]] وإدخال بريد إلكتروني صالح في صفحة [[Special:Preferences|التفضيلات]] لتتمكن من إرسال الرسائل لمستخدمين آخرين.",
        "emailuser": "مراسلة المستخدم",
index 6a175dd..7315895 100644 (file)
        "recentchanges-label-minor": "بو بیر کیچیک دَییشدیرمه‌دیر",
        "recentchanges-label-bot": "بو دییشیک بیر بوت طرفیندن ائدیلیب‌دیر",
        "recentchanges-label-unpatrolled": "بو دییشیکلیک هله گؤزدن گئچیریلمه‌ییب‌دیر",
+       "recentchanges-legend-heading": "'''ایختیصارلار:'''",
+       "recentchanges-legend-newpage": "(هم‌ده [[Special:NewPages|یئنی صحیفه‌لرین لیستینه]] باخین)",
        "rcnotefrom": "آشاغیدا '''$2'''-دن ('''$1'''-ه قدر) ديَیشیکلیکلر گلیبلر.",
        "rclistfrom": "$3 $2 واختیندان باشلایاراق یئنی دییشیکلری گؤستر",
        "rcshowhideminor": "کیچیک دَییشیکلری $1",
index 23f6fbd..03f5363 100644 (file)
        "action-createpage": "стварэньне старонак",
        "action-createtalk": "стварэньне старонак абмеркаваньняў",
        "action-createaccount": "стварэньне гэтага рахунку ўдзельніка",
+       "action-history": "прагляд гісторыі гэтай старонкі",
        "action-minoredit": "пазначэньне гэтай праўкі як дробнай",
        "action-move": "перанос гэтай старонкі",
        "action-move-subpages": "перанос гэтай старонкі і яе падстаронак",
index 2e26ebf..ebd81d3 100644 (file)
        "htmlform-no": "না",
        "htmlform-yes": "হ্যাঁ",
        "htmlform-chosen-placeholder": "অপশন নির্বাচন করুন",
+       "htmlform-cloner-delete": "অপসারণ",
        "sqlite-has-fts": "$1 সহ পূর্ণ টেক্সট সার্চ সমর্থন",
        "sqlite-no-fts": "$1 বাদে পূর্ণ টেক্সট সার্চ সমর্থন",
        "logentry-delete-delete": "$1 কর্তৃক $3 পাতাটি অপসারিত হয়েছে",
index 505980b..7635d22 100644 (file)
        "action-createpage": "vytvářet stránky",
        "action-createtalk": "vytvářet diskusní stránky",
        "action-createaccount": "vytvořit tento uživatelský účet",
+       "action-history": "prohlížet si historii této stránky",
        "action-minoredit": "označit tuto editaci jako malou",
        "action-move": "přesunout tuto stránku",
        "action-move-subpages": "přesunout tuto stránku a její podstránky",
        "htmlform-no": "Ne",
        "htmlform-yes": "Ano",
        "htmlform-chosen-placeholder": "Zvolte možnost",
+       "htmlform-cloner-create": "Přidat další",
+       "htmlform-cloner-delete": "Odstranit",
+       "htmlform-cloner-required": "Je povinná nejméně jedna hodnota.",
        "sqlite-has-fts": "$1 s podporou plnotextového vyhledávání",
        "sqlite-no-fts": "$1 bez podpory plnotextového vyhledávání",
        "logentry-delete-delete": "$1 {{GENDER:$2|smazal|smazala}} stránku $3",
index 75e88a7..82c50f4 100644 (file)
        "action-createpage": "creu tudalennau",
        "action-createtalk": "creu tudalennau sgwrs",
        "action-createaccount": "creu'r cyfrif defnyddiwr hwn",
+       "action-history": "gweld hanes y dudalen",
        "action-minoredit": "marcio'r golygiad yn un bach",
        "action-move": "symud y dudalen",
        "action-move-subpages": "symud y dudalen a'i is-dudalennau",
        "listgrouprights-removegroup-self": "Yn gallu tynnu {{PLURAL:$2|grŵp}} oddi ar eich cyfrif eich hunan: $1",
        "listgrouprights-addgroup-self-all": "Yn gallu ychwanegu'r holl grwpiau at eich cyfrif eich hunan",
        "listgrouprights-removegroup-self-all": "Yn gallu tynnu'r holl grwpiau oddi ar eich cyfrif eich hunan",
+       "listgrouprights-namespaceprotection-namespace": "Parth",
+       "listgrouprights-namespaceprotection-restrictedto": "Gallu(oedd) yn caniatau i'r defnyddiwr olygu",
        "trackingcategories-name": "Enw'r neges",
        "trackingcategories-nodesc": "Dim disgrifiad ar gael.",
        "trackingcategories-disabled": "Categorïau yr analluogwyd",
        "htmlform-no": "Na/Nac ydw/Na fydd...",
        "htmlform-yes": "Ie/Iawn/Ydw/Oes...",
        "htmlform-chosen-placeholder": "Dewiswch opsiwn",
+       "htmlform-cloner-create": "Ychwaneger rhes",
+       "htmlform-cloner-delete": "Tynner i ffwrdd",
        "sqlite-has-fts": "$1 gyda chymorth chwilio yr holl destun",
        "sqlite-no-fts": "$1 heb gymorth chwiliad yr holl destun",
        "logentry-delete-delete": "Dileodd $1 y dudalen $3",
index baeb37f..9ac3216 100644 (file)
                        "לערי ריינהארט",
                        "Chocolate con galleta",
                        "Csbotero",
-                       "아라"
+                       "아라",
+                       "Mcervera"
                ]
        },
        "tog-underline": "Subrayar los enlaces:",
        "action-createpage": "crear páginas",
        "action-createtalk": "crear páginas de discusión",
        "action-createaccount": "crear esta cuenta de usuario",
+       "action-history": "Ver el historial de esta página",
        "action-minoredit": "marcar este cambio como menor",
        "action-move": "trasladar esta página",
        "action-move-subpages": "trasladar esta página y sus subpáginas",
        "htmlform-no": "No",
        "htmlform-yes": "Sí",
        "htmlform-chosen-placeholder": "Selecciona una opción",
+       "htmlform-cloner-create": "Añadir más",
+       "htmlform-cloner-delete": "Eliminar",
+       "htmlform-cloner-required": "Se requiere al menos un valor",
        "sqlite-has-fts": "$1 con soporte para búsqueda de texto completo",
        "sqlite-no-fts": "$1 sin soporte para búsqueda de texto completo",
        "logentry-delete-delete": "$1 {{GENDER:$2|borró}} la página «$3»",
index 953dcf4..44d82ee 100644 (file)
        "action-createpage": "créer des pages",
        "action-createtalk": "créer des pages de discussion",
        "action-createaccount": "créer ce compte utilisateur",
+       "action-history": "afficher l’historique de cette page",
        "action-minoredit": "marquer cette modification comme mineure",
        "action-move": "renommer cette page",
        "action-move-subpages": "renommer cette page et ses sous-pages",
        "htmlform-no": "Non",
        "htmlform-yes": "Oui",
        "htmlform-chosen-placeholder": "Choisir une option",
+       "htmlform-cloner-create": "Ajouter encore",
+       "htmlform-cloner-delete": "Supprimer",
+       "htmlform-cloner-required": "Une valeur au moins est obligatoire.",
        "sqlite-has-fts": "$1 avec recherche en texte intégral supportée",
        "sqlite-no-fts": "$1 sans recherche en texte intégral supportée",
        "logentry-delete-delete": "$1 {{GENDER:$2|a supprimé}} la page $3",
index 0954165..94a7b70 100644 (file)
        "contributions-title": "Suradnički doprinosi za $1",
        "mycontris": "Moji doprinosi",
        "contribsub2": "Za {{GENDER:$3|$1}} ($2)",
+       "contributions-userdoesnotexist": "Suradnički račun \"$1\" nije registriran.",
        "nocontribs": "Nema promjena koje udovoljavaju ovim kriterijima.",
        "uctop": "(vrh)",
        "month": "Od mjeseca (i ranije):",
index 3ee6d15..3c30cf1 100644 (file)
        "action-createpage": "creare pagine",
        "action-createtalk": "creare pagine di discussione",
        "action-createaccount": "effettuare questa registrazione",
+       "action-history": "vedere la cronologia di questa pagina",
        "action-minoredit": "segnare questa modifica come minore",
        "action-move": "spostare questa pagina",
        "action-move-subpages": "spostare questa pagina e le relative sottopagine",
        "htmlform-no": "No",
        "htmlform-yes": "Sì",
        "htmlform-chosen-placeholder": "Seleziona un'opzione",
+       "htmlform-cloner-create": "Aggiungi altro",
+       "htmlform-cloner-delete": "Rimuovi",
+       "htmlform-cloner-required": "È necessario almeno un valore.",
        "sqlite-has-fts": "$1 con la possibilità di ricerca completa nel testo",
        "sqlite-no-fts": "$1 senza la possibilità di ricerca completa nel testo",
        "logentry-delete-delete": "$1 {{GENDER:$2|ha cancellato}} la pagina $3",
index 819efe8..7a364e1 100644 (file)
        "action-createpage": "ページの作成",
        "action-createtalk": "議論ページの作成",
        "action-createaccount": "この利用者アカウントの作成",
+       "action-history": "このページの履歴の閲覧",
        "action-minoredit": "細部の編集の印を付ける",
        "action-move": "このページの移動",
        "action-move-subpages": "このページとその下位ページの移動",
        "expiringblock": "$1$2に解除",
        "anononlyblock": "匿名利用者のみ",
        "noautoblockblock": "自動ブロック無効",
-       "createaccountblock": "ã\82¢ã\82«ã\82¦ã\83³ã\83\88ä½\9cæ\88\90ã\81®禁止",
-       "emailblock": "ã\83¡ã\83¼ã\83«é\80\81ä¿¡ã\81®禁止",
+       "createaccountblock": "ã\82¢ã\82«ã\82¦ã\83³ã\83\88ä½\9cæ\88\90ã\82\82禁止",
+       "emailblock": "ã\83¡ã\83¼ã\83«é\80\81ä¿¡ã\82\82禁止",
        "blocklist-nousertalk": "自分のトークページも編集禁止",
        "ipblocklist-empty": "ブロック一覧は空です。",
        "ipblocklist-no-results": "指定されたIPアドレスまたは利用者名はブロックされていません。",
        "reblock-logentry": "が [[$1]] のブロック設定を$2に変更しました。ブロックの詳細: $3",
        "blocklogtext": "このページは利用者のブロックと解除の記録です。\n自動的にブロックされたIPアドレスは表示されていません。\n現時点で有効なブロックは[[Special:BlockList|ブロックの一覧]]をご覧ください。",
        "unblocklogentry": "$1のブロックを解除しました",
-       "block-log-flags-anononly": "匿名利用者のみ",
-       "block-log-flags-nocreate": "アカウント作成禁止",
+       "block-log-flags-anononly": "対象ã\81¯å\8c¿å\90\8då\88©ç\94¨è\80\85ã\81®ã\81¿",
+       "block-log-flags-nocreate": "アカウント作成禁止",
        "block-log-flags-noautoblock": "自動ブロック無効",
        "block-log-flags-noemail": "メール送信禁止",
-       "block-log-flags-nousertalk": "è\87ªå\88\86ã\81®ã\83\88ã\83¼ã\82¯ã\83\9aã\83¼ã\82¸ã\81®編集禁止",
+       "block-log-flags-nousertalk": "è\87ªå\88\86ã\81®ã\83\88ã\83¼ã\82¯ã\83\9aã\83¼ã\82¸ã\82\82編集禁止",
        "block-log-flags-angry-autoblock": "拡張自動ブロック有効",
        "block-log-flags-hiddenname": "利用者名の秘匿",
        "range_block_disabled": "範囲ブロックを作成する管理者機能は無効化されています。",
index 3806315..8f5ca8d 100644 (file)
@@ -80,9 +80,9 @@
        "tog-prefershttps": "로그인할 때 항상 보안 연결 사용",
        "underline-always": "항상",
        "underline-never": "항상 치지 않기",
-       "underline-default": "스킨 또는 브라우저 기본 값을 따르기",
+       "underline-default": "스킨 또는 브라우저 기본",
        "editfont-style": "편집 창의 글꼴:",
-       "editfont-default": "브라우저 기본 값을 따르기",
+       "editfont-default": "브라우저 기본",
        "editfont-monospace": "고정폭 글꼴",
        "editfont-sansserif": "산세리프 글꼴",
        "editfont-serif": "세리프 글꼴",
        "prefsnologintext2": "사용자 환경 설정을 설정하려면 $1하십시오.",
        "prefs-skin": "스킨",
        "skin-preview": "미리 보기",
-       "datedefault": "기본 값",
+       "datedefault": "설정하지 않음",
        "prefs-labs": "실험 중인 기능",
        "prefs-user-pages": "사용자 문서",
        "prefs-personal": "사용자 정보",
        "prefs-custom-css": "사용자 CSS",
        "prefs-custom-js": "사용자 자바스크립트",
        "prefs-common-css-js": "모든 스킨에 대한 공통 CSS/자바스크립트:",
-       "prefs-reset-intro": "이 사이트의 기본 값으로 환경 설정을 재설정할 수 있습니다.\n재설정한 환경 설정은 되돌릴 수 없습니다.",
+       "prefs-reset-intro": "이 페이지를 사용해 사이트 기본값으로 환경 설정을 재설정할 수 있습니다.\n이는 되돌릴 수 없습니다.",
        "prefs-emailconfirm-label": "이메일 인증:",
        "youremail": "이메일:",
        "username": "{{GENDER:$1|사용자 이름}}:",
        "action-createpage": "문서 만들기",
        "action-createtalk": "토론 문서 만들기",
        "action-createaccount": "새 계정 만들기",
+       "action-history": "이 문서의 역사 보기",
        "action-minoredit": "이 편집을 사소한 편집으로 표시하기",
        "action-move": "이 문서 옮기기",
        "action-move-subpages": "이 문서와 하위 문서를 함께 옮기기",
index eb8fd5e..02d1033 100644 (file)
        "action-createpage": "Säiten unzelleeën",
        "action-createtalk": "Diskussiounssäiten unzeleeën",
        "action-createaccount": "dëse Benotzerkont unzeleeën",
+       "action-history": "d'Versioune vun dëser Säit weisen",
        "action-minoredit": "dës Ännerung als kleng Ännerung ze markéieren",
        "action-move": "dës Säit ze réckelen",
        "action-move-subpages": "dës Säit an déi Ënnersäiten déi dozou gehéieren ze réckelen",
index ca35276..6d1f3f9 100644 (file)
        "action-createpage": "создавање страници",
        "action-createtalk": "создавање страници за разговор",
        "action-createaccount": "создај ја оваа корисничка сметка",
+       "action-history": "преглед на историјата на оваа страница",
        "action-minoredit": "означување на ова уредување како ситно",
        "action-move": "преместување на оваа страница",
        "action-move-subpages": "преместување на оваа страница и нејзините потстраници",
index 40c2325..38993b7 100644 (file)
        "action-createpage": "opprette sider",
        "action-createtalk": "opprette diskusjonssider",
        "action-createaccount": "opprette denne kontoen",
+       "action-history": "se historikken til denne siden",
        "action-minoredit": "merke denne redigeringen som mindre",
        "action-move": "flytte denne siden",
        "action-move-subpages": "flytte denne siden og dens undersider",
        "htmlform-no": "Nei",
        "htmlform-yes": "Ja",
        "htmlform-chosen-placeholder": "Velg et alternativ",
+       "htmlform-cloner-create": "Legg til mer",
+       "htmlform-cloner-delete": "Fjern",
+       "htmlform-cloner-required": "Minst én verdi kreves.",
        "sqlite-has-fts": "$1 med støtte for fulltekstsøk",
        "sqlite-no-fts": "$1 uten støtte for fulltekstsøk",
        "logentry-delete-delete": "$1 {{GENDER:$2|slettet}} siden $3",
index 687ea97..9e2c860 100644 (file)
        "htmlform-no": "Nie",
        "htmlform-yes": "Tak",
        "htmlform-chosen-placeholder": "Wybierz opcję",
+       "htmlform-cloner-delete": "Usuń",
        "sqlite-has-fts": "$1 z obsługą pełnotekstowego wyszukiwania",
        "sqlite-no-fts": "$1 bez obsługi pełnotekstowego wyszukiwania",
        "logentry-delete-delete": "$1 {{GENDER:$2|usunął|usunęła}} stronę $3",
index 9182149..35fa6d6 100644 (file)
        "search-redirect": "(redirecionamento de $1)",
        "search-section": "(seção $1)",
        "search-file-match": "(coincide com o conteúdo do ficheiro)",
-       "search-suggest": "Será que queria dizer: $1",
+       "search-suggest": "Será que você quis dizer: $1",
        "search-interwiki-caption": "Projetos irmãos",
        "search-interwiki-default": "Resultados de $1:",
        "search-interwiki-more": "(mais)",
index 9557885..1974690 100644 (file)
        "action-createpage": "creați pagini",
        "action-createtalk": "creați pagini de discuție",
        "action-createaccount": "creați acest cont de utilizator",
+       "action-history": "vizualizați istoricul acestei pagini",
        "action-minoredit": "marcați această modificare ca minoră",
        "action-move": "redenumiți această pagină",
        "action-move-subpages": "redenumiți această pagină și subpaginile sale",
index 2ce537c..ac042a3 100644 (file)
        "recentchangescount": "Privzeto število prikazanih urejanj:",
        "prefs-help-recentchangescount": "Vključuje zadnje spremembe, zgodovine strani in dnevniške zapise.",
        "prefs-help-watchlist-token2": "To je skrivni ključ do spletnega vira vašega spiska nadzorov. Kdor ve zanj, lahko bere vaš spisek nadzorov, zato ključa ne delite. [[Special:ResetTokens|Kliknite tukaj, če ga želite ponastaviti]].",
-       "savedprefs": "Spremembe so bile uspešno shranjene.",
+       "savedprefs": "Spremembe so uspešno shranjene.",
        "timezonelegend": "Časovni pas",
        "localtime": "Krajevni čas:",
        "timezoneuseserverdefault": "Uporabi privzeti wiki čas ($1)",
        "action-createpage": "ustvarjenje strani",
        "action-createtalk": "ustvarjanje pogovornih strani",
        "action-createaccount": "registracija tega uporabniškega računa",
+       "action-history": "ogled zgodovine strani",
        "action-minoredit": "označevanje tega urejanja kot manjšega",
        "action-move": "premik te strani",
        "action-move-subpages": "premik te strani in njenih podstrani",
index f7548c5..3b64e43 100644 (file)
        "edit-conflict": "רעדאקטירן קאנפֿליקט.",
        "edit-no-change": "מ'האט איגנארירט אײַער רעדאַקטירונג, ווײַל קיין שום ענדערונג איז נישט געמאַכט צום טעקסט.",
        "postedit-confirmation-created": "דער בלאט איז געווארן געשאפן.",
+       "postedit-confirmation-restored": "דער בלאט איז געווארן צוריקגעשטעלט.",
        "postedit-confirmation-saved": "אייער רעדאקטירונג איז געווארן אויפגעהיטן.",
        "edit-already-exists": "נישט מעגליך צו שאַפֿן נייעם בלאט.\nער עקזיסטירט שוין.",
        "defaultmessagetext": "גרונטלעכער מעלדונג טעקסט",
        "action-createpage": "שאַפֿן בלעטער",
        "action-createtalk": "שאַפֿן שמועס בלעטער",
        "action-createaccount": "שאַפֿן די באַניצער קאנטע",
+       "action-history": "באקוקן רעדאקטירן היסטאריע פון דעם בלאט.",
        "action-minoredit": "באַצייכענען די רעדאַקטירונג ווי מינערדיק",
        "action-move": "באַוועגן דעם בלאַט",
        "action-move-subpages": "באַוועגן דעם בלאַט מיט זײַנע אונטערבלעטער",
index f8140bd..33fadab 100644 (file)
        "action-createpage": "创建页面",
        "action-createtalk": "创建讨论页面",
        "action-createaccount": "创建该用户账户",
+       "action-history": "查看此页历史",
        "action-minoredit": "标记该编辑为小编辑",
        "action-move": "移动本页",
        "action-move-subpages": "移动本页及其子页面",
index 2144898..c8e9837 100644 (file)
        "action-createpage": "建立這個頁面",
        "action-createtalk": "建立討論頁面",
        "action-createaccount": "建立這個使用者帳號",
+       "action-history": "查閱此頁面歷史",
        "action-minoredit": "標示此編輯為小修訂",
        "action-move": "移動這個頁面",
        "action-move-subpages": "移動這個頁面跟它的子頁面",
index a3fb042..abbfd3a 100644 (file)
@@ -159,11 +159,13 @@ ALTER TABLE page_restrictions ADD CONSTRAINT page_restrictions_pk PRIMARY KEY (p
 CREATE TABLE page_props (
   pp_page      INTEGER  NOT NULL  REFERENCES page (page_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   pp_propname  TEXT     NOT NULL,
-  pp_value     TEXT     NOT NULL
+  pp_value     TEXT     NOT NULL,
+  pp_sortkey   FLOAT
 );
 ALTER TABLE page_props ADD CONSTRAINT page_props_pk PRIMARY KEY (pp_page,pp_propname);
 CREATE INDEX page_props_propname ON page_props (pp_propname);
 CREATE UNIQUE INDEX pp_propname_page ON page_props (pp_propname,pp_page);
+CREATE INDEX pp_propname_sortkey_page ON page_props (pp_propname, pp_sortkey, pp_page) WHERE (pp_sortkey IS NOT NULL);
 
 CREATE SEQUENCE archive_ar_id_seq;
 CREATE TABLE archive (
@@ -676,7 +678,7 @@ CREATE INDEX user_properties_property ON user_properties (up_property);
 CREATE TABLE l10n_cache (
   lc_lang   TEXT  NOT NULL,
   lc_key    TEXT  NOT NULL,
-  lc_value  TEXT  NOT NULL
+  lc_value  BYTEA NOT NULL
 );
 CREATE INDEX l10n_cache_lc_lang_key ON l10n_cache (lc_lang, lc_key);
 
index 6b7c78a..f7bbff4 100644 (file)
@@ -4,5 +4,10 @@
                        "RajeshPandey",
                        "सरोज कुमार ढकाल"
                ]
-       }
+       },
+       "ooui-dialog-action-close": "बन्द गर्ने",
+       "ooui-outline-control-move-down": "वस्तुलाई तल सार्ने",
+       "ooui-outline-control-move-up": "वस्तुलाई माथि सार्ने",
+       "ooui-outline-control-remove": "वस्तुलाई हटाउने",
+       "ooui-toolbar-more": "थप"
 }
index 952e05e..63a66fb 100644 (file)
   bottom: 4.8em;
 }
 
+.oo-ui-dialog-content-footless .oo-ui-window-body {
+  bottom: 0;
+}
+
 .oo-ui-dialog > .oo-ui-window-frame {
   top: 1em;
   bottom: 1em;
index 6ba7ac8..b5f8824 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (9a6c625f5f)
+ * OOjs UI v0.1.0-pre (7d2507b267)
  * 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: Fri May 02 2014 12:04:40 GMT-0700 (PDT)
+ * Date: Mon May 05 2014 14:13:13 GMT-0700 (PDT)
  */
 ( function ( OO ) {
 
@@ -1946,7 +1946,7 @@ OO.ui.ButtonedElement = function OoUiButtonedElement( $button, config ) {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
-       if ( this.disabled || e.which !== 1 ) {
+       if ( this.isDisabled() || e.which !== 1 ) {
                return false;
        }
        // tabIndex should generally be interacted with via the property,
@@ -1967,7 +1967,7 @@ OO.ui.ButtonedElement.prototype.onMouseDown = function ( e ) {
  * @param {jQuery.Event} e Mouse up event
  */
 OO.ui.ButtonedElement.prototype.onMouseUp = function ( e ) {
-       if ( this.disabled || e.which !== 1 ) {
+       if ( this.isDisabled() || e.which !== 1 ) {
                return false;
        }
        // Restore the tab-index after the button is up to restore the button's accesssibility
@@ -3545,7 +3545,7 @@ OO.ui.ToolGroup.prototype.updateDisabled = function () {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.ToolGroup.prototype.onMouseDown = function ( e ) {
-       if ( !this.disabled && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
                this.pressed = this.getTargetTool( e );
                if ( this.pressed ) {
                        this.pressed.setActive( true );
@@ -3577,7 +3577,7 @@ OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
 OO.ui.ToolGroup.prototype.onMouseUp = function ( e ) {
        var tool = this.getTargetTool( e );
 
-       if ( !this.disabled && e.which === 1 && this.pressed && this.pressed === tool ) {
+       if ( !this.isDisabled() && e.which === 1 && this.pressed && this.pressed === tool ) {
                this.pressed.onSelect();
        }
 
@@ -4096,7 +4096,7 @@ OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {boolean} [continuous=false] Show all pages, one after another
- * @cfg {boolean} [autoFocus=false] Focus on the first focusable element when changing to a page
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
  * @cfg {boolean} [outlined=false] Show an outline
  * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
  * @cfg {Object[]} [adders] List of adders for controls, each with name, icon and title properties
@@ -4113,7 +4113,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        this.pages = {};
        this.ignoreFocus = false;
        this.stackLayout = new OO.ui.StackLayout( { '$': this.$, 'continuous': !!config.continuous } );
-       this.autoFocus = !!config.autoFocus;
+       this.autoFocus = config.autoFocus === undefined ? true : !!config.autoFocus;
        this.outlineVisible = false;
        this.outlined = !!config.outlined;
        if ( this.outlined ) {
@@ -4898,7 +4898,7 @@ OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
  * @inheritdoc
  */
 OO.ui.PopupToolGroup.prototype.onMouseUp = function ( e ) {
-       if ( !this.disabled && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
                this.setActive( false );
        }
        return OO.ui.ToolGroup.prototype.onMouseUp.call( this, e );
@@ -4919,7 +4919,7 @@ OO.ui.PopupToolGroup.prototype.onHandleMouseUp = function () {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.PopupToolGroup.prototype.onHandleMouseDown = function ( e ) {
-       if ( !this.disabled && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
                this.setActive( !this.active );
        }
        return false;
@@ -5066,7 +5066,7 @@ OO.mixinClass( OO.ui.PopupTool, OO.ui.PopuppableElement );
  * @inheritdoc
  */
 OO.ui.PopupTool.prototype.onSelect = function () {
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                if ( this.popup.isVisible() ) {
                        this.hidePopup();
                } else {
@@ -5357,7 +5357,7 @@ OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggableElement );
  * @fires click
  */
 OO.ui.ButtonWidget.prototype.onClick = function () {
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                this.emit( 'click' );
                if ( this.isHyperlink ) {
                        return true;
@@ -5373,7 +5373,7 @@ OO.ui.ButtonWidget.prototype.onClick = function () {
  * @fires click
  */
 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
-       if ( !this.disabled && e.which === OO.ui.Keys.SPACE ) {
+       if ( !this.isDisabled() && e.which === OO.ui.Keys.SPACE ) {
                if ( this.isHyperlink ) {
                        this.onClick();
                        return true;
@@ -5414,7 +5414,7 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) {
        // Initialization
        this.$input
                .attr( 'name', config.name )
-               .prop( 'disabled', this.disabled );
+               .prop( 'disabled', this.isDisabled() );
        this.setReadOnly( config.readOnly );
        this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
        this.setValue( config.value );
@@ -5449,7 +5449,7 @@ OO.ui.InputWidget.prototype.getInputElement = function () {
  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
  */
 OO.ui.InputWidget.prototype.onEdit = function () {
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                // Allow the stack to clear so the value will be updated
                setTimeout( OO.ui.bind( function () {
                        this.setValue( this.$input.val() );
@@ -5562,7 +5562,7 @@ OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
        OO.ui.Widget.prototype.setDisabled.call( this, state );
        if ( this.$input ) {
-               this.$input.prop( 'disabled', this.disabled );
+               this.$input.prop( 'disabled', this.isDisabled() );
        }
        return this;
 };
@@ -5635,7 +5635,7 @@ OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
  * @inheritdoc
  */
 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                // Allow the stack to clear so the value will be updated
                setTimeout( OO.ui.bind( function () {
                        this.setValue( this.$input.prop( 'checked' ) );
@@ -6005,7 +6005,7 @@ OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
  * @return {boolean} Item is selectable
  */
 OO.ui.OptionWidget.prototype.isSelectable = function () {
-       return this.constructor.static.selectable && !this.disabled;
+       return this.constructor.static.selectable && !this.isDisabled();
 };
 
 /**
@@ -6014,7 +6014,7 @@ OO.ui.OptionWidget.prototype.isSelectable = function () {
  * @return {boolean} Item is highlightable
  */
 OO.ui.OptionWidget.prototype.isHighlightable = function () {
-       return this.constructor.static.highlightable && !this.disabled;
+       return this.constructor.static.highlightable && !this.isDisabled();
 };
 
 /**
@@ -6023,7 +6023,7 @@ OO.ui.OptionWidget.prototype.isHighlightable = function () {
  * @return {boolean} Item is pressable
  */
 OO.ui.OptionWidget.prototype.isPressable = function () {
-       return this.constructor.static.pressable && !this.disabled;
+       return this.constructor.static.pressable && !this.isDisabled();
 };
 
 /**
@@ -6060,7 +6060,7 @@ OO.ui.OptionWidget.prototype.isPressed = function () {
  * @chainable
  */
 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
-       if ( !this.disabled && this.constructor.static.selectable ) {
+       if ( !this.isDisabled() && this.constructor.static.selectable ) {
                this.selected = !!state;
                if ( this.selected ) {
                        this.$element.addClass( 'oo-ui-optionWidget-selected' );
@@ -6081,7 +6081,7 @@ OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
  * @chainable
  */
 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
-       if ( !this.disabled && this.constructor.static.highlightable ) {
+       if ( !this.isDisabled() && this.constructor.static.highlightable ) {
                this.highlighted = !!state;
                if ( this.highlighted ) {
                        this.$element.addClass( 'oo-ui-optionWidget-highlighted' );
@@ -6099,7 +6099,7 @@ OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
  * @chainable
  */
 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
-       if ( !this.disabled && this.constructor.static.pressable ) {
+       if ( !this.isDisabled() && this.constructor.static.pressable ) {
                this.pressed = !!state;
                if ( this.pressed ) {
                        this.$element.addClass( 'oo-ui-optionWidget-pressed' );
@@ -6121,7 +6121,7 @@ OO.ui.OptionWidget.prototype.flash = function () {
        var $this = this.$element,
                deferred = $.Deferred();
 
-       if ( !this.disabled && this.constructor.static.pressable ) {
+       if ( !this.isDisabled() && this.constructor.static.pressable ) {
                $this.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
                setTimeout( OO.ui.bind( function () {
                        // Restore original classes
@@ -6246,7 +6246,7 @@ OO.ui.SelectWidget.static.tagName = 'ul';
 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
        var item;
 
-       if ( !this.disabled && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
                this.togglePressed( true );
                item = this.getTargetItem( e );
                if ( item && item.isSelectable() ) {
@@ -6274,7 +6274,7 @@ OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
                        this.selecting = item;
                }
        }
-       if ( !this.disabled && e.which === 1 && this.selecting ) {
+       if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
                this.pressItem( null );
                this.chooseItem( this.selecting );
                this.selecting = null;
@@ -6292,7 +6292,7 @@ OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
        var item;
 
-       if ( !this.disabled && this.pressed ) {
+       if ( !this.isDisabled() && this.pressed ) {
                item = this.getTargetItem( e );
                if ( item && item !== this.selecting && item.isSelectable() ) {
                        this.pressItem( item );
@@ -6311,7 +6311,7 @@ OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
        var item;
 
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                item = this.getTargetItem( e );
                this.highlightItem( item && item.isHighlightable() ? item : null );
        }
@@ -6325,7 +6325,7 @@ OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
  * @param {jQuery.Event} e Mouse over event
  */
 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                this.highlightItem( null );
        }
        return false;
@@ -6720,7 +6720,7 @@ OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
                handled = false,
                highlightItem = this.getHighlightedItem();
 
-       if ( !this.disabled && this.visible ) {
+       if ( !this.isDisabled() && this.visible ) {
                if ( !highlightItem ) {
                        highlightItem = this.getSelectedItem();
                }
@@ -6997,7 +6997,7 @@ OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
                return;
        }
 
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                if ( this.menu.isVisible() ) {
                        this.menu.hide();
                } else {
@@ -7653,7 +7653,7 @@ OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
                return;
        }
 
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                if ( this.popup.isVisible() ) {
                        this.hidePopup();
                } else {
@@ -8223,7 +8223,7 @@ OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
  * @inheritdoc
  */
 OO.ui.ToggleButtonWidget.prototype.onClick = function () {
-       if ( !this.disabled ) {
+       if ( !this.isDisabled() ) {
                this.setValue( !this.value );
        }
 
@@ -8295,7 +8295,7 @@ OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
-       if ( !this.disabled && e.which === 1 ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
                this.setValue( !this.value );
        }
 };
index 955b71a..0c2cfaf 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (9a6c625f5f)
+ * OOjs UI v0.1.0-pre (7d2507b267)
  * 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: Fri May 02 2014 12:04:40 GMT-0700 (PDT)
+ * Date: Mon May 05 2014 14:13:12 GMT-0700 (PDT)
  */
 
 /* Textures */
   float: right;
 }
 
-.oo-ui-dialog-content-footless .oo-ui-window-body {
-  bottom: 0;
-}
-
 .oo-ui-dialog-content-footless .oo-ui-window-foot {
   display: none;
 }
 
 .oo-ui-indicator-up {
   background-image: /* @embed */ url(images/indicators/up.svg);
-}
+}
\ No newline at end of file
index b37e2a6..9962534 100644 (file)
                                        function ( code ) {
                                                if ( code === 'badtoken' ) {
                                                        // Clear from cache
-                                                       deferreds[ this.defaults.ajax.url ][ tokenType + 'Token' ] =
+                                                       deferreds[ api.defaults.ajax.url ][ tokenType + 'Token' ] =
                                                                params.token = undefined;
 
                                                        // Try again, once
index 36f1bd4..6556af9 100644 (file)
@@ -64,3 +64,9 @@
        -webkit-transition: @string;
        transition: @string;
 }
+
+.box-sizing(@value) {
+       -moz-box-sizing: @value;
+       -webkit-box-sizing: @value;
+       box-sizing: @value;
+}
index a201a4e..3d7b732 100644 (file)
@@ -1,9 +1,3 @@
-.box-sizing(@value) {
-       -moz-box-sizing: @value;
-       -webkit-box-sizing: @value;
-       box-sizing: @value;
-}
-
 .agora-flush-left() {
        float: left;
        margin-left: 0;
index 24e9843..8b384ac 100644 (file)
@@ -86,37 +86,37 @@ div#mw-panel {
        left: 0;
 
        div.portal {
-               padding-bottom: 1.5em;
+               margin: 0 0.6em 0 0.7em;
+               padding: 0.25em 0;
                direction: ltr;
+               background-position: top left;
+               background-repeat: no-repeat;
+               .background-image('images/portal-break.png');
 
                h3 {
+                       font-size: @menu-main-heading-font-size;
+                       color: @menu-main-heading-color;
                        font-weight: normal;
-                       color: #444;
+                       margin: 0;
                        padding: @menu-main-heading-padding;
                        cursor: default;
                        border: none;
-                       font-size: @menu-main-heading-font-size;
                }
 
                div.body {
-                       padding-top: 0.5em;
                        margin: @menu-main-body-margin;
-
-                       .background-image('images/portal-break.png');
-                       background-repeat: no-repeat;
-                       background-position: top left;
+                       padding-top: 0;
 
                        ul {
                                list-style-type: none;
                                list-style-image: none;
-                               padding: @menu-main-body-padding;
                                margin: 0;
+                               padding: @menu-main-body-padding;
 
                                li {
                                        line-height: 1.125em;
-                                       padding: 0;
-                                       padding-bottom: 0.5em;
                                        margin: 0;
+                                       padding: 0.25em 0;
                                        font-size: @menu-main-body-font-size;
                                        word-wrap: break-word;
 
@@ -129,5 +129,16 @@ div#mw-panel {
                                }
                        }
                }
+
+               &.first {
+                       background-image: none;
+                       margin-top: 0;
+                       h3 {
+                               display: none;
+                       }
+                       div.body {
+                               margin-left: 0.5em;
+                       }
+               }
        }
 }
index b76a825..2c38516 100644 (file)
@@ -5,7 +5,6 @@
 #ca-watch.icon a {
        margin: 0;
        padding: 0;
-       outline: none;
        display: block;
        width: 26px;
        /* This hides the text but shows the background image */
index 438fbcf..41cb1da 100644 (file)
 
 // Main menu
 @menu-main-font-size: inherit;
+
 @menu-main-heading-font-size: 0.75em;
-@menu-main-heading-padding: 0 1.75em 0.25em 0.25em;
+@menu-main-heading-padding: 0.25em 0 0.25em 0.25em;
+@menu-main-heading-color: #4d4d4d;
 
 @menu-main-body-font-size: 0.75em;
 @menu-main-body-link-color: #0645ad;
 @menu-main-body-link-visited-color: #0b0080;
 @menu-main-body-margin: 0 0 0 1.25em;
 @menu-main-body-padding: 0;
+
 @menu-main-logo-left: 0.5em;
 
 // Personal menu
 @menu-personal-font-size: 0.75em;
-
-// Collapsible nav
-@collapsible-nav-heading-color: #4d4d4d;
-@collapsible-nav-heading-collapsed-color: #0645ad;
-
-@collapsible-nav-heading-padding: 4px 0 3px 1.5em;
-@collapsible-nav-body-margin: 0 0 0 1.25em;
index 0bc114a..d8ac3c8 100644 (file)
@@ -25,6 +25,11 @@ jQuery( function ( $ ) {
                        .attr( 'tabindex', '-1' );
        } );
 
+       /**
+        * Sidebar
+        */
+       $( '#mw-panel > .portal:first' ).addClass( 'first' );
+
        /**
         * Collapsible tabs for Vector
         */
diff --git a/tests/phpunit/data/gitinfo/info-testValidJsonData.json b/tests/phpunit/data/gitinfo/info-testValidJsonData.json
new file mode 100644 (file)
index 0000000..e955a2b
--- /dev/null
@@ -0,0 +1 @@
+{\r    "head": "refs/heads/master",\r    "headSHA1": "0123456789abcdef0123456789abcdef01234567",\r    "headCommitDate": "1070884800",\r    "branch": "master",\r    "remoteURL": "https://gerrit.wikimedia.org/r/mediawiki/core"\r}\r
\ No newline at end of file
diff --git a/tests/phpunit/includes/GitInfoTest.php b/tests/phpunit/includes/GitInfoTest.php
new file mode 100644 (file)
index 0000000..7c684d5
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @covers GitInfo
+ */
+class GitInfoTest extends MediaWikiTestCase {
+
+       protected function setUp() {
+               parent::setUp();
+               $this->setMwGlobals( 'wgCacheDirectory', __DIR__ . '/../data' );
+       }
+
+       public function testValidJsonData() {
+               $dir = $GLOBALS['IP'] . '/testValidJsonData';
+               $fixture = new GitInfo( $dir );
+
+               $this->assertTrue( $fixture->cacheIsComplete() );
+               $this->assertEquals( 'refs/heads/master', $fixture->getHead() );
+               $this->assertEquals( '0123456789abcdef0123456789abcdef01234567',
+                       $fixture->getHeadSHA1() );
+               $this->assertEquals( '1070884800', $fixture->getHeadCommitDate() );
+               $this->assertEquals( 'master', $fixture->getCurrentBranch() );
+               $this->assertContains( '0123456789abcdef0123456789abcdef01234567',
+                       $fixture->getHeadViewUrl() );
+       }
+
+       public function testMissingJsonData() {
+               $dir = $GLOBALS['IP'] . '/testMissingJsonData';
+               $fixture = new GitInfo( $dir );
+
+               $this->assertFalse( $fixture->cacheIsComplete() );
+
+               $this->assertEquals( false, $fixture->getHead() );
+               $this->assertEquals( false, $fixture->getHeadSHA1() );
+               $this->assertEquals( false, $fixture->getHeadCommitDate() );
+               $this->assertEquals( false, $fixture->getCurrentBranch() );
+               $this->assertEquals( false, $fixture->getHeadViewUrl() );
+
+               // After calling all the outputs, the cache should be complete
+               $this->assertTrue( $fixture->cacheIsComplete() );
+       }
+
+}
index a93f572..05eb6b9 100644 (file)
                } );
        } );
 
+       QUnit.test( 'postWithToken()', function ( assert ) {
+               QUnit.expect( 1 );
+
+               var api = new mw.Api( { ajax: { url: '/postWithToken/api.php' } } );
+
+               // - Requests token
+               // - Performs action=example
+               api.postWithToken( 'testsimpletoken', { action: 'example', key: 'foo' } )
+                       .done( function ( data ) {
+                               assert.deepEqual( data, { example: { foo: 'quux' } } );
+                       } );
+
+               this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "tokens": { "testsimpletokentoken": "a-bad-token" } }'
+               );
+
+               this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "example": { "foo": "quux" } }'
+               );
+       } );
+
+       QUnit.test( 'postWithToken() - badtoken', function ( assert ) {
+               QUnit.expect( 1 );
+
+               var api = new mw.Api();
+
+               // - Request: token
+               // - Request: action=example -> badtoken error
+               // - Request: new token
+               // - Request: action=example
+               api.postWithToken( 'testbadtoken', { action: 'example', key: 'foo' } )
+                       .done( function ( data ) {
+                               assert.deepEqual( data, { example: { foo: 'quux' } } );
+                       } );
+
+               this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "tokens": { "testbadtokentoken": "a-bad-token" } }'
+               );
+
+               this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "error": { "code": "badtoken" } }'
+               );
+
+               this.server.requests[2].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "tokens": { "testbadtokentoken": "a-good-token" } }'
+               );
+
+               this.server.requests[3].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "example": { "foo": "quux" } }'
+               );
+
+       } );
+
+       QUnit.test( 'postWithToken() - badtoken-cached', function ( assert ) {
+               QUnit.expect( 2 );
+
+               var api = new mw.Api();
+
+               // - Request: token
+               // - Request: action=example
+               api.postWithToken( 'testbadtokencache', { action: 'example', key: 'foo' } )
+                       .done( function ( data ) {
+                               assert.deepEqual( data, { example: { foo: 'quux' } } );
+                       } );
+
+               // - Cache: Try previously cached token
+               // - Request: action=example -> badtoken error
+               // - Request: new token
+               // - Request: action=example
+               api.postWithToken( 'testbadtokencache', { action: 'example', key: 'bar' } )
+                       .done( function ( data ) {
+                               assert.deepEqual( data, { example: { bar: 'quux' } } );
+                       } );
+
+               this.server.requests[0].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "tokens": { "testbadtokencachetoken": "a-good-token-once" } }'
+               );
+
+               this.server.requests[1].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "example": { "foo": "quux" } }'
+               );
+
+               this.server.requests[2].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "error": { "code": "badtoken" } }'
+               );
+
+               this.server.requests[3].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "tokens": { "testbadtokencachetoken": "a-good-new-token" } }'
+               );
+
+               this.server.requests[4].respond( 200, { 'Content-Type': 'application/json' },
+                       '{ "example": { "bar": "quux" } }'
+               );
+
+       } );
+
 }( mediaWiki ) );