* The deprecated function mw.util.toggleToc was removed.
* The Special:Search hooks SpecialSearchGo and SpecialSearchResultsAppend
were removed as they were unused.
+* mediawiki.util.$content no longer supports old versions of the Vector,
+ Monobook, Modern and CologneBlue skins that don't yet implement the "mw-body"
+ and/or "mw-body-primary" class name in their html.
==== Renamed classes ====
* CLDRPluralRuleConverter_Expression to CLDRPluralRuleConverterExpression
*/
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
*/
$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 );
*
* @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 );
}
$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' ),
}
}
+ 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": "移動這個頁面跟它的子頁面",
$bookstoreList = array(
"AddALL" => "http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN",
- "PriceSCAN" => "http://www.pricescan.com/books/bookDetail.asp?isbn=$1",
"Barnes & Noble" => "http://search.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1",
"Amazon.com" => "http://www.amazon.com/exec/obidos/ISBN=$1",
"Amazon.co.uk" => "http://www.amazon.co.uk/exec/obidos/ISBN=$1"
*/
$bookstoreList = array(
'AddALL' => 'http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN',
- 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1',
'Barnes & Noble' => 'http://search.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1',
'Amazon.com' => 'http://www.amazon.com/gp/search/?field-isbn=$1'
);
'minu Raamat' => 'http://www.raamat.ee/advanced_search_result.php?keywords=$1',
'Raamatukoi' => 'http://www.raamatukoi.ee/cgi-bin/index?valik=otsing&paring=$1',
'AddALL' => 'http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN',
- 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1',
'Barnes & Noble' => 'http://search.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1',
'Amazon.com' => 'http://www.amazon.com/exec/obidos/ISBN=$1'
);
'Barnes & Noble' => 'http://search.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1',
'Bhinneka.com bookstore' => 'http://www.bhinneka.com/Buku/Engine/search.asp?fisbn=$1',
'Gramedia Cyberstore (via Google)' => 'http://www.google.com/search?q=%22ISBN+:+$1%22+%22product_detail%22+site:www.gramediacyberstore.com+OR+site:www.gramediaonline.com+OR+site:www.kompas.com&hl=id',
- 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1',
);
$magicWords = array(
'Яндекс.Маркет' => 'http://market.yandex.ru/search.xml?text=$1',
'Amazon.com' => 'http://www.amazon.com/exec/obidos/ISBN=$1',
'AddALL' => 'http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN',
- 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1',
'Barnes & Noble' => 'http://shop.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1'
);
$bookstoreList = array(
'AddALL' => 'http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN',
- 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1',
'Barnes & Noble' => 'http://search.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1',
'亞馬遜' => 'http://www.amazon.com/exec/obidos/ISBN=$1',
'博客來書店' => 'http://www.books.com.tw/exep/prod/booksfile.php?item=$1',
$bookstoreList = array(
'AddALL' => 'http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN',
- 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1',
'Barnes & Noble' => 'http://search.barnesandnoble.com/bookSearch/isbnInquiry.asp?isbn=$1',
'亚马逊' => 'http://www.amazon.com/exec/obidos/ISBN=$1',
'卓越亚马逊' => 'http://www.amazon.cn/mn/advancedSearchApp?isbn=$1',
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;
+}
.removeClass( 'client-nojs' );
$( function () {
- // Initialize utilities as soon as the document is ready (mw.util.$content).
- // In the domready here instead of in mediawiki.page.ready to ensure that it gets enqueued
- // before other modules hook into domready, so that mw.util.$content (defined by
- // mw.util.init), is defined for them.
mw.util.init();
/**
// Form elements and layouts
+@import "mediawiki.mixins";
@import "../../mixins/utilities";
@import "../../mixins/forms";
-.box-sizing(@value) {
- -moz-box-sizing: @value;
- -webkit-box-sizing: @value;
- box-sizing: @value;
-}
-
.agora-flush-left() {
float: left;
margin-left: 0;
* Ignored (and defaulted to `true`) if the document-ready event has already occurred.
*/
function addScript( src, callback, async ) {
- /*jshint evil:true */
- var script, head, done;
-
- // Using isReady directly instead of storing it locally from
- // a $.fn.ready callback (bug 31895).
+ // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
if ( $.isReady || async ) {
- // Can't use jQuery.getScript because that only uses <script> for cross-domain,
- // it uses XHR and eval for same-domain scripts, which we don't want because it
- // messes up line numbers.
- // The below is based on jQuery ([jquery@1.9.1]/src/ajax/script.js)
-
- // IE-safe way of getting an append target. In old IE document.head isn't supported
- // and its getElementsByTagName can't find <head> until </head> is parsed.
- done = false;
- head = document.head || document.getElementsByTagName( 'head' )[0] || document.documentElement;
-
- script = document.createElement( 'script' );
- script.async = true;
- script.src = src;
- if ( $.isFunction( callback ) ) {
- script.onload = script.onreadystatechange = function () {
- if (
- !done
- && (
- !script.readyState
- || /loaded|complete/.test( script.readyState )
- )
- ) {
- done = true;
-
- // Handle memory leak in IE
- script.onload = script.onreadystatechange = null;
-
- // Detach the element from the document
- if ( script.parentNode ) {
- script.parentNode.removeChild( script );
- }
-
- // Dereference the element from javascript
- script = undefined;
-
- callback();
- }
- };
- }
-
- if ( window.opera ) {
- // Appending to the <head> blocks rendering completely in Opera,
- // so append to the <body> after document ready. This means the
- // scripts only start loading after the document has been rendered,
- // but so be it. Opera users don't deserve faster web pages if their
- // browser makes it impossible.
- $( function () {
- document.body.appendChild( script );
- } );
- } else {
- // Circumvent IE6 bugs with base elements (jqbug.com/2709, jqbug.com/4378)
- // by prepending instead of appending.
- head.insertBefore( script, head.firstChild );
- }
+ $.ajax( {
+ url: src,
+ dataType: 'script',
+ // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
+ // XHR for a same domain request instead of <script>, which changes the request
+ // headers (potentially missing a cache hit), and reduces caching in general
+ // since browsers cache XHR much less (if at all). And XHR means we retreive
+ // text, so we'd need to $.globalEval, which then messes up line numbers.
+ crossDomain: true,
+ cache: true,
+ async: true
+ } ).always( function () {
+ if ( callback ) {
+ callback();
+ }
+ } );
} else {
+ /*jshint evil:true */
document.write( mw.html.element( 'script', { 'src': src }, '' ) );
- if ( $.isFunction( callback ) ) {
- // Document.write is synchronous, so this is called when it's done
- // FIXME: that's a lie. doc.write isn't actually synchronous
+ if ( callback ) {
+ // Document.write is synchronous, so this is called when it's done.
+ // FIXME: That's a lie. doc.write isn't actually synchronous.
callback();
}
}
* (don't call before document ready)
*/
init: function () {
- /* Fill $content var */
util.$content = ( function () {
- var i, l, $content, selectors;
+ var i, l, $node, selectors;
+
selectors = [
- // The preferred standard for setting $content (class="mw-body")
- // You may also use (class="mw-body mw-body-primary") if you use
- // mw-body in multiple locations.
- // Or class="mw-body-primary" if you want $content to be deeper
- // in the dom than mw-body
+ // The preferred standard is class "mw-body".
+ // You may also use class "mw-body mw-body-primary" if you use
+ // mw-body in multiple locations. Or class "mw-body-primary" if
+ // you use mw-body deeper in the DOM.
'.mw-body-primary',
'.mw-body',
- /* Legacy fallbacks for setting the content */
- // Vector, Monobook, Chick, etc... based skins
- '#bodyContent',
-
- // Modern based skins
- '#mw_contentholder',
-
- // Standard, CologneBlue
- '#article',
-
- // #content is present on almost all if not all skins. Most skins (the above cases)
- // have #content too, but as an outer wrapper instead of the article text container.
- // The skins that don't have an outer wrapper do have #content for everything
- // so it's a good fallback
- '#content',
-
- // If nothing better is found fall back to our bodytext div that is guaranteed to be here
+ // If the skin has no such class, fall back to the parser output
'#mw-content-text',
- // Should never happen... well, it could if someone is not finished writing a skin and has
- // not inserted bodytext yet. But in any case <body> should always exist
+ // Should never happen... well, it could if someone is not finished writing a
+ // skin and has not yet inserted bodytext yet.
'body'
];
+
for ( i = 0, l = selectors.length; i < l; i++ ) {
- $content = $( selectors[i] ).first();
- if ( $content.length ) {
- return $content;
+ $node = $( selectors[i] );
+ if ( $node.length ) {
+ return $node.first();
}
}
- // Make sure we don't unset util.$content if it was preset and we don't find anything
+ // Preserve existing customized value in case it was preset
return util.$content;
}() );
},
$nodes.updateTooltipAccessKeys();
},
- /*
+ /**
+ * The content wrapper of the skin (e.g. `.mw-body`).
+ *
+ * Populated on document ready by #init. To use this property,
+ * wait for `$.ready` and be sure to have a module depedendency on
+ * `mediawiki.util` and `mediawiki.page.startup` which will ensure
+ * your document ready handler fires after #init.
+ *
+ * Because of the lazy-initialised nature of this property,
+ * you're discouraged from using it.
+ *
+ * If you need just the wikipage content (not any of the
+ * extra elements output by the skin), use `$( '#mw-content-text' )`
+ * instead. Or listen to mw.hook#wikipage_content which will
+ * allow your code to re-run when the page changes (e.g. live preview
+ * or re-render after ajax save).
+ *
* @property {jQuery}
- * A jQuery object that refers to the content area element.
- * Populated by #init.
*/
$content: null,
#ca-watch.icon a {
margin: 0;
padding: 0;
- outline: none;
display: block;
width: 26px;
/* This hides the text but shows the background image */
public static function main( $exit = true ) {
$command = new self;
-
- # Makes MediaWiki PHPUnit directory includable so the PHPUnit will
- # be able to resolve relative files inclusion such as suites/*
- # PHPUnit uses stream_resolve_include_path() internally
- # See bug 32022
- set_include_path(
- __DIR__
- . PATH_SEPARATOR
- . get_include_path()
- );
-
$command->run( $_SERVER['argv'], $exit );
}
}
}
- public function run( array $argv, $exit = true ) {
- wfProfileIn( __METHOD__ );
-
- $ret = parent::run( $argv, false );
-
- wfProfileOut( __METHOD__ );
-
- // Return to real wiki db, so profiling data is preserved
- MediaWikiTestCase::teardownTestDB();
-
- // Log profiling data, e.g. in the database or UDP
- wfLogProfilingData();
-
- if ( $exit ) {
- exit( $ret );
- } else {
- return $ret;
- }
- }
-
public function showHelp() {
parent::showHelp();
EOF;
require_once __DIR__ . "/phpunit.php";
}
+
+class MediaWikiPHPUnitBootstrap {
+
+ public function __construct() {
+ wfProfileIn( __CLASS__ );
+ }
+
+ public function __destruct() {
+ wfProfileOut( __CLASS__ );
+
+ // Return to real wiki db, so profiling data is preserved
+ MediaWikiTestCase::teardownTestDB();
+
+ // Log profiling data, e.g. in the database or UDP
+ wfLogProfilingData();
+ }
+
+}
+
+// This will be destructed after all tests have been run
+$mediawikiPHPUnitBootstrap = new MediaWikiPHPUnitBootstrap();
--- /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() );
+ }
+
+}
array_splice( $_SERVER['argv'], 1, 0, '--colors' );
}
}
+
+ # Makes MediaWiki PHPUnit directory includable so the PHPUnit will
+ # be able to resolve relative files inclusion such as suites/*
+ # PHPUnit uses stream_resolve_include_path() internally
+ # See bug 32022
+ $key = array_search( '--include-path', $_SERVER['argv'] );
+ if( $key === false ) {
+ array_splice( $_SERVER['argv'], 1, 0,
+ __DIR__
+ . PATH_SEPARATOR
+ . get_include_path()
+ );
+ array_splice( $_SERVER['argv'], 1, 0, '--include-path' );
+ }
}
public function getDbType() {
} );
} );
+ 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 ) );