array.
* (bug 29753) mw.util.tooltipAccessKeyPrefix should be alt-shift for Chrome
on Windows
-* (bug 25095) Special:Categories should also include the first relevant item
- when "from" is filled.
* (bug 12262) Indents and lists are now aligned
* (bug 34972) An error occurred while changing your watchlist settings for
[[Special:WhatLinksHere/Example]]
$wgDebugLogFile['memcached'] to some filepath.
* (bug 35685) api.php URL and other entry point URLs are now listed on
Special:Version
+* Edit notices can now be translated.
=== Bug fixes in 1.20 ===
* (bug 30245) Use the correct way to construct a log page title.
required.
&$apis: array of services
+'ApiTokensGetTokenTypes': use this hook to extend action=tokens with new
+token types.
+&$tokenTypes: supported token types in format 'type' => callback function
+used to retrieve this type of tokens.
+
'ArticleAfterFetchContent': after fetching content of an article from
the database
$article: the article (object) being loaded from the database
$user: user who performed the deletion
$reason: reason
+'FileTransformed': When a file is transformed and moved into storage
+$file: reference to the File object
+$thumb: the MediaTransformOutput object
+$tmpThumbPath: The temporary file system path of the transformed file
+$thumbPath: The permanent storage path of the transformed file
+
'FileUpload': When a file upload occurs
$file : Image object representing the file that was uploaded
$reupload : Boolean indicating if there was a previously another image there or not (since 1.17)
'FileBackend' => 'includes/filerepo/backend/FileBackend.php',
'FileBackendStore' => 'includes/filerepo/backend/FileBackendStore.php',
'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackendStore.php',
+ 'FileBackendStoreShardDirIterator' => 'includes/filerepo/backend/FileBackendStore.php',
+ 'FileBackendStoreShardFileIterator' => 'includes/filerepo/backend/FileBackendStore.php',
'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
+ 'FSFileBackendList' => 'includes/filerepo/backend/FSFileBackend.php',
+ 'FSFileBackendDirList' => 'includes/filerepo/backend/FSFileBackend.php',
'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
+ 'SwiftFileBackendList' => 'includes/filerepo/backend/SwiftFileBackend.php',
+ 'SwiftFileBackendDirList' => 'includes/filerepo/backend/SwiftFileBackend.php',
'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
'FileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
'DBFileJournal' => 'includes/filerepo/backend/filejournal/DBFileJournal.php',
# Optional notices on a per-namespace and per-page basis
$editnotice_ns = 'editnotice-' . $this->mTitle->getNamespace();
- $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage();
+ $editnotice_ns_message = wfMessage( $editnotice_ns );
if ( $editnotice_ns_message->exists() ) {
$wgOut->addWikiText( $editnotice_ns_message->plain() );
}
$editnotice_base = $editnotice_ns;
while ( count( $parts ) > 0 ) {
$editnotice_base .= '-' . array_shift( $parts );
- $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage();
+ $editnotice_base_msg = wfMessage( $editnotice_base );
if ( $editnotice_base_msg->exists() ) {
$wgOut->addWikiText( $editnotice_base_msg->plain() );
}
} else {
# Even if there are no subpages in namespace, we still don't want / in MW ns.
$editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->mTitle->getDBkey() );
- $editnoticeMsg = wfMessage( $editnoticeText )->inContentLanguage();
+ $editnoticeMsg = wfMessage( $editnoticeText );
if ( $editnoticeMsg->exists() ) {
$wgOut->addWikiText( $editnoticeMsg->plain() );
}
$extraQuery
);
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
- // Drop modules that know they're empty
+ // Extract modules that know they're empty
+ $emptyModules = array ();
foreach ( $modules as $key => $module ) {
if ( $module->isKnownEmpty( $context ) ) {
+ $emptyModules[$key] = 'ready';
unset( $modules[$key] );
}
}
+ // Inline empty modules: since they're empty, just mark them as 'ready'
+ if ( count( $emptyModules ) > 0 && $only !== ResourceLoaderModule::TYPE_STYLES ) {
+ // If we're only getting the styles, we don't need to do anything for empty modules.
+ $links .= Html::inlineScript(\r
+ ResourceLoader::makeLoaderConditionalScript(\r
+ ResourceLoader::makeLoaderStateScript( $emptyModules )\r
+ )\r
+ ) . "\n";
+ }
+
// If there are no modules left, skip this group
if ( $modules === array() ) {
continue;
// Automatically select style/script elements
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
$link = Html::linkedStyle( $url );
- } else if ( $loadCall ) {
+ } else if ( $loadCall ) {
$link = Html::inlineScript(
ResourceLoader::makeLoaderConditionalScript(
Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) )
*/
function getHeadScripts() {
global $wgResourceLoaderExperimentalAsyncLoading;
-
+
// Startup - this will immediately load jquery and mediawiki modules
$scripts = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
)
);
}
-
+
if ( $wgResourceLoaderExperimentalAsyncLoading ) {
$scripts .= $this->getScriptsForBottomQueue( true );
}
// Legacy Scripts
$scripts .= "\n" . $this->mScripts;
- $userScripts = array();
+ $defaultModules = array();
// Add site JS if enabled
if ( $wgUseSiteJs ) {
$scripts .= $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS,
/* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
);
- if( $this->getUser()->isLoggedIn() ){
- $userScripts[] = 'user.groups';
- }
+ $defaultModules['site'] = 'loading';
+ } else {
+ // The wiki is configured to not allow a site module.
+ $defaultModules['site'] = 'missing';
}
// Add user JS if enabled
- if ( $wgAllowUserJs && $this->getUser()->isLoggedIn() ) {
- if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
- # XXX: additional security check/prompt?
- // We're on a preview of a JS subpage
- // Exclude this page from the user module in case it's in there (bug 26283)
- $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
- array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
- );
- // Load the previewed JS
- $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
+ if ( $wgAllowUserJs ) {
+ if ( $this->getUser()->isLoggedIn() ) {
+ if( $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) {
+ # XXX: additional security check/prompt?
+ // We're on a preview of a JS subpage
+ // Exclude this page from the user module in case it's in there (bug 26283)
+ $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false,
+ array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead
+ );
+ // Load the previewed JS
+ $scripts .= Html::inlineScript( "\n" . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n";
+ // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
+ // asynchronously and may arrive *after* the inline script here. So the previewed code
+ // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js...
+ } else {
+ // Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
+ $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+ /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
+ );
+ }
+ $defaultModules['user'] = 'loading';
} else {
- // Include the user module normally
- // We can't do $userScripts[] = 'user'; because the user module would end up
- // being wrapped in a closure, so load it raw like 'site'
- $scripts .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS,
+ // Non-logged-in users have no user module. Treat it as empty and 'ready' to avoid
+ // blocking default gadgets that might depend on it. Although arguably default-enabled
+ // gadgets should not depend on the user module, it's harmless and less error-prone to
+ // handle this case.
+ $defaultModules['user'] = 'ready';
+ }
+ } else {
+ // User JS diabled
+ $defaultModules['user'] = 'missing';
+ }
+
+ // Group JS is only enabled if site JS is enabled.
+ if ( $wgUseSiteJs ) {
+ if ( $this->getUser()->isLoggedIn() ) {
+ $scripts .= $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED,
/* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
);
+ $defaultModules['user.groups'] = 'loading';
+ } else {
+ // Non-logged-in users have no user.groups module. Treat it as empty and 'ready' to
+ // avoid blocking gadgets that might depend upon the module.
+ $defaultModules['user.groups'] = 'ready';
}
+ } else {
+ // Site (and group JS) disabled
+ $defaultModules['user.groups'] = 'missing';
}
- $scripts .= $this->makeResourceLoaderLink( $userScripts, ResourceLoaderModule::TYPE_COMBINED,
- /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead
- );
- return $scripts;
+ $loaderInit = '';
+ if ( $inHead ) {
+ // We generate loader calls anyway, so no need to fix the client-side loader's state to 'loading'.
+ foreach ( $defaultModules as $m => $state ) {
+ if ( $state == 'loading' ) {
+ unset( $defaultModules[$m] );
+ }
+ }
+ }
+ if ( count( $defaultModules ) > 0 ) {
+ $loaderInit = Html::inlineScript(
+ ResourceLoader::makeLoaderConditionalScript(
+ ResourceLoader::makeLoaderStateScript( $defaultModules )
+ )
+ ) . "\n";
+ }
+ return $loaderInit . $scripts;
}
/**
}
/**
- * @param $unused
- * @param $addContentType bool
+ * @param $addContentType bool: Whether <meta> specifying content type should be returned
*
- * @return string HTML tag links to be put in the header.
+ * @return array in format "link name or number => 'link html'".
*/
- public function getHeadLinks( $unused = null, $addContentType = false ) {
+ public function getHeadLinksArray( $addContentType = false ) {
global $wgUniversalEditButton, $wgFavicon, $wgAppleTouchIcon, $wgEnableAPI,
$wgSitename, $wgVersion, $wgHtml5, $wgMimeType,
$wgFeed, $wgOverrideSiteFeed, $wgAdvertisedFeedTypes,
if ( $wgHtml5 ) {
# More succinct than <meta http-equiv=Content-Type>, has the
# same effect
- $tags[] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
+ $tags['meta-charset'] = Html::element( 'meta', array( 'charset' => 'UTF-8' ) );
} else {
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-content-type'] = Html::element( 'meta', array(
'http-equiv' => 'Content-Type',
'content' => "$wgMimeType; charset=UTF-8"
) );
- $tags[] = Html::element( 'meta', array( // bug 15835
+ $tags['meta-content-style-type'] = Html::element( 'meta', array( // bug 15835
'http-equiv' => 'Content-Style-Type',
'content' => 'text/css'
) );
}
}
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-generator'] = Html::element( 'meta', array(
'name' => 'generator',
'content' => "MediaWiki $wgVersion",
) );
if( $p !== 'index,follow' ) {
// http://www.robotstxt.org/wc/meta-user.html
// Only show if it's different from the default robots policy
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-robots'] = Html::element( 'meta', array(
'name' => 'robots',
'content' => $p,
) );
"/<.*?" . ">/" => '',
"/_/" => ' '
);
- $tags[] = Html::element( 'meta', array(
+ $tags['meta-keywords'] = Html::element( 'meta', array(
'name' => 'keywords',
'content' => preg_replace(
array_keys( $strip ),
} else {
$a = 'name';
}
- $tags[] = Html::element( 'meta',
+ $tagName = "meta-{$tag[0]}";
+ if ( isset( $tags[$tagName] ) ) {
+ $tagName .= $tag[1];
+ }
+ $tags[$tagName] = Html::element( 'meta',
array(
$a => $tag[0],
'content' => $tag[1]
&& ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create', $user ) ) ) {
// Original UniversalEditButton
$msg = $this->msg( 'edit' )->text();
- $tags[] = Html::element( 'link', array(
+ $tags['universal-edit-button'] = Html::element( 'link', array(
'rel' => 'alternate',
'type' => 'application/x-wiki',
'title' => $msg,
'href' => $this->getTitle()->getLocalURL( 'action=edit' )
) );
// Alternate edit link
- $tags[] = Html::element( 'link', array(
+ $tags['alternative-edit'] = Html::element( 'link', array(
'rel' => 'edit',
'title' => $msg,
'href' => $this->getTitle()->getLocalURL( 'action=edit' )
# uses whichever one appears later in the HTML source. Make sure
# apple-touch-icon is specified first to avoid this.
if ( $wgAppleTouchIcon !== false ) {
- $tags[] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
+ $tags['apple-touch-icon'] = Html::element( 'link', array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
}
if ( $wgFavicon !== false ) {
- $tags[] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
+ $tags['favicon'] = Html::element( 'link', array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
}
# OpenSearch description link
- $tags[] = Html::element( 'link', array(
+ $tags['opensearch'] = Html::element( 'link', array(
'rel' => 'search',
'type' => 'application/opensearchdescription+xml',
'href' => wfScript( 'opensearch_desc' ),
# for the MediaWiki API (and potentially additional custom API
# support such as WordPress or Twitter-compatible APIs for a
# blogging extension, etc)
- $tags[] = Html::element( 'link', array(
+ $tags['rsd'] = Html::element( 'link', array(
'rel' => 'EditURI',
'type' => 'application/rsd+xml',
// Output a protocol-relative URL here if $wgServer is protocol-relative
if ( !$urlvar ) {
$variants = $lang->getVariants();
foreach ( $variants as $_v ) {
- $tags[] = Html::element( 'link', array(
+ $tags["variant-$_v"] = Html::element( 'link', array(
'rel' => 'alternate',
'hreflang' => $_v,
'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) )
);
}
} else {
- $tags[] = Html::element( 'link', array(
+ $tags['canonical'] = Html::element( 'link', array(
'rel' => 'canonical',
'href' => $this->getTitle()->getCanonicalUrl()
) );
}
if ( $copyright ) {
- $tags[] = Html::element( 'link', array(
+ $tags['copyright'] = Html::element( 'link', array(
'rel' => 'copyright',
'href' => $copyright )
);
}
}
}
- return implode( "\n", $tags );
+ return $tags;
+ }
+
+ /**
+ * @param $unused
+ * @param $addContentType bool: Whether <meta> specifying content type should be returned
+ *
+ * @return string HTML tag links to be put in the header.
+ */
+ public function getHeadLinks( $unused = null, $addContentType = false ) {
+ return implode( "\n", $this->getHeadLinksArray( $addContentType ) );
}
/**
$otherTags .= $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false,
array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() )
);
-
+
// Load the previewed CSS
// If needed, Janus it first. This is user-supplied CSS, so it's
// assumed to be right for the content language directionality.
* the relevant contents
* - The default behavior is equivalent to `array( 'title' => '$1' )`,
* if you don't want the title parameter you can explicitly use `array( 'title' => false )`
- * - You can specify a value that won't have replacements in it
+ * - You can specify a value that won't have replacements in it
* using `'foo' => array( 'value' => 'bar' );`
*
* Options:
*/
class PathRouter {
+ /**
+ * @var array
+ */
+ private $patterns = array();
+
/**
* Protected helper to do the actual bulk work of adding a single pattern.
* This is in a separate method so that add() can handle the difference between
* a single string $path and an array() $path that contains multiple path
* patterns each with an associated $key to pass on.
+ * @param $path string
+ * @param $params array
+ * @param $options array
+ * @param $key null|string
*/
protected function doAdd( $path, $params, $options, $key = null ) {
// Make sure all paths start with a /
/**
* Add a new path pattern to the path router with the strict option on
* @see self::add
+ * @param $path string|array
+ * @param $params array
+ * @param $options array
*/
public function addStrict( $path, $params = array(), $options = array() ) {
$options['strict'] = true;
array_multisort( $weights, SORT_DESC, SORT_NUMERIC, $this->patterns );
}
+ /**
+ * @param $pattern object
+ * @return float|int
+ */
protected static function makeWeight( $pattern ) {
# Start with a weight of 0
$weight = 0;
// Make sure our patterns are sorted by weight so the most specific
// matches are tested first
$this->sortByWeight();
-
+
$matches = null;
foreach ( $this->patterns as $pattern ) {
return is_null( $matches ) ? array() : $matches;
}
+ /**
+ * @param $path string
+ * @param $pattern string
+ * @return array|null
+ */
protected static function extractTitle( $path, $pattern ) {
// Convert the path pattern into a regexp we can match with
$regexp = preg_quote( $pattern->path, '#' );
* We do this inside of a replacement callback because after replacement we can't tell the
* difference between a $1 that was not replaced and a $1 that was part of
* the content a $1 was replaced with.
+ * @param $value string
+ * @return string
*/
public function replace( $value ) {
$this->error = false;
return $value;
}
+ /**
+ * @param $m array
+ * @return string
+ */
protected function callback( $m ) {
if ( $m[1] == "key" ) {
if ( is_null( $this->key ) ) {
/**
* HTML-safe version of appendQuery().
+ * @deprecated: Deprecated in 1.20, warnings in 1.21, remove in 1.22.
*
* @param $query String: query string fragment; do not include initial '?'
* @return String
* @return WikiPage|null
*/
public static function newFromID( $id ) {
- $t = Title::newFromID( $id );
- if ( $t ) {
- return self::factory( $t );
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
+ if ( !$row ) {
+ return null;
}
- return null;
+ return self::newFromRow( $row );
+ }
+
+ /**
+ * Constructor from a database row
+ *
+ * @since 1.20
+ * @param $row object: database row containing at least fields returned
+ * by selectFields().
+ * @return WikiPage
+ */
+ public static function newFromRow( $row ) {
+ $page = self::factory( Title::newFromRow( $row ) );
+ $page->loadFromRow( $row );
+ return $page;
}
/**
}
}
+ $this->loadFromRow( $data );
+ }
+
+ /**
+ * Load the object from a database row
+ *
+ * @since 1.20
+ * @param $data object: database row containing at least fields returned
+ * by selectFields()
+ */
+ public function loadFromRow( $data ) {
$lc = LinkCache::singleton();
if ( $data ) {
if ( !$titleObj ) {
$this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
}
+ $pageObj = WikiPage::factory( $titleObj );
+ $pageObj->loadPageData( 'fromdbmaster' );
} elseif ( isset( $params['pageid'] ) ) {
- $titleObj = Title::newFromID( $params['pageid'] );
- if ( !$titleObj ) {
+ $pageObj = WikiPage::newFromID( $params['pageid'] );
+ if ( !$pageObj ) {
$this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
}
+ $titleObj = $pageObj->getTitle();
}
$errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() );
$watch = $params['watch'] ? 'watch' : $params['watchlist'];
$this->setWatch( $watch, $titleObj );
- $pageObj = WikiPage::factory( $titleObj );
$status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() );
if ( !$status->isOK() ) {
$lastUserData = null;
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from',
- $this->keyToTitle( $lastUserData['name'] ) );
+ $this->setContinueEnumParameter( 'from', $lastUserData['name'] );
break;
}
}
if ( $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->user_name ) );
+ $this->setContinueEnumParameter( 'from', $row->user_name );
break;
}
'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' );
}
- $lastUserObj = User::newFromName( $lastUser );
+ $lastUserObj = User::newFromId( $row->user_id );
// Add user's group info
if ( $fld_groups ) {
- if ( !isset( $lastUserData['groups'] ) && $lastUserObj ) {
- $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj );
+ if ( !isset( $lastUserData['groups'] ) ) {
+ if ( $lastUserObj ) {
+ $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj );
+ } else {
+ // This should not normally happen
+ $lastUserData['groups'] = array();
+ }
}
if ( !is_null( $row->ug_group2 ) ) {
$lastUserData['groups'][] = $row->ug_group2;
}
+
$result->setIndexedTagName( $lastUserData['groups'], 'g' );
}
$result->setIndexedTagName( $lastUserData['implicitgroups'], 'g' );
}
if ( $fld_rights ) {
- if ( !isset( $lastUserData['rights'] ) && $lastUserObj ) {
- $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+ if ( !isset( $lastUserData['rights'] ) ) {
+ if ( $lastUserObj ) {
+ $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() );
+ } else {
+ // This should not normally happen
+ $lastUserData['rights'] = array();
+ }
}
+
if ( !is_null( $row->ug_group2 ) ) {
$lastUserData['rights'] = array_unique( array_merge( $lastUserData['rights'],
User::getGroupPermissions( array( $row->ug_group2 ) ) ) );
}
+
$result->setIndexedTagName( $lastUserData['rights'], 'r' );
}
}
$fit = $result->addValue( array( 'query', $this->getModuleName() ),
null, $lastUserData );
if ( !$fit ) {
- $this->setContinueEnumParameter( 'from',
- $this->keyToTitle( $lastUserData['name'] ) );
+ $this->setContinueEnumParameter( 'from', $lastUserData['name'] );
}
}
}
public function execute() {
+ wfProfileIn( __METHOD__ );
$params = $this->extractRequestParams();
$res = array();
+ $types = $this->getTokenTypes();
foreach ( $params['type'] as $type ) {
$type = strtolower( $type );
- $func = 'get' .
- ucfirst( $type ) .
- 'Token';
- if ( $type === 'patrol' ) {
- $val = call_user_func( array( 'ApiQueryRecentChanges', $func ), null, null );
- } else {
- $val = call_user_func( array( 'ApiQueryInfo', $func ), null, null );
- }
+
+ $val = call_user_func( $types[$type], null, null );
+
if ( $val === false ) {
$this->setWarning( "Action '$type' is not allowed for the current user" );
} else {
}
$this->getResult()->addValue( null, $this->getModuleName(), $res );
+ wfProfileOut( __METHOD__ );
+ }
+
+ private function getTokenTypes() {
+ static $types = null;
+ if ( $types ) {
+ return $types;
+ }
+ wfProfileIn( __METHOD__ );
+ $types = array( 'patrol' => 'ApiQueryRecentChanges::getPatrolToken' );
+ $names = array( 'edit', 'delete', 'protect', 'move', 'block', 'unblock',
+ 'email', 'import', 'watch' );
+ foreach ( $names as $name ) {
+ $types[$name] = 'ApiQUeryInfo::get' . ucfirst( $name ) . 'Token';
+ }
+ wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) );
+ ksort( $types );
+ wfProfileOut( __METHOD__ );
+ return $types;
}
public function getAllowedParams() {
'type' => array(
ApiBase::PARAM_DFLT => 'edit',
ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => array(
- 'edit', 'delete', 'protect', 'move', 'block', 'unblock',
- 'email', 'import', 'watch', 'patrol'
- )
- )
+ ApiBase::PARAM_TYPE => array_keys( $this->getTokenTypes() ),
+ ),
);
}
/**
* @defgroup Database Database
*
+ * This file deals with database interface functions
+ * and query specifics/optimisations.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @file
* @ingroup Database
- * This file deals with database interface functions
- * and query specifics/optimisations
*/
/** Number of times to re-try an operation in case of deadlock */
<?php
+/**
+ * This file contains database error classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
/**
* Database error base class
/**
* This is the IBM DB2 database abstraction layer.
* See maintenance/ibm_db2/README for development notes
- * and other specific information
+ * and other specific information.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Database
/**
* This is the MS SQL Server Native database abstraction layer.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @file
* @ingroup Database
* @author Joel Penner <a-joelpe at microsoft dot com>
/**
* This is the MySQL database abstraction layer.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @file
* @ingroup Database
*/
/**
* This is the Oracle database abstraction layer.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @file
* @ingroup Database
*/
/**
* This is the Postgres database abstraction layer.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @file
* @ingroup Database
*/
* This is the SQLite database abstraction layer.
* See maintenance/sqlite/README for development notes and other specific information
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @file
* @ingroup Database
*/
<?php
+/**
+ * This file contains database-related utiliy classes.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
/**
* Utility class.
* @ingroup Database
<?php
/**
- * Generator of database load balancing objects
+ * Generator of database load balancing objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Database
<?php
/**
- * Advanced generator of database load balancing objects for wiki farms
+ * Advanced generator of database load balancing objects for wiki farms.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Database
<?php
+/**
+ * Simple generator of database connections that always returns the same object.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
/**
* An LBFactory class that always returns a single database object.
<?php
/**
- * Database load balancing
+ * Database load balancing.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Database
<?php
/**
- * Database load monitoring
+ * Database load monitoring.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Database
<?php
-
/**
* Result of a ORMTable::select, which returns ORMRow objects.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @since 1.20
*
* @file ORMResult.php
* @licence GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
+
class ORMResult implements Iterator {
/**
<?php
-
/**
* Abstract base class for representing objects that are stored in some DB table.
* This is basically an ORM-like wrapper around rows in database tables that
* aims to be both simple and very flexible. It is centered around an associative
* array of fields and various methods to do common interaction with the database.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* These methods are likely candidates for overriding:
* * getDefaults
* * remove
* @licence GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
+
abstract class ORMRow {
/**
<?php
-
/**
* Abstract base class for representing a single database table.
*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
* @since 1.20
*
* @file ORMTable.php
* @licence GNU GPL v2 or later
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
+
abstract class ORMTable {
/**
/**
* Import a file from the local file system into the repo.
* This does no locking nor journaling and overrides existing files.
+ * This function can be used to write to otherwise read-only foreign repos.
* This is intended for copying generated thumbnails into the repo.
*
* @param $src string File system path
/**
* Purge a file from the repo. This does no locking nor journaling.
- * This is intended for purging thumbnail.
+ * This function can be used to write to otherwise read-only foreign repos.
+ * This is intended for purging thumbnails.
*
* @param $path string Virtual URL or storage path
* @return FileRepoStatus
return $this->quickPurgeBatch( array( $path ) );
}
+ /**
+ * Deletes a directory if empty.
+ * This function can be used to write to otherwise read-only foreign repos.
+ *
+ * @param $dir string Virtual URL (or storage path) of directory to clean
+ * @return Status
+ */
+ public function quickCleanDir( $dir ) {
+ $status = $this->newGood();
+ $status->merge( $this->backend->clean(
+ array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
+
+ return $status;
+ }
+
/**
* Import a batch of files from the local file system into the repo.
* This does no locking nor journaling and overrides existing files.
+ * This function can be used to write to otherwise read-only foreign repos.
* This is intended for copying generated thumbnails into the repo.
*
* @param $src Array List of tuples (file system path, virtual URL or storage path)
* @return FileRepoStatus
*/
public function quickImportBatch( array $pairs ) {
- $this->assertWritableRepo(); // fail out if read-only
-
$status = $this->newGood();
$operations = array();
foreach ( $pairs as $pair ) {
}
/**
- * Purge a batch of files from the repo. This does no locking nor journaling.
- * This is intended for purging thumbnails.
+ * Purge a batch of files from the repo.
+ * This function can be used to write to otherwise read-only foreign repos.
+ * This does no locking nor journaling and is intended for purging thumbnails.
*
* @param $path Array List of virtual URLs or storage paths
* @return FileRepoStatus
*/
public function quickPurgeBatch( array $paths ) {
- $this->assertWritableRepo(); // fail out if read-only
-
$status = $this->newGood();
$operations = array();
foreach ( $paths as $path ) {
}
/**
- * Deletes a directory if empty
+ * Deletes a directory if empty.
*
* @param $dir string Virtual URL (or storage path) of directory to clean
* @return Status
/**
* @brief Class for a file system (FS) based file backend.
- *
+ *
* All "containers" each map to a directory under the backend's base directory.
* For backwards-compatibility, some container paths can be set to custom paths.
* The wiki ID will not be used in any custom paths, so this should be avoided.
- *
+ *
* Having directories with thousands of files will diminish performance.
* Sharding can be accomplished by using FileRepo-style hash paths.
*
/**
* Sanity check a relative file system path for validity
- *
+ *
* @param $path string Normalized relative path
* @return bool
*/
/**
* Given the short (unresolved) and full (resolved) name of
* a container, return the file system path of the container.
- *
+ *
* @param $shortCont string
* @param $fullCont string
- * @return string|null
+ * @return string|null
*/
protected function containerFSRoot( $shortCont, $fullCont ) {
if ( isset( $this->containerPaths[$shortCont] ) ) {
- return $this->containerPaths[$shortCont];
+ return $this->containerPaths[$shortCont];
} elseif ( isset( $this->basePath ) ) {
return "{$this->basePath}/{$fullCont}";
}
/**
* Get the absolute file system path for a storage path
- *
+ *
* @param $storagePath string Storage path
* @return string|null
*/
clearstatcache(); // clear the PHP file stat cache
}
+ /**
+ * @see FileBackendStore::doDirectoryExists()
+ * @return bool|null
+ */
+ protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
+ list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+ $this->trapWarnings(); // don't trust 'false' if there were errors
+ $exists = is_dir( $dir );
+ $hadError = $this->untrapWarnings();
+
+ return $hadError ? null : $exists;
+ }
+
+ /**
+ * @see FileBackendStore::getDirectoryListInternal()
+ * @return Array|null
+ */
+ public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
+ list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+ $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+ $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+ $exists = is_dir( $dir );
+ if ( !$exists ) {
+ wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+ return array(); // nothing under this dir
+ } elseif ( !is_readable( $dir ) ) {
+ wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+ return null; // bad permissions?
+ }
+ return new FSFileBackendDirList( $dir, $params );
+ }
+
/**
* @see FileBackendStore::getFileListInternal()
* @return array|FSFileBackendFileList|null
if ( !$exists ) {
wfDebug( __METHOD__ . "() given directory does not exist: '$dir'\n" );
return array(); // nothing under this dir
- }
- $readable = is_readable( $dir );
- if ( !$readable ) {
+ } elseif ( !is_readable( $dir ) ) {
wfDebug( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
return null; // bad permissions?
}
- return new FSFileBackendFileList( $dir );
+ return new FSFileBackendFileList( $dir, $params );
}
/**
}
/**
- * Wrapper around RecursiveDirectoryIterator that catches
- * exception or does any custom behavoir that we may want.
+ * Wrapper around RecursiveDirectoryIterator/DirectoryIterator that
+ * catches exception or does any custom behavoir that we may want.
* Do not use this class from places outside FSFileBackend.
*
* @ingroup FileBackend
*/
-class FSFileBackendFileList implements Iterator {
- /** @var RecursiveIteratorIterator */
+abstract class FSFileBackendList implements Iterator {
+ /** @var Iterator */
protected $iter;
protected $suffixStart; // integer
protected $pos = 0; // integer
+ /** @var Array */
+ protected $params = array();
/**
* @param $dir string file system directory
*/
- public function __construct( $dir ) {
+ public function __construct( $dir, array $params ) {
$dir = realpath( $dir ); // normalize
$this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
+ $this->params = $params;
+
try {
+ $this->iter = $this->initIterator( $dir );
+ } catch ( UnexpectedValueException $e ) {
+ $this->iter = null; // bad permissions? deleted?
+ }
+ }
+
+ /**
+ * Return an appropriate iterator object to wrap
+ *
+ * @param $dir string file system directory
+ * @return Iterator
+ */
+ protected function initIterator( $dir ) {
+ if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
+ # Get an iterator that will get direct sub-nodes
+ return new DirectoryIterator( $dir );
+ } else { // recursive
# Get an iterator that will return leaf nodes (non-directories)
if ( MWInit::classExists( 'FilesystemIterator' ) ) { // PHP >= 5.3
# RecursiveDirectoryIterator extends FilesystemIterator.
# FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
- $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
- $this->iter = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator( $dir, $flags ) );
+ $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
+ return new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $dir, $flags ),
+ RecursiveIteratorIterator::CHILD_FIRST // include dirs
+ );
} else { // PHP < 5.3
# RecursiveDirectoryIterator extends DirectoryIterator
- $this->iter = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator( $dir ) );
+ return new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $dir ),
+ RecursiveIteratorIterator::CHILD_FIRST // include dirs
+ );
}
- } catch ( UnexpectedValueException $e ) {
- $this->iter = null; // bad permissions? deleted?
}
}
- /**
- * @see Iterator::current()
- * @return string|bool String or false
- */
- public function current() {
- // Return only the relative path and normalize slashes to FileBackend-style
- // Make sure to use the realpath since the suffix is based upon that
- return str_replace( '\\', '/',
- substr( realpath( $this->iter->current() ), $this->suffixStart ) );
- }
-
/**
* @see Iterator::key()
* @return integer
return $this->pos;
}
+ /**
+ * @see Iterator::current()
+ * @return string|bool String or false
+ */
+ public function current() {
+ return $this->getRelPath( $this->iter->current()->getPathname() );
+ }
+
/**
* @see Iterator::next()
* @return void
public function next() {
try {
$this->iter->next();
+ $this->filterViaNext();
} catch ( UnexpectedValueException $e ) {
$this->iter = null;
}
$this->pos = 0;
try {
$this->iter->rewind();
+ $this->filterViaNext();
} catch ( UnexpectedValueException $e ) {
$this->iter = null;
}
public function valid() {
return $this->iter && $this->iter->valid();
}
+
+ /**
+ * Filter out items by advancing to the next ones
+ */
+ protected function filterViaNext() {}
+
+ /**
+ * Return only the relative path and normalize slashes to FileBackend-style.
+ * Uses the "real path" since the suffix is based upon that.
+ *
+ * @param $path string
+ * @return string
+ */
+ protected function getRelPath( $path ) {
+ return strtr( substr( realpath( $path ), $this->suffixStart ), '\\', '/' );
+ }
+}
+
+class FSFileBackendDirList extends FSFileBackendList {
+ protected function filterViaNext() {
+ while ( $this->iter->valid() ) {
+ if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
+ $this->iter->next(); // skip non-directories and dot files
+ } else {
+ break;
+ }
+ }
+ }
+}
+
+class FSFileBackendFileList extends FSFileBackendList {
+ protected function filterViaNext() {
+ while ( $this->iter->valid() ) {
+ if ( !$this->iter->current()->isFile() ) {
+ $this->iter->next(); // skip non-files and dot files
+ } else {
+ break;
+ }
+ }
+ }
}
abstract public function getLocalCopy( array $params );
/**
- * Get an iterator to list out all stored files under a storage directory.
+ * Check if a directory exists at a given storage path.
+ * Backends using key/value stores will check if the path is a
+ * virtual directory, meaning there are files under the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * $params include:
+ * dir : storage directory
+ *
+ * @return bool|null Returns null on failure
+ * @since 1.20
+ */
+ abstract public function directoryExists( array $params );
+
+ /**
+ * Get an iterator to list *all* directories under a storage directory.
+ * If the directory is of the form "mwstore://backend/container",
+ * then all directories in the container should be listed.
+ * If the directory is of form "mwstore://backend/container/dir",
+ * then all directories directly under that directory should be listed.
+ * Results should be storage directories relative to the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * $params include:
+ * dir : storage directory
+ * topOnly : only return direct child directories of the directory
+ *
+ * @return Traversable|Array|null Returns null on failure
+ * @since 1.20
+ */
+ abstract public function getDirectoryList( array $params );
+
+ /**
+ * Same as FileBackend::getDirectoryList() except only lists
+ * directories that are immediately under the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * $params include:
+ * dir : storage directory
+ *
+ * @return Traversable|Array|null Returns null on failure
+ * @since 1.20
+ */
+ final public function getTopDirectoryList( array $params ) {
+ return $this->getDirectoryList( array( 'topOnly' => true ) + $params );
+ }
+
+ /**
+ * Get an iterator to list *all* stored files under a storage directory.
* If the directory is of the form "mwstore://backend/container",
* then all files in the container should be listed.
* If the directory is of form "mwstore://backend/container/dir",
- * then all files under that container directory should be listed.
+ * then all files under that directory should be listed.
* Results should be storage paths relative to the given directory.
*
* Storage backends with eventual consistency might return stale data.
*
* $params include:
- * dir : storage path directory
+ * dir : storage directory
+ * topOnly : only return direct child files of the directory
*
* @return Traversable|Array|null Returns null on failure
*/
abstract public function getFileList( array $params );
+ /**
+ * Same as FileBackend::getFileList() except only lists
+ * files that are immediately under the given directory.
+ *
+ * Storage backends with eventual consistency might return stale data.
+ *
+ * $params include:
+ * dir : storage directory
+ *
+ * @return Traversable|Array|null Returns null on failure
+ * @since 1.20
+ */
+ final public function getTopFileList( array $params ) {
+ return $this->getFileList( array( 'topOnly' => true ) + $params );
+ }
+
/**
* Invalidate any in-process file existence and property cache.
* If $paths is given, then only the cache for those files will be cleared.
*
* @param $path string
* @return bool
+ * @since 1.20
*/
final public static function isPathTraversalFree( $path ) {
return ( self::normalizeContainerPath( $path ) !== null );
$status = Status::newGood();
$performOps = array(); // list of FileOp objects
- $filesRead = array(); // storage paths read from
- $filesChanged = array(); // storage paths written to
+ $paths = array(); // storage paths read from or written to
// Build up a list of FileOps. The list will have all the ops
// for one backend, then all the ops for the next, and so on.
// These batches of ops are all part of a continuous array.
if ( $index == 0 ) { // first batch
// Get the files used for these operations. Each backend has a batch of
// the same operations, so we only need to get them from the first batch.
- foreach ( $performOps as $fileOp ) {
- $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() );
- $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() );
- }
+ $paths = $backend->getPathsToLockForOpsInternal( $performOps );
// Get the paths under the proxy backend's name
- $filesRead = $this->unsubstPaths( $filesRead );
- $filesChanged = $this->unsubstPaths( $filesChanged );
+ $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
+ $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
}
}
// Try to lock those files for the scope of this function...
if ( empty( $opts['nonLocking'] ) ) {
- $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization
- $filesLockEx = $filesChanged;
- // Get a shared lock on the parent directory of each path changed
- $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
// Try to lock those files for the scope of this function...
- $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
- $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+ $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
+ $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
if ( !$status->isOK() ) {
return $status; // abort
}
// Do a consistency check to see if the backends agree
if ( count( $this->backends ) > 1 ) {
- $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) );
+ $status->merge( $this->consistencyCheck( array_merge( $paths['sh'], $paths['ex'] ) ) );
if ( !$status->isOK() ) {
return $status; // abort
}
}
/**
- * @see FileBackend::getFileList()
+ * @see FileBackend::concatenate()
*/
public function concatenate( array $params ) {
// We are writing to an FS file, so we don't need to do this per-backend
return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
}
+ /**
+ * @see FileBackend::directoryExists()
+ */
+ public function directoryExists( array $params ) {
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ return $this->backends[$this->masterIndex]->directoryExists( $realParams );
+ }
+
+ /**
+ * @see FileBackend::getSubdirectoryList()
+ */
+ public function getDirectoryList( array $params ) {
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
+ }
+
/**
* @see FileBackend::getFileList()
*/
* @since 1.19
*/
abstract class FileBackendStore extends FileBackend {
+ /** @var BagOStuff */
+ protected $memCache;
+
/** @var Array Map of paths to small (RAM/disk) cache items */
protected $cache = array(); // (storage path => key => value)
protected $maxCacheSize = 100; // integer; max paths with entries
protected $maxFileSize = 4294967296; // integer bytes (4GiB)
+ /**
+ * @see FileBackend::__construct()
+ *
+ * @param $config Array
+ */
+ public function __construct( array $config ) {
+ parent::__construct( $config );
+ $this->memCache = new EmptyBagOStuff(); // disabled by default
+ }
+
/**
* Get the maximum allowable file size given backend
* medium restrictions and basic performance constraints.
if ( $shard !== null ) { // confined to a single container/shard
$status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
+ $this->deleteContainerCache( $fullCont ); // purge cache
} else { // directory is on several shards
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
$status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+ $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
}
}
}
/**
- * @copydoc FileBackend::getFileList()
+ * @see FileBackend::directoryExists()
+ * @return bool|null
+ */
+ final public function directoryExists( array $params ) {
+ list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+ if ( $dir === null ) {
+ return false; // invalid storage path
+ }
+ if ( $shard !== null ) { // confined to a single container/shard
+ return $this->doDirectoryExists( $fullCont, $dir, $params );
+ } else { // directory is on several shards
+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ $res = false; // response
+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+ $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
+ if ( $exists ) {
+ $res = true;
+ break; // found one!
+ } elseif ( $exists === null ) { // error?
+ $res = null; // if we don't find anything, it is indeterminate
+ }
+ }
+ return $res;
+ }
+ }
+
+ /**
+ * @see FileBackendStore::directoryExists()
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $params Array
+ * @return bool|null
+ */
+ abstract protected function doDirectoryExists( $container, $dir, array $params );
+
+ /**
+ * @see FileBackend::getDirectoryList()
+ * @return Array|null|Traversable
+ */
+ final public function getDirectoryList( array $params ) {
+ list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+ if ( $dir === null ) { // invalid storage path
+ return null;
+ }
+ if ( $shard !== null ) {
+ // File listing is confined to a single container/shard
+ return $this->getDirectoryListInternal( $fullCont, $dir, $params );
+ } else {
+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+ // File listing spans multiple containers/shards
+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+ return new FileBackendStoreShardDirIterator( $this,
+ $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+ }
+ }
+
+ /**
+ * Do not call this function from places outside FileBackend
+ *
+ * @see FileBackendStore::getDirectoryList()
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $params Array
+ * @return Traversable|Array|null
+ */
+ abstract public function getDirectoryListInternal( $container, $dir, array $params );
+
+ /**
+ * @see FileBackend::getFileList()
* @return Array|null|Traversable
*/
final public function getFileList( array $params ) {
wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
// File listing spans multiple containers/shards
list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
- return new FileBackendStoreShardListIterator( $this,
+ return new FileBackendStoreShardFileIterator( $this,
$fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
}
}
return $performOps;
}
+ /**
+ * Get a list of storage paths to lock for a list of operations
+ * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys,
+ * each corresponding to a list of storage paths to be locked.
+ *
+ * @param $performOps Array List of FileOp objects
+ * @return Array ('sh' => list of paths, 'ex' => list of paths)
+ */
+ final public function getPathsToLockForOpsInternal( array $performOps ) {
+ // Build up a list of files to lock...
+ $paths = array( 'sh' => array(), 'ex' => array() );
+ foreach ( $performOps as $fileOp ) {
+ $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
+ $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
+ }
+ // Optimization: if doing an EX lock anyway, don't also set an SH one
+ $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
+ // Get a shared lock on the parent directory of each path changed
+ $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
+
+ return $paths;
+ }
+
/**
* @see FileBackend::doOperationsInternal()
* @return Status
// Acquire any locks as needed...
if ( empty( $opts['nonLocking'] ) ) {
// Build up a list of files to lock...
- $filesLockEx = $filesLockSh = array();
- foreach ( $performOps as $fileOp ) {
- $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
- $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
- }
- // Optimization: if doing an EX lock anyway, don't also set an SH one
- $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
- // Get a shared lock on the parent directory of each path changed
- $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
+ $paths = $this->getPathsToLockForOpsInternal( $performOps );
// Try to lock those files for the scope of this function...
- $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
- $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+ $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
+ $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
if ( !$status->isOK() ) {
wfProfileOut( __METHOD__ . '-' . $this->name );
wfProfileOut( __METHOD__ );
}
}
- // Clear any cache entries (after locks acquired)
+ // Clear any file cache entries (after locks acquired)
$this->clearCache();
+ // Load from the persistent container cache
+ $this->primeContainerCache( $performOps );
+
// Actually attempt the operation batch...
$subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
return ''; // no sharding
}
+ /**
+ * Check if a storage path maps to a single shard.
+ * Container dirs like "a", where the container shards on "x/xy",
+ * can reside on several shards. Such paths are tricky to handle.
+ *
+ * @param $storagePath string Storage path
+ * @return bool
+ */
+ final public function isSingleShardPathInternal( $storagePath ) {
+ list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
+ return ( $shard !== null );
+ }
+
/**
* Get the sharding config for a container.
* If greater than 0, then all file storage paths within
protected function resolveContainerPath( $container, $relStoragePath ) {
return $relStoragePath;
}
+
+ /**
+ * Get the cache key for a container
+ *
+ * @param $container Resolved container name
+ * @return string
+ */
+ private function containerCacheKey( $container ) {
+ return wfMemcKey( 'backend', $this->getName(), 'container', $container );
+ }
+
+ /**
+ * Set the cached info for a container
+ *
+ * @param $container Resolved container name
+ * @param $val mixed Information to cache
+ * @return void
+ */
+ final protected function setContainerCache( $container, $val ) {
+ $this->memCache->set( $this->containerCacheKey( $container ), $val, 7*86400 );
+ }
+
+ /**
+ * Delete the cached info for a container
+ *
+ * @param $container Resolved container name
+ * @return void
+ */
+ final protected function deleteContainerCache( $container ) {
+ $this->memCache->delete( $this->containerCacheKey( $container ) );
+ }
+
+ /**
+ * Do a batch lookup from cache for container stats for all containers
+ * used in a list of container names, storage paths, or FileOp objects.
+ *
+ * @param $items Array List of storage paths or FileOps
+ * @return void
+ */
+ final protected function primeContainerCache( array $items ) {
+ $paths = array(); // list of storage paths
+ $contNames = array(); // (cache key => resolved container name)
+ // Get all the paths/containers from the items...
+ foreach ( $items as $item ) {
+ if ( $item instanceof FileOp ) {
+ $paths = array_merge( $paths, $item->storagePathsRead() );
+ $paths = array_merge( $paths, $item->storagePathsChanged() );
+ } elseif ( self::isStoragePath( $item ) ) {
+ $paths[] = $item;
+ } elseif ( is_string( $item ) ) { // full container name
+ $contNames[$this->containerCacheKey( $item )] = $item;
+ }
+ }
+ // Get all the corresponding cache keys for paths...
+ foreach ( $paths as $path ) {
+ list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+ if ( $fullCont !== null ) { // valid path for this backend
+ $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
+ }
+ }
+
+ $contInfo = array(); // (resolved container name => cache value)
+ // Get all cache entries for these container cache keys...
+ $values = $this->memCache->getBatch( array_keys( $contNames ) );
+ foreach ( $values as $cacheKey => $val ) {
+ $contInfo[$contNames[$cacheKey]] = $val;
+ }
+
+ // Populate the container process cache for the backend...
+ $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+ }
+
+ /**
+ * Fill the backend-specific process cache given an array of
+ * resolved container names and their corresponding cached info.
+ * Only containers that actually exist should appear in the map.
+ *
+ * @param $containerInfo Array Map of resolved container names to cached info
+ * @return void
+ */
+ protected function doPrimeContainerCache( array $containerInfo ) {}
}
/**
- * FileBackendStore helper function to handle file listings that span container shards.
+ * FileBackendStore helper function to handle listings that span container shards.
* Do not use this class from places outside of FileBackendStore.
*
* @ingroup FileBackend
*/
-class FileBackendStoreShardListIterator implements Iterator {
- /* @var FileBackendStore */
+abstract class FileBackendStoreShardListIterator implements Iterator {
+ /** @var FileBackendStore */
protected $backend;
- /* @var Array */
+ /** @var Array */
protected $params;
- /* @var Array */
+ /** @var Array */
protected $shardSuffixes;
- protected $container; // string
- protected $directory; // string
+ protected $container; // string; full container name
+ protected $directory; // string; resolved relative path
- /* @var Traversable */
+ /** @var Traversable */
protected $iter;
protected $curShard = 0; // integer
protected $pos = 0; // integer
+ /** @var Array */
+ protected $multiShardPaths = array(); // (rel path => 1)
+
/**
* @param $backend FileBackendStore
* @param $container string Full storage container name
} else {
$this->iter->next();
}
+ // Filter out items that we already listed
+ $this->filterViaNext();
// Find the next non-empty shard if no elements are left
$this->nextShardIteratorIfNotValid();
}
$this->pos = 0;
$this->curShard = 0;
$this->setIteratorFromCurrentShard();
+ // Filter out items that we already listed
+ $this->filterViaNext();
// Find the next non-empty shard if this one has no elements
$this->nextShardIteratorIfNotValid();
}
}
}
+ /**
+ * Filter out duplicate items by advancing to the next ones
+ */
+ protected function filterViaNext() {
+ while ( $this->iter->valid() ) {
+ $rel = $this->iter->current(); // path relative to given directory
+ $path = $this->params['dir'] . "/{$rel}"; // full storage path
+ if ( !$this->backend->isSingleShardPathInternal( $path ) ) {
+ // Don't keep listing paths that are on multiple shards
+ if ( isset( $this->multiShardPaths[$rel] ) ) {
+ $this->iter->next(); // we already listed this path
+ } else {
+ $this->multiShardPaths[$rel] = 1;
+ break;
+ }
+ }
+ }
+ }
+
/**
* If the list iterator for this container shard is out of items,
* then move on to the next container that has items.
*/
protected function setIteratorFromCurrentShard() {
$suffix = $this->shardSuffixes[$this->curShard];
- $this->iter = $this->backend->getFileListInternal(
+ $this->iter = $this->listFromShard(
"{$this->container}{$suffix}", $this->directory, $this->params );
}
+
+ /**
+ * Get the list for a given container shard
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $params Array
+ * @return Traversable|Array|null
+ */
+ abstract protected function listFromShard( $container, $dir, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
+ protected function listFromShard( $container, $dir, array $params ) {
+ return $this->backend->getDirectoryListInternal( $container, $dir, $params );
+ }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
+ protected function listFromShard( $container, $dir, array $params ) {
+ return $this->backend->getFileListInternal( $container, $dir, $params );
+ }
}
$this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
? $config['shardViaHashLevels']
: '';
+ // Cache container info to mask latency
+ $this->memCache = wfGetMainCache();
}
/**
return $data;
}
+ /**
+ * @see FileBackendStore::doDirectoryExists()
+ * @return bool|null
+ */
+ protected function doDirectoryExists( $fullCont, $dir, array $params ) {
+ try {
+ $container = $this->getContainer( $fullCont );
+ $prefix = ( $dir == '' ) ? null : "{$dir}/";
+ return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
+ } catch ( NoSuchContainerException $e ) {
+ return false;
+ } catch ( InvalidResponseException $e ) {
+ } catch ( Exception $e ) { // some other exception?
+ $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+ }
+
+ return null; // error
+ }
+
+ /**
+ * @see FileBackendStore::getDirectoryListInternal()
+ * @return SwiftFileBackendDirList
+ */
+ public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
+ return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
+ }
+
/**
* @see FileBackendStore::getFileListInternal()
* @return SwiftFileBackendFileList
*/
public function getFileListInternal( $fullCont, $dir, array $params ) {
- return new SwiftFileBackendFileList( $this, $fullCont, $dir );
+ return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
}
/**
*
* @param $fullCont string Resolved container name
* @param $dir string Resolved storage directory with no trailing slash
- * @param $after string Storage path of file to list items after
+ * @param $after string|null Storage path of file to list items after
* @param $limit integer Max number of items to list
- * @return Array
+ * @param $params Array Includes flag for 'topOnly'
+ * @return Array List of relative paths of dirs directly under $dir
+ */
+ public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
+ $dirs = array();
+
+ try {
+ $container = $this->getContainer( $fullCont );
+ $prefix = ( $dir == '' ) ? null : "{$dir}/";
+ // Non-recursive: only list dirs right under $dir
+ if ( !empty( $params['topOnly'] ) ) {
+ $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
+ foreach ( $objects as $object ) { // files and dirs
+ if ( substr( $object, -1 ) === '/' ) {
+ $dirs[] = $object; // directories end in '/'
+ }
+ $after = $object; // update last item
+ }
+ // Recursive: list all dirs under $dir and its subdirs
+ } else {
+ // Get directory from last item of prior page
+ $lastDir = $this->getParentDir( $after ); // must be first page
+ $objects = $container->list_objects( $limit, $after, $prefix );
+ foreach ( $objects as $object ) { // files
+ $objectDir = $this->getParentDir( $object ); // directory of object
+ if ( $objectDir !== false ) { // file has a parent dir
+ // Swift stores paths in UTF-8, using binary sorting.
+ // See function "create_container_table" in common/db.py.
+ // If a directory is not "greater" than the last one,
+ // then it was already listed by the calling iterator.
+ if ( $objectDir > $lastDir ) {
+ $pDir = $objectDir;
+ do { // add dir and all its parent dirs
+ $dirs[] = "{$pDir}/";
+ $pDir = $this->getParentDir( $pDir );
+ } while ( $pDir !== false // sanity
+ && $pDir > $lastDir // not done already
+ && strlen( $pDir ) > strlen( $dir ) // within $dir
+ );
+ }
+ $lastDir = $objectDir;
+ }
+ $after = $object; // update last item
+ }
+ }
+ } catch ( NoSuchContainerException $e ) {
+ } catch ( InvalidResponseException $e ) {
+ } catch ( Exception $e ) { // some other exception?
+ $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
+ }
+
+ return $dirs;
+ }
+
+ protected function getParentDir( $path ) {
+ return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
+ }
+
+ /**
+ * Do not call this function outside of SwiftFileBackendFileList
+ *
+ * @param $fullCont string Resolved container name
+ * @param $dir string Resolved storage directory with no trailing slash
+ * @param $after string|null Storage path of file to list items after
+ * @param $limit integer Max number of items to list
+ * @param $params Array Includes flag for 'topOnly'
+ * @return Array List of relative paths of files under $dir
*/
- public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
+ public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
$files = array();
try {
$container = $this->getContainer( $fullCont );
$prefix = ( $dir == '' ) ? null : "{$dir}/";
- $files = $container->list_objects( $limit, $after, $prefix );
+ // Non-recursive: only list files right under $dir
+ if ( !empty( $params['topOnly'] ) ) { // files and dirs
+ $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
+ foreach ( $objects as $object ) {
+ if ( substr( $object, -1 ) !== '/' ) {
+ $files[] = $object; // directories end in '/'
+ }
+ }
+ // Recursive: list all files under $dir and its subdirs
+ } else { // files
+ $files = $container->list_objects( $limit, $after, $prefix );
+ }
+ $after = end( $files ); // update last item
+ reset( $files ); // reset pointer
} catch ( NoSuchContainerException $e ) {
} catch ( InvalidResponseException $e ) {
} catch ( Exception $e ) { // some other exception?
* Use $reCache if the file count or byte count is needed.
*
* @param $container string Container name
- * @param $reCache bool Refresh the process cache
+ * @param $bypassCache bool Bypass all caches and load from Swift
* @return CF_Container
+ * @throws InvalidResponseException
*/
- protected function getContainer( $container, $reCache = false ) {
+ protected function getContainer( $container, $bypassCache = false ) {
$conn = $this->getConnection(); // Swift proxy connection
- if ( $reCache ) {
- unset( $this->connContainers[$container] ); // purge cache
+ if ( $bypassCache ) { // purge cache
+ unset( $this->connContainers[$container] );
+ } elseif ( !isset( $this->connContainers[$container] ) ) {
+ $this->primeContainerCache( array( $container ) ); // check persistent cache
}
if ( !isset( $this->connContainers[$container] ) ) {
$contObj = $conn->get_container( $container );
// NoSuchContainerException not thrown: container must exist
if ( count( $this->connContainers ) >= $this->maxContCacheSize ) { // trim cache?
reset( $this->connContainers );
- $key = key( $this->connContainers );
- unset( $this->connContainers[$key] );
+ unset( $this->connContainers[key( $this->connContainers )] );
}
$this->connContainers[$container] = $contObj; // cache it
+ if ( !$bypassCache ) {
+ $this->setContainerCache( $container, // update persistent cache
+ array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count )
+ );
+ }
}
return $this->connContainers[$container];
}
*
* @param $container string Container name
* @return CF_Container
+ * @throws InvalidResponseException
*/
protected function createContainer( $container ) {
$conn = $this->getConnection(); // Swift proxy connection
*
* @param $container string Container name
* @return void
+ * @throws InvalidResponseException
*/
protected function deleteContainer( $container ) {
$conn = $this->getConnection(); // Swift proxy connection
unset( $this->connContainers[$container] ); // purge cache
}
+ /**
+ * @see FileBackendStore::doPrimeContainerCache()
+ * @return void
+ */
+ protected function doPrimeContainerCache( array $containerInfo ) {
+ try {
+ $conn = $this->getConnection(); // Swift proxy connection
+ foreach ( $containerInfo as $container => $info ) {
+ $this->connContainers[$container] = new CF_Container(
+ $conn->cfs_auth,
+ $conn->cfs_http,
+ $container,
+ $info['count'],
+ $info['bytes']
+ );
+ }
+ } catch ( InvalidResponseException $e ) {
+ } catch ( Exception $e ) { // some other exception?
+ $this->logException( $e, __METHOD__, array() );
+ }
+ }
+
/**
* Log an unexpected exception for this backend
*
}
/**
- * SwiftFileBackend helper class to page through object listings.
+ * SwiftFileBackend helper class to page through listings.
* Swift also has a listing limit of 10,000 objects for sanity.
* Do not use this class from places outside SwiftFileBackend.
*
* @ingroup FileBackend
*/
-class SwiftFileBackendFileList implements Iterator {
+abstract class SwiftFileBackendList implements Iterator {
/** @var Array */
protected $bufferIter = array();
protected $bufferAfter = null; // string; list items *after* this path
protected $pos = 0; // integer
+ /** @var Array */
+ protected $params = array();
/** @var SwiftFileBackend */
protected $backend;
- protected $container; //
- protected $dir; // string storage directory
+ protected $container; // string; container name
+ protected $dir; // string; storage directory
protected $suffixStart; // integer
const PAGE_SIZE = 5000; // file listing buffer size
* @param $backend SwiftFileBackend
* @param $fullCont string Resolved container name
* @param $dir string Resolved directory relative to container
+ * @param $params Array
*/
- public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) {
+ public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
$this->backend = $backend;
$this->container = $fullCont;
$this->dir = $dir;
} else { // dir within container
$this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
}
- }
-
- /**
- * @see Iterator::current()
- * @return string|bool String or false
- */
- public function current() {
- return substr( current( $this->bufferIter ), $this->suffixStart );
+ $this->params = $params;
}
/**
// Check if there are no files left in this page and
// advance to the next page if this page was not empty.
if ( !$this->valid() && count( $this->bufferIter ) ) {
- $this->bufferAfter = end( $this->bufferIter );
- $this->bufferIter = $this->backend->getFileListPageInternal(
- $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
- );
+ $this->bufferIter = $this->pageFromList(
+ $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+ ); // updates $this->bufferAfter
}
}
public function rewind() {
$this->pos = 0;
$this->bufferAfter = null;
- $this->bufferIter = $this->backend->getFileListPageInternal(
- $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
- );
+ $this->bufferIter = $this->pageFromList(
+ $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
+ ); // updates $this->bufferAfter
}
/**
public function valid() {
return ( current( $this->bufferIter ) !== false ); // no paths can have this value
}
+
+ /**
+ * Get the given list portion (page)
+ *
+ * @param $container string Resolved container name
+ * @param $dir string Resolved path relative to container
+ * @param $after string|null
+ * @param $limit integer
+ * @param $params Array
+ * @return Traversable|Array|null
+ */
+ abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class SwiftFileBackendDirList extends SwiftFileBackendList {
+ /**
+ * @see Iterator::current()
+ * @return string|bool String (relative path) or false
+ */
+ public function current() {
+ return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
+ }
+
+ /**
+ * @see SwiftFileBackendList::pageFromList()
+ * @return Array
+ */
+ protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+ return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
+ }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class SwiftFileBackendFileList extends SwiftFileBackendList {
+ /**
+ * @see Iterator::current()
+ * @return string|bool String (relative path) or false
+ */
+ public function current() {
+ return substr( current( $this->bufferIter ), $this->suffixStart );
+ }
+
+ /**
+ * @see SwiftFileBackendList::pageFromList()
+ * @return Array
+ */
+ protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
+ return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
+ }
}
}
}
+ // If the backend is ready-only, don't keep generating thumbnails
+ // only to return transformation errors, just return the error now.
+ if ( $this->repo->getReadOnlyReason() !== false ) {
+ $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
+ break;
+ }
+
// Create a temp FS file with the same extension and the thumbnail
$thumbExt = FileBackend::extensionFromPath( $thumbPath );
$tmpFile = TempFSFile::factory( 'transform_', $thumbExt );
} else {
$thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
}
+ // Give extensions a chance to do something with this thumbnail...
+ wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) );
}
// Purge. Useful in the event of Core -> Squid connection failure or squid
# Delete the thumbnails
$this->repo->quickPurgeBatch( $purgeList );
# Clear out the thumbnail directory if empty
- $this->repo->cleanDir( $dir );
+ $this->repo->quickCleanDir( $dir );
}
}
# Delete the thumbnails
$this->repo->quickPurgeBatch( $purgeList );
# Clear out the thumbnail directory if empty
- $this->repo->cleanDir( $dir );
+ $this->repo->quickCleanDir( $dir );
}
/** purgeDescription inherited */
public function __construct( $conf = array() ) {
$this->mConf = $conf;
$this->mUrlProtocols = wfUrlProtocols();
- $this->mExtLinkBracketedRegex = '/\[((' . wfUrlProtocols() . ')'.
+ $this->mExtLinkBracketedRegex = '/\[((' . $this->mUrlProtocols . ')'.
self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
if ( isset( $conf['preprocessorClass'] ) ) {
$this->mPreprocessorClass = $conf['preprocessorClass'];
# Don't allow internal links to pages containing
# PROTO: where PROTO is a valid URL protocol; these
# should be external links.
- if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
+ if ( preg_match( '/^(?:' . $this->mUrlProtocols . ')/', $m[1] ) ) {
$s .= $prefix . '[[' . $line ;
wfProfileOut( __METHOD__."-misc" );
continue;
* @return String: less-or-more HTML with NOPARSE bits
*/
function armorLinks( $text ) {
- return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
+ return preg_replace( '/\b(' . $this->mUrlProtocols . ')/',
"{$this->mUniqPrefix}NOPARSE$1", $text );
}
# @todo FIXME: Not tolerant to blank link text
# I.E. [http://www.mediawiki.org] will render as [1] or something depending
# on how many empty links there are on the page - need to figure that out.
- $text = preg_replace( '/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
+ $text = preg_replace( '/\[(?:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
# Parse wikitext quotes (italics & bold)
$text = $this->doQuotes( $text );
* @ingroup SpecialPage Pager
*/
class CategoryPager extends AlphabeticPager {
- private $conds = array( 'cat_pages > 0' );
-
function __construct( IContextSource $context, $from ) {
parent::__construct( $context );
$from = str_replace( ' ', '_', $from );
if( $from !== '' ) {
$from = Title::capitalize( $from, NS_CATEGORY );
- $dbr = wfGetDB( DB_SLAVE );
- $this->conds[] = 'cat_title >= ' . $dbr->addQuotes( $from );
- $this->setOffset( '' );
+ $this->mOffset = $from;
}
}
return array(
'tables' => array( 'category' ),
'fields' => array( 'cat_title','cat_pages' ),
- 'conds' => $this->conds,
+ 'conds' => array( 'cat_pages > 0' ),
'options' => array( 'USE INDEX' => 'cat_title' ),
);
}
هذا يحدث أحيانا عندما تستخدم خدمة بروكسي مجهول معيبة مبنية على الوب.'''",
'edit_form_incomplete' => "'''بعض أجزاء من نموذج التعديل لم تصل إلى الخادم؛ تأكد من أن تعديلاتك لم تمس وحاول مجددا.'''",
'editing' => 'تحرير $1',
+'creating' => 'إنشاء $1',
'editingsection' => 'تحرير $1 (قسم)',
'editingcomment' => 'تعديل $1 (قسم جديد)',
'editconflict' => 'تضارب في التحرير: $1',
'internalerror' => 'ܦܘܕܐ ܓܘܝܐ',
'internalerror_info' => 'ܦܘܕܐ ܓܘܝܐ: $1',
'badtitle' => 'ܟܘܢܝܐ ܠܐ ܛܒܐ',
+'perfcached' => 'ܓܠܝܬ̈ܐ ܗܠܝܢ ܐܣܢܝܢ ܐܢܘܢ ܘܡܬܡܨܝܢܬܐ ܐܝܬܝܗܝ ܕܠܐ ܢܗܘܢ ܚܘ̈ܕܬܐ. ܡܬܚܐ ܥܠܝܐ ܕ {{PLURAL:$1|ܚܕ ܦܠܛܐ|$1 ܦܠܛ̈ܐ}} ܐܝܬ ܒܐܣܢܐ.',
+'perfcachedts' => 'ܓܠܝܬ̈ܐ ܗܠܝܢ ܐܣܢܝܢ ܐܢܘܢ ܘܚܘܕܬܐ ܐܚܪܝܐ ܗܘܐ ܒ $1. ܡܬܚܐ ܥܠܝܐ ܕ {{PLURAL:$4|ܚܕ ܦܠܛܐ|$4 ܦܠܛ̈ܐ}} ܐܝܬ ܒܐܣܢܐ.',
'viewsource' => 'ܚܙܝ ܡܒܘܥܐ',
-'actionthrottled' => 'ܠܐ ܘܪܕ ܠܡܥܒܕ ܝܬܝܪ ܡܢ ܐܗܐ ܥܒܕܐ',
-'viewsourcetext' => 'ܡܨܐ ܐܢܬ ܠܚܙܝܐ ܘܢܣܚܐ ܠܡܒܘܥ̈ܐ ܕܐܗܐ ܦܐܬܐ:',
-'protectedinterface' => 'ܐܗܐ ܦܐܬܐ ܡܘܬܪܐ ܟܬܝܒܬܐ ܕܦܐܬܐ ܠܚܘܪܙܐ, ܘܐܝܠܗ ܢܛܪܬܐ ܠܡܘܢܥܐ ܚܪܒܐ.',
+'viewsource-title' => 'ܚܙܝ ܡܒܘܥܐ ܕ $1',
+'actionthrottled' => 'ܠܐ ܡܬܡܨܝܢܬܐ ܐܝܬܝܗܝ ܠܡܥܒܕ ܝܬܝܪ ܡܢ ܗܢܐ ܥܒܕܐ',
+'viewsourcetext' => 'ܡܨܐ ܐܢܬ ܕܢܚܙܐ ܘܢܣܚܐ ܠܡܒܘ̈ܥܐ ܕܗܕܐ ܦܐܬܐ:',
+'protectedinterface' => 'ܗܕܐ ܦܐܬܐ ܡܘܬܪܐ ܟܬܝܒܬܐ ܕܦܐܬܐ ܠܚܘܪܙܐ, ܘܐܝܬܝܗܝ ܢܛܪܬܐ ܠܡܘܢܥ ܚܘܒܠܐ.',
'editinginterface' => "''ܙܘܗܪܐ:''' ܐܢܬ ܥܒܕܬ ܫܚܠܦܬܐ ܒܦܐܬܐ ܡܬܦܠܚܬ ܠܡܘܬܘܪ̈ܐ ܦܐܬܘܬ̈ܐ ܟܬܝܒ̈ܐ ܠܚܘܪܙܐ.
ܟܠ ܫܘܚܠܦܐ ܒܐܗܐ ܦܐܬܐ ܒܕ ܥܒܕ ܟܪ ܥܠ ܡܚܙܝܬܐ ܦܐܬܐ ܕܡܦܠܚܢܐ ܠܡܦܠܚܢ̈ܐ ܐܚܪ̈ܝܢܐ.
ܠܬܘܪ̈ܓܡܐ، ܡܦܠܚ ܬܪܡܝܬܐ ܬܘܪܓܡܐ ܕܡܝܕܝܐܘܝܩܝ [//translatewiki.net/wiki/Main_Page?setlang=ar translatewiki.net].",
ܗܫܐ ܐܝܬܝܗܝ ܨܘܝܒܐ ܠ [[$2]].',
'brokenredirects' => 'ܨܘܝܒ̈ܐ ܬܒܝܪ̈ܐ',
-'brokenredirectstext' => 'ܨܘ̈ܝܒܐ ܗܠܝܢ ܡܛܝܢ ܠܕ̈ܦܐ ܕܠܝܬ:',
+'brokenredirectstext' => 'ܨܘ̈ܝܒܐ ܗܠܝܢ ܡܛܝܢ ܠܕ̈ܦܐ ܕܠܝܬܠܗܘܢ ܐܝܬܘܬܐ:',
'brokenredirects-edit' => 'ܫܚܠܦ',
'brokenredirects-delete' => 'ܫܘܦ',
'feedback-error1' => 'ত্রুটি: এপিআই থেকে অজানা ফলাফল এসেছে',
'feedback-error2' => 'ত্রুটি: সম্পাদনা ব্যর্থ',
'feedback-error3' => 'ত্রুটি: এপিআই থেকে কোন সাড়া নেই',
+'feedback-close' => 'সম্পন্ন',
# API errors
'api-error-badaccess-groups' => 'আপনার এই উইকিতে ফাইল আপলোডের অনুমতি নেই।',
'api-error-uploaddisabled' => 'এই উইকির জন্য আপলোড সুবিধা নিস্ক্রিয় রয়েছে।',
'api-error-verification-error' => 'সম্ভবত এই ফাইলটি ত্রুটিপূর্ণ অথবা এর এক্সটেনশনটি ভুল।',
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|সেকেন্ড|সেকেন্ড}}',
+'duration-minutes' => '$1 {{PLURAL:$1|মিনিট|মিনিট}}',
+'duration-hours' => '$1 {{PLURAL:$1|ঘন্টা|ঘন্টা}}',
+'duration-days' => '$1 {{PLURAL:$1|দিন|দিন}}',
+'duration-weeks' => '{{PLURAL: $1|সপ্তাহ|সপ্তাহ}}',
+'duration-years' => '$1 {{PLURAL:$1|বছর|বছর}}',
+'duration-decades' => '$1 {{PLURAL:$1|দশক|দশক}}',
+'duration-centuries' => '$1 {{PLURAL:$1|শতাব্দী|শতাব্দী}}',
+'duration-millennia' => '$1 {{PLURAL:$1|সহস্রাব্দ|সহস্রাব্দ}}',
+
);
'note' => "'''Poznámka:''' ",
'previewnote' => "'''Pamatujte, že toto je pouze náhled.'''
Změny zatím nebyly uloženy!",
+'continue-editing' => 'Pokračovat v editaci',
'previewconflict' => 'Tento náhled ukazuje text tak, jak bude vypadat po uložení stránky.',
'session_fail_preview' => "'''Váš požadavek se nepodařilo zpracovat kvůli ztrátě dat z relace.
Zkuste to prosím znovu.
'version-software' => 'Nainstalovaný software',
'version-software-product' => 'Název',
'version-software-version' => 'Verze',
+'version-entrypoints' => 'URL vstupních bodů',
+'version-entrypoints-header-entrypoint' => 'Vstupní bod',
+'version-entrypoints-header-url' => 'URL',
# Special:FilePath
'filepath' => 'Cesta k souboru',
'note' => "'''Bemærk:'''",
'previewnote' => "'''Husk at dette er kun en forhåndsvisning.'''
Dine ændringer er endnu ikke blevet gemt!",
+'continue-editing' => 'Fortsæt med at redigere',
'previewconflict' => 'Denne forhåndsvisning er resultatet af den redigérbare tekst ovenfor, sådan vil det komme til at se ud hvis du vælger at gemme teksten.',
'session_fail_preview' => "'''Din ændring kunne ikke gemmes, da dine sessionsdata er gået tabt.
Prøv venligst igen. Hvis problemet fortsætter, log af og log på igen.'''",
'note' => "'''Pokazka:'''",
'previewnote' => "'''Wobmysli, až to jo jano pśeglěd.'''
Twóje změny hyšći njejsu składowane!",
+'continue-editing' => 'Dalej wobźěłaś',
'previewconflict' => 'Toś ten pśeglěd wótbłyšćujo tekst górjejcnego póla. Bok buźo tak wuglědaś, jolic jen něnto składujoš.',
'session_fail_preview' => "'''Wódaj! Twójo wobźěłanje njejo se mógało składowaś, dokulaž su daty twójogo pósejźenja se zgubili. Pšosym wopytaj hyšći raz. Jolic až to pón pśecej hyšći njejźo, wopytaj se wótzjawiś a zasej pśizjawiś.'''",
'session_fail_preview_html' => "'''Wódaj! Twójo wobźěłanje njejo se mógało składowaś, dokulaž su daty twójogo pósejźenja se zgubili.'''
* @author Hercule
* @author Icvav
* @author Imre
+ * @author Invadinado
* @author Jatrobat
* @author Jens Liebenau
* @author Jurock
'tog-previewontop' => 'Mostrar previsualización antes del cuadro de edición',
'tog-previewonfirst' => 'Mostrar previsualización en la primera edición',
'tog-nocache' => 'Desactivar la caché de páginas del navegador',
-'tog-enotifwatchlistpages' => 'Enviarme un correo electrónico cuando una página en mi lista de seguimiento sea modificada',
-'tog-enotifusertalkpages' => 'Enviarme un correo electrónico cuando mi página de discusión sea modificada',
+'tog-enotifwatchlistpages' => 'Enviarme un correo electrónico cuando se modifique una página en mi lista de seguimiento',
+'tog-enotifusertalkpages' => 'Enviarme un correo electrónico cuando se modifique mi página de discusión',
'tog-enotifminoredits' => 'Notificarme también los cambios menores de páginas',
'tog-enotifrevealaddr' => 'Revelar mi dirección de correo electrónico en los correos de notificación',
'tog-shownumberswatching' => 'Mostrar el número de usuarios que la vigilan',
'tog-oldsig' => 'Firma actual:',
'tog-fancysig' => 'Tratar firma como wikitexto (sin un enlace automático)',
-'tog-externaleditor' => 'Utilizar editor externo por defecto (sólo para expertos pues necesitas ajustes especiales en tu ordenador. [//www.mediawiki.org/wiki/Manual:External_editors Más información.])',
-'tog-externaldiff' => 'Utilizar diff externo por defecto (sólo para expertos pues necesitas ajustes especiales en tu ordenador. [//www.mediawiki.org/wiki/Manual:External_editors Más información.])',
+'tog-externaleditor' => 'Utilizar editor externo por defecto (sólo para expertos, pues necesitas ajustes especiales en tu ordenador; [//www.mediawiki.org/wiki/Manual:External_editors más información])',
+'tog-externaldiff' => 'Utilizar diff externo por defecto (sólo para expertos, pues necesitas ajustes especiales en tu ordenador; [//www.mediawiki.org/wiki/Manual:External_editors más información])',
'tog-showjumplinks' => 'Habilitar enlaces de accesibilidad «saltar a»',
'tog-uselivepreview' => 'Usar live preview (JavaScript) (Experimental)',
'tog-forceeditsummary' => 'Alertar al grabar sin resumen de edición.',
'category-empty' => "''La categoría no contiene actualmente ningún artículo o archivo multimedia.''",
'hidden-categories' => '{{PLURAL:$1|Categoría escondida|Categorías escondidas}}',
'hidden-category-category' => 'Categorías ocultas',
-'category-subcat-count' => '{{PLURAL:$2|Esta categoría comprende solamente la siguiente categoría.|Esta categoría incluye {{PLURAL:$1|la siguiente categorías|las siguientes $1 subcategorías}}, de un total de $2.}}',
+'category-subcat-count' => '{{PLURAL:$2|Esta categoría solo contiene la siguiente subcategoría.|Esta categoría contiene {{PLURAL:$1|la siguiente subcategoría|las siguientes $1 subcategorías}}, de un total de $2.}}',
'category-subcat-count-limited' => 'Esta categoría contiene {{PLURAL:$1|la siguiente subcategoría|las siguientes $1 subcategorías}}.',
'category-article-count' => '{{PLURAL:$2|Esta categoría incluye solamente la siguiente página.|{{PLURAL:$1|La siguiente página página pertenece|Las siguientes $1 páginas pertenecen}} a esta categoría, de un total de $2.}}',
'category-article-count-limited' => '{{PLURAL:$1|La siguiente página pertenece|Las siguientes $1 páginas pertenecen}} a esta categoría.',
'category-file-count-limited' => '{{PLURAL:$1|El siguiente fichero pertenece|Los siguientes $1 ficheros pertenecen}} a esta categoría.',
'listingcontinuesabbrev' => 'cont.',
'index-category' => 'Páginas indexadas',
-'noindex-category' => 'Páginas no indizadas',
+'noindex-category' => 'Páginas no indexadas',
'broken-file-category' => 'Páginas con enlaces rotos a archivos',
'about' => 'Acerca de',
'article' => 'Artículo',
-'newwindow' => '(Se abre en una ventana nueva)',
+'newwindow' => '(se abre en una ventana nueva)',
'cancel' => 'Cancelar',
'moredotdotdot' => 'Más...',
'mypage' => 'Mi página',
'login-throttled' => 'Has intentado demasiadas veces iniciar sesión. Por favor espera antes de intentarlo nuevamente.',
'login-abort-generic' => 'Tu inicio de sesión no fue exitoso - Cancelado',
'loginlanguagelabel' => 'Idioma: $1',
-'suspicious-userlogout' => 'Tu solicitud de desconexión ha sido denegada debido a que parece que ésta ha sido enviada desde un navegador defectuoso o un proxy caché.',
+'suspicious-userlogout' => 'Tu solicitud de desconexión ha sido denegada, pues parece haber sido enviada desde un navegador defectuoso o un proxy caché.',
# E-mail sending
'php-mail-error-unknown' => 'Error desconocido en la función mail() de PHP',
Tu dirección IP se almacenará en el historial de ediciones de la página.",
'anonpreviewwarning' => "''No has iniciado sesión con una cuenta de usuario. Al guardar los cambios se almacenará tu dirección IP en el historial de edición de la página.''",
'missingsummary' => "'''Atención:''' No has escrito un resumen de edición. Si haces clic nuevamente en «{{int:savearticle}}» tu edición se grabará sin él.",
-'missingcommenttext' => 'Por favor introduce texto debajo.',
+'missingcommenttext' => 'Por favor, introduce un texto debajo.',
'missingcommentheader' => "'''Recordatorio:''' No has escrito un título para este comentario. Si haces clic nuevamente en \"{{int:savearticle}}\" tu edición se grabará sin él.",
'summary-preview' => 'Previsualización del resumen:',
'subject-preview' => 'Previsualización del tema/título:',
'note' => "'''Nota:'''",
'previewnote' => "'''¡Recuerda que esto es solo una previsualización.'''
¡Tus cambios aún no se ha guardado!",
+'continue-editing' => 'Continuar editando',
'previewconflict' => 'Esta previsualización refleja el texto en el área de edición superior como aparecerá una vez guardados los cambios.',
'session_fail_preview' => "'''Lo sentimos, no pudimos procesar la edición debido a una pérdida de los datos de sesión.'''
Por favor, inténtalo de nuevo.
'note' => "'''Meeldetuletus:'''",
'previewnote' => "'''Ära unusta, et see on kõigest eelvaade!'''
Sinu muudatused pole veel salvestatud!",
+'continue-editing' => 'Jätka redigeerimist',
'previewconflict' => 'See eelvaade näitab, kuidas ülemises toimetuskastis olev tekst hakkab välja nägema, kui otsustate salvestada.',
'session_fail_preview' => "'''Vabandust! Meil ei õnnestunud seansiandmete kaotuse tõttu sinu muudatust töödelda.'''
Palun proovi uuesti.
'note' => "'''نکته:'''",
'previewnote' => "'''به یاد داشته باشید که این فقط پیشنمایش است.'''
تغییرات شما هنوز ذخیره نشدهاست!",
+'continue-editing' => 'ادامهٔ ویرایش',
'previewconflict' => 'این پیشنمایش منعکسکنندهٔ متن ناحیهٔ ویرایش متن بالایی است، به شکلی که اگر متن را ذخیره کنید نمایش خواهد یافت.',
'session_fail_preview' => "'''شرمنده! به علت از دست رفتن اطلاعات نشست کاربری نمیتوانیم ویرایش شما را پردازش کنیم.'''
لطفاً دوباره سعی کنید.
'logentry-newusers-newusers' => '$1 یک حساب کاربری ایجاد کرد',
'logentry-newusers-create' => '$1 یک حساب کاربری ایجاد کرد',
'logentry-newusers-create2' => '$1 یک حساب کاربری ایجاد کرد $3',
-'logentry-newusers-autocreate' => 'کاروری حساب $1 بساتن به شکل خودکار',
+'logentry-newusers-autocreate' => 'حساب $1 به شکل خودکار ساخته شد',
'newuserlog-byemail' => 'گذرواژه با پست الکترونیکی ارسال شد',
# Feedback
'customjsprotected' => 'Sinulla ei ole oikeutta muuttaa tätä JavaScript-sivua, koska se sisältää toisen käyttäjän henkilökohtaisia asetuksia.',
'ns-specialprotected' => 'Toimintosivuja ei voi muokata.',
'titleprotected' => "Käyttäjä [[User:$1|$1]] on asettanut tämän sivun luontikieltoon: ''$2''.",
+'filereadonlyerror' => 'Tiedostoa "$1" ei voi muuttaa, koska jaettu mediavarasto "$2" on "vain luku" -tilassa.
+
+Lukituksen asettanut ylläpitäjä on antanut seuraavan syyn toimenpiteelle: "$3".',
# Virus scanner
'virus-badscanner' => "Virheellinen asetus: Tuntematon virustutka: ''$1''",
'emailconfirmlink' => 'Varmenna sähköpostiosoite',
'invalidemailaddress' => 'Sähköpostiosoitetta ei voida hyväksyä, koska se ei ole oikeassa muodossa. Ole hyvä ja anna oikea sähköpostiosoite tai jätä kenttä tyhjäksi.',
'cannotchangeemail' => 'Tunnuksien sähköpostiosoitteita ei voi muuttaa tässä wikissä.',
+'emaildisabled' => 'Tältä sivustolta ei voi lähettää sähköpostia.',
'accountcreated' => 'Käyttäjätunnus luotiin',
'accountcreatedtext' => 'Käyttäjän $1 käyttäjätunnus luotiin.',
'createaccount-title' => 'Tunnuksen luominen {{GRAMMAR:illative|{{SITENAME}}}}',
'userinvalidcssjstitle' => "'''Varoitus:''' Tyyliä nimeltä ”$1” ei ole olemassa. Muista, että käyttäjän määrittelemät .css- ja .js-sivut alkavat pienellä alkukirjaimella, esim. {{ns:user}}:Matti Meikäläinen/vector.css eikä {{ns:user}}:Matti Meikäläinen/Vector.css.",
'updated' => '(Päivitetty)',
'note' => "'''Huomautus:'''",
-'previewnote' => "'''Tämä on vasta sivun esikatselu. Sivua ei ole vielä tallennettu!'''",
+'previewnote' => "'''Tämä on vasta sivun esikatselu. Tekemiäsi muokkauksia ei ole vielä tallennettu!'''",
+'continue-editing' => 'Jatka muokkaamista',
'previewconflict' => 'Tämä esikatselu näyttää miltä muokkausalueella oleva teksti näyttää tallennettuna.',
'session_fail_preview' => "'''Muokkaustasi ei voitu tallentaa, koska istuntosi tiedot ovat kadonneet.''' Yritä uudelleen. Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ulos]] ja takaisin sisään.",
'session_fail_preview_html' => "'''Muokkaustasi ei voitu tallentaa, koska istuntosi tiedot ovat kadonneet.'''
'token_suffix_mismatch' => "'''Muokkauksesi on hylätty, koska asiakasohjelmasi ei osaa käsitellä välimerkkejä muokkaustarkisteessa. Syynä voi olla viallinen välityspalvelin.'''",
'edit_form_incomplete' => "'''Osa muokkauslomakkeesta ei saavuttanut palvelinta. Tarkista, että muokkauksesi ovat vahingoittumattomia ja yritä uudelleen.'''",
'editing' => 'Muokataan sivua $1',
+'creating' => 'Sivun $1 luonti',
'editingsection' => 'Muokataan osiota sivusta $1',
'editingcomment' => 'Muokataan uutta osiota sivulla $1',
'editconflict' => 'Päällekkäinen muokkaus: $1',
'yourdiff' => 'Eroavaisuudet',
'copyrightwarning' => "'''Muutoksesi astuvat voimaan välittömästi.''' Kaikki {{GRAMMAR:illative|{{SITENAME}}}} tehtävät tuotokset katsotaan julkaistuksi $2 -lisenssin mukaisesti ($1). Jos et halua, että kirjoitustasi muokataan armottomasti ja uudelleenkäytetään vapaasti, älä tallenna kirjoitustasi. Tallentamalla muutoksesi lupaat, että kirjoitit tekstisi itse, tai kopioit sen jostain vapaasta lähteestä. '''ÄLÄ KÄYTÄ TEKIJÄNOIKEUDEN ALAISTA MATERIAALIA ILMAN LUPAA!'''",
'copyrightwarning2' => "Huomaa, että kuka tahansa voi muokata, muuttaa ja poistaa kaikkia sivustolle tekemiäsi lisäyksiä ja muutoksia. Muokkaamalla sivustoa luovutat sivuston käyttäjille tämän oikeuden ja takaat, että lisäämäsi aineisto on joko itse kirjoittamaasi tai peräisin jostain vapaasta lähteestä. Lisätietoja sivulla $1. '''TEKIJÄNOIKEUDEN ALAISEN MATERIAALIN KÄYTTÄMINEN ILMAN LUPAA ON EHDOTTOMASTI KIELLETTYÄ!'''",
-'longpageerror' => "'''Sivun koko on $1 binäärikilotavua. Sivua ei voida tallentaa, koska enimmäiskoko on $2 binäärikilotavua.'''",
+'longpageerror' => "'''Virhe: Lähettämäsi tekstin pituus on {{PLURAL:$1|kilotavu|$1 kilotavua}}. Tekstiä ei voida tallentaa, koska se on pitempi kuin sallittu enimmäispituus {{PLURAL:$2|yksi kilotavu|$2 kilotavua}}.'''",
'readonlywarning' => "'''Varoitus: Tietokanta on lukittu huoltoa varten, joten et pysty tallentamaan muokkauksiasi juuri nyt.'''
Saattaa olla paras leikata ja liimata tekstisi omaan tekstitiedostoosi ja tallentaa se tänne myöhemmin.
'edit-no-change' => 'Muokkauksesi sivuutettiin, koska tekstiin ei tehty mitään muutoksia.',
'edit-already-exists' => 'Uuden sivun luominen ei onnistunut.
Se on jo olemassa.',
+'defaultmessagetext' => 'Viestin oletusteksti',
# Parser/template warnings
'expensive-parserfunction-warning' => 'Tällä sivulla on liian monta hitaiden laajennusfunktioiden kutsua.
# Suppression log
'suppressionlog' => 'Häivytysloki',
-'suppressionlogtext' => 'Alla on lista uusimmista poistoista ja muokkausestoista, jotka sisältävät ylläpitäjiltä piilotettua materiaalia.
-[[Special:BlockList|Muokkausestolistassa]] on tämänhetkiset muokkausestot.',
+'suppressionlogtext' => 'Alla on luettelo poistoista ja muokkausestoista, jotka sisältävät ylläpitäjiltä piilotettua materiaalia.
+[[Special:BlockList|Estolistassa]] on lueteltu voimassa olevat muokkauskiellot ja muokkausestot.',
# History merging
'mergehistory' => 'Yhdistä muutoshistoriat',
$1 {{int:pipe-separator}} $2',
'searchmenu-legend' => 'Hakuasetukset',
-'searchmenu-exists' => "'''Sivu [[:$1]] löytyy tästä wikistä.'''",
+'searchmenu-exists' => "'''Tässä wikissä on sivu nimellä [[:$1]].'''",
'searchmenu-new' => "'''Luo sivu ''[[:$1]]'' tähän wikiin.'''",
'searchhelp-url' => 'Help:Sisällys',
'searchmenu-prefix' => '[[Special:PrefixIndex/$1|Selaa sivuja tällä etuliitteellä]]',
'newsectionsummary' => '/* $1 */ uusi osio',
'rc-enhanced-expand' => 'Näytä yksityiskohdat (JavaScript)',
'rc-enhanced-hide' => 'Piilota yksityiskohdat',
+'rc-old-title' => 'alun perin luotu nimellä "$1"',
# Recent changes linked
'recentchangeslinked' => 'Linkitettyjen sivujen muutokset',
'backend-fail-closetemp' => 'Väliaikaista tiedostoa ei voitu sulkea.',
'backend-fail-read' => 'Tiedostoa $1 ei voitu lukea.',
'backend-fail-create' => 'Tiedostoa $1 ei voitu luoda.',
+'backend-fail-connect' => 'Varastojärjestelmään "$1" ei saada yhteyttä.',
# Lock manager
'lockmanager-notlocked' => 'Kohteen $1 lukitusta ei voitu poistaa, koska se ei ole lukittu.',
Katso [$2 tiedoston kuvaussivulta] lisätietoja.',
'sharedupload-desc-here' => 'Tämä tiedosto on jaettu kohteesta $1 ja muut projektit saattavat käyttää sitä.
Tiedot [$2 tiedoston kuvaussivulta] näkyvät alla.',
+'sharedupload-desc-edit' => 'Tämä tiedosto on jaettu kohteesta $1 ja muut projektit saattavat käyttää sitä.
+Voit tarvittaessa muokata [$2 tiedoston kuvaussivua] kohteessa.',
+'sharedupload-desc-create' => 'Tämä tiedosto on jaettu kohteesta $1 ja muut projektit saattavat käyttää sitä.
+Voit tarvittaessa muokata [$2 tiedoston kuvaussivua] kohteessa.',
'filepage-nofile' => 'Tämän nimistä tiedostoa ei ole olemassa.',
'filepage-nofile-link' => 'Tämän nimistä tiedostoa ei ole olemassa, mutta voit [$1 tallentaa sen].',
'uploadnewversion-linktext' => 'Tallenna uusi versio tästä tiedostosta',
'wantedpages' => 'Halutut sivut',
'wantedpages-badtitle' => 'Virheellinen otsikko tuloksissa: $1',
'wantedfiles' => 'Halutut tiedostot',
+'wantedfiletext-cat' => 'Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del>. Lisäksi sellaiset sivut, joihin on sisällytetty tiedostoja, jotka eivät ole olemassa, on luetteloitu [[:$1|täällä]].',
+'wantedfiletext-nocat' => 'Seuraavia tiedostoja käytetään, mutta niitä ei ole olemassa. Ulkopuolissa mediavarastoissa olevat tiedostot voivat näkyä tällä listalla, vaikka ne ovat olemassa. Tällaiset väärät merkinnät on <del>yliviivattu</del.>',
'wantedtemplates' => 'Halutut mallineet',
'mostlinked' => 'Viitatuimmat sivut',
'mostlinkedcategories' => 'Viitatuimmat luokat',
'allpagesprefix' => 'Katkaisuhaku',
'allpagesbadtitle' => 'Annettu otsikko oli kelvoton tai siinä oli wikien välinen etuliite.',
'allpages-bad-ns' => '{{GRAMMAR:inessive|{{SITENAME}}}} ei ole nimiavaruutta ”$1”.',
+'allpages-hide-redirects' => 'Piilota ohjaussivut',
+
+# SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => 'Katselet arkistoitua versiota tästä sivusta, joka voi olla jopa $1 vanha.',
+'cachedspecial-viewing-cached-ts' => 'Katselet arkistoitua versiota tästä sivusta, joka ei välttämättä ole sivun viimeisin versio.',
+'cachedspecial-refresh-now' => 'Näytä uusin versio.',
# Special:Categories
'categories' => 'Luokat',
'undeletedrevisions' => '{{PLURAL:$1|Yksi versio|$1 versiota}} palautettiin',
'undeletedrevisions-files' => '{{PLURAL:$1|Yksi versio|$1 versiota}} ja {{PLURAL:$2|yksi tiedosto|$2 tiedostoa}} palautettiin',
'undeletedfiles' => '{{PLURAL:$1|1 tiedosto|$1 tiedostoa}} palautettiin',
-'cannotundelete' => 'Palauttaminen epäonnistui.',
+'cannotundelete' => 'Palauttaminen epäonnistui; joku muu on voinut jo palauttaa sivun.',
'undeletedpage' => "'''$1 on palautettu.'''
[[Special:Log/delete|Poistolokista]] löydät listan viimeisimmistä poistoista ja palautuksista.",
'badipaddress' => 'IP-osoite on väärin muotoiltu.',
'blockipsuccesssub' => 'Esto onnistui',
'blockipsuccesstext' => 'Käyttäjä tai IP-osoite [[Special:Contributions/$1|$1]] on estetty.<br />
-Nykyiset estot löytyvät [[Special:BlockList|estolistalta]].',
+Voimassa olevat estot näkyvät [[Special:BlockList|estolistasta]].',
'ipb-blockingself' => 'Olet estämässä itseäsi. Oletko varma, että haluat tehdä niin?',
'ipb-confirmhideuser' => 'Olet estämässä käyttäjää ”piilota käyttäjä” -toiminnon kanssa. Tämä piilottaa käyttäjän nimen kaikissa luetteloissa ja lokitapahtumissa. Oletko varma, että haluat tehdä näin?',
'ipb-edit-dropdown' => 'Muokkaa estosyitä',
Alla on ote häivytyslokista.',
'blocklogentry' => 'esti käyttäjän tai IP-osoitteen [[$1]]. Eston kesto $2 $3',
'reblock-logentry' => 'muutti käyttäjän tai IP-osoitteen [[$1]] eston asetuksia. Eston kesto $2 $3',
-'blocklogtext' => 'Tämä on loki muokkausestoista ja niiden purkamisista. Automaattisesti estettyjä IP-osoitteita ei kirjata. Tutustu [[Special:BlockList|estolistaan]] nähdäksesi listan tällä hetkellä voimassa olevista estoista.',
+'blocklogtext' => 'Tämä on loki muokkausestoista ja niiden purkamisista. Automaattisesti estettyjä IP-osoitteita ei kirjata. Tutustu [[Special:BlockList|estolistaan]] nähdäksesi luettelon tällä hetkellä voimassa olevista estoista.',
'unblocklogentry' => 'poisti käyttäjältä $1 muokkauseston',
'block-log-flags-anononly' => 'vain kirjautumattomat käyttäjät estetty',
'block-log-flags-nocreate' => 'tunnusten luonti estetty',
# JavaScriptTest
'javascripttest' => 'JavaScriptin testaus',
-'javascripttest-disabled' => 'Tämä toiminto ei ole käytössä.',
+'javascripttest-disabled' => 'Tämä toiminto ei ole käytössä tässä wikissä.',
'javascripttest-title' => 'Suoritetaan $1-testejä.',
'javascripttest-pagetext-noframework' => 'Tämä sivu on varattu JavaScript-testien suorittamiseen.',
'javascripttest-pagetext-unknownframework' => 'Tuntematon testausalusta $1.',
'version-software' => 'Asennettu ohjelmisto',
'version-software-product' => 'Tuote',
'version-software-version' => 'Versio',
+'version-entrypoints' => 'Aloituskohtien URL-osoitteet',
+'version-entrypoints-header-entrypoint' => 'Aloituskohta',
+'version-entrypoints-header-url' => 'URL',
# Special:FilePath
'filepath' => 'Tiedoston osoite',
'api-error-duplicate-archive-popup-title' => 'Tiedostolla on {{PLURAL:$1|poistettu kaksoiskappale|poistettuja kaksoiskappaleita}}',
'api-error-duplicate-popup-title' => 'Tiedoston {{PLURAL:$1|kaksoiskappale|kaksoiskappaleet}}',
'api-error-empty-file' => 'Määrittämäsi tiedosto on tyhjä.',
+'api-error-emptypage' => 'Ei ole sallittua luoda uutta, tyhjää sivua.',
'api-error-fetchfileerror' => 'Sisäinen virhe: jotakin meni pieleen tiedoston haussa.',
'api-error-file-too-large' => 'Määrittämäsi tiedosto on liian iso.',
'api-error-filename-tooshort' => 'Tiedoston nimi on liian lyhyt.',
'api-error-uploaddisabled' => 'Tiedostojen tallentaminen ei ole käytössä.',
'api-error-verification-error' => 'Tiedosto voi olla vioittunut, tai sillä saattaa olla väärä tiedostopääte.',
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|sekunti|sekuntia}}',
+'duration-minutes' => '$1 {{PLURAL:$1|minuutti|minuuttia}}',
+'duration-hours' => '$1 {{PLURAL:$1|tunti|tuntia}}',
+'duration-days' => '$1 {{PLURAL:$1|päivä|päivää}}',
+'duration-weeks' => '$1 {{PLURAL:$1|viikko|viikkoa}}',
+'duration-years' => '$1 {{PLURAL:$1|vuosi|vuotta}}',
+'duration-decades' => '$1 {{PLURAL:$1|vuosikymmen|vuosikymmentä}}',
+'duration-centuries' => '$1 {{PLURAL:$1|vuosisata|vuosisataa}}',
+'duration-millennia' => '$1 {{PLURAL:$1|vuosituhat|vuosituhatta}}',
+
);
'note' => "'''Note :'''",
'previewnote' => "'''Rappelez-vous que ce n’est qu’une prévisualisation.'''
Vos modifications n’ont pas encore été enregistrées !",
+'continue-editing' => 'Continuer la modification',
'previewconflict' => 'Cette prévisualisation montre le texte de la boîte supérieure de modification tel qu’il apparaîtra si vous choisissez de le publier.',
'session_fail_preview' => "'''Nous ne pouvons enregistrer votre modification à cause d’une perte d’informations concernant votre session.'''
Veuillez réessayer.
'updated' => '(Actualizado)',
'note' => "'''Nota:'''",
'previewnote' => "'''Lembre que esta é só unha vista previa e que aínda non gardou os seus cambios!'''",
+'continue-editing' => 'Continuar editando',
'previewconflict' => 'Esta vista previa mostra o texto na área superior tal e como aparecerá se escolle gardar.',
'session_fail_preview' => "'''O sistema non pode procesar a súa edición porque se perderon os datos de inicio da sesión.
Por favor, inténteo de novo.
'note' => "'''הערה:'''",
'previewnote' => "'''זכרו שזו רק תצוגה מקדימה.'''
השינויים שלכם טרם נשמרו!",
+'continue-editing' => 'להמשך העריכה',
'previewconflict' => 'תצוגה מקדימה זו מציגה כיצד ייראה הטקסט בחלון העריכה העליון, אם תבחרו לשמור אותו.',
'session_fail_preview' => "'''לא ניתן לבצע את עריכתכם עקב אובדן מידע הכניסה.'''
אנא נסו שוב.
'note' => "'''Kedźbu:'''",
'previewnote' => "'''Wobmysl, zo to je jenož přehlad.'''
Twoje změny hišće njejsu składowane!",
+'continue-editing' => 'Dale wobdźěłać',
'previewconflict' => 'Tutón přehlad zwobraznja tekst w hornim tekstowym polu, kaž so zjewi, jeli jón składuješ.',
'session_fail_preview' => "'''Njemóžachmy twoju změnu předźěłać, dokelž su so posedźenske daty zhubili.'''
Spytaj prošu hišće raz.
Ez a probléma akkor fordulhat elő, ha hibás, web-alapú proxyszolgáltatást használsz.'''",
'edit_form_incomplete' => "'''A szerkesztési űrlap egyes részei nem érkeztek meg a szerverre; ellenőrizd újra, hogy a szerkesztés sértetlen-e, majd próbáld újra.'''",
'editing' => '$1 szerkesztése',
+'creating' => '$1 létrehozása',
'editingsection' => '$1 szerkesztése (szakasz)',
'editingcomment' => '$1 szerkesztése (új szakasz)',
'editconflict' => 'Szerkesztési ütközés: $1',
'''Եթե սա բարեխիղճ խմբագրման փորձ է, խնդրում ենք փորձել կրկին։ Սխալի կրկնման դեպքում՝ փորձեք [[Special:UserLogout|դուրս գալ]], ապա կրկին մտնել համակարգ։'''",
'token_suffix_mismatch' => "'''Ձեր խմբագրումը մերժվել է, քանի որ ձեր օգտագործած ծրագիրը աղավաղել է կետադրության նշանները խմբագրման դաշտում։ Խմբագրումը մերժվել է էջի տեքստի խաթարումը կանխելու նպատակով։ Սա երբեմն պայմանավորված է սխալներ պարունակող անանվանեցնող վեբ-փոխարինորդ (proxy) ծառայության օգտագործմամբ։'''",
'editing' => 'Խմբագրում. $1',
+'creating' => 'Ստեղծում $1',
'editingsection' => 'Խմբագրում. $1 (բաժին)',
'editingcomment' => 'Խմբագրում $1 (նոր բաժին)',
'editconflict' => 'Խմբագրման ընդհարում. $1',
'note' => "'''Nota:'''",
'previewnote' => "'''Isto es solmente un previsualisation.'''
Le modificationes non ha ancora essite publicate!",
+'continue-editing' => 'Continuar a modificar',
'previewconflict' => 'Iste previsualisation reflecte le apparentia final del texto in le area de modification superior
si tu opta pro publicar lo.',
'session_fail_preview' => "'''Nos non poteva processar tu modification proque nos perdeva le datos del session.
Ti naited a rason ket ''$2''.",
'filereadonlyerror' => 'Di nabaliwan ti papales "$1" gapu ket ti repositorio ti papeles "$2" ket basaen laeng a moda.
-Ti rason a naited ket "\'\'$3\'\'".',
+Ti administrador a nagserra ket nagited iti daytoy a panagilawlawag "\'\'$3\'\'".',
# Virus scanner
'virus-badscanner' => 'Madi di panaka-aramidna: Di am-ammo a birus a panagskan: "$1"',
'invalidemailaddress' => 'Ti e-surat a pagtaengam ket saan a maawat, ket kasla addaan ti saan a napudno a nakabuklan.
Pangngaasi ta ikkam ti nasayaat a nakabuklan a pagtaengan wenno ikkatem amin dagiti naikabil mo.',
'cannotchangeemail' => 'Dagiti pakabilangan nga e-surat a pagtaengan ket saan a mabaliwan ditoy a wiki.',
+'emaildisabled' => 'Daytoy a pagsaaadan ket saan a makaipatuod kadagiti e-surat.',
'accountcreated' => 'Naaramiden ti pakabilangan',
'accountcreatedtext' => 'Naaramiden ti pakabilangan a pagaramat ni $1.',
'createaccount-title' => 'Panagaramid iti pakabilangan para iti {{SITENAME}}',
Annawid a .css ken .js dagiti titulo ket agususar ti napababa a letra, a kas dagiti {{ns:user}}:Foo/vector.css saan ket a {{ns:user}}:Foo/Vector.css.",
'updated' => '(Napabaro)',
'note' => "'''Paammo:'''",
-'previewnote' => "'''Maysa laeng a pagpadas daytoy; dagiti sinukatam ket saan pay a naidulin!'''",
+'previewnote' => "'''Laglagipem a daytoy ket panagipadas laeng.'''
+Dagiti sinukatam ket saan pay a naidulin!",
+'continue-editing' => 'Agtultuloy nga agurnos',
'previewconflict' => 'Daytoy a panagpadas ket agiparang ti testo dita ngato a panagurnos a lugar a kasla agparang no kayatmo nga idulin.',
'session_fail_preview' => "'''Pasensian a! Saan mi a maaramid ti panag-urnos gapu ngamin ta naawanan ti gimong ti data.'''
Pangngaasi ta padasem manen.
Mapasamak daytoy no agus-usar ka ti saan a nasayaat a naibasta ti sapot a diamammo a proxy a panagserbi.",
'edit_form_incomplete' => "'''Adda dagiti paset ti panag-urnos a kabuklan a saan a nakadanon dita server; kitkitaen nga dagiti pianag-urnos mo ket saan a naikkatan ken padasem manen.'''",
'editing' => 'Ururnosen ti $1',
+'creating' => 'Agparpartuat ti $1',
'editingsection' => 'Ururnosen ti $1 (paset)',
'editingcomment' => 'Ururnosen ti $1 (baro a paset)',
'editconflict' => 'Adda kasinnungat ti panag-urnos: $1',
'edit-no-change' => 'Ti inurnos mo ket saan a naikaskaso, ngamin ket awan ti nasukatan a testo.',
'edit-already-exists' => 'Saan a makaaramid ti baro a panid.
Adda met daytoyen.',
+'defaultmessagetext' => 'Naisigud a testo ti mensahe',
# Parser/template warnings
'expensive-parserfunction-warning' => "'''Ballaag:''' Daytoy a panid ket adu unay kadagiti nangina a parser nga opisio a pinagtawtawag.
# Suppression log
'suppressionlog' => 'Listaan ti nadepdepan',
-'suppressionlogtext' => 'Dita baba ket addaan dagiti listaan ti pinagikkat ken naserraan nga adda nagyanna a nailemmeng kadagiti administrador.
-Kitaen ti [[Special:BlockList|Listaan ti naserraan nga IP]] iti listaan ti agdama a kadagiti operasional a pinagparit ken panagserra',
+'suppressionlogtext' => 'Dita baba ket addaan dagiti listaan ti pinagikkat ken npanagserra a nairaman dagiti linaon a nailemmeng manipud kadagiti administrador.
+Kitaen ti [[Special:BlockList|Listaan ti lapden nga IP]] para iti listaan kadagiti agdama nga operasional a pinagparit ken panagserra.',
# History merging
'mergehistory' => 'Pagtiponen dagiti pakasaritaan ti pampanid',
'allpages-bad-ns' => 'Awan ti {{SITENAME}} iti nagan ti lugar a "$1".',
'allpages-hide-redirects' => 'Ilemmeng dagiti baw-ing',
+# SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => 'Kitkitaenm ti naidulin a bersion iti daytoy a panid, nga addan ti kadaanan a $1.',
+'cachedspecial-refresh-now' => 'Kitaen ti kinaudian.',
+
# Special:Categories
'categories' => 'Dagiti kategoria',
'categoriespagetext' => 'Dagiti sumaganad a {{PLURAL:$1|nagyan ti kategoria|dagiti nagyan ti kategoria}} pampanid wenno midia.
'ipb-confirm' => 'Pasingkedan ti serra',
'badipaddress' => 'Imbalido nga IP a pagtaengan',
'blockipsuccesssub' => 'Balligi ti panangserra',
-'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] ket naserraan.<br />
-Kitaen ti [[Special:BlockList|listaan ti IP a naserraan]] ta kitaen dagiti serra.',
+'blockipsuccesstext' => '[[Special:Contributions/$1|$1]] ket naserraanen.<br />
+Kitaen ti [[Special:BlockList|listaan ti lapden nga IP ]] tapno marepaso dagiti serra.',
'ipb-blockingself' => 'Mangrugrugi ka nga agserra kenka! Sigurado nga kayatmo nga aramiden daytoy?',
'ipb-confirmhideuser' => 'Mangrugrugi ka ti mangserra ti agar-aramat nga addaan ti napabalin na nga "ilemmeng ti agar-aramat". Iddeppen na ti nagan daytoy nga agar-aramat kadagiti amin a listaan ken dagiti naikabkabil ti listaan. Sigurado ka a kasta ti kayatmo?',
'ipb-edit-dropdown' => 'Urnosen dagiti rason ti panagserra',
Ti listaan ti napasardeng ket naikabil dita baba tapno mausar a reperensia:',
'blocklogentry' => 'naserraan ni [[$1]] nga addaan ti oras ti panagpaso nga $2 $3',
'reblock-logentry' => 'sinukatan ti panakaserra ni [[$1]] ti agtapos nga oras nga $2 $3',
-'blocklogtext' => 'Daytoy ket listaan ti gar-aramat kadagiti pinagserraken panaglukat ti serra
-Dagiti na-automatiko a panakaserra ti IP a pagtaengan ket saan a nailista.
-Kitaen ti [[Special:BlockList|Listaan ti serra ti IP]] ti listaan kadagiti agdama a naiparit a pagpataray ken dagiti serra.',
+'blocklogtext' => 'Daytoy ket listaan ti agar-aramat kadagiti pinagserra ken panaglukat ti serra
+Dagiti na-atomatiko a panakaserra ti IP a pagtaengan ket saan a nailista.
+Kitaen ti [[Special:BlockList|Listaan ti lapden nga IP]] para iti listaan kadagiti agdama a naiparit a pagpataray ken dagiti serra.',
'unblocklogentry' => 'lukatan ti serra ni $1',
'block-log-flags-anononly' => 'dagiti di am-ammo nga agar-aramat laeng',
'block-log-flags-nocreate' => 'naisardeng ti pinagaramid iti pakabilangan',
'exif-planarconfiguration-1' => 'chunky format',
'exif-planarconfiguration-2' => 'planar format',
+'exif-colorspace-65535' => 'Di-nakalibrar',
+
'exif-componentsconfiguration-0' => 'awan',
'exif-exposureprogram-0' => 'Saan a naipalpalawag',
'version-software' => 'Naikabil a software',
'version-software-product' => 'Produkto',
'version-software-version' => 'Bersion',
+'version-entrypoints' => 'Paserrekan a puntos dagiti URL',
+'version-entrypoints-header-entrypoint' => 'Pagserrekan a puntos',
+'version-entrypoints-header-url' => 'URL',
# Special:FilePath
'filepath' => 'Dalanan ti papeles',
'api-error-uploaddisabled' => 'Nabaldado ti mangipapan iti daytoy a wiki.',
'api-error-verification-error' => 'Dakes ngata daytoy a papeles, wenno addaan ti madi a pagpa-atiddog.',
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|segundo|seg-segundo}}',
+'duration-minutes' => '$1 {{PLURAL:$1|minuto|min-minuto}}',
+'duration-hours' => '$1 {{PLURAL:$1|oras|or-oras}}',
+'duration-days' => '$1 {{PLURAL:$1|aldaw|al-aldaw}}',
+'duration-weeks' => '$1 {{PLURAL:$1|lawas|law-lawas}}',
+'duration-years' => '$1 {{PLURAL:$1|tawen|taw-tawen}}',
+'duration-decades' => '$1 {{PLURAL:$1|dekada|dek-dekada}}',
+'duration-centuries' => '$1 {{PLURAL:$1|siglo|sig-siglo}}',
+'duration-millennia' => '$1 {{PLURAL:$1|milenio|mil-milenio}}',
+
);
'note' => "'''NOTA:'''",
'previewnote' => "'''Ricorda che questa è solo un'anteprima.'''
Le tue modifiche NON sono ancora state salvate!",
+'continue-editing' => 'Continua a modificare',
'previewconflict' => 'L\'anteprima corrisponde al testo presente nella casella di modifica superiore e rappresenta la pagina come apparirà se si sceglie di premere "Salva la pagina" in questo momento.',
'session_fail_preview' => "'''Non è stato possibile elaborare la modifica perché sono andati persi i dati relativi alla sessione.
Riprovare.
*
* @author Akaniji
* @author Alexsh
+ * @author Ant176
* @author Aotake
* @author Aphaia
* @author Broad-Sky
'category-empty' => "''このカテゴリには、ページまたはメディアがひとつもありません。''",
'hidden-categories' => '{{PLURAL:$1|隠しカテゴリ}}',
'hidden-category-category' => '隠しカテゴリ',
-'category-subcat-count' => '{{PLURAL:$2|このカテゴリには以下の下位カテゴリのみが含まれています。|このカテゴリには $2 下位カテゴリが含まれており、そのうち以下の{{PLURAL:$1|下位カテゴリ| $1 下位カテゴリ}}を表示しています。}}',
-'category-subcat-count-limited' => 'このカテゴリには以下の{{PLURAL:$1|下位カテゴリ| $1 下位カテゴリ}}が含まれています。',
-'category-article-count' => '{{PLURAL:$2|このカテゴリには以下のページのみ含まれています。|このカテゴリには $2 ページが含まれており、そのうち以下の {{PLURAL:$1|$1 ページ}}を表示しています。}}',
-'category-article-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ページ| $1 ページ}}が含まれています。',
+'category-subcat-count' => '{{PLURAL:$2|このカテゴリには以下の下位カテゴリのみが含まれています。|このカテゴリには $2 下位カテゴリが含まれており、そのうち以下の{{PLURAL:$1|下位カテゴリ| $1 下位カテゴリ}}を表示しています。}}',
+'category-subcat-count-limited' => 'このカテゴリには以下の{{PLURAL:$1|下位カテゴリ| $1 下位カテゴリ}}が含まれています。',
+'category-article-count' => '{{PLURAL:$2|このカテゴリには以下のページのみが含まれています。|このカテゴリには $2 ページが含まれており、そのうち以下の {{PLURAL:$1|$1 ページ}}を表示しています。}}',
+'category-article-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ページ| $1 ページ}}が含まれています。',
'category-file-count' => '{{PLURAL:$2|このカテゴリには以下のファイルのみが含まれています。|このカテゴリには $2 ファイルが含まれており、そのうち以下の {{PLURAL:$1|$1 ファイル}}を表示しています。}}',
-'category-file-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ファイル| $1 ファイル}}が含まれています。',
+'category-file-count-limited' => '現在のカテゴリには以下の{{PLURAL:$1|ファイル| $1 ファイル}}が含まれています。',
'listingcontinuesabbrev' => 'の続き',
'index-category' => '検索エンジンに収集されるページ',
'noindex-category' => '検索エンジンに収集されないページ',
'collapsible-collapse' => '折り畳む',
'collapsible-expand' => '展開する',
'thisisdeleted' => '$1を閲覧または復帰しますか?',
-'viewdeleted' => '$1を表示しますか?',
+'viewdeleted' => '$1を閲覧しますか?',
'restorelink' => '削除された$1編集',
'feedlinks' => 'フィード:',
'feed-invalid' => 'フィード形式の指定が間違っています。',
'yourname' => '利用者名:',
'yourpassword' => 'パスワード:',
'yourpasswordagain' => 'パスワード再入力:',
-'remembermypassword' => 'このブラウザーにログイン情報を保存 (最長{{PLURAL:$1|日|日間}})',
+'remembermypassword' => 'このブラウザーにログイン情報を保存 (最長 $1 {{PLURAL:$1|日|日間}})',
'securelogin-stick-https' => 'ログイン後にHTTPS接続を維持',
'yourdomainname' => 'ドメイン:',
'externaldberror' => '外部の認証データベースでエラーが発生したか、または外部アカウント情報の更新が許可されていません。',
'updated' => '(更新)',
'note' => "'''お知らせ:'''",
'previewnote' => "'''これはプレビューです。'''
-変更箇所はまだ保存されていません",
+変更箇所はまだ保存されていません!",
+'continue-editing' => '編集を続行',
'previewconflict' => 'このプレビューは、上の文章編集エリアの文章を保存した場合にどう見えるようになるかを示すものです。',
'session_fail_preview' => "'''申し訳ありません!セッションデータが消失したため編集を処理できませんでした。'''
もう一度やり直してください。
参考のため以下にこのページの削除と移動の記録を表示します:",
'moveddeleted-notice' => 'このページは削除されています。
参考のため、このページの削除と移動の記録を以下に表示します。',
-'log-fulllog' => '完全な記録を見る',
+'log-fulllog' => '完全な記録を閲覧',
'edit-hook-aborted' => 'フックによって編集が破棄されました。
理由は不明です。',
'edit-gone-missing' => 'ページを更新できませんでした。
$3が示した理由は ''$2'' です。",
# History pages
-'viewpagelogs' => 'このページに関する記録を表示',
+'viewpagelogs' => 'このページに関する記録を閲覧',
'nohistory' => 'このページには編集履歴がありません。',
'currentrev' => '最新版',
'currentrev-asof' => '$1時点における最新版',
'saveusergroups' => '利用者グループを保存',
'userrights-groupsmember' => '所属グループ:',
'userrights-groupsmember-auto' => '自動的に付与される権限:',
-'userrights-groups-help' => 'ã\81\93ã\81®å\88©ç\94¨è\80\85ã\81\8cå±\9eã\81\99ã\82\8bã\82°ã\83«ã\83¼ã\83\97ã\82\92å¤\89æ\9b´ã\81\99ã\82\8bã\81\93ã\81¨ã\81\8cã\81§ã\81\8dã\81¾ã\81\99ã\80\82
+'userrights-groups-help' => 'この利用者が属するグループを変更できます。
* チェックが入っているボックスは、この利用者がそのグループに属していることを意味します。
* チェックが入っていないボックスは、この利用者がそのグループに属していないことを意味します。
* 「*」はグループに一旦追加した場合に除去(あるいはその逆)ができないことを示しています。',
# Rights
'right-read' => 'ページを閲覧',
'right-edit' => 'ページを編集',
-'right-createpage' => '(議論ページではない)ページを作成',
+'right-createpage' => '(議論ページ以外の)ページを作成',
'right-createtalk' => '議論ページを作成',
'right-createaccount' => '新しい利用者アカウントを作成',
'right-minoredit' => '細部の編集の印を付ける',
* アップロード中のファイルの名前:'''<tt>[[:$1]]</tt>'''
* 既存ファイルの名前:'''<tt>[[:$2]]</tt>'''
違う名前を選択してください。",
-'fileexists-thumbnail-yes' => "このファイルは元の画像から縮小されたもの(サムネイル)のようです。
+'fileexists-thumbnail-yes' => "このファイルは元の画像から縮小されたもの''(サムネイル)''のようです。
[[$1|thumb]]
ファイル'''<tt>[[:$1]]</tt>'''を確認してください。
-確認したファイルが同じ画像のもとのサイズの版である場合、サムネイルを個別にアップロードする必要はありません。",
+確認したファイルが同じ画像の元のサイズの版の場合は、サムネイルを別途アップロードする必要はありません。",
'file-thumbnail-no' => "ファイル名が'''<tt>$1</tt>'''から始まっています。
-他の画像から縮小されたもの(サムネイル)のようです。
-ã\82\88ã\82\8aé«\98精細ã\81ªç\94»å\83\8fã\82\92ã\81\8aæ\8c\81ã\81¡ã\81®å ´å\90\88ã\81¯ã\80\81ã\81\9dã\81¡ã\82\89ã\82\92ã\82¢ã\83\83ã\83\97ã\83ã\83¼ã\83\89ã\81\97ã\81¦ã\81\8fã\81 ã\81\95ã\81\84ã\80\82ã\81\9dã\81\86ã\81§ない場合はファイル名を変更してください。",
+他の画像から縮小されたもの''(サムネイル)''のようです。
+ã\82\88ã\82\8aé«\98精細ã\81ªç\94»å\83\8fã\82\92ã\81\8aæ\8c\81ã\81¡ã\81®å ´å\90\88ã\81¯ã\81\9dã\82\8cã\82\92ã\82¢ã\83\83ã\83\97ã\83ã\83¼ã\83\89ã\81\97ã\81¦ã\81\8fã\81 ã\81\95ã\81\84ã\80\82ã\81\8aæ\8c\81ã\81¡ã\81§ã\81¯ない場合はファイル名を変更してください。",
'fileexists-forbidden' => 'この名前のファイルは既に存在しており、上書きできません。
アップロードを継続したい場合は、前のページに戻り、別のファイル名を使用してください。
[[File:$1|thumb|center|$1]]',
'filehist-dimensions' => '解像度',
'filehist-filesize' => 'ファイルサイズ',
'filehist-comment' => 'コメント',
-'filehist-missing' => 'ã\83\95ã\82¡ã\82¤ã\83«ã\81\8cã\81¿ã\81¤ã\81\8bりません',
+'filehist-missing' => 'ã\83\95ã\82¡ã\82¤ã\83«ã\81\8cã\81\82りません',
'imagelinks' => 'ファイルの使用状況',
'linkstoimage' => 'このファイルへは以下の {{PLURAL:$1|ページ| $1 ページ}}からリンクしています:',
'linkstoimage-more' => 'このファイルへは $1 を超える数のページからリンクがあります。
** 複数アカウントの不正利用
** 不適切な利用者名',
'ipb-hardblock' => 'ログインしている利用者によるこのIPアドレスからの編集を不許可',
-'ipbcreateaccount' => 'アカウント作成を禁止する',
+'ipbcreateaccount' => 'アカウント作成を禁止',
'ipbemailban' => 'メール送信を防止',
'ipbenableautoblock' => 'この利用者が最後に使用したIPアドレスと、後に編集しようとしたIPアドレスを自動的にブロック',
'ipbsubmit' => 'この利用者をブロック',
'tooltip-pt-login' => 'ログインすることが推奨されます。ただし、必須ではありません。',
'tooltip-pt-anonlogin' => 'ログインすることが推奨されます。ただし、必須ではありません。',
'tooltip-pt-logout' => 'ログアウト',
-'tooltip-ca-talk' => '記事についての議論',
+'tooltip-ca-talk' => '本文ページについての議論',
'tooltip-ca-edit' => 'このページを編集できます。保存する前にプレビューボタンを使用してください。',
'tooltip-ca-addsection' => '新しい節を開始する',
'tooltip-ca-viewsource' => 'このページは保護されています。
'tooltip-t-specialpages' => '特別ページの一覧',
'tooltip-t-print' => 'このページの印刷用ページ',
'tooltip-t-permalink' => 'このページのこの版への固定リンク',
-'tooltip-ca-nstab-main' => '本文を表示',
+'tooltip-ca-nstab-main' => '本文を閲覧',
'tooltip-ca-nstab-user' => '利用者ページを表示',
'tooltip-ca-nstab-media' => 'メディアページを表示',
'tooltip-ca-nstab-special' => 'これは特別ページです。編集することはできません。',
'tooltip-ca-nstab-mediawiki' => 'システムメッセージを表示',
'tooltip-ca-nstab-template' => 'テンプレートを表示',
'tooltip-ca-nstab-help' => 'ヘルプページを表示',
-'tooltip-ca-nstab-category' => 'カテゴリページを表示',
+'tooltip-ca-nstab-category' => 'カテゴリページを閲覧',
'tooltip-minoredit' => 'この編集を細部の変更とマーク',
'tooltip-save' => '変更を保存',
'tooltip-preview' => '変更をプレビューで確認できます。保存前に使用してください!',
'fileduplicatesearch-info' => '$1×$2 ピクセル<br />ファイルサイズ:$3<br />MIMEタイプ:$4',
'fileduplicatesearch-result-1' => 'ファイル「$1」と重複するファイルはありません。',
'fileduplicatesearch-result-n' => 'ファイル「$1」は$2件のファイルと重複しています。',
-'fileduplicatesearch-noresults' => 'ã\80\8c$1ã\80\8dã\81¨ã\81\84ã\81\86å\90\8då\89\8dã\81®ã\83\95ã\82¡ã\82¤ã\83«ã\81\8cã\81¿ã\81¤ã\81\8bりません。',
+'fileduplicatesearch-noresults' => 'ã\80\8c$1ã\80\8dã\81¨ã\81\84ã\81\86å\90\8då\89\8dã\81®ã\83\95ã\82¡ã\82¤ã\83«ã\81¯ã\81\82りません。',
# Special:SpecialPages
'specialpages' => '特別ページ',
'note' => "'''შენიშვნა:'''",
'previewnote' => "'''დაიმახსოვრეთ, ეს მხოლოდ წინასწარი გადახედვაა.'''
თქვენი ცვლილებები ჯერ არ შენახულა!",
+'continue-editing' => 'რედაქტირების გაგრძელება',
'previewconflict' => 'შავი ნიმუში უჩვენებს ტექსტს ზედა რედაქტირების ფანჯარაში, როგორც ის გამოჩნდება თუ თქვენ მას შეინახავთ.',
'session_fail_preview' => "'''უკაცრავად! ვერ შევძელით თქვენი რედაქტირების შენახვა სესიის მონაცემთა დაკარგვის გამო.
გთხოვთ ისევ სცადოთ.
'note' => "'''Notiz:'''",
'previewnote' => "'''Denkt drun datt dëst nëmmen eng net gespäichert Versioun ass.'''
Är Ännerunge sinn nach net gespäichert!",
+'continue-editing' => 'Weider änneren',
'previewconflict' => 'Dir gesitt an dem ieweschten Textfeld wéi den Text ausgesi wäert, wann Dir späichert.',
'session_fail_preview' => "'''Är Ännerung konnt net gespäichert gi well d'Date vun Ärer Sessioun verluergaange sinn.
Versicht et w.e.g. nach eng Kéier.
'wantedpages' => 'Gewënschte Säiten',
'wantedpages-badtitle' => 'Net valabelen Titel am Resultat: $1',
'wantedfiles' => 'Gewënschte Fichieren',
+'wantedfiletext-cat' => 'Dës Fichiere gi benotzt awer et gëtt se net. Fichiere aus frieme Repositorie kënnen hei gewise ginn och wann et se gëtt. All esou falsch Positiver ginn <del>duerchgestrach</del>. Zousätzlech gi Säiten an deene Fichieren dra sinn déi et net gëtt op [[:$1]] gewisen.',
'wantedtemplates' => 'Gewënschte Schablounen',
'mostlinked' => 'Dacks verlinkte Säiten',
'mostlinkedcategories' => 'Dacks benotzte Kategorien',
'note' => "'''Напомена:'''",
'previewnote' => "'''Имајте предвид дека ова е само преглед.'''
Вашите промени сè уште не се зачувани!",
+'continue-editing' => 'Продолжете со уредување',
'previewconflict' => 'Овој преглед прикажува како ќе изгледа текстот внесен во горниот дел откако ќе се зачува страницата.',
'session_fail_preview' => "'''Жалиме! Не можевме да го обработиме вашето уредување поради загуба на сесиски податоци.'''
Обидете се повторно.
Секој што го знае клучот во полево ќе може да го чита вашиот список на набљудувања, па затоа изберете некоја безбедна вредност.
Еве една случајно-создадена вредност што можете да ја користите: $1',
'savedprefs' => 'Вашите нагодувања се зачувани.',
-'timezonelegend' => 'ЧаÑ\81овна зона:',
+'timezonelegend' => 'ЧаÑ\81овен поÑ\98аÑ\81:',
'localtime' => 'Локално време:',
'timezoneuseserverdefault' => 'Од викито ($1)',
'timezoneuseoffset' => 'Друго (посочете отстапување)',
'rcshowhidemine' => '$1 мои уредувања',
'rclinks' => 'Прикажи скорешни $1 промени во последните $2 дена<br />$3',
'diff' => 'разл',
-'hist' => 'ист',
+'hist' => 'истор',
'hide' => 'Скриј',
'show' => 'Прикажи',
'minoreditletter' => 'с',
'note' => "'''പ്രത്യേക ശ്രദ്ധയ്ക്ക്:'''",
'previewnote' => "'''ഇതൊരു പ്രിവ്യൂ മാത്രമാണെന്ന് ഓർക്കുക.'''
താങ്കൾ വരുത്തിയ മാറ്റങ്ങൾ ഇതുവരെ സേവ് ചെയ്തിട്ടില്ല!",
+'continue-editing' => 'തിരുത്തൽ തുടരുക',
'previewconflict' => 'ഈ പ്രിവ്യൂവിൽ മുകളിലെ ടെക്സ്റ്റ് ഏരിയയിലുള്ള എഴുത്ത് മാത്രമാണ് കാട്ടുന്നത്, സേവ് ചെയ്യാൻ താങ്കൾ തീരുമാനിച്ചാൽ അത് സേവ് ആകുന്നതാണ്.',
'session_fail_preview' => "'''ക്ഷമിക്കണം! സെഷൻ ഡാറ്റ നഷ്ടപ്പെട്ടതിനാൽ താങ്കളുടെ തിരുത്തലിന്റെ തുടർപ്രക്രിയ നടത്തുവാൻ സാധിച്ചില്ല.'''
ദയവായി വീണ്ടും ശ്രമിക്കൂ.
'notanarticle' => 'ലേഖന താൾ അല്ല',
'notvisiblerev' => 'മറ്റൊരു ഉപയോക്താവ് സൃഷ്ടിച്ച അവസാനത്തെ നാൾപ്പതിപ്പ് മായ്ച്ചിരിക്കുന്നു',
'watchnochange' => 'താങ്കൾ ശ്രദ്ധിക്കുന്ന താളുകൾ ഒന്നും തന്നെ ഇക്കാലയളവിൽ തിരുത്തപ്പെട്ടിട്ടില്ല.',
-'watchlist-details' => 'à´¸à´\82â\80\8cവാദà´\82 താളàµ\81à´\95ൾ à´\85à´²àµ\8dലാതàµ\8dà´¤ {{PLURAL:$1|ഒരു താൾ|$1 താളുകൾ}} താങ്കൾ ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിലുണ്ട്.',
+'watchlist-details' => 'à´¸à´\82â\80\8cവാദà´\82 താളàµ\81à´\95ൾ à´\89ൾപàµ\8dà´ªàµ\86à´\9fàµ\81à´¤àµ\8dതാതàµ\86 {{PLURAL:$1|ഒരു താൾ|$1 താളുകൾ}} താങ്കൾ ശ്രദ്ധിക്കുന്നവയുടെ പട്ടികയിലുണ്ട്.',
'wlheader-enotif' => '* ഇമെയിൽ വിജ്ഞാപനം സാധ്യമാക്കിയിരിക്കുന്നു.',
'wlheader-showupdated' => "* താങ്കളുടെ അവസാന സന്ദർശനത്തിനു ശേഷം തിരുത്തപ്പെട്ട താളുകൾ '''കടുപ്പിച്ച്''' കാണിച്ചിരിക്കുന്നു",
'watchmethod-recent' => 'ശ്രദ്ധിക്കുന്ന താളുകൾക്കുവേണ്ടി പുതിയ മാറ്റങ്ങൾ പരിശോധിക്കുന്നു',
'delete_and_move' => 'മായ്ക്കുകയും മാറ്റുകയും ചെയ്യുക',
'delete_and_move_text' => '==താൾ മായ്ക്കേണ്ടിയിരിക്കുന്നു==
-താà´\99àµ\8dà´\95ൾ à´¸àµ\83à´·àµ\8dà´\9fà´¿à´\95àµ\8dà´\95ാൻ à´¶àµ\8dരമിà´\9aàµ\8dà´\9a "[[:$1]]" à´\8eà´¨àµ\8dà´¨ താൾ നിലവിലàµ\81à´£àµ\8dà´\9fàµ\8d. à´\86 താൾ മായàµ\8dà´\9aàµ\8dà´\9aàµ\8d à´ªàµ\81തിയ തലà´\95àµ\8dà´\95àµ\86à´\9fàµ\8dà´\9fàµ\8d നൽകേണ്ടതുണ്ടോ?',
+മാറàµ\8dറാനായി നൽà´\95à´¿à´¯ "[[:$1]]" à´\8eà´¨àµ\8dà´¨ താൾ നിലവിലàµ\81à´£àµ\8dà´\9fàµ\8d. à´\88 മാറàµ\8dà´±à´\82 à´¨à´\9fà´¤àµ\8dà´¤àµ\81à´¨àµ\8dനതിനàµ\81à´µàµ\87à´£àµ\8dà´\9fà´¿ à´\86 താൾ മായàµ\8dà´\95àµ\8dകേണ്ടതുണ്ടോ?',
'delete_and_move_confirm' => 'ശരി, താൾ നീക്കം ചെയ്യുക',
'delete_and_move_reason' => '"[[$1]]" എന്നതിൽ നിന്നും മാറ്റാനുള്ള സൗകര്യത്തിനായി മായ്ച്ചു',
'selfmove' => 'പഴയ തലക്കെട്ടു തന്നെയാണ് മാറ്റാനായി നൽകിയിരിക്കുന്നത്; അതിനാൽ തലക്കെട്ടുമാറ്റം സാദ്ധ്യമല്ല.',
'note' => "'''Catatan:'''",
'previewnote' => "'''Ingatlah bahawa ini hanya pralihat.'''
Perubahan anda belum disimpan!",
+'continue-editing' => 'Teruskan menyunting',
'previewconflict' => 'Paparan ini merupakan teks di bahagian atas dalam kotak sunting teks. Teks ini akan disimpan sekiranya anda memilih berbuat demikian.',
'session_fail_preview' => "'''Kami tidak dapat memproses suntingan anda kerana kehilangan data sesi. Sila cuba lagi. Jika masalah ini berlanjutan, [[Special:UserLogout|log keluar]] dahulu, kemudian log masuk sekali lagi.'''",
'session_fail_preview_html' => "'''Kami tidak dapat memproses suntingan anda kerana kehilangan data sesi.'''
'note' => "'''Opmerking:'''",
'previewnote' => "'''Let op: dit is een controlepagina.'''
Uw tekst is niet opgeslagen!",
+'continue-editing' => 'Doorgaan met bewerken',
'previewconflict' => 'Deze voorvertoning geeft aan hoe de tekst in het bovenste veld eruit ziet als u deze opslaat.',
'session_fail_preview' => "'''Uw bewerking is niet verwerkt, omdat de sessiegegevens verloren zijn gegaan.
Probeer het opnieuw.
'uploaderror' => 'Файл сæвæрыны рæдыд',
'uploadlogpage' => 'Æвгæндты лог',
'filename' => 'Файлы ном',
-'filedesc' => 'Ð\98вдÑ\82Ñ\8bÑ\82Ñ\8b афыст:',
+'filedesc' => 'Ð\90фыст:',
'minlength1' => 'Файлы номы хъуамæ æппынкъаддæр иу дамгъæ уа.',
'badfilename' => 'Нывы ном ивд æрцыдис. Ныр хуины «$1».',
'savefile' => 'Бавæр æй',
'newuserlog-byemail' => 'palavra-chave enviada por correio-electrónico',
# Feedback
+'feedback-bugornote' => 'Se está pronto para descrever um problema técnico em detalhe, por favor, [$1 denuncie um defeito].
+Caso contrário, pode facilmente usar o formulário abaixo. O seu comentário será adicionado à página "[$3 $2]", junto com o seu nome de utilizador e o navegador que está a usar.',
'feedback-subject' => 'Assunto:',
'feedback-message' => 'Mensagem:',
'feedback-cancel' => 'Cancelar',
'feedback-error1' => 'Erro: O resultado da API não foi reconhecido',
'feedback-error2' => 'Erro: A edição falhou',
'feedback-error3' => 'Erro: A API não responde',
+'feedback-thanks' => 'Obrigado! O seu comentário foi adicionado à página "[ $2 $1 ]".',
'feedback-close' => 'Feito',
'feedback-bugcheck' => 'Perfeito! Verifique apenas que não é já um dos [$1 defeitos conhecidos].',
-'feedback-bugnew' => 'Eu verifiquei. Reportar um novo bug.',
+'feedback-bugnew' => 'Eu verifiquei. Denunciar um novo defeito.',
# API errors
'api-error-badaccess-groups' => 'Não tem permissão para enviar ficheiros para esta wiki.',
'note' => "'''Notă:'''",
'previewnote' => "'''Țineți cont că aceasta este doar o previzualizare.'''
Modificările dumneavoastră nu au fost încă salvate!",
+'continue-editing' => 'Continuare editare',
'previewconflict' => 'Această pre-vizualizare reflectă textul din caseta de sus, respectiv felul în care va arăta articolul dacă alegeți să-l salvați acum.',
'session_fail_preview' => "'''Ne pare rău! Nu am putut procesa modificarea dumneavoastră din cauza pierderii datelor sesiunii.
Vă rugăm să încercați din nou.
'note' => "'''Примечание:'''",
'previewnote' => "'''Помните, что это только предварительный просмотр.'''
Ваши изменения ещё не были сохранены!",
+'continue-editing' => 'Продолжить редактирование',
'previewconflict' => 'Этот предварительный просмотр отражает текст в верхнем окне редактирования так, как он будет выглядеть, если вы решите записать его.',
'session_fail_preview' => "'''К сожалению, сервер не смог обработать вашу правку из-за потери идентификатора сессии.
Пожалуйста, попробуйте ещё раз.
'mytalk' => 'என் பேச்சு',
'anontalk' => 'இந்த ஐ.பி. முகவரிக்கான பேச்சு',
'navigation' => 'வழிசெலுத்தல்',
-'and' => ' மற்றும்',
+'and' => ' மற்றும்',
# Cologne Blue skin
'qbfind' => 'கண்டுபிடி',
'version-variables' => 'மாறிகள்',
'version-antispam' => ' குப்பை (spam) தடுப்பு',
'version-skins' => 'தோல்கள்',
-'version-other' => 'மறà¯\8dறவà¯\88',
+'version-other' => 'பிறரà¯\8d',
'version-mediahandlers' => 'ஊடக கையாளிகள்',
'version-hooks' => 'கொக்கிகள்',
'version-extension-functions' => 'நீட்சி செயற்பாடுகள்',
'version-version' => '(பதிப்பு $1)',
'version-license' => 'அனுமதி',
'version-poweredby-credits' => "இந்த் விக்கி '''[//www.mediawiki.org/ MediaWiki]''' இதன் மூலம் வழங்கப்படுகிறது, காப்புரிமை © 2001-$1 $2.",
-'version-poweredby-others' => 'மறà¯\8dறவà¯\88à®\95ள்',
+'version-poweredby-others' => 'பிறர்',
'version-license-info' => 'மீடியாவிக்கியானது இலவச மென்பொருள்.இதை நீங்கள் மற்றவர்களுக்கு கொடுப்பது அல்லது திருத்தம் செய்வது இலவச மென்பொருள் அறக்கட்டளை வழங்கிய GNUவின் பொது உரிம விதிகளுக்குட்பட்டது;உரிமத்தின் இரண்டாவது பதிப்பு அல்லது அதற்கு மேற்பட்ட பதிப்பு (உங்கள் விருப்பத்திற்க்கேற்றவாறு).
மீடியா உபயோகப்படக்கூடியது என்ற நம்பிக்கையில் வெளியிடப்பட்டுள்ளது, ஆனால் இதற்க்கு உத்தரவாதம் கிடையாது.மேலும் வணிகத்தன்மைக்கான அல்லது ஒரு குறிப்பிட்ட செயலுக்காகவும் உத்தரவாதம் கிடையாது.மேலும் விவரங்களுக்கு GNU பொது உரிமத்தை பார்க்கவும்.
நீங்கள் இந்த மென்பொருளுடன் [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License] பெற்றீருப்பிர்கள்;இல்லையெனில் , Free Software Foundation, Inc.,51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA க்கு எழுதவும்.அல்லது [//www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].',
Подібні проблеми можуть виникати при використанні анонімізуючих веб-проксі, що містять помилки.'''",
'edit_form_incomplete' => "'''Частина даних із форми редагування не досягла сервера. Уважно перевірте чи не пошкоджено ваших правок і спробуйте ще раз.'''",
'editing' => 'Редагування $1',
+'creating' => 'Створення $1',
'editingsection' => 'Редагування $1 (розділ)',
'editingcomment' => 'Редагування $1 (новий розділ)',
'editconflict' => 'Конфлікт редагування: $1',
'nologin' => "איר האט נישט קיין קאנטע? '''$1'''.",
'nologinlink' => 'באשאפֿט א קאנטע',
'createaccount' => 'באשאפֿט א נייע קאנטע',
-'gotaccount' => "האסט שוין א קאנטע? '''$1'''.",
-'gotaccountlink' => 'אריינלאגירן',
+'gotaccount' => "האסטו שוין א קאנטע? '''$1'''.",
+'gotaccountlink' => 'אַרײַנלאגירן',
'userlogin-resetlink' => 'פארגעסן אײַערע אַרײַנלאָגירן פרטים?',
'createaccountmail' => 'דורך ע-פאסט',
'createaccountreason' => 'אורזאַך:',
'api-error-uploaddisabled' => 'ארויפֿלאָדן איז אומאַקטיווירט אויף דער וויקי',
'api-error-verification-error' => 'די טעקע איז מעגלעך פֿארדארבן, אדער האט א פֿאַלשע ענדונג.',
+# Durations
+'duration-seconds' => '$1 {{PLURAL:$1|סעקונדע|סעקונדעס}}',
+'duration-minutes' => '$1 {{PLURAL:$1|מינוט|מינוט}}',
+'duration-hours' => "$1 {{PLURAL:$1|שעה|שעה'ן}}",
+'duration-days' => '$1 {{PLURAL:$1|טאג|טעג}}',
+'duration-weeks' => '$1 {{PLURAL:$1|וואך|וואכן}}',
+'duration-years' => '$1 {{PLURAL:$1|יאר|יאר}}',
+'duration-decades' => '$1 {{PLURAL:$1|צענדליקער|צענדליקערס}}',
+'duration-centuries' => '$1 {{PLURAL:$1|יארהונדערט|יארהונדערטער}}',
+
);
'updated' => '(已更新)',
'note' => "'''注意:'''",
'previewnote' => "'''请记住这仅为预览。'''您的更改还未保存!",
+'continue-editing' => '继续编辑',
'previewconflict' => '这个预览显示了上面文字编辑区中的内容。它将在你选择保存后出现。',
'session_fail_preview' => "'''抱歉!由于会话数据丢失,我们不能处理你的编辑。'''请重试。如果再次失败,请尝试[[Special:UserLogout|退出]]后重新登录。",
'session_fail_preview_html' => "'''抱歉!我们不能处理你在进程数据丢失时的编辑。'''
'right-editusercss' => '编辑其他用户的CSS文件',
'right-edituserjs' => '编辑其他用户的JavaScript文件',
'right-rollback' => '快速回退最后对特定页面作出的编辑的用户的所有编辑',
-'right-markbotedits' => '标示复原编辑作机械人编辑',
+'right-markbotedits' => '将回退编辑标记为机器人编辑动作',
'right-noratelimit' => '没有使用频率限制',
'right-import' => '由其它wiki中导入页面',
'right-importupload' => '由文件上传中导入页面',
'allpages-hide-redirects' => '隐藏重定向页',
# SpecialCachedPage
+'cachedspecial-viewing-cached-ttl' => '你正在浏览本页的缓存版本,至多可能存在 $1 的延迟。',
'cachedspecial-refresh-now' => '查看最新的。',
# Special:Categories
'version-software' => '已安装的软件',
'version-software-product' => '产品',
'version-software-version' => '版本',
+'version-entrypoints-header-url' => 'URL',
# Special:FilePath
'filepath' => '文件路径',
} else {
$text = $this->getTextDb( $id );
}
+
+ // No more checks for texts from DB for now.
+ // If we received something that is not false,
+ // We treat it as good text, regardless of whether it actually is or is not
+ if ( $text !== false ) {
+ return $text;
+ }
}
if ( $text === false ) {
// Something went wrong; we did not a text that was plausible :(
$failures++;
-
- // After backing off for some time, we try to reboot the whole process as
- // much as possible to not carry over failures from one part to the other
- // parts
- sleep( $this->failureTimeout );
- try {
- $this->rotateDb();
- if ( $this->spawn ) {
- $this->closeSpawn();
- $this->openSpawn();
+ // A failure in a prefetch hit does not warrant resetting db connection etc.
+ if ( ! $tryIsPrefetch ) {
+ // After backing off for some time, we try to reboot the whole process as
+ // much as possible to not carry over failures from one part to the other
+ // parts
+ sleep( $this->failureTimeout );
+ try {
+ $this->rotateDb();
+ if ( $this->spawn ) {
+ $this->closeSpawn();
+ $this->openSpawn();
+ }
+ } catch ( Exception $e ) {
+ $this->progress( "Rebooting getText infrastructure failed (" . $e->getMessage() . ")" .
+ " Trying to continue anyways" );
}
- } catch ( Exception $e ) {
- $this->progress( "Rebooting getText infrastructure failed (" . $e->getMessage() . ")" .
- " Trying to continue anyways" );
}
}
* @param $id int The page_id of the redirect
*/
private function fixRedirect( $id ) {
- $title = Title::newFromID( $id );
+ $page = WikiPage::newFromID( $id );
$dbw = wfGetDB( DB_MASTER );
- if ( is_null( $title ) ) {
+ if ( $page === null ) {
// This page doesn't exist (any more)
// Delete any redirect table entry for it
$dbw->delete( 'redirect', array( 'rd_from' => $id ),
return;
}
- $page = WikiPage::factory( $title );
$rt = $page->getRedirectTarget();
if ( $rt === null ) {
- // $title is not a redirect
+ // The page is not a redirect
// Delete any redirect table entry for it
$dbw->delete( 'redirect', array( 'rd_from' => $id ),
__METHOD__ );
* @param $id int The page_id
*/
public static function fixLinksFromArticle( $id ) {
- global $wgParser;
+ global $wgParser, $wgContLang;
- $title = Title::newFromID( $id );
- $dbw = wfGetDB( DB_MASTER );
+ $page = WikiPage::newFromID( $id );
LinkCache::singleton()->clear();
- if ( is_null( $title ) ) {
+ if ( $page === null ) {
return;
}
- $revision = Revision::newFromTitle( $title );
- if ( !$revision ) {
+ $text = $page->getRawText();
+ if ( $text === false ) {
return;
}
+ $dbw = wfGetDB( DB_MASTER );
$dbw->begin( __METHOD__ );
- $options = new ParserOptions;
- $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
- $update = new LinksUpdate( $title, $parserOutput, false );
+ $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+ $parserOutput = $wgParser->parse( $text, $page->getTitle(), $options, true, true, $page->getLatest() );
+ $update = new LinksUpdate( $page->getTitle(), $parserOutput, false );
$update->doUpdate();
+
$dbw->commit( __METHOD__ );
}
'common/commonContent.css' => array( 'media' => 'screen' ),
'common/commonInterface.css' => array( 'media' => 'screen' ),
'vector/screen.css' => array( 'media' => 'screen' ),
+ 'vector/screen-hd.css' => array( 'media' => 'screen and (min-width: 900px)' ),
),
'scripts' => 'vector/vector.js',
'remoteBasePath' => $GLOBALS['wgStylePath'],
*/
state: function ( module, state ) {
var m;
+
if ( typeof module === 'object' ) {
for ( m in module ) {
mw.loader.state( m, module[m] );
if ( registry[module] === undefined ) {
mw.loader.register( module );
}
- registry[module].state = state;
+ if ( state === 'ready' && registry[module].state !== state) {
+ // Make sure pending modules depending on this one get executed if their
+ // dependencies are now fulfilled!
+ registry[module].state = state;
+ handlePending( module );
+ } else {
+ registry[module].state = state;
+ }
},
/**
--- /dev/null
+/* Vector screen styles for high definition displays */
+
+div#content {
+ margin-left: 11em;
+ padding: 1.25em 1.5em 1.5em 1.5em;
+}
+#p-logo {
+ left: 0.5em;
+}
+div#footer {
+ margin-left: 11em;
+ padding: 1.25em;
+}
+div#mw-panel div.portal div.body {
+ padding-left: 0.5em;
+}
+div#mw-panel div.portal h5 {
+ padding-left: 2.25em;
+}
+#p-search {
+ margin-right: 1em;
+}
+#left-navigation {
+ left: 11em;
+}
+#p-personal {
+ right: 1em;
+}
+#mw-head-base {
+ margin-left: 11em;
+}
/* Personal */
#p-personal {
position: absolute;
- top: 0;
+ top: 0.33em;
right: 0.75em;
}
#p-personal h5 {
.tipsy {
font-size: 0.8em;
}
+
+/* Animate between standard and high definition layouts */
+
+div#content,
+div#footer {
+ transition: margin-left 250ms, padding 250ms;
+ -moz-transition: margin-left 250ms, padding 250ms;
+ -webkit-transition: margin-left 250ms, padding 250ms;
+ -o-transition: margin-left 250ms, padding 250ms;
+}
+#p-logo,
+#left-navigation {
+ transition: left 250ms;
+ -moz-transition: left 250ms;
+ -webkit-transition: left 250ms;
+ -o-transition: left 250ms;
+}
+div#mw-panel div.portal div.body,
+div#mw-panel div.portal h5 {
+ transition: padding-left 250ms;
+ -moz-transition: padding-left 250ms;
+ -webkit-transition: padding-left 250ms;
+ -o-transition: padding-left 250ms;
+}
+#p-search {
+ transition: margin-right 250ms;
+ -moz-transition: margin-right 250ms;
+ -webkit-transition: margin-right 250ms;
+ -o-transition: margin-right 250ms;
+}
+#p-personal {
+ transition: right 250ms;
+ -moz-transition: right 250ms;
+ -webkit-transition: right 250ms;
+ -o-transition: right 250ms;
+}
+#mw-head-base {
+ transition: margin-left 250ms;
+ -moz-transition: margin-left 250ms;
+ -webkit-transition: margin-left 250ms;
+ -o-transition: margin-left 250ms;
+}
$this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." );
+ // Expected listing (top files only)
+ $expected = array(
+ "test1.txt",
+ "test2.txt",
+ "test3.txt",
+ "test4.txt",
+ "test5.txt"
+ );
+ sort( $expected );
+
+ // Actual listing (top files only)
+ $list = array();
+ $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
foreach ( $files as $file ) { // clean up
$this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
}
foreach ( $iter as $iter ) {} // no errors
}
+ public function testGetDirectoryList() {
+ $this->backend = $this->singleBackend;
+ $this->tearDownFiles();
+ $this->doTestGetDirectoryList();
+ $this->tearDownFiles();
+
+ $this->backend = $this->multiBackend;
+ $this->tearDownFiles();
+ $this->doTestGetDirectoryList();
+ $this->tearDownFiles();
+ }
+
+ private function doTestGetDirectoryList() {
+ $backendName = $this->backendClass();
+
+ $base = $this->baseStorePath();
+ $files = array(
+ "$base/unittest-cont1/test1.txt",
+ "$base/unittest-cont1/test2.txt",
+ "$base/unittest-cont1/test3.txt",
+ "$base/unittest-cont1/subdir1/test1.txt",
+ "$base/unittest-cont1/subdir1/test2.txt",
+ "$base/unittest-cont1/subdir2/test3.txt",
+ "$base/unittest-cont1/subdir2/test4.txt",
+ "$base/unittest-cont1/subdir2/subdir/test1.txt",
+ "$base/unittest-cont1/subdir3/subdir/test2.txt",
+ "$base/unittest-cont1/subdir4/subdir/test3.txt",
+ "$base/unittest-cont1/subdir4/subdir/test4.txt",
+ "$base/unittest-cont1/subdir4/subdir/test5.txt",
+ "$base/unittest-cont1/subdir4/subdir/sub/test0.txt",
+ "$base/unittest-cont1/subdir4/subdir/sub/120-px-file.txt",
+ );
+
+ // Add the files
+ $ops = array();
+ foreach ( $files as $file ) {
+ $this->prepare( array( 'dir' => dirname( $file ) ) );
+ $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
+ }
+ $status = $this->backend->doOperations( $ops );
+ $this->assertEquals( array(), $status->errors,
+ "Creation of files succeeded ($backendName)." );
+ $this->assertEquals( true, $status->isOK(),
+ "Creation of files succeeded with OK status ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "subdir1",
+ "subdir2",
+ "subdir3",
+ "subdir4",
+ );
+ sort( $expected );
+
+ $this->assertEquals( true,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir1" ) ),
+ "Directory exists in ($backendName)." );
+ $this->assertEquals( true,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) ),
+ "Directory exists in ($backendName)." );
+ $this->assertEquals( false,
+ $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/test1.txt" ) ),
+ "Directory does not exists in ($backendName)." );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (with trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Expected listing
+ $expected = array(
+ "subdir",
+ );
+ sort( $expected );
+
+ // Actual listing (no trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (with trailing slash)
+ $list = array();
+ $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+ // Actual listing (using iterator second time)
+ $list = array();
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." );
+
+ // Expected listing (recursive)
+ $expected = array(
+ "subdir1",
+ "subdir2",
+ "subdir3",
+ "subdir4",
+ "subdir2/subdir",
+ "subdir3/subdir",
+ "subdir4/subdir",
+ "subdir4/subdir/sub",
+ );
+ sort( $expected );
+
+ // Actual listing (recursive)
+ $list = array();
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ // Expected listing (recursive)
+ $expected = array(
+ "subdir",
+ "subdir/sub",
+ );
+ sort( $expected );
+
+ // Actual listing (recursive)
+ $list = array();
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir4" ) );
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ // Actual listing (recursive, second time)
+ $list = array();
+ foreach ( $iter as $file ) {
+ $list[] = $file;
+ }
+ sort( $list );
+
+ $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+ foreach ( $files as $file ) { // clean up
+ $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+ }
+
+ $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+ foreach ( $iter as $iter ) {} // no errors
+ }
+
// test helper wrapper for backend prepare() function
private function prepare( array $params ) {
$this->dirsToPrune[] = $params['dir'];
require_once( "$IP/maintenance/Maintenance.php" );
class PHPUnitMaintClass extends Maintenance {
+
+ function __construct() {
+ parent::__construct();
+ $this->addOption( 'with-phpunitdir'
+ , 'Directory to include PHPUnit from, for example when using a git fetchout from upstream. Path will be prepended to PHP `include_path`.'
+ , false # not required
+ , true # need arg
+ );
+ }
+
public function finalSetup() {
parent::finalSetup();
$wgLocalisationCacheConf['storeClass'] = 'LCStore_Null';
}
- public function execute() { }
+
+ public function execute() {
+ global $IP;
+
+ # Make sure we have --configuration or PHPUnit might complain
+ if( !in_array( '--configuration', $_SERVER['argv'] ) ) {
+ //Hack to eliminate the need to use the Makefile (which sucks ATM)
+ array_splice( $_SERVER['argv'], 1, 0,
+ array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) );
+ }
+
+ # --with-phpunitdir let us override the default PHPUnit version
+ if( $phpunitDir = $this->getOption( 'with-phpunitdir' ) ) {
+ # Sanity checks
+ if( !is_dir($phpunitDir) ) {
+ $this->error( "--with-phpunitdir should be set to an existing directory", 1 );
+ }
+ if( !is_readable( $phpunitDir."/PHPUnit/Runner/Version.php" ) ) {
+ $this->error( "No usable PHPUnit installation in $phpunitDir.\nAborting.\n", 1 );
+ }
+
+ # Now prepends provided PHPUnit directory
+ $this->output( "Will attempt loading PHPUnit from `$phpunitDir`\n" );
+ set_include_path( $phpunitDir
+ . PATH_SEPARATOR . get_include_path() );
+
+ # Cleanup $args array so the option and its value do not
+ # pollute PHPUnit
+ $key = array_search( '--with-phpunitdir', $_SERVER['argv'] );
+ unset( $_SERVER['argv'][$key] ); // the option
+ unset( $_SERVER['argv'][$key+1] ); // its value
+ $_SERVER['argv'] = array_values( $_SERVER['argv'] );
+
+ }
+ }
+
public function getDbType() {
return Maintenance::DB_ADMIN;
}
$maintClass = 'PHPUnitMaintClass';
require( RUN_MAINTENANCE_IF_MAIN );
-if( !in_array( '--configuration', $_SERVER['argv'] ) ) {
- //Hack to eliminate the need to use the Makefile (which sucks ATM)
- array_splice( $_SERVER['argv'], 1, 0,
- array( '--configuration', $IP . '/tests/phpunit/suite.xml' ) );
-}
-
require_once( 'PHPUnit/Runner/Version.php' );
-if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) {
+
+if( PHPUnit_Runner_Version::id() !== '@package_version@'
+ && version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) {
die( 'PHPUnit 3.5 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" );
}
require_once( 'PHPUnit/Autoload.php' );
require_once( "$IP/tests/TestsAutoLoader.php" );
MediaWikiPHPUnitCommand::main();
-