* $wgDataCenterId and $wgDataCenterRoles where added, which will serve as
basic configuration settings needed for multi-datacenter setups.
$wgDataCenterUpdateStickTTL was also added.
+* Added a new hook, 'UserMailerTransformContent', to transform the contents
+ of an email. This is similar to the EmailUser hook but applies to all mail
+ sent via UserMailer.
+* Added a new hook, 'UserMailerTransformMessage', to transform the contents
+ of an emai after MIME encoding.
+* Added a new hook, 'UserMailerSplitTo', to control which users have to be
+ emailed separately (ie. there is a single address in the To: field) so
+ user-specific changes to the email can be applied safely.
+* $wgCdnMaxageLagged was added, which limits the CDN cache TTL
+ when any load balancer uses a DB that is lagged beyond the 'max lag'
+ setting in the relevant section of $wgLBFactoryConf.
==== External libraries ====
'DBLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
'DBMasterPos' => __DIR__ . '/includes/db/DatabaseUtility.php',
'DBQueryError' => __DIR__ . '/includes/db/DatabaseError.php',
+ 'DBReadOnlyError' => __DIR__ . '/includes/db/DatabaseError.php',
'DBSiteStore' => __DIR__ . '/includes/site/DBSiteStore.php',
'DBUnexpectedError' => __DIR__ . '/includes/db/DatabaseError.php',
'DataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedBagOStuff.php',
'MemcachedPeclBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPeclBagOStuff.php',
'MemcachedPhpBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPhpBagOStuff.php',
+ 'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php',
'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
$to: Array of MailAddress objects for the recipients
&$returnPath: The return address string
+'UserMailerSplitTo': Called in UserMailer::send() to give extensions a chance
+to split up an email with multiple the To: field into separate emails.
+$to: array of MailAddress objects; unset the ones which should be mailed separately
+
+'UserMailerTransformContent': Called in UserMailer::send() to change email contents.
+Extensions can block sending the email by returning false and setting $error.
+$to: array of MailAdresses of the targets
+$from: MailAddress of the sender
+&$body: email body, either a string (for plaintext emails) or an array with 'text' and 'html' keys
+&$error: should be set to an error message string
+
+'UserMailerTransformMessage': Called in UserMailer::send() to change email after it has gone through
+the MIME transform. Extensions can block sending the email by returning false and setting $error.
+$to: array of MailAdresses of the targets
+$from: MailAddress of the sender
+&$subject: email subject (not MIME encoded)
+&$headers: email headers (except To: and Subject:) as an array of header name => value pairs
+&$body: email body (in MIME format) as a string
+&$error: should be set to an error message string
+
'UserRemoveGroup': Called when removing a group; return false to override stock
group removal.
$user: the user object that is to have a group removed
/** @var string "AND" or "OR" */
protected $mode;
- /** @var DatabaseBase Read-DB slave */
+ /** @var IDatabase Read-DB slave */
protected $dbr;
/**
$wgInternalServer = false;
/**
- * Cache timeout for the squid, will be sent as s-maxage (without ESI) or
- * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in
- * the Squid config.
+ * Cache TTL for the CDN sent as s-maxage (without ESI) or
+ * Surrogate-Control (with ESI). Without ESI, you should strip
+ * out s-maxage in the Squid config.
*
-* 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days.
+ * 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days.
*/
$wgSquidMaxage = 18000;
+/**
+ * Cache timeout for the CDN when DB slave lag is high
+ * @see $wgSquidMaxage
+ * @since 1.27
+ */
+$wgCdnMaxageLagged = 30;
+
/**
* Default maximum age for raw CSS/JS accesses
*
// or revision is missing, so check for isOK() rather than isGood()
if ( $deleteStatus->isOK() ) {
$status = $file->delete( $reason, $suppress, $user );
- if ( !$status->isOK() ) {
+ if ( $status->isOK() ) {
+ $dbw->commit( __METHOD__ );
+ } else {
$dbw->rollback( __METHOD__ );
}
}
}
/**
- * Make an array to be used for calls to DatabaseBase::buildLike(), which
+ * Make an array to be used for calls to Database::buildLike(), which
* will match the specified string. There are several kinds of filter entry:
* *.domain.com - Produces http://com.domain.%, matches domain.com
* and www.domain.com
*
* @param string $filterEntry Domainparts
* @param string $protocol Protocol (default http://)
- * @return array Array to be passed to DatabaseBase::buildLike() or false on error
+ * @return array Array to be passed to Database::buildLike() or false on error
*/
public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) {
$db = wfGetDB( DB_SLAVE );
$expires = time() + $this->config->get( 'DataCenterUpdateStickTTL' );
$request->response()->setCookie( 'UseDC', 'master', $expires );
}
+
+ // Avoid letting a few seconds of slave lag cause a month of stale data
+ if ( $factory->laggedSlaveUsed() ) {
+ $maxAge = $this->config->get( 'CdnMaxageLagged' );
+ $this->context->getOutput()->lowerCdnMaxage( $maxAge );
+ wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
+ }
}
/**
/** @var int Cache stuff. Looks like mEnableClientCache */
protected $mSquidMaxage = 0;
+ /** @var in Upper limit on mSquidMaxage */
+ protected $mCdnMaxageLimit = INF;
/**
* @var bool Controls if anti-clickjacking / frame-breaking headers will
* @param int $maxage Maximum cache time on the Squid, in seconds.
*/
public function setSquidMaxage( $maxage ) {
- $this->mSquidMaxage = $maxage;
+ $this->mSquidMaxage = min( $maxage, $this->mCdnMaxageLimit );
+ }
+
+ /**
+ * Lower the value of the "s-maxage" part of the "Cache-control" HTTP header
+ *
+ * @param int $maxage Maximum cache time on the Squid, in seconds
+ * @since 1.27
+ */
+ public function lowerCdnMaxage( $maxage ) {
+ $this->mCdnMaxageLimit = $this->min( $maxage, $this->mCdnMaxageLimit );
+ $this->setSquidMaxage( $this->mSquidMaxage );
}
/**
/**
* Do the DB query to iterate through the objects.
- * @param IDatabase $db DatabaseBase object to use for the query
+ * @param IDatabase $db DB object to use for the query
*/
abstract public function doQuery( $db );
/**
* Constructor
- * @param bool|DatabaseBase $database
- * - Boolean: whether to use the master DB
- * - DatabaseBase: database connection to use
+ * @param bool|IDatabase $database
+ * - boolean: Whether to use the master DB
+ * - IDatabase: Database connection to use
*/
public function __construct( $database = false ) {
- if ( $database instanceof DatabaseBase ) {
+ if ( $database instanceof IDatabase ) {
$this->db = $database;
} else {
$this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE );
* for the original initStats, but without output.
*
* @param IDatabase|bool $database
- * - Boolean: whether to use the master DB
- * - DatabaseBase: database connection to use
+ * - boolean: Whether to use the master DB
+ * - IDatabase: Database connection to use
* @param array $options Array of options, may contain the following values
- * - activeUsers Boolean: whether to update the number of active users (default: false)
+ * - activeUsers boolean: Whether to update the number of active users (default: false)
*/
public static function doAllAndCommit( $database, array $options = array() ) {
$options += array( 'update' => false, 'activeUsers' => false );
* on the number of links. Typically called on create and delete.
*/
public function touchLinks() {
- $u = new HTMLCacheUpdate( $this, 'pagelinks' );
- $u->doUpdate();
-
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
if ( $this->getNamespace() == NS_CATEGORY ) {
- $u = new HTMLCacheUpdate( $this, 'categorylinks' );
- $u->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
}
}
$data['mVersion'] = self::VERSION;
$key = wfMemcKey( 'user', 'id', $this->mId );
- $opts = DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+ $opts = Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
ObjectCache::getMainWANInstance()->set( $key, $data, 3600, $opts );
}
*
* @param string $database
* @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
- * @return DatabaseBase|null If invalid selection
+ * @return IDatabase|null If invalid selection
*/
public static function getDB( $database, $ignoreInvalidDB = false ) {
global $wgDBname;
$dbr = wfGetDB( DB_SLAVE );
$dbrWatchlist = wfGetDB( DB_SLAVE, 'watchlist' );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr, $dbrWatchlist );
+ $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
$result = array();
"apihelp-main-param-servedby": "Inclúa o nome do servidor que servía a solicitude nos resultados.",
"apihelp-main-param-curtimestamp": "Incluir a marca de tempo actual no resultado.",
"apihelp-main-param-origin": "Cando se accede á API usando unha petición AJAX entre-dominios (CORS), inicializar o parámetro co dominio orixe. Isto debe incluírse en calquera petición pre-flight, e polo tanto debe ser parte da petición URI (non do corpo POST). Debe coincidir exactamente cunha das orixes na cabeceira <code>Origin</code>, polo que ten que ser fixado a algo como <kbd>https://en.wikipedia.org</kbd> ou <kbd>https://meta.wikimedia.org</kbd>. Se este parámetro non coincide coa cabeceira <code>Origin</code>, devolverase unha resposta 403. Se este parámetro coincide coa cabeceira <code>Origin</code> e a orixe está na lista branca, porase unha cabeceira <code>Access-Control-Allow-Origin</code>.",
- "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. Pode consultarse unha lista de códigos en <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd>, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.",
+ "apihelp-main-param-uselang": "Linga a usar para a tradución de mensaxes. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> con <kbd>siprop=languages</kbd> devolve unha lista de códigos de lingua, ou especificando <kbd>user</kbd> coa preferencia de lingua do usuario actual, ou especificando <kbd>content</kbd> para usar a lingua do contido desta wiki.",
"apihelp-block-description": "Bloquear un usuario.",
"apihelp-block-param-user": "Nome de usuario, dirección ou rango de IPs que quere bloquear.",
"apihelp-block-param-expiry": "Tempo de caducidade. Pode ser relativo (p. ex.<kbd>5 meses</kbd> ou <kbd>2 semanas</kbd>) ou absoluto (p. ex. 2014-09-18T12:34:56Z</kbd>). Se se pon kbd>infinite</kbd>, <kbd>indefinite</kbd>, ou <kbd>never</kbd>, o bloqueo nunca caducará.",
"apihelp-parse-param-pst": "Fai unha transformación antes de gardar a entrada antes de analizala. Válida unicamente para usar con texto.",
"apihelp-parse-param-onlypst": "Facer unha transformación antes de gardar (PST) a entrada, pero sen analizala. Devolve o mesmo wikitexto, despois de que a PST foi aplicada. Só válida cando se usa con <var>$1text</var>.",
"apihelp-parse-param-effectivelanglinks": "Inclúe ligazóns de idioma proporcionadas polas extensións (para usar con <kbd>$1prop=langlinks</kbd>).",
- "apihelp-parse-param-section": "Recuperar unicamente o contido deste número de sección ou cando <kbd>new</kbd> xera unha nova sección.\n\nA sección <kbd>new</kbd> só é atendida cando se especifica <var>text</var>.",
+ "apihelp-parse-param-section": "Analizar unicamente o contido deste número de sección.\n\nCando <kbd>nova</kbd>, analiza <var>$1text</var> e <var>$1sectiontitle</var> como se fose a engadir unha nova sección da páxina.\n\n<kbd>novo</kbd> só se permite cando especifica <var>text</var>.",
"apihelp-parse-param-sectiontitle": "Novo título de sección cando <var>section</var> é <kbd>new</kbd>.\n\nA diferenza da edición de páxinas, non se oculta no <var>summary</var> cando se omite ou está baleiro.",
"apihelp-parse-param-disablelimitreport": "Omitir o informe de límite (\"Informe de límite NewPP\") da saída do analizador.",
"apihelp-parse-param-disablepp": "Use <var>$1disablelimitreport</var> no seu lugar.",
"apihelp-query+pagepropnames-description": "Listar os nomes de todas as propiedades de páxina usados na wiki.",
"apihelp-query+pagepropnames-param-limit": "Máximo número de nomes a retornar.",
"apihelp-query+pagepropnames-example-simple": "Obter os dez primeiros nomes de propiedade.",
- "apihelp-query+pageprops-description": "Obter varias propiedades definidas no contido da páxina.",
- "apihelp-query+pageprops-param-prop": "Listar só esas propiedades. Útil para verificar se unha páxina concreta usa unha propiedade de páxina determinada.",
+ "apihelp-query+pageprops-description": "Obter varias propiedades de páxina definidas no contido da páxina.",
+ "apihelp-query+pageprops-param-prop": "Listar só estas propiedades de páxina (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devolve os nomes das propiedades de páxina usados). Útil para verificar se as páxinas usan unha determinada propiedade de páxina.",
"apihelp-query+pageprops-example-simple": "Obter as propiedades para as páxinas <kbd>Main Page</kbd> e <kbd>MediaWiki</kbd>",
"apihelp-query+pageswithprop-description": "Mostrar a lista de páxinas que empregan unha propiedade determinada.",
- "apihelp-query+pageswithprop-param-propname": "Propiedade de páxina pola que enumerar as páxinas.",
+ "apihelp-query+pageswithprop-param-propname": "Propiedade de páxina para a que enumerar as páxinas (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> devolve os nomes das propiedades de páxina en uso).",
"apihelp-query+pageswithprop-param-prop": "Que información incluír:",
"apihelp-query+pageswithprop-paramvalue-prop-ids": "Engade o ID da páxina.",
"apihelp-query+pageswithprop-paramvalue-prop-title": "Engade o título e o ID do espazo de nomes da páxina.",
- "apihelp-query+pageswithprop-paramvalue-prop-value": "Engade o valor da propiedade da páxina.",
+ "apihelp-query+pageswithprop-paramvalue-prop-value": "Engade o valor da propiedade de páxina.",
"apihelp-query+pageswithprop-param-limit": "Máximo número de páxinas a retornar.",
"apihelp-query+pageswithprop-param-dir": "En que dirección ordenar.",
"apihelp-query+pageswithprop-example-simple": "Lista as dez primeiras páxinas que usan <code>{{DISPLAYTITLE:}}</code>.",
"apihelp-setnotificationtimestamp-example-page": "Restaurar o estado de notificación para a <kbd>Páxina Principal</kbd>.",
"apihelp-setnotificationtimestamp-example-pagetimestamp": "Fixar o selo de tempo de notificación para a <kbd>Main page</kbd> de forma que todas as edicións dende o 1 se xaneiro de 2012 queden sen revisar.",
"apihelp-setnotificationtimestamp-example-allpages": "Restaurar o estado de notificación para as páxinas no espazo de nomes de <kbd>{{ns:user}}</kbd>.",
+ "apihelp-stashedit-param-title": "Título da páxina que se está a editar.",
+ "apihelp-stashedit-param-section": "Número de selección. O <kbd>0</kbd> é para a sección superior, <kbd>novo</kbd> para unha sección nova.",
+ "apihelp-stashedit-param-sectiontitle": "Título para unha nova sección.",
"apihelp-stashedit-param-text": "Contido da páxina.",
"apihelp-stashedit-param-contentmodel": "Modelo de contido para o novo contido.",
"apihelp-stashedit-param-contentformat": "Formato de serialización de contido utilizado para o texto de entrada.",
"apihelp-query+pageprops-example-simple": "Holl de Eijeschaffte för di Sigge „<kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">Main Page</kbd>“ un „<kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">MediaWiki</kbd>“.",
"apihelp-query+pageswithprop-description": "Donn alle Sigge met bechtemmpte Sigge_Eijeschaff opleßte.",
"apihelp-query+pageswithprop-paramvalue-prop-ids": "Deiht de Kännong vun de Sigge derbei.",
+ "apihelp-query+pageswithprop-paramvalue-prop-value": "Deiht der Wäät för de Eijeschaff vun dä Sigg derbei.",
"apihelp-query+pageswithprop-param-limit": "De jrüüßte Zahl Sigge för ußzejävve.",
"apihelp-query+pageswithprop-param-dir": "En wälsche Reihjefollsch opleßte.",
"apihelp-query+pageswithprop-example-generator": "Holl zohsäzlejje Aanjahbe övver de eezde zehn Sigge, woh <code>__NOTOC__</code> dren vörkütt.",
"apihelp-parse-example-text": "Wikitext parseren.",
"apihelp-parse-example-summary": "Een samenvatting parseren.",
"apihelp-protect-example-protect": "Een pagina beveiligen",
+ "apihelp-stashedit-param-text": "Pagina-inhoud.",
"api-help-flag-readrights": "Voor deze module zijn leesrechten nodig.",
"api-help-flag-writerights": "Voor deze module zijn schrijfrechten nodig.",
"api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:",
"apihelp-login-param-domain": "شپول (اختياري).",
"apihelp-login-example-login": "ننوتل.",
"apihelp-move-description": "يو مخ لېږدول.",
- "apihelp-query+search-example-simple": "د <kbd>مانا</kbd> پلټل.",
+ "apihelp-query+search-example-simple": "د <kbd>meaning</kbd> پلټل.",
"apihelp-query+search-example-text": "د <kbd>مانا</kbd> لپاره متنونه پلټل.",
"apihelp-query+watchlist-paramvalue-prop-title": "د يو مخ سرليک ورگډوي.",
"apihelp-tag-param-reason": "د بدلون سبب.",
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'active-tags' ),
function ( $oldValue, &$ttl, array &$setOpts ) {
- $setOpts += DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+ $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
// Ask extensions which tags they consider active
$extensionActive = array();
function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_SLAVE );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
$tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', array(), $fname );
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'valid-tags-hook' ),
function ( $oldValue, &$ttl, array &$setOpts ) {
- $setOpts += DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+ $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
$tags = array();
Hooks::run( 'ListDefinedTags', array( &$tags ) );
function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_SLAVE, 'vslow' );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
$res = $dbr->select(
'change_tag',
$isWriteQuery = $this->isWriteQuery( $sql );
if ( $isWriteQuery ) {
- if ( !$this->mDoneWrites ) {
- wfDebug( __METHOD__ . ': Writes done: ' .
- DatabaseBase::generalizeSQL( $sql ) . "\n" );
+ $reason = $this->getLBInfo( 'readOnlyReason' );
+ if ( is_string( $reason ) ) {
+ throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
# Set a flag indicating that writes have been done
$this->mDoneWrites = microtime( true );
*/
class DBUnexpectedError extends DBError {
}
+
+/**
+ * @ingroup Database
+ */
+class DBReadOnlyError extends DBError {
+}
$this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
$ret = $ret || $lb->hasMasterChanges();
} );
+
+ return $ret;
+ }
+
+ /**
+ * Detemine if any lagged slave connection was used
+ * @since 1.27
+ * @return bool
+ */
+ public function laggedSlaveUsed() {
+ $ret = false;
+ $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
+ $ret = $ret || $lb->laggedSlaveUsed();
+ } );
+
return $ret;
}
$trxProf->recordConnection( $host, $dbname, $masterOnly );
}
+ # Make master connections read only if in lagged slave mode
+ if ( $masterOnly && $this->getServerCount() > 1 && $this->getLaggedSlaveMode() ) {
+ $conn->setLBInfo( 'readOnlyReason',
+ 'The database has been automatically locked ' .
+ 'while the slave database servers catch up to the master'
+ );
+ }
+
return $conn;
}
}
/**
+ * @note This method will trigger a DB connection if not yet done
* @return bool Whether the generic connection for reads is highly "lagged"
*/
public function getLaggedSlaveMode() {
return $this->mLaggedSlaveMode;
}
+ /**
+ * @note This method will never cause a new DB connection
+ * @return bool Whether any generic connection used for reads was highly "lagged"
+ * @since 1.27
+ */
+ public function laggedSlaveUsed() {
+ return $this->mLaggedSlaveMode;
+ }
+
/**
* Disables/enables lag checks
* @param null|bool $mode
* Get a slave database connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlave( $cluster ) {
global $wgDefaultExternalStore;
* Get a master database connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMaster( $cluster ) {
$wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
* Helper function for self::batchFetchBlobs for merging master/slave results
* @param array &$ret Current self::batchFetchBlobs return value
* @param array &$ids Map from blob_id to requested itemIDs
- * @param mixed $res DB result from DatabaseBase::select
+ * @param mixed $res DB result from Database::select
*/
private function mergeBatchResult( array &$ret, array &$ids, $res ) {
foreach ( $res as $row ) {
protected function initFromGlobals() {
global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends;
+ // Register explicitly defined backends
+ $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() );
+
$autoBackends = array();
// Automatically create b/c backends for file repos...
$repos = array_merge( $wgForeignFileRepos, array( $wgLocalFileRepo ) );
);
}
- $backends = array_merge( $autoBackends, $wgFileBackends );
-
- // Apply $wgReadOnly to all backends if not already read-only
- foreach ( $backends as &$backend ) {
- $backend['readOnly'] = !empty( $backend['readOnly'] )
- ? $backend['readOnly']
- : wfConfiguredReadOnlyReason();
- }
-
- $this->register( $backends );
+ // Register implicitly defined backends
+ $this->register( $autoBackends, wfConfiguredReadOnlyReason() );
}
/**
* Register an array of file backend configurations
*
* @param array $configs
+ * @param string|null $readOnlyReason
* @throws FileBackendException
*/
- protected function register( array $configs ) {
+ protected function register( array $configs, $readOnlyReason = null ) {
foreach ( $configs as $config ) {
if ( !isset( $config['name'] ) ) {
throw new FileBackendException( "Cannot register a backend with no name." );
}
$class = $config['class'];
+ $config['readOnly'] = !empty( $config['readOnly'] )
+ ? $config['readOnly']
+ : $readOnlyReason;
+
unset( $config['class'] ); // backend won't need this
$this->backends[$name] = array(
'class' => $class,
* @since 1.20
*/
class DBFileJournal extends FileJournal {
- /** @var DatabaseBase */
+ /** @var IDatabase */
protected $dbw;
protected $wiki = false; // string; wiki DB name
/**
* Get a master connection to the logging DB
*
- * @return DatabaseBase
+ * @return IDatabase
* @throws DBError
*/
protected function getMasterDB() {
* Get (or reuse) a connection to a lock DB
*
* @param string $lockDb
- * @return DatabaseBase
+ * @return IDatabase
* @throws DBError
*/
protected function getConnection( $lockDb ) {
* Do additional initialization for new lock DB connection
*
* @param string $lockDb
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @throws DBError
*/
- protected function initConnection( $lockDb, DatabaseBase $db ) {
+ protected function initConnection( $lockDb, IDatabase $db ) {
}
/**
/**
* @param string $lockDb
- * @param DatabaseBase $db
+ * @param IDatabase $db
*/
- protected function initConnection( $lockDb, DatabaseBase $db ) {
+ protected function initConnection( $lockDb, IDatabase $db ) {
# Let this transaction see lock rows from other transactions
$db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
}
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMasterDB() {
if ( !isset( $this->dbConn ) ) {
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlaveDB() {
return $this->getMasterDB();
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMasterDB() {
return wfGetDB( DB_MASTER, array(), $this->wiki );
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlaveDB() {
return wfGetDB( DB_SLAVE, array(), $this->wiki );
function ( $oldValue, &$ttl, array &$setOpts ) use ( $that, $title ) {
$dbr = $that->getSlaveDB(); // possibly remote DB
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
if ( $title instanceof Title ) {
$row = $dbr->selectRow(
// Purge cache of all pages using this file
$title = $this->getTitle();
if ( $title ) {
- $update = new HTMLCacheUpdate( $title, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
}
}
// Cache presence for 1 week and negatives for 1 day
$ttl = $this->fileExists ? 86400 * 7 : 86400;
- $opts = DatabaseBase::getCacheSetOptions( $this->repo->getSlaveDB() );
+ $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
}
function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) {
$dbr = wfGetDB( DB_SLAVE );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
$row = $dbr->selectRow(
'interwiki',
// Re-ping all masters with transactions. This throws DBError if some
// connection died while waiting on locks/slaves, triggering a rollback.
wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $fname ) {
- $lb->forEachOpenConnection( function( DatabaseBase $conn ) use ( $fname ) {
+ $lb->forEachOpenConnection( function( IDatabase $conn ) use ( $fname ) {
if ( $conn->writesOrCallbacksPending() ) {
$conn->query( "SELECT 1", $fname );
}
}
public function run() {
+ /** @noinspection PhpUnusedLocalVariableInspection */
$scope = RequestContext::importScopedSession( $this->params['session'] );
$context = RequestContext::getMain();
$user = $context->getUser();
$upload->continueChunks(
$this->params['filename'],
$this->params['filekey'],
- $context->getRequest()
+ new WebRequestUpload( $context->getRequest(), 'null' )
);
// Combine all of the chunks into a local file and upload that to a new stash file
'status' => Status::newFatal( 'api-error-stashfailed' )
)
);
- $this->setLastError( get_class( $e ) . ": " . $e->getText() );
+ $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
// To be extra robust.
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
}
public function run() {
+ /** @noinspection PhpUnusedLocalVariableInspection */
$scope = RequestContext::importScopedSession( $this->params['session'] );
$context = RequestContext::getMain();
$user = $context->getUser();
'status' => Status::newFatal( 'api-error-publishfailed' )
)
);
- $this->setLastError( get_class( $e ) . ": " . $e->getText() );
+ $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
// To prevent potential database referential integrity issues.
// See bug 32551.
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
--- /dev/null
+<?php
+/**
+ * APC-backed function memoization
+ *
+ * This class provides memoization for pure functions. A function is pure
+ * if its result value depends on nothing other than its input parameters
+ * and if invoking it does not cause any side-effects.
+ *
+ * The first invocation of the memoized callable with a particular set of
+ * arguments will be delegated to the underlying callable. Repeat invocations
+ * with the same input parameters will be served from APC.
+ *
+ * @par Example:
+ * @code
+ * $memoizedStrrev = new MemoizedCallable( 'range' );
+ * $memoizedStrrev->invoke( 5, 8 ); // result: array( 5, 6, 7, 8 )
+ * $memoizedStrrev->invokeArgs( array( 5, 8 ) ); // same
+ * MemoizedCallable::call( 'range', array( 5, 8 ) ); // same
+ * @endcode
+ *
+ * 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
+ * @author Ori Livneh
+ * @since 1.27
+ */
+class MemoizedCallable {
+
+ /** @var callable */
+ private $callable;
+
+ /** @var string Unique name of callable; used for cache keys. */
+ private $callableName;
+
+ /**
+ * Constructor.
+ *
+ * @throws InvalidArgumentException if $callable is not a callable.
+ * @param callable $callable Function or method to memoize.
+ * @param int $ttl TTL in seconds. Defaults to 3600 (1hr). Capped at 86400 (24h).
+ */
+ public function __construct( $callable, $ttl = 3600 ) {
+ if ( !is_callable( $callable, false, $this->callableName ) ) {
+ throw new InvalidArgumentException(
+ 'Argument 1 passed to MemoizedCallable::__construct() must ' .
+ 'be an instance of callable; ' . gettype( $callable ) . ' given'
+ );
+ }
+
+ if ( $this->callableName === 'Closure::__invoke' ) {
+ // Differentiate anonymous functions from one another
+ $this->callableName .= uniqid();
+ }
+
+ $this->callable = $callable;
+ $this->ttl = min( max( $ttl, 1 ), 86400 );
+ }
+
+ /**
+ * Fetch the result of a previous invocation from APC.
+ *
+ * @param string $key
+ * @param bool &$success
+ */
+ protected function fetchResult( $key, &$success ) {
+ $success = false;
+ if ( function_exists( 'apc_fetch' ) ) {
+ return apc_fetch( $key, $success );
+ }
+ return false;
+ }
+
+ /**
+ * Store the result of an invocation in APC.
+ *
+ * @param string $key
+ * @param mixed $result
+ */
+ protected function storeResult( $key, $result ) {
+ if ( function_exists( 'apc_store' ) ) {
+ apc_store( $key, $result, $this->ttl );
+ }
+ }
+
+ /**
+ * Invoke the memoized function or method.
+ *
+ * @throws InvalidArgumentException If parameters list contains non-scalar items.
+ * @param array $args Parameters for memoized function or method.
+ * @return mixed The memoized callable's return value.
+ */
+ public function invokeArgs( Array $args = array() ) {
+ foreach ( $args as $arg ) {
+ if ( $arg !== null && !is_scalar( $arg ) ) {
+ throw new InvalidArgumentException(
+ 'MemoizedCallable::invoke() called with non-scalar ' .
+ 'argument'
+ );
+ }
+ }
+
+ $hash = md5( serialize( $args ) );
+ $key = __CLASS__ . ':' . $this->callableName . ':' . $hash;
+ $success = false;
+ $result = $this->fetchResult( $key, $success );
+ if ( !$success ) {
+ $result = call_user_func_array( $this->callable, $args );
+ $this->storeResult( $key, $result );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Invoke the memoized function or method.
+ *
+ * Like MemoizedCallable::invokeArgs(), but variadic.
+ *
+ * @param mixed ...$params Parameters for memoized function or method.
+ * @return mixed The memoized callable's return value.
+ */
+ public function invoke() {
+ return $this->invokeArgs( func_get_args() );
+ }
+
+ /**
+ * Shortcut method for creating a MemoizedCallable and invoking it
+ * with the specified arguments.
+ *
+ * @param callable $callable
+ * @param array $args
+ * @param int $ttl
+ */
+ public static function call( $callable, Array $args = array(), $ttl = 3600 ) {
+ $instance = new self( $callable, $ttl );
+ return $instance->invokeArgs( $args );
+ }
+}
/** @var string|null proxy */
protected $proxy;
/** @var string */
- protected $userAgent = 'MW-MultiHttpClient';
+ protected $userAgent = 'wikimedia/multi-http-client v1.0';
/**
* @param array $options
* Values in the arguments collection which are Closure instances will be
* expanded by invoking them with no arguments before passing the
* resulting value on to the constructor/callable. This can be used to
- * pass DatabaseBase instances or other live objects to the
+ * pass IDatabase instances or other live objects to the
* constructor/callable. This behavior can be suppressed by adding
* closure_expansion => false to the specification.
*
* - d) T1 reads the row and calls set() due to a cache miss
* - e) Stale value is stuck in cache
*
+ * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
+ *
* Example usage:
* @code
* $dbr = wfGetDB( DB_SLAVE );
- * $setOpts = DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts = Database::getCacheSetOptions( $dbr );
* // Fetch the row from the DB
* $row = $dbr->selectRow( ... );
* $key = wfMemcKey( 'building', $buildingId );
* can be set dynamically by altering $ttl in the callback (by reference).
* The $setOpts array can be altered and is given to set() when called;
* it is recommended to set the 'since' field to avoid race conditions.
+ * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
*
* Usually, callbacks ignore the current value, but it can be used
* to maintain "most recent X" values that come from time or sequence
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* return $dbr->selectRow( ... );
* },
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatConfig::newFromRow( $dbr->selectRow( ... ) );
* },
* // Determine new value from the DB
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatState::newFromResults( $dbr->select( ... ) );
* },
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* // Start off with the last cached list
* $list = $oldValue ?: array();
* @return Status
*/
public static function send( $to, $from, $subject, $body, $options = array() ) {
- global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail;
+ global $wgAllowHTMLEmail;
$contentType = 'text/plain; charset=UTF-8';
- $headers = array();
- if ( is_array( $options ) ) {
- $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
- $contentType = isset( $options['contentType'] ) ? $options['contentType'] : $contentType;
- $headers = isset( $options['headers'] ) ? $options['headers'] : $headers;
- } else {
+ if ( !is_array( $options ) ) {
// Old calling style
wfDeprecated( __METHOD__ . ' with $replyto as 5th parameter', '1.26' );
- $replyto = $options;
+ $options = array( 'replyTo' => $options );
if ( func_num_args() === 6 ) {
- $contentType = func_get_arg( 5 );
+ $options['contentType'] = func_get_arg( 5 );
}
}
- $mime = null;
if ( !is_array( $to ) ) {
$to = array( $to );
}
return Status::newFatal( 'user-mail-no-addy' );
}
+ // give a chance to UserMailerTransformContents subscribers who need to deal with each
+ // target differently to split up the address list
+ if ( count( $to ) > 1 ) {
+ $oldTo = $to;
+ Hooks::run( 'UserMailerSplitTo', array( &$to ) );
+ if ( $oldTo != $to ) {
+ $splitTo = array_diff( $oldTo, $to );
+ $to = array_diff( $oldTo, $splitTo ); // ignore new addresses added in the hook
+ // first send to non-split address list, then to split addresses one by one
+ $status = Status::newGood();
+ if ( $to ) {
+ $status->merge( UserMailer::sendInternal(
+ $to, $from, $subject, $body, $options ) );
+ }
+ foreach ( $splitTo as $newTo ) {
+ $status->merge( UserMailer::sendInternal(
+ array( $newTo ), $from, $subject, $body, $options ) );
+ }
+ return $status;
+ }
+ }
+
+ return UserMailer::sendInternal( $to, $from, $subject, $body, $options );
+ }
+
+ /**
+ * Helper function fo UserMailer::send() which does the actual sending. It expects a $to
+ * list which the UserMailerSplitTo hook would not split further.
+ * @param MailAddress[] $to Array of recipients' email addresses
+ * @param MailAddress $from Sender's email
+ * @param string $subject Email's subject.
+ * @param string $body Email's text or Array of two strings to be the text and html bodies
+ * @param array $options:
+ * 'replyTo' MailAddress
+ * 'contentType' string default 'text/plain; charset=UTF-8'
+ * 'headers' array Extra headers to set
+ *
+ * @throws MWException
+ * @throws Exception
+ * @return Status
+ */
+ protected static function sendInternal(
+ array $to,
+ MailAddress $from,
+ $subject,
+ $body,
+ $options = array()
+ ) {
+ global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams;
+ $mime = null;
+
+ $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
+ $contentType = isset( $options['contentType'] ) ?
+ $options['contentType'] : 'text/plain; charset=UTF-8';
+ $headers = isset( $options['headers'] ) ? $options['headers'] : array();
+
+ // Allow transformation of content, such as encrypting/signing
+ $error = false;
+ if ( !Hooks::run( 'UserMailerTransformContent', array( $to, $from, &$body, &$error ) ) ) {
+ if ( $error ) {
+ return Status::newFatal( 'php-mail-error', $error );
+ } else {
+ return Status::newFatal( 'php-mail-error-unknown' );
+ }
+ }
+
/**
* Forge email headers
* -------------------
$headers['Content-transfer-encoding'] = '8bit';
}
+ // allow transformation of MIME-encoded message
+ if ( !Hooks::run( 'UserMailerTransformMessage',
+ array( $to, $from, &$subject, &$headers, &$body, &$error ) )
+ ) {
+ if ( $error ) {
+ return Status::newFatal( 'php-mail-error', $error );
+ } else {
+ return Status::newFatal( 'php-mail-error-unknown' );
+ }
+ }
+
$ret = Hooks::run( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) );
if ( $ret === false ) {
// the hook implementation will return false to skip regular mail sending
* Get a connection to the specified database
*
* @param int $serverIndex
- * @return DatabaseBase
+ * @return IDatabase
* @throws MWException
*/
protected function getDB( $serverIndex ) {
}
/**
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @param string $exptime
* @return bool
*/
}
/**
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @return string
*/
protected function getMaxDateTime( $db ) {
$this->loadFile();
if ( $this->mFile->exists() ) {
wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" );
- $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ) );
$this->mFile->upgradeRow();
$this->mFile->purgeCache( array( 'forThumbRefresh' => true ) );
} else {
// Images
if ( $title->getNamespace() == NS_FILE ) {
- $update = new HTMLCacheUpdate( $title, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
}
// User talk pages
/**
* Get the Database object in use
*
- * @return DatabaseBase
+ * @return IDatabase
*/
public function getDatabase() {
return $this->mDb;
$this->missingLocalFileRefs[] = $file;
}
}
- return CSSMin::remap(
- $style, $localDir, $remoteDir, true
- );
+ return MemoizedCallable::call( 'CSSMin::remap',
+ array( $style, $localDir, $remoteDir, true ) );
}
/**
*/
protected $sites = null;
- /**
- * @var ORMTable
- */
- protected $sitesTable;
-
/**
* @since 1.25
- *
- * @param ORMTable|null $sitesTable
+ * @param null $sitesTable Unused since 1.27
*/
- public function __construct( ORMTable $sitesTable = null ) {
- if ( $sitesTable === null ) {
- $sitesTable = $this->newSitesTable();
+ public function __construct( $sitesTable = null ) {
+ if ( $sitesTable !== null ) {
+ throw new InvalidArgumentException(
+ __METHOD__ . ': $sitesTable parameter must be null'
+ );
}
-
- $this->sitesTable = $sitesTable;
}
/**
return $this->sites;
}
- /**
- * Returns a new Site object constructed from the provided ORMRow.
- *
- * @since 1.25
- *
- * @param ORMRow $siteRow
- *
- * @return Site
- */
- protected function siteFromRow( ORMRow $siteRow ) {
-
- $site = Site::newForType( $siteRow->getField( 'type', Site::TYPE_UNKNOWN ) );
-
- $site->setGlobalId( $siteRow->getField( 'global_key' ) );
-
- $site->setInternalId( $siteRow->getField( 'id' ) );
-
- if ( $siteRow->hasField( 'forward' ) ) {
- $site->setForward( $siteRow->getField( 'forward' ) );
- }
-
- if ( $siteRow->hasField( 'group' ) ) {
- $site->setGroup( $siteRow->getField( 'group' ) );
- }
-
- if ( $siteRow->hasField( 'language' ) ) {
- $site->setLanguageCode( $siteRow->getField( 'language' ) === ''
- ? null
- : $siteRow->getField( 'language' )
- );
- }
-
- if ( $siteRow->hasField( 'source' ) ) {
- $site->setSource( $siteRow->getField( 'source' ) );
- }
-
- if ( $siteRow->hasField( 'data' ) ) {
- $site->setExtraData( $siteRow->getField( 'data' ) );
- }
-
- if ( $siteRow->hasField( 'config' ) ) {
- $site->setExtraConfig( $siteRow->getField( 'config' ) );
- }
-
- return $site;
- }
-
- /**
- * Get a new ORMRow from a Site object
- *
- * @since 1.25
- *
- * @param Site $site
- *
- * @return ORMRow
- */
- protected function getRowFromSite( Site $site ) {
- $fields = array(
- // Site data
- 'global_key' => $site->getGlobalId(), // TODO: check not null
- 'type' => $site->getType(),
- 'group' => $site->getGroup(),
- 'source' => $site->getSource(),
- 'language' => $site->getLanguageCode() === null ? '' : $site->getLanguageCode(),
- 'protocol' => $site->getProtocol(),
- 'domain' => strrev( $site->getDomain() ) . '.',
- 'data' => $site->getExtraData(),
-
- // Site config
- 'forward' => $site->shouldForward(),
- 'config' => $site->getExtraConfig(),
- );
-
- if ( $site->getInternalId() !== null ) {
- $fields['id'] = $site->getInternalId();
- }
-
- return new ORMRow( $this->sitesTable, $fields );
- }
-
/**
* Fetches the site from the database and loads them into the sites field.
*
protected function loadSites() {
$this->sites = new SiteList();
- $siteRows = $this->sitesTable->select( null, array(), array(
- 'ORDER BY' => 'site_global_key'
- ) );
+ $dbr = wfGetDB( DB_SLAVE );
- foreach ( $siteRows as $siteRow ) {
- $this->sites[] = $this->siteFromRow( $siteRow );
+ $res = $dbr->select(
+ 'sites',
+ array(
+ 'site_id',
+ 'site_global_key',
+ 'site_type',
+ 'site_group',
+ 'site_source',
+ 'site_language',
+ 'site_protocol',
+ 'site_domain',
+ 'site_data',
+ 'site_forward',
+ 'site_config',
+ ),
+ '',
+ __METHOD__,
+ array( 'ORDER BY' => 'site_global_key' )
+ );
+
+ foreach ( $res as $row ) {
+ $site = Site::newForType( $row->site_type );
+ $site->setGlobalId( $row->site_global_key );
+ $site->setInternalId( (int)$row->site_id );
+ $site->setForward( (bool)$row->site_forward );
+ $site->setGroup( $row->site_group );
+ $site->setLanguageCode( $row->site_language === ''
+ ? null
+ : $row->site_language
+ );
+ $site->setSource( $row->site_source );
+ $site->setExtraData( unserialize( $row->site_data ) );
+ $site->setExtraConfig( unserialize( $row->site_config ) );
+ $this->sites[] = $site;
}
// Batch load the local site identifiers.
- $ids = wfGetDB( $this->sitesTable->getReadDb() )->select(
+ $ids = $dbr->select(
'site_identifiers',
array(
'si_site',
return true;
}
- $dbw = $this->sitesTable->getWriteDbConnection();
+ $dbw = wfGetDB( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
$internalIds[] = $site->getInternalId();
}
- $siteRow = $this->getRowFromSite( $site );
- $success = $siteRow->save( __METHOD__ ) && $success;
+ $fields = array(
+ // Site data
+ 'site_global_key' => $site->getGlobalId(), // TODO: check not null
+ 'site_type' => $site->getType(),
+ 'site_group' => $site->getGroup(),
+ 'site_source' => $site->getSource(),
+ 'site_language' => $site->getLanguageCode() === null ? '' : $site->getLanguageCode(),
+ 'site_protocol' => $site->getProtocol(),
+ 'site_domain' => strrev( $site->getDomain() ) . '.',
+ 'site_data' => serialize( $site->getExtraData() ),
+
+ // Site config
+ 'site_forward' => $site->shouldForward() ? 1 : 0,
+ 'site_config' => serialize( $site->getExtraConfig() ),
+ );
+
+ $rowId = $site->getInternalId();
+ if ( $rowId !== null ) {
+ $success = $dbw->update(
+ 'sites', $fields, array( 'site_id' => $rowId ), __METHOD__
+ ) && $success;
+ } else {
+ $rowId = $dbw->nextSequenceValue( 'sites_site_id_seq' );
+ $fields['site_id'] = $rowId;
+ $success = $dbw->insert( 'sites', $fields, __METHOD__ ) && $success;
+ $rowId = $dbw->insertId();
+ }
foreach ( $site->getLocalIds() as $idType => $ids ) {
foreach ( $ids as $id ) {
- $localIds[] = array( $siteRow->getId(), $idType, $id );
+ $localIds[] = array( $rowId, $idType, $id );
}
}
}
* @return bool Success
*/
public function clear() {
- $dbw = $this->sitesTable->getWriteDbConnection();
+ $dbw = wfGetDB( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
$ok = $dbw->delete( 'sites', '*', __METHOD__ );
return $ok;
}
- /**
- * @since 1.25
- *
- * @return ORMTable
- */
- protected function newSitesTable() {
- return new ORMTable(
- 'sites',
- array(
- 'id' => 'id',
-
- // Site data
- 'global_key' => 'str',
- 'type' => 'str',
- 'group' => 'str',
- 'source' => 'str',
- 'language' => 'str',
- 'protocol' => 'str',
- 'domain' => 'str',
- 'data' => 'array',
-
- // Site config
- 'forward' => 'bool',
- 'config' => 'array',
- ),
- array(
- 'type' => Site::TYPE_UNKNOWN,
- 'group' => Site::GROUP_NONE,
- 'source' => Site::SOURCE_LOCAL,
- 'data' => array(),
-
- 'forward' => false,
- 'config' => array(),
- 'language' => '',
- ),
- 'ORMRow',
- 'site_'
- );
- }
-
}
* @since 1.21
* @deprecated 1.25 Construct a SiteStore instance directly instead.
*
- * @param ORMTable|null $sitesTable
+ * @param null $sitesTable Unused
* @param BagOStuff|null $cache
*
* @return SiteStore
*/
- public static function newInstance( ORMTable $sitesTable = null, BagOStuff $cache = null ) {
+ public static function newInstance( $sitesTable = null, BagOStuff $cache = null ) {
+ if ( $sitesTable !== null ) {
+ throw new InvalidArgumentException(
+ __METHOD__ . ': $sitesTable parameter is unused and must be null'
+ );
+ }
+
if ( $cache === null ) {
$cache = wfGetCache( wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING );
}
class BatchRowIterator implements RecursiveIterator {
/**
- * @var DatabaseBase $db The database to read from
+ * @var IDatabase $db The database to read from
*/
protected $db;
/**
* @var array $fetchColumns List of column names to select from the
- * table suitable for use with DatabaseBase::select()
+ * table suitable for use with IDatabase::select()
*/
protected $fetchColumns;
/**
* @param array $condition Query conditions suitable for use with
- * DatabaseBase::select
+ * IDatabase::select
*/
public function addConditions( array $conditions ) {
$this->conditions = array_merge( $this->conditions, $conditions );
/**
* @param array $condition Query join conditions suitable for use
- * with DatabaseBase::select
+ * with IDatabase::select
*/
public function addJoinConditions( array $conditions ) {
$this->joinConditions = array_merge( $this->joinConditions, $conditions );
/**
* @param array $columns List of column names to select from the
- * table suitable for use with DatabaseBase::select()
+ * table suitable for use with IDatabase::select()
*/
public function setFetchColumns( array $columns ) {
// If it's not the all column selector merge in the primary keys we need
*/
class BatchRowWriter {
/**
- * @var DatabaseBase $db The database to write to
+ * @var IDatabase $db The database to write to
*/
protected $db;
"changeemail-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
"changeemail-oldemail": "Цяперашні адрас электроннай пошты:",
"changeemail-newemail": "Новы адрас электроннай пошты:",
+ "changeemail-newemail-help": "Поле трэба пакінуць пустым, калі вы хочаце выдаліць свой адрас электроннай пошты. Пасьля выдаленьня вы ня зможаце ануляваць забыты пароль і ня будзеце атрымліваць лісты электроннай пошты з гэтай вікі.",
"changeemail-none": "(няма)",
"changeemail-password": "Ваш пароль у {{GRAMMAR:месны|{{SITENAME}}}}:",
"changeemail-submit": "Зьмяніць адрас электроннай пошты",
"nopagetext": "Пазначанай мэтавай старонкі не існуе.",
"pager-newer-n": "$1 {{PLURAL:$1|навейшая|навейшыя|навейшых}}",
"pager-older-n": "$1 {{PLURAL:$1|старэйшая|старэйшыя|старэйшых}}",
- "suppress": "Ð Ñ\8dвÑ\96заваÑ\86Ñ\8c",
+ "suppress": "Ð\9fадавÑ\96Ñ\86Ñ\8c вÑ\8dÑ\80Ñ\81Ñ\96Ñ\8e",
"querypage-disabled": "Гэта спэцыяльная старонка адключаная для падвышэньня прадукцыйнасьці",
"apihelp": "Даведка API",
"apihelp-no-such-module": "Модуль «$1» ня знойдзены.",
"nstab-template": "обраꙁьць",
"nstab-help": "страница помощи",
"nstab-category": "катигорїꙗ",
+ "mainpage-nstab": "главьна страница",
"nosuchspecialpage": "си нарочнꙑ страницѧ нѣстъ",
"error": "блаꙁна",
"internalerror": "вънѫтрѣнꙗ блаꙁна",
"block-log-flags-anononly": "тъкъмо анѡнѷмьнꙑ польꙃєватєлє",
"move-page": "прѣимєнованиѥ ⁖ $1 ⁖",
"move-page-legend": "страницѧ прѣимєнованиѥ",
- "movearticle": "страница :",
"newtitle": "ново имѧ :",
"move-watch": "си страницѧ блюдєниѥ",
"movepagebtn": "прѣимєнованиѥ",
"nstab-template": "Şablon",
"nstab-help": "Pela peşti",
"nstab-category": "Kategoriye",
+ "mainpage-nstab": "Pela seri",
"nosuchaction": "Fealiyeto wınasi çıniyo",
"nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.",
"nosuchspecialpage": "Pela xasa wınasiye çıniya",
"tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
"tooltip-pt-logout": "Bıveciye",
"tooltip-ca-talk": "Zerrekê pele sero werênayış",
- "tooltip-ca-edit": "Tı şenay na pele bıvurnê. Kerem ke, qeydkerdış ra ver gocega verqayti bıgurene.",
+ "tooltip-ca-edit": "Ena pele bıvurne",
"tooltip-ca-addsection": "Zu bınnusteya newi ak",
"tooltip-ca-viewsource": "Ena pele kılit biya.\nŞıma şenê çımeyê aye bıvênê",
"tooltip-ca-history": "Versiyonê verênê ena pele",
"api-error-badaccess-groups": "Ena wiki de dosya barkerdışi rê mısade nêdeyêno.",
"api-error-badtoken": "Xetaya zerreki: Antışo xırabın.",
"api-error-copyuploaddisabled": "URL barkerdış ena waster dı qefılyayo.",
- "api-error-duplicate": "Ena {{PLURAL:$1|ze ke zeq|biya zey dosya da}} zeq wesiqa biya wendeyê.",
+ "api-error-duplicate": "Pele de xora be nê zerreki ra {{PLURAL:$1|dosyaya bine esta|dosyeyê bini estê}}.",
"api-error-duplicate-archive": "Ena {{PLURAL:$1|vurneyaya zey na dosya|zerrey cı zey dosya}} aseno,feqet {{PLURAL:$1|ena dosya|tewr veri}} besterneyaya.",
"api-error-empty-file": "Dosyaya ke şıma rışta venga.",
"api-error-emptypage": "Newi, pelaya veng vıraştışi rê mısade nêdeyêno.",
"broken-file-category": "Pages avec des liens de fichiers brisés",
"about": "À propos",
"article": "Page de contenu",
- "newwindow": "(ouvre une nouvelle fenêtre)",
+ "newwindow": "(ouvre dans une nouvelle fenêtre)",
"cancel": "Annuler",
"moredotdotdot": "Plus...",
"morenotlisted": "Cette liste n’est pas complète.",
"viewsource": "Ver o código fonte",
"viewsource-title": "Ver o código fonte de \"$1\"",
"actionthrottled": "Acción limitada",
- "actionthrottledtext": "Como unha medida de loita contra o ''spam'', limítase a realización desta acción a un número determinado de veces nun curto espazo de tempo, e vostede superou este límite.\nInténteo de novo nuns minutos.",
+ "actionthrottledtext": "Como medida contra os abusos, a acción que está realizando está limitada a un número determinado de veces nun periodo curto de tempo, e superou ese límite.\nInténteo de novo nuns minutos.",
"protectedpagetext": "Esta páxina foi protexida para evitar a edición e outras accións.",
"viewsourcetext": "Pode ver e copiar o código fonte desta páxina.",
"viewyourtext": "Pode ver e copiar o código fonte <strong>das súas edicións</strong> nesta páxina.",
"createacct-captcha": "Comprobación de seguridade",
"createacct-imgcaptcha-ph": "Insira o texto que ve enriba",
"createacct-submit": "Crear a conta",
- "createacct-another-submit": "Crear outra conta",
+ "createacct-another-submit": "Crear conta",
"createacct-benefit-heading": "Xente coma vostede elabora {{SITENAME}}.",
"createacct-benefit-body1": "{{PLURAL:$1|edición|edicións}}",
"createacct-benefit-body2": "{{PLURAL:$1|páxina|páxinas}}",
"passwordreset-emailtext-ip": "Alguén (probablemente vostede, desde o enderezo IP $1) solicitou o restablecemento do seu\ncontrasinal de {{SITENAME}} ($4). {{PLURAL:$3|A seguinte conta de usuario está asociada|As seguintes contas de usuarios están asociadas}}\na este enderezo de correo electrónico:\n\n$2\n\n{{PLURAL:$3|Este contrasinal temporal caducará|Estes contrasinais temporais caducarán}} {{PLURAL:$5|nun día|en $5 días}}.\nDebería acceder ao sistema e elixir un novo contrasinal agora. Se outra persoa fixo esta\nsolicitude ou se lembrou o seu contrasinal orixinal e xa non o quere cambiar,\nignore esta mensaxe e continúe empregando o seu contrasinal vello.",
"passwordreset-emailtext-user": "O usuario $1 solicitou o restablecemento do contrasinal de {{SITENAME}}\n($4). {{PLURAL:$3|A seguinte conta de usuario está asociada|As seguintes contas de usuarios están asociadas}}\na este enderezo de correo electrónico:\n\n$2\n\n{{PLURAL:$3|Este contrasinal temporal caducará|Estes contrasinais temporais caducarán}} {{PLURAL:$5|nun día|en $5 días}}.\nDebería acceder ao sistema e elixir un novo contrasinal agora. Se outra persoa fixo esta\nsolicitude ou se lembrou o seu contrasinal orixinal e xa non o quere cambiar,\nignore esta mensaxe e continúe empregando o seu contrasinal vello.",
"passwordreset-emailelement": "Nome de usuario: \n$1\n\nContrasinal temporal: \n$2",
- "passwordreset-emailsent": "Enviouse o correo electrónico de restablecemento do contrasinal.",
+ "passwordreset-emailsent": "Se esta é unha dirección de correo electrónico rexistrada para a túa conta, entón enviarase un correo electrónico para o restablecemento do teu contrasinal.",
"passwordreset-emailsent-capture": "Enviouse un correo electrónico de restablecemento do contrasinal, mostrado a continuación.",
"passwordreset-emailerror-capture": "Xerouse un correo electrónico de restablecemento do contrasinal, mostrado a continuación, pero o envío {{GENDER:$2|ao usuario|á usuaria}} fallou: $1",
- "changeemail": "Cambiar o enderezo de correo electrónico",
- "changeemail-text": "Encha este formulario para cambiar o seu enderezo de correo electrónico. Terá que escribir o seu contrasinal para confirmar este cambio.",
+ "changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
+ "changeemail-text": "Encha este formulario para cambiar o seu enderezo de correo electrónico. Terá que escribir o seu contrasinal para confirmar este cambio. Se vostede quere eliminar a asociación da dirección de correo electrónico da súa conta, deixe en branco a nova dirección de correo electrónico cando envíe o formulario.",
"changeemail-no-info": "Debe rexistrarse para acceder directamente a esta páxina.",
"changeemail-oldemail": "Enderezo de correo electrónico actual:",
"changeemail-newemail": "Novo enderezo de correo electrónico:",
"permissionserrorstext-withaction": "Non ten os permisos necesarios para $2, {{PLURAL:$1|pola seguinte razón|polas seguintes razóns}}:",
"recreate-moveddeleted-warn": "'''Atención: Vai volver crear unha páxina que xa foi eliminada anteriormente.'''\n\nDebería considerar se é apropiado continuar a editar esta páxina.\nVelaquí están o rexistro de borrados e mais o de traslados desta páxina, por se quere consultalos:",
"moveddeleted-notice": "Esta páxina foi borrada.\nA continuación pódese ver o rexistro de borrados e traslados desta páxina, por se quere consultalos.",
+ "moveddeleted-notice-recent": "Sentímolo, esta página foi borrada recentemente (dentro das últimas 24 horas).\nO rexistro de borrado e traslado da páxina amósanse abaixo como referencia.",
"log-fulllog": "Ver o rexistro completo",
"edit-hook-aborted": "A edición foi abortada polo asociador.\nEste non deu ningunha explicación.",
"edit-gone-missing": "Non se pode actualizar a páxina.\nSemella que foi borrada.",
"mergehistory-go": "Mostrar as edicións que se poden fusionar",
"mergehistory-submit": "Fusionar as revisións",
"mergehistory-empty": "Non hai revisións que se poidan fusionar.",
- "mergehistory-done": "{{PLURAL:$3|Unha revisión|$3 revisións}} de \"$1\" {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con \"[[:$2]]\".",
+ "mergehistory-done": "$3 {{PLURAL:$3|revisión|revisións}} de $1 {{PLURAL:$3|fusionouse|fusionáronse}} sen problemas con [[:$2]].",
"mergehistory-fail": "Non se puido fusionar o historial; comprobe outra vez os parámetros de páxina e data.",
"mergehistory-fail-toobig": "Non se puido fusionar o historial, xa que supón trasladar máis revisións que o límite de $1 {{PLURAL:$1|revisión|revisións}}.",
"mergehistory-no-source": "Non existe a páxina de orixe \"$1\".",
"prefs-watchlist-token": "Pase para a lista de vixilancia:",
"prefs-misc": "Preferencias varias",
"prefs-resetpass": "Cambiar o contrasinal",
- "prefs-changeemail": "Cambiar o enderezo de correo electrónico",
+ "prefs-changeemail": "Cambiar ou eliminar o enderezo de correo electrónico",
"prefs-setemail": "Establecer un enderezo de correo electrónico",
"prefs-email": "Opcións de correo electrónico",
"prefs-rendering": "Aparencia",
"group-bot": "Bots",
"group-sysop": "Administradores",
"group-bureaucrat": "Burócratas",
- "group-suppress": "Supervisores",
+ "group-suppress": "Supresores",
"group-all": "(todos)",
"group-user-member": "{{GENDER:$1|usuario|usuaria}}",
"group-autoconfirmed-member": "{{GENDER:$1|usuario autoconfirmado|usuaria autoconfirmada}}",
"group-bot-member": "{{GENDER:$1|bot}}",
"group-sysop-member": "{{GENDER:$1|administrador|administradora}}",
"group-bureaucrat-member": "{{GENDER:$1|burócrata}}",
- "group-suppress-member": "{{GENDER:$1|supervisor|supervisora}}",
+ "group-suppress-member": "{{GENDER:$1|supresor|supresora}}",
"grouppage-user": "{{ns:project}}:Usuarios",
"grouppage-autoconfirmed": "{{ns:project}}:Usuarios autoconfirmados",
"grouppage-bot": "{{ns:project}}:Bots",
"grouppage-sysop": "{{ns:project}}:Administradores",
"grouppage-bureaucrat": "{{ns:project}}:Burócratas",
- "grouppage-suppress": "{{ns:project}}:Supervisores",
+ "grouppage-suppress": "{{ns:project}}:Supresores",
"right-read": "Ler páxinas",
"right-edit": "Editar páxinas",
"right-createpage": "Crear páxinas (que non son de conversa)",
"recentchanges-page-added-to-category-bundled": "\"[[:$1]]\" e {{PLURAL:$2|unha páxina|$2 páxinas}} engadíronse á categoría",
"recentchanges-page-removed-from-category": "\"[[:$1]]\" eliminouse da categoría",
"recentchanges-page-removed-from-category-bundled": "\"[[:$1]]\" e {{PLURAL:$2|unha páxina|$2 páxinas}} elimináronse da categoría",
+ "autochange-username": "Cambio automático de MediaWiki",
"upload": "Subir un ficheiro",
"uploadbtn": "Subir un ficheiro",
"reuploaddesc": "Cancelar a carga e volver ao formulario de carga",
"upload-options": "Opcións de carga",
"watchthisupload": "Vixiar este ficheiro",
"filewasdeleted": "Un ficheiro con ese nome foi cargado con anterioridade e a continuación borrado.\nDebe comprobar o $1 antes de proceder a cargalo outra vez.",
+ "filename-thumb-name": "Semella que este título é dunha miniatura. Non cargue miniaturas no wiki do que as sacou. Se non é así, corrixa o nome do ficheiro para que sexa máis significativo e non teña o prefixo das miniaturas.",
"filename-bad-prefix": "O nome do ficheiro que está cargando comeza con '''\"$1\"''', que é un típico nome non descritivo asignado automaticamente polas cámaras dixitais.\nPor favor, escolla un nome máis descritivo para o seu ficheiro.",
"filename-prefix-blacklist": " #<!-- Deixe esta liña tal e como está --> <pre>\n# A sintaxe é a seguinte:\n# * Todo o que vaia despois dun carácter \"#\" ata o final da liña é un comentario\n# * Toda liña que non estea en branco é un prefixo para os nomes típicos dos ficheiros asignados automaticamente polas cámaras dixitais\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # algúns teléfonos móbiles\nIMG # xenérico\nJD # Jenoptik\nMGP # Pentax\nPICT # varios\n #</pre> <!-- Deixe esta liña tal e como está -->",
"upload-success-subj": "A carga realizouse correctamente",
"upload-form-label-infoform-description": "Descrición",
"upload-form-label-usage-title": "Uso",
"upload-form-label-usage-filename": "Nome do ficheiro",
+ "foreign-structured-upload-form-label-own-work": "Isto é o meu propio traballo",
+ "foreign-structured-upload-form-label-infoform-categories": "Categorías",
+ "foreign-structured-upload-form-label-infoform-date": "Data",
"backend-fail-stream": "Non se puido transmitir o ficheiro \"$1\".",
"backend-fail-backup": "Non se puido facer unha copia de seguridade do ficheiro \"$1\".",
"backend-fail-notexists": "O ficheiro \"$1\" non existe.",
"nopagetext": "A páxina que especificou non existe.",
"pager-newer-n": "{{PLURAL:$1|unha posterior|$1 posteriores}}",
"pager-older-n": "{{PLURAL:$1|unha anterior|$1 anteriores}}",
- "suppress": "Supervisor",
+ "suppress": "Supresor",
"querypage-disabled": "Esta páxina especial está desactivada por razóns de rendemento.",
"apihelp": "Axuda coa API",
"apihelp-no-such-module": "Non se atopou o módulo \"$1\".",
"emailccsubject": "Copia da súa mensaxe para $1: $2",
"emailsent": "Mensaxe enviada",
"emailsenttext": "A súa mensaxe de correo electrónico foi enviada.",
- "emailuserfooter": "Este correo electrónico foi enviado por $1 a $2 mediante a función \"{{int:emailuser}}\" en {{SITENAME}}.",
+ "emailuserfooter": "Este correo electrónico foi {{GENDER:$1|enviado}} por $1 a {{GENDER:$2|$2}} mediante a función \"{{int:emailuser}}\" en {{SITENAME}}.",
"usermessage-summary": "Mensaxe deixada polo sistema.",
"usermessage-editor": "Editor das mensaxes do sistema",
"watchlist": "Lista de vixilancia",
"deletepage": "Borrar a páxina",
"confirm": "Confirmar",
"excontent": "o contido era: \"$1\"",
- "excontentauthor": "o contido era: \"$1\" (e o único editor foi [[Special:Contributions/$2|$2]])",
+ "excontentauthor": "o contido era: \"$1\", e o único editor foi \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|conversa]])",
"exbeforeblank": "o contido antes do baleirado era: \"$1\"",
"delete-confirm": "Borrar \"$1\"",
"delete-legend": "Borrar",
"move-page-legend": "Mover unha páxina",
"movepagetext": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nPode actualizar automaticamente as redireccións que van dar ao título orixinal.\nSe escolle non facelo, asegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
"movepagetext-noredirectfixer": "Ao usar o formulario inferior vai cambiar o nome da páxina, movendo todo o seu historial ao novo nome.\nO título vello vaise converter nunha páxina de redirección ao novo título.\nAsegúrese de verificar que non hai redireccións [[Special:DoubleRedirects|dobres]] ou [[Special:BrokenRedirects|crebadas]].\nVostede é responsable de asegurarse de que as ligazóns continúan a apuntar cara a onde se supón que deberían.\n\nTeña en conta que a páxina '''non''' será trasladada se xa existe unha páxina co novo título, a menos que esta última sexa unha redirección e non teña historial de edicións.\nIsto significa que pode volver renomear unha páxina ao seu nome antigo se comete un erro, e que non pode sobrescribir unha páxina que xa existe.\n\n'''Atención!'''\nEste cambio nunha páxina popular pode ser drástico e inesperado;\npor favor, asegúrese de que entende as consecuencias disto antes de proseguir.",
- "movepagetalktext": "A páxina de conversa asociada, se existe, será automaticamente movida con esta '''agás que''':\n*Estea a mover a páxina empregando espazos de nomes,\n*Xa exista unha páxina de conversa con ese nome, ou\n*Desactive a opción de abaixo.\n\nNestes casos, terá que mover ou mesturar a páxina manualmente se o desexa.",
+ "movepagetalktext": "Se marca esta caixa, a páxina de conversa asociada trasladarase automáticamente ó título novo a menos que xa exista unha páxina de conversa non baleira alí.\n\nNeste caso, deberá trasladar ou fusionar manualmente a páxina se así o quere.",
"moveuserpage-warning": "'''Aviso:''' Está a piques de mover unha páxina de usuario. Por favor, teña en conta que só se trasladará a páxina e que o usuario '''non''' será renomeado.",
"movecategorypage-warning": "'''Aviso:''' Está a piques de mover unha páxina de categoría. Por favor, teña en conta que só se trasladará a páxina e que as páxinas presentes na categoría vella '''non''' serán recategorizadas na categoría nova.",
"movenologintext": "Debe ser un usuario rexistrado e [[Special:UserLogin|acceder ao sistema]] para mover unha páxina.",
"cant-move-to-user-page": "Non ten os permisos necesarios para mover unha páxina a unha páxina de usuario (agás a unha subpáxina).",
"cant-move-category-page": "Non ten os permisos necesarios para mover páxinas de categoría.",
"cant-move-to-category-page": "Non ten os permisos necesarios para mover unha páxina a unha páxina de categoría.",
- "newtitle": "Ao novo título:",
+ "newtitle": "Novo título:",
"move-watch": "Vixiar esta páxina",
"movepagebtn": "Mover a páxina",
"pagemovedsub": "O movemento foi un éxito",
"logentry-newusers-byemail": "$1 {{GENDER:$2|creou}} a conta de usuario $3; o contrasinal enviouse por correo electrónico",
"logentry-newusers-autocreate": "A conta de {{GENDER:$2|usuario|usuaria}} $1 creouse automaticamente",
"logentry-protect-move_prot": "$1 {{GENDER:$2|trasladou}} a protección de \"$4\" a \"$3\"",
+ "logentry-protect-unprotect": "$1 {{GENDER:$2|eliminou}} a protección de $3",
+ "logentry-protect-protect": "$1 {{GENDER:$2|protexeu}} a $3 $4",
+ "logentry-protect-protect-cascade": "$1 {{GENDER:$2|protexeu}} a $3 $4 [en cascada]",
+ "logentry-protect-modify": "$1 {{GENDER:$2|cambiou}} o nivel de protección de $3 $4",
+ "logentry-protect-modify-cascade": "$1 {{GENDER:$2|cambiou}} o nivel de protección de $3 $4 [en cascada]",
"logentry-rights-rights": "$1 {{GENDER:$2|cambiou}} o grupo ao que pertence $3 de $4 a $5",
"logentry-rights-rights-legacy": "$1 {{GENDER:$2|cambiou}} o grupo ao que pertence $3",
"logentry-rights-autopromote": "$1 foi {{GENDER:$2|promovido|promovida}} automaticamente de $4 a $5",
"tooltip-rollback": "Mbalèkaké suntingan-suntingan ing kaca iki menyang kontributor pungkasan nganggo sak klik.",
"tooltip-undo": "Mbalèkaké révisi iki lan mbukak kothak panyuntingan jroning mode pratayang. Wènèhi kasempatan kanggo ngisi alesan ing kothak ringkesan.",
"tooltip-preferences-save": "Simpen préperensi",
- "tooltip-summary": "Lebkaké ringkesan cedhèk",
+ "tooltip-summary": "Lebokna ringkesan cendhèk",
"anonymous": "{{PLURAL:$1|Panganggo|panganggo}} anon ing {{SITENAME}}.",
"siteuser": "Panganggo {{SITENAME}} $1",
"anonuser": "Panganggo anonim {{SITENAME}} $1",
"undo-failure": "Dat kunnt mer nit zeröck nämme, dä Afschnedd wood enzwesche ald widder beärbeidt.",
"undo-norev": "Do kam_mer nix zeröck nämme. Di väsjohn jidd_et nit, udder se es verschtoche udder fottjeschmeße woode.",
"undo-nochange": "Di Änderong schingk ald retuur jemaat woode ze sin.",
- "undo-summary": "De Änderong $1 fum [[Special:Contributions/$2|$2]] ([[User talk:$2|Klaaf]]) zeröck jenomme.",
+ "undo-summary": "Di Änderong $1 wood {{GENDER:$2|vum|vum|vumm Metmaacher|vun dä|vum}} [[Special:Contributions/$2|$2]] ([[User talk:$2|Klaaf]]) zeröck jenomme.",
"undo-summary-username-hidden": "Nemm di Väsjohn $1 vun enem verschtoche Metmaacher widder retuhr.",
"cantcreateaccounttitle": "Kann keine Zojang enrichte",
"cantcreateaccount-text": "Dä [[User:$3|$3]] hät verbodde, dat mer sich vun dä IP-Adress '''$1''' uß als ene neue Metmaacher aanmelde könne soll.\n\nAls Jrund för et Sperre es enjedraare: ''$2''",
"recentchangeslinked-summary": "Heh di {{int:nstab-special}} hädd en Leß met Änderonge aan Sigge, di vun dä aanjejovve Sigg uß verlengk sin.\nBei Saachjroppe sen et de Sigge en dä Saachjropp.\nSigge uß Dinge [[Special:Watchlist|Opaßleß]] sin en '''Fättschreff''' jeschrevve.",
"recentchangeslinked-page": "Dä Sigg ier Övverschreff:",
"recentchangeslinked-to": "Zeisch de Änderonge aan dä Sigge, woh Lengks op di aanjejovve Sigg drop sin",
+ "autochange-username": "Automattesche Ännderong aam MediaWiki",
"upload": "Daate huhlade",
"uploadbtn": "Huhlade!",
"reuploaddesc": "Zeröck noh de Sigg zem Huhlade.",
"cant-move-to-user-page": "Do häs nit dat Rääsch, en Sigg tirkäk op en Metmaacher-Sigg ömzenänne, Do kanns se ävver op en Ungersigg dofun ömnenne.",
"cant-move-category-page": "Do häß nit dat Rääsch, Saachjroppesigge ömzebenänne.",
"cant-move-to-category-page": "Do häß nit dat Rääsch, en Sigg obb en Saachjroppesigg ömzebenänne.",
- "newtitle": "op dä neue Nahme",
+ "newtitle": "Dä neuje Nahme:",
"move-watch": "Op di Sigg heh oppaßße",
"movepagebtn": "Ömnenne",
"pagemovedsub": "Dat Ömnenne hät jeflupp",
"cancel": "Ticcāhuaz",
"moredotdotdot": "Huehca ōmpa...",
"mypage": "Noāmauh",
- "mytalk": "Notēixnāmiquiliz",
+ "mytalk": "Nozānīl",
"anontalk": "Inīn IP ītēixnāmiquiliz",
"navigation": "Nènemòwalistli",
"and": " īhuān",
- "qbfind": "Tlatēmōz",
- "qbbrowse": "Titlatēmōz",
+ "qbfind": "Ticahciz",
+ "qbbrowse": "Titlatepotztocaz",
"qbedit": "Ticpatlaz",
- "qbpageoptions": "Inīn zāzanilli",
- "qbmyoptions": "Nozāzanil",
+ "qbpageoptions": "Inīn tlaīxtli",
+ "qbmyoptions": "Notlaīx",
"faq": "Zan īc tētlatlanīliztli",
"faqpage": "Project:FAQ",
"actions": "Āyiliztli",
- "namespaces": "Tòkâyeyàntìn",
+ "namespaces": "Tōcātlacāuhtli",
"errorpagetitle": "Aiuhcāyōtl",
"returnto": "Timocuepāz īhuīc $1.",
"tagline": "Īhuīcpa {{SITENAME}}",
"help": "Tēpalēhuiliztli",
- "search": "Tlatēmōz",
- "searchbutton": "Tlatēmōz",
- "go": "Yāuh",
- "searcharticle": "Yāuh",
+ "search": "Titlatēmōz",
+ "searchbutton": "Tictēmōz",
+ "go": "Tiyāz",
+ "searcharticle": "Tiyāz",
"history": "Tlaīxtli ītlahtōllo",
"history_short": "Tlahtōllōtl",
"updatedmarker": "ōmoyancuīx īhuīcpa xōcoyōc notlahpololiz",
"printableversion": "Tepoztlahcuilōlli",
"permalink": "Mochipa tzonhuiliztli",
"print": "Tictepoztlahcuilōz",
- "view": "Mà mỏta",
+ "view": "Tiquittaz",
+ "view-foreign": "Īpan tiquittaz in $1",
"edit": "Ticpatlaz",
"edit-local": "Ticpatlaz nicān tlahtōlli",
"create": "Ticchīhuaz",
"undeletethispage": "Ticmāquīxtīz inīn tlaīxtli",
"undelete_short": "Ahticpolōz {{PLURAL:$1|cē tlapatlaliztli|$1 tlapatlaliztli}}",
"viewdeleted_short": "Mà mỏta {{PLURAL:$1|se tlatlaìxpôpolòlli tlayèktlàlilistli|$1 tlatlaìxpôpolòltin tlayèktlàlilistin}}",
- "protect": "Ticquīxtīz",
+ "protect": "Ticpiyaz",
"protect_change": "ticpatlaz",
- "protectthispage": "Ticquīxtiāz inīn zāzanilli",
- "unprotect": "Ticpatlaz in tlaquīxtīliztli",
- "unprotectthispage": "Ticpatlaz inīn āmatl ītlaquīxtīliz",
+ "protectthispage": "Ticpiyaz inīn tlaīxtli",
+ "unprotect": "Ticpatlaz in tlapiyaliztli",
+ "unprotectthispage": "Ticpatlaz inīn tlaīxtli ītlapiyaliz",
"newpage": "Yancuic tlaīxtli",
"talkpage": "Tictlahtōz inīn zāzaniltechcopa",
"talkpagelinktext": "Zānīlli",
"badaccess": "Tlahuelītiliztechcopa ahcuallōtl",
"badaccess-group0": "Tehhuātl ahmo tiquichīhua inōn tiquiēlēhuia.",
"badaccess-groups": "Inōn tiquiēlēhuia zan quichīhuah tlatequitiltilīlli {{PLURAL:$2|oncān}}: $1.",
- "ok": "Nopan iti",
+ "ok": "Cualli",
"retrievedfrom": "Ōquīzqui ītech \"$1\"",
"youhavenewmessages": "Tiquimpiya $1 ($2).",
"youhavenewmessagesmulti": "Tiquimpiya yancuīc tlahcuilōlli īpan $1",
"editsection": "ticpatlaz",
"editold": "ticpatlaz",
- "viewsourceold": "xiquitta tlahtōlcaquiliztilōni",
+ "viewsourceold": "tiquittaz mēyalli",
"editlink": "ticpatlaz",
- "viewsourcelink": "tiquittaz tlahtōlcaquiliztilōni",
+ "viewsourcelink": "tiquittaz mēyalli",
"editsectionhint": "Ticpatlacah: $1",
"toc": "Inīn tlahcuilōlco",
- "showtoc": "xiquitta",
+ "showtoc": "ticnēxtīz",
"hidetoc": "tictlātīz",
"collapsible-collapse": "Motlàtìs",
"collapsible-expand": "Monèxtìs",
"nstab-media": "Mēdiatl",
"nstab-special": "Nònkuâkìskàtlaìxtlapalli",
"nstab-project": "Ìtlaìxtlapal in tlayẻkàntekitl",
- "nstab-image": "Īxiptli",
+ "nstab-image": "Ihcuilōlli",
"nstab-mediawiki": "Tlahcuilōltzintli",
"nstab-template": "Nemachiòtl",
"nstab-help": "Tèpalèwilistli",
"badtitle": "Ahcualli tōcāitl",
"badtitletext": "Zāzanilli ticnequi in ītōca cah ahcualli, ahtlein quipiya nozo ahcualtzonhuiliztli interwiki tōcāhuicpa.\nHueliz quimpiya tlahtōl tlein ahmo mohuelītih motequitiltia tōcāpan.",
"viewsource": "Tiquittaz tlahtōlcaquiliztilōni",
+ "viewsource-title": "Tiquittaz $1 īmēyal",
"actionthrottled": "Tlachīhualiztli ōmotzacuili",
"viewsourcetext": "Tihuelīti tiquitta auh ticcopīna inīn zāzanilli ītlahtōlcaquiliztilōni:",
"namespaceprotected": "Ahmo tiquihuelīti tiquimpatla zāzaniltin īpan '''$1'''.",
"logout": "Tiquīzaz",
"userlogout": "Tiquīzaz",
"notloggedin": "Ahmō ōtimocalac",
- "nologin": "¿Ahmō ticpiya cuentah? '''$1'''.",
+ "userlogin-noaccount": "Cuix ahmō titlapōhualeh?",
+ "nologin": "Cuix ahmō titlapōhualeh? $1.",
"nologinlink": "Ticchīhuaz cē cuentah",
"createaccount": "Ticchīhuaz cuentah",
"gotaccount": "¿Ye ticpiya cē tlapōhualli? '''$1'''.",
"powersearch-toggleall": "Mochi",
"powersearch-togglenone": "Ahtlein",
"search-external": "Tlatēmotiliztli calāmpa",
- "preferences": "Tlaēlēhuiliztli",
+ "preferences": "Panitlatlālīlli",
"mypreferences": "Notlaēlēhuiliz",
"prefs-edits": "Tlapatlaliztli tlapōhualli:",
"prefs-skin": "Ēhuatl",
"right-block": "Tiquintzacuilīz occequīntīn tlatequitiltilīlli",
"right-blockemail": "Titēquīxtīz tlatequitiltilīlli ic tēch-e-mailīz",
"right-hideuser": "Ticquīxtīz cē tlatequitiltilīltōcāitl, āuh ichtac",
+ "right-editmyoptions": "Ticpatlaz mopanitlatlālīl",
"right-import": "Ticcōhuāz zāzaniltin occequīntīn huiquihuīcpa",
"right-importupload": "Tiquincōhuāz zāzaniltin tlahcuilōlquetzalizhuīcpa",
"right-patrolmarks": "Tiquinttāz tlapiyalizmachiyōtl īpan yancuīc tlapatlaliztli",
"sp-contributions-newbies-title": "Yancuīc tlatequitiltilīlli ītlahcuilōl",
"sp-contributions-blocklog": "Tlatzacuiliztli tlahcuilōlloh",
"sp-contributions-uploads": "tlahcuilōlquetzaliztli",
- "sp-contributions-talk": "tēixnāmiquiliztli",
+ "sp-contributions-talk": "zānīlli",
"sp-contributions-search": "Tiquintlatēmōz tlapatlaliztli",
"sp-contributions-username": "IP nozo tlatequitiltilīlli ītōcā:",
"sp-contributions-submit": "Tlatēmōz",
"import-upload": "Tiquinquetzāz XML tlahcuilōlli",
"importlogpage": "Tiquincōhuāz tlahcuilōlloh",
"tooltip-pt-userpage": "Notlatequitiltilīlzāzanil",
- "tooltip-pt-mytalk": "Notēixnāmiquiliz",
+ "tooltip-pt-mytalk": "Mozānīl",
"tooltip-pt-preferences": "Mopanitlatlālīl",
"tooltip-pt-watchlist": "Zāzaniltin tiquintlachiya ic tlapatlaliztli",
"tooltip-pt-mycontris": "Notlahcuilōl",
"recentchangeslinked-summary": "Deze speciale pagina geeft de laatste bewerkingen weer op pagina's waarheen verwezen wordt vanaf een opgegeven pagina of op pagina's in een opgegeven categorie.\nPagina's die op [[Special:Watchlist|uw volglijst]] staan worden '''vet''' weergegeven.",
"recentchangeslinked-page": "Paginanaam:",
"recentchangeslinked-to": "Wijzigingen aan pagina's met koppelingen naar deze pagina bekijken",
+ "recentchanges-page-added-to-category": "[[:$1]] aan categorie toegevoegd",
"upload": "Bestand uploaden",
"uploadbtn": "Bestand uploaden",
"reuploaddesc": "Upload annuleren en terugkeren naar het uploadformulier",
"htmlform-cloner-create": "Meer toevoegen",
"htmlform-cloner-delete": "Verwijderen",
"htmlform-cloner-required": "Ten minste één waarde is vereist.",
+ "htmlform-title-not-exists": "[[:$1]] bestaat niet.",
+ "htmlform-user-not-exists": "<strong>$1</strong> bestaat niet.",
+ "htmlform-user-not-valid": "<strong>$1</strong> is geen geldige gebruikersnaam.",
"sqlite-has-fts": "Versie $1 met ondersteuning voor \"full-text\" zoeken",
"sqlite-no-fts": "Versie $1 zonder ondersteuning voor \"full-text\" zoeken",
"logentry-delete-delete": "$1 {{GENDER:$2|heeft}} de pagina $3 verwijderd",
"tooltip-recreate": "Recreează",
"tooltip-upload": "Pornește încărcarea",
"tooltip-rollback": "„Revenire” anulează modificarea(ările) de pe această pagină a(le) ultimului contribuitor printr-o singură apăsare",
- "tooltip-undo": "„Anulează” șterge această modificare și deschide formularul de modificare în modulul de previzualizare.\nPermite adăugarea unui motiv în descrierea modificărilor.",
+ "tooltip-undo": "„Anulează” revine asupra acestei modificări către versiunea anterioară și deschide formularul de modificare în modul de previzualizare.\nPermite adăugarea unui motiv în descrierea modificărilor.",
"tooltip-preferences-save": "Salvează preferințele",
"tooltip-summary": "Descrieți pe scurt modificarea",
"interlanguage-link-title": "$1 – $2",
"watchlistanontext": "Пожалуйста, войдите, чтобы просмотреть или отредактировать элементы в списке наблюдения.",
"watchnologin": "Нужно представиться системе",
"addwatch": "Добавить в список наблюдения",
- "addedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были добавлены в ваш [[Special:Watchlist|список наблюдения]].",
+ "addedwatchtext": "Страница «[[:$1]]» вместе с её обсуждением были добавлены в ваш [[Special:Watchlist|список наблюдения]].",
"addedwatchtext-short": "Страница «$1» была добавлена в ваш список наблюдения.",
"removewatch": "Удалить из списка наблюдения",
"removedwatchtext": "Статья «[[:$1]]» и её страница обсуждения были удалены из вашего [[Special:Watchlist|списка наблюдения]].",
"exbeforeblank": "содержимое до очистки: «$1»",
"delete-confirm": "$1 — удаление",
"delete-legend": "Удаление",
- "historywarning": "<strong>Ð\92нимание:</strong> У Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b, коÑ\82оÑ\80Ñ\83Ñ\8e вÑ\8b Ñ\81обиÑ\80аеÑ\82еÑ\81Ñ\8c Ñ\83далиÑ\82Ñ\8c, еÑ\81Ñ\82Ñ\8c иÑ\81Ñ\82оÑ\80иÑ\8f пÑ\80авок, Ñ\81одеÑ\80жаÑ\89аÑ\8f $1 {{PLURAL:$1|веÑ\80Ñ\81иÑ\8e|версий}}:",
+ "historywarning": "<strong>Ð\92нимание:</strong> Ð\92Ñ\8b Ñ\81обиÑ\80аеÑ\82еÑ\81Ñ\8c Ñ\83далиÑ\82Ñ\8c Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\83, Ñ\83 коÑ\82оÑ\80ой еÑ\81Ñ\82Ñ\8c иÑ\81Ñ\82оÑ\80иÑ\8f пÑ\80авок, Ñ\81одеÑ\80жаÑ\89аÑ\8f $1 {{PLURAL:$1|веÑ\80Ñ\81иÑ\8e|веÑ\80Ñ\81ии|версий}}:",
"confirmdeletetext": "Вы запросили полное удаление страницы (или изображения) и всей её истории изменений. Пожалуйста, подтвердите, что вы действительно желаете это сделать, понимаете последствия своих действий, и делаете это в соответствии [[{{MediaWiki:Policy-url}}|с правилами]].",
"actioncomplete": "Действие выполнено",
"actionfailed": "Действие не выполнено",
"undeletepagetext": "{{PLURAL:$1|Следующая $1 страница была удалена|Следующие $1 страниц были удалены|Следующие $1 страницы были удалены|1=Следующая страница была удалена}}, однако {{PLURAL:$1|1=она всё ещё находится в архиве и поэтому может быть восстановлена|они всё ещё находятся в архиве и поэтому могут быть восстановлены}}.\nАрхив может периодически очищаться.",
"undelete-fieldset-title": "Восстановить версии",
"undeleteextrahelp": "Для полного восстановления истории страницы оставьте все отметки пустыми и нажмите '''«{{int:undeletebtn}}»'''.\nДля частичного восстановления отметьте те версии страницы, которые нужно восстановить, и нажмите '''«{{int:undeletebtn}}»'''.",
- "undeleterevisions": "$1 {{PLURAL:$1|версия|версий|версии}} {{PLURAL:$1|удалена|удалены}}",
+ "undeleterevisions": "$1 {{PLURAL:$1|удалённая версия|удалённые версии|удалённых версий}}",
"undeletehistory": "При восстановлении страницы восстанавливается и её история правок.\nЕсли после удаления была создана новая страница с тем же названием, то восстановленные версии появятся в истории правок перед новыми версиями.",
"undeleterevdel": "Восстановление не будет произведено, если оно приведёт к частичному удалению последней версии страницы или файла.\nВ подобном случае вы должны снять отметку или показать последние удалённые версии.",
"undeletehistorynoadmin": "Статья была удалена. Причина удаления и список участников, редактировавших статью до её удаления, показаны ниже. Текст удалённой статьи могут просмотреть только администраторы.",
'styles' => array(
// @todo: Remove mediawiki.page.gallery when cache has cleared
'resources/src/mediawiki/page/gallery-print.css' => array( 'media' => 'print' ),
- // @todo: Remove mediawiki.action.view.filepage.print.css when cache has cleared
- 'resources/src/mediawiki.action/mediawiki.action.view.filepage.print.css' =>
- array( 'media' => 'print' ),
'resources/src/mediawiki.legacy/commonPrint.css' => array( 'media' => 'print' )
),
),
'styles' => array(
// @todo: Remove when mediawiki.page.gallery in cached html.
'resources/src/mediawiki/page/gallery.css',
- // @todo: Remove mediawiki.action.view.filepage.css
- // and mediawiki.legacy/images/checker.png when cache has cleared
- 'resources/src/mediawiki.action/mediawiki.action.view.filepage.css',
'resources/src/mediawiki.legacy/shared.css' => array( 'media' => 'screen' )
),
),
$this->assertRegExp( '/<li class="[\w\s-]*mw-tag-newbie[\w\s-]*">/', $line );
}
+ public function testRecentChangesLine_numberOfWatchingUsers() {
+ $oldChangesList = $this->getOldChangesList();
+
+ $recentChange = $this->getEditChange();
+ $recentChange->numberofWatchingusers = 100;
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( "/(number_of_watching_users_RCview: 100)/", $line );
+ }
+
+ public function testRecentChangesLine_watchlistCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $oldChangesList->setWatchlistDivs( true );
+
+ $recentChange = $this->getEditChange();
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( "/watchlist-0-Cat/", $line );
+ }
+
private function getNewBotEditChange() {
$user = $this->getTestUser();
--- /dev/null
+<?php
+/**
+ * A MemoizedCallable subclass that stores function return values
+ * in an instance property rather than APC.
+ */
+class ArrayBackedMemoizedCallable extends MemoizedCallable {
+ public $cache = array();
+
+ protected function fetchResult( $key, &$success ) {
+ if ( array_key_exists( $key, $this->cache ) ) {
+ $success = true;
+ return $this->cache[$key];
+ }
+ $success = false;
+ return false;
+ }
+
+ protected function storeResult( $key, $result ) {
+ $this->cache[$key] = $result;
+ }
+}
+
+
+/**
+ * PHP Unit tests for MemoizedCallable class.
+ * @covers MemoizedCallable
+ */
+class MemoizedCallableTest extends PHPUnit_Framework_TestCase {
+
+ /**
+ * The memoized callable should relate inputs to outputs in the same
+ * way as the original underlying callable.
+ */
+ public function testReturnValuePassedThrough() {
+ $mock = $this->getMock( 'stdClass', array( 'reverse' ) );
+ $mock->expects( $this->any() )
+ ->method( 'reverse' )
+ ->will( $this->returnCallback( 'strrev' ) );
+
+ $memoized = new MemoizedCallable( array( $mock, 'reverse' ) );
+ $this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
+ }
+
+ /**
+ * Consecutive calls to the memoized callable with the same arguments
+ * should result in just one invocation of the underlying callable.
+ *
+ * @requires function apc_store
+ */
+ public function testCallableMemoized() {
+ $observer = $this->getMock( 'stdClass', array( 'computeSomething' ) );
+ $observer->expects( $this->once() )
+ ->method( 'computeSomething' )
+ ->will( $this->returnValue( 'ok' ) );
+
+ $memoized = new ArrayBackedMemoizedCallable( array( $observer, 'computeSomething' ) );
+
+ // First invocation -- delegates to $observer->computeSomething()
+ $this->assertEquals( 'ok', $memoized->invoke() );
+
+ // Second invocation -- returns memoized result
+ $this->assertEquals( 'ok', $memoized->invoke() );
+ }
+
+ /**
+ * @covers MemoizedCallable::invoke
+ */
+ public function testInvokeVariadic() {
+ $memoized = new MemoizedCallable( 'sprintf' );
+ $this->assertEquals(
+ $memoized->invokeArgs( array( 'this is %s', 'correct' ) ),
+ $memoized->invoke( 'this is %s', 'correct' )
+ );
+ }
+
+ /**
+ * @covers MemoizedCallable::call
+ */
+ public function testShortcutMethod() {
+ $this->assertEquals(
+ 'this is correct',
+ MemoizedCallable::call( 'sprintf', array( 'this is %s', 'correct' ) )
+ );
+ }
+
+ /**
+ * Outlier TTL values should be coerced to range 1 - 86400.
+ */
+ public function testTTLMaxMin() {
+ $memoized = new MemoizedCallable( 'abs', 100000 );
+ $this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );
+
+ $memoized = new MemoizedCallable( 'abs', -10 );
+ $this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
+ }
+
+ /**
+ * Closure names should be distinct.
+ */
+ public function testMemoizedClosure() {
+ $a = new MemoizedCallable( function () {
+ return 'a';
+ } );
+
+ $b = new MemoizedCallable( function () {
+ return 'b';
+ } );
+
+ $this->assertEquals( $a->invokeArgs(), 'a' );
+ $this->assertEquals( $b->invokeArgs(), 'b' );
+
+ $this->assertNotEquals(
+ $this->readAttribute( $a, 'callableName' ),
+ $this->readAttribute( $b, 'callableName' )
+ );
+ }
+
+ /**
+ * @expectedExceptionMessage non-scalar argument
+ * @expectedException InvalidArgumentException
+ */
+ public function testNonScalarArguments() {
+ $memoized = new MemoizedCallable( 'gettype' );
+ $memoized->invoke( new stdClass() );
+ }
+
+ /**
+ * @expectedExceptionMessage must be an instance of callable
+ * @expectedException InvalidArgumentException
+ */
+ public function testNotCallable() {
+ $memoized = new MemoizedCallable( 14 );
+ }
+}