* 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.
*/
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;
}
/**
- * 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
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';
}
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
* 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
*/
$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 ) {
$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;
}
$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__ );
*/
/** @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 */
'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' ) {
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;
}
* $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.
}
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 );
public function getNativeData();
/**
- * Returns the content's nominal size in bogo-bytes.
+ * Returns the content's nominal size in "bogo-bytes".
*
* @return int
*/
}
/**
- * 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();
*
* @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.
*
* 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()
*/
$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 );
}
* 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 ) {
* @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
*/
}
}
- /**
- * 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.
$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 {
$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,
$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,
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 )' ),
);
}
}
}
+ 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 );
"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]"
}
"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 이 보안 취약점을 해결할 것]을 매우 권장합니다.",
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars();
+ $lc = $this->legalSearchChars();
$this->searchTerms = array();
# @todo FIXME: This doesn't handle parenthetical expressions.
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars(); // Minus format chars
+ $lc = $this->legalSearchChars(); // Minus format chars
$searchon = '';
$this->searchTerms = array();
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars();
+ $lc = $this->legalSearchChars();
$this->searchTerms = array();
# @todo FIXME: This doesn't handle parenthetical expressions.
*/
function parseQuery( $filteredText, $fulltext ) {
global $wgContLang;
- $lc = SearchEngine::legalSearchChars(); // Minus format chars
+ $lc = $this->legalSearchChars(); // Minus format chars
$searchon = '';
$this->searchTerms = array();
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();
"نصوح",
"وهراني",
"아라",
- "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": "مراسلة المستخدم",
"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",
"action-createpage": "стварэньне старонак",
"action-createtalk": "стварэньне старонак абмеркаваньняў",
"action-createaccount": "стварэньне гэтага рахунку ўдзельніка",
+ "action-history": "прагляд гісторыі гэтай старонкі",
"action-minoredit": "пазначэньне гэтай праўкі як дробнай",
"action-move": "перанос гэтай старонкі",
"action-move-subpages": "перанос гэтай старонкі і яе падстаронак",
"htmlform-no": "না",
"htmlform-yes": "হ্যাঁ",
"htmlform-chosen-placeholder": "অপশন নির্বাচন করুন",
+ "htmlform-cloner-delete": "অপসারণ",
"sqlite-has-fts": "$1 সহ পূর্ণ টেক্সট সার্চ সমর্থন",
"sqlite-no-fts": "$1 বাদে পূর্ণ টেক্সট সার্চ সমর্থন",
"logentry-delete-delete": "$1 কর্তৃক $3 পাতাটি অপসারিত হয়েছে",
"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",
"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",
"לערי ריינהארט",
"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»",
"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",
"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):",
"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",
"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": "範囲ブロックを作成する管理者機能は無効化されています。",
"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": "이 문서와 하위 문서를 함께 옮기기",
"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",
"action-createpage": "создавање страници",
"action-createtalk": "создавање страници за разговор",
"action-createaccount": "создај ја оваа корисничка сметка",
+ "action-history": "преглед на историјата на оваа страница",
"action-minoredit": "означување на ова уредување како ситно",
"action-move": "преместување на оваа страница",
"action-move-subpages": "преместување на оваа страница и нејзините потстраници",
"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",
"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",
"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)",
"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",
"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",
"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": "באַוועגן דעם בלאַט מיט זײַנע אונטערבלעטער",
"action-createpage": "创建页面",
"action-createtalk": "创建讨论页面",
"action-createaccount": "创建该用户账户",
+ "action-history": "查看此页历史",
"action-minoredit": "标记该编辑为小编辑",
"action-move": "移动本页",
"action-move-subpages": "移动本页及其子页面",
"action-createpage": "建立這個頁面",
"action-createtalk": "建立討論頁面",
"action-createaccount": "建立這個使用者帳號",
+ "action-history": "查閱此頁面歷史",
"action-minoredit": "標示此編輯為小修訂",
"action-move": "移動這個頁面",
"action-move-subpages": "移動這個頁面跟它的子頁面",
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 (
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);
"RajeshPandey",
"सरोज कुमार ढकाल"
]
- }
+ },
+ "ooui-dialog-action-close": "बन्द गर्ने",
+ "ooui-outline-control-move-down": "वस्तुलाई तल सार्ने",
+ "ooui-outline-control-move-up": "वस्तुलाई माथि सार्ने",
+ "ooui-outline-control-remove": "वस्तुलाई हटाउने",
+ "ooui-toolbar-more": "थप"
}
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;
/*!
- * 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 ) {
* @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,
* @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
* @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 );
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();
}
* @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
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 ) {
* @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 );
* @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;
* @inheritdoc
*/
OO.ui.PopupTool.prototype.onSelect = function () {
- if ( !this.disabled ) {
+ if ( !this.isDisabled() ) {
if ( this.popup.isVisible() ) {
this.hidePopup();
} else {
* @fires click
*/
OO.ui.ButtonWidget.prototype.onClick = function () {
- if ( !this.disabled ) {
+ if ( !this.isDisabled() ) {
this.emit( 'click' );
if ( this.isHyperlink ) {
return true;
* @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;
// 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 );
* @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() );
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;
};
* @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' ) );
* @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();
};
/**
* @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();
};
/**
* @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();
};
/**
* @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' );
* @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' );
* @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' );
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
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() ) {
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;
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 );
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 );
}
* @param {jQuery.Event} e Mouse over event
*/
OO.ui.SelectWidget.prototype.onMouseLeave = function () {
- if ( !this.disabled ) {
+ if ( !this.isDisabled() ) {
this.highlightItem( null );
}
return false;
handled = false,
highlightItem = this.getHighlightedItem();
- if ( !this.disabled && this.visible ) {
+ if ( !this.isDisabled() && this.visible ) {
if ( !highlightItem ) {
highlightItem = this.getSelectedItem();
}
return;
}
- if ( !this.disabled ) {
+ if ( !this.isDisabled() ) {
if ( this.menu.isVisible() ) {
this.menu.hide();
} else {
return;
}
- if ( !this.disabled ) {
+ if ( !this.isDisabled() ) {
if ( this.popup.isVisible() ) {
this.hidePopup();
} else {
* @inheritdoc
*/
OO.ui.ToggleButtonWidget.prototype.onClick = function () {
- if ( !this.disabled ) {
+ if ( !this.isDisabled() ) {
this.setValue( !this.value );
}
* @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 );
}
};
/*!
- * 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
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
-webkit-transition: @string;
transition: @string;
}
+
+.box-sizing(@value) {
+ -moz-box-sizing: @value;
+ -webkit-box-sizing: @value;
+ box-sizing: @value;
+}
-.box-sizing(@value) {
- -moz-box-sizing: @value;
- -webkit-box-sizing: @value;
- box-sizing: @value;
-}
-
.agora-flush-left() {
float: left;
margin-left: 0;
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;
}
}
}
+
+ &.first {
+ background-image: none;
+ margin-top: 0;
+ h3 {
+ display: none;
+ }
+ div.body {
+ margin-left: 0.5em;
+ }
+ }
}
}
#ca-watch.icon a {
margin: 0;
padding: 0;
- outline: none;
display: block;
width: 26px;
/* This hides the text but shows the background image */
// 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;
.attr( 'tabindex', '-1' );
} );
+ /**
+ * Sidebar
+ */
+ $( '#mw-panel > .portal:first' ).addClass( 'first' );
+
/**
* Collapsible tabs for Vector
*/
--- /dev/null
+{\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
--- /dev/null
+<?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() );
+ }
+
+}
} );
} );
+ 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 ) );