=== New user-facing features in 1.34 ===
* Special:Mute has been added as a quick way for users to block unwanted emails
from other users originating from Special:EmailUser.
+* (T207577) Special:NewSection has been created as a shortcut to creating a new
+ section on a page. When linked to, its subpage is used as the target
+ ([[Special:NewSection/Test]] redirects to creating a new section in "Test").
+ Otherwise, it displays a basic interface to allow the end user to specify
+ the target manually.
=== New developer features in 1.34 ===
* The ImgAuthModifyHeaders hook was added to img_auth.php to allow modification
template option 'searchaction' instead.
* LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
been deprecated.
+* FileBackend::getWikiId() has been deprecated.
+ Use FileBackend::getDomainId() instead.
* User::getRights() and User::$mRights have been deprecated. Use
PermissionManager::getUserPermissions() instead.
* The LocalisationCacheRecache hook no longer allows purging of message blobs
'SpecialMytalk' => __DIR__ . '/includes/specials/redirects/SpecialMytalk.php',
'SpecialMyuploads' => __DIR__ . '/includes/specials/redirects/SpecialMyuploads.php',
'SpecialNewFiles' => __DIR__ . '/includes/specials/SpecialNewimages.php',
+ 'SpecialNewSection' => __DIR__ . '/includes/specials/SpecialNewSection.php',
'SpecialNewpages' => __DIR__ . '/includes/specials/SpecialNewpages.php',
'SpecialPage' => __DIR__ . '/includes/specialpage/SpecialPage.php',
'SpecialPageAction' => __DIR__ . '/includes/actions/SpecialPageAction.php',
use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\NameTableAccessException;
use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
class ChangeTags {
/**
);
}
- $prevTags = self::getPrevTags( $rc_id, $log_id, $rev_id );
+ $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
// add tags
$tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
return [ $tagsToAdd, $tagsToRemove, $prevTags ];
}
- private static function getPrevTags( $rc_id = null, $log_id = null, $rev_id = null ) {
+ /**
+ * Return all the tags associated with the given recent change ID,
+ * revision ID, and/or log entry ID.
+ *
+ * @param IDatabase $db the database to query
+ * @param int|null $rc_id
+ * @param int|null $rev_id
+ * @param int|null $log_id
+ * @return string[]
+ */
+ public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
$conds = array_filter(
[
'ct_rc_id' => $rc_id,
- 'ct_log_id' => $log_id,
'ct_rev_id' => $rev_id,
+ 'ct_log_id' => $log_id,
]
);
- $dbw = wfGetDB( DB_MASTER );
- $tagIds = $dbw->selectFieldValues( 'change_tag', 'ct_tag_id', $conds, __METHOD__ );
+ $tagIds = $db->selectFieldValues(
+ 'change_tag',
+ 'ct_tag_id',
+ $conds,
+ __METHOD__
+ );
$tags = [];
+ $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
foreach ( $tagIds as $tagId ) {
- $tags[] = MediaWikiServices::getInstance()->getChangeTagDefStore()->getName( (int)$tagId );
+ $tags[] = $changeTagDefStore->getName( (int)$tagId );
}
return $tags;
* Alias to getDomainId()
* @return string
* @since 1.20
+ * @deprecated Since 1.34 Use getDomainId()
*/
final public function getWikiId() {
return $this->getDomainId();
if ( $value === false ) {
$value = $callback( $ttl );
- if ( $value !== false ) {
+ if ( $value !== false && $ttl >= 0 ) {
$this->set( $key, $value, $ttl, $flags );
}
}
unset( $this->bag[$key] );
$this->bag[$key] = [
self::KEY_VAL => $value,
- self::KEY_EXP => $this->convertToExpiry( $exptime ),
+ self::KEY_EXP => $this->getExpirationAsTimestamp( $exptime ),
self::KEY_CAS => $this->token . ':' . ++self::$casCounter
];
* @return bool
*/
protected function doChangeTTL( $key, $exptime, $flags ) {
- $expiry = $this->convertToExpiry( $exptime );
- $delete = ( $expiry != 0 && $expiry < $this->getCurrentTime() );
-
if ( !$this->lock( $key, 0 ) ) {
return false;
}
+
+ $expiry = $this->getExpirationAsTimestamp( $exptime );
+ $delete = ( $expiry != self::TTL_INDEFINITE && $expiry < $this->getCurrentTime() );
+
// Use doGet() to avoid having to trigger resolveSegments()
$blob = $this->doGet( $key, self::READ_LATEST );
if ( $blob ) {
/**
* @param int $exptime
* @return bool
+ * @since 1.34
*/
- final protected function expiryIsRelative( $exptime ) {
- return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
+ final protected function isRelativeExpiration( $exptime ) {
+ return ( $exptime != self::TTL_INDEFINITE && $exptime < ( 10 * self::TTL_YEAR ) );
}
/**
* - positive (>= 10 years): absolute UNIX timestamp; return this value
*
* @param int $exptime
- * @return int Absolute TTL or 0 for indefinite
+ * @return int Expiration timestamp or TTL_INDEFINITE for indefinite
+ * @since 1.34
*/
- final protected function convertToExpiry( $exptime ) {
- return $this->expiryIsRelative( $exptime )
- ? (int)$this->getCurrentTime() + $exptime
+ final protected function getExpirationAsTimestamp( $exptime ) {
+ if ( $exptime == self::TTL_INDEFINITE ) {
+ return $exptime;
+ }
+
+ return $this->isRelativeExpiration( $exptime )
+ ? intval( $this->getCurrentTime() + $exptime )
: $exptime;
}
* - positive (>= 10 years): absolute UNIX timestamp; return offset to current time
*
* @param int $exptime
- * @return int Relative TTL or 0 for indefinite
+ * @return int Relative TTL or TTL_INDEFINITE for indefinite
+ * @since 1.34
*/
- final protected function convertToRelative( $exptime ) {
- return $this->expiryIsRelative( $exptime ) || !$exptime
- ? (int)$exptime
- : max( $exptime - (int)$this->getCurrentTime(), 1 );
+ final protected function getExpirationAsTTL( $exptime ) {
+ if ( $exptime == self::TTL_INDEFINITE ) {
+ return $exptime;
+ }
+
+ return $this->isRelativeExpiration( $exptime )
+ ? $exptime
+ : (int)max( $exptime - $this->getCurrentTime(), 1 );
}
/**
* discarded immediately because the expiry is in the past.
* Clamp expires >30d at 30d, unless they're >=1e9 in which
* case they are likely to really be absolute (1e9 = 2011-09-09)
- * @param int $expiry
+ * @param int $exptime
* @return int
*/
- function fixExpiry( $expiry ) {
- if ( $expiry > 2592000 && $expiry < 1000000000 ) {
- $expiry = 2592000;
- }
- return (int)$expiry;
+ protected function fixExpiry( $exptime ) {
+ return ( $exptime > self::TTL_MONTH && !$this->isRelativeExpiration( $exptime ) )
+ ? self::TTL_MONTH
+ : (int)$exptime;
}
}
*/
/**
- * Redis-based caching module for redis server >= 2.6.12
+ * Redis-based caching module for redis server >= 2.6.12 and phpredis >= 2.2.4
*
+ * @see https://github.com/phpredis/phpredis/blob/d310ed7c8/Changelog.md
* @note Avoid use of Redis::MULTI transactions for twemproxy support
*
* @ingroup Cache
return false;
}
- $ttl = $this->convertToRelative( $exptime );
+ $ttl = $this->getExpirationAsTTL( $exptime );
$e = null;
try {
$e = null;
try {
// Note that redis does not return false if the key was not there
- $result = ( $conn->delete( $key ) !== false );
+ $result = ( $conn->del( $key ) !== false );
} catch ( RedisException $e ) {
$result = false;
$this->handleException( $conn, $e );
}
}
- $ttl = $this->convertToRelative( $exptime );
+ $ttl = $this->getExpirationAsTTL( $exptime );
$op = $ttl ? 'setex' : 'set';
$result = true;
// Avoid delete() with array to reduce CPU hogging from a single request
$conn->multi( Redis::PIPELINE );
foreach ( $batchKeys as $key ) {
- $conn->delete( $key );
+ $conn->del( $key );
}
$batchResult = $conn->exec();
if ( $batchResult === false ) {
}
}
- $relative = $this->expiryIsRelative( $exptime );
- $op = ( $exptime == 0 ) ? 'persist' : ( $relative ? 'expire' : 'expireAt' );
+ $relative = $this->isRelativeExpiration( $exptime );
+ $op = ( $exptime == self::TTL_INDEFINITE )
+ ? 'persist'
+ : ( $relative ? 'expire' : 'expireAt' );
$result = true;
foreach ( $batches as $server => $batchKeys ) {
try {
$conn->multi( Redis::PIPELINE );
foreach ( $batchKeys as $key ) {
- if ( $exptime == 0 ) {
+ if ( $exptime == self::TTL_INDEFINITE ) {
$conn->persist( $key );
} elseif ( $relative ) {
- $conn->expire( $key, $this->convertToRelative( $exptime ) );
+ $conn->expire( $key, $this->getExpirationAsTTL( $exptime ) );
} else {
- $conn->expireAt( $key, $this->convertToExpiry( $exptime ) );
+ $conn->expireAt( $key, $this->getExpirationAsTimestamp( $exptime ) );
}
}
$batchResult = $conn->exec();
return false;
}
- $ttl = $this->convertToRelative( $expiry );
+ $ttl = $this->getExpirationAsTTL( $expiry );
try {
$result = $conn->set(
$key,
return false;
}
- $relative = $this->expiryIsRelative( $exptime );
+ $relative = $this->isRelativeExpiration( $exptime );
try {
- if ( $exptime == 0 ) {
+ if ( $exptime == self::TTL_INDEFINITE ) {
$result = $conn->persist( $key );
$this->logRequest( 'persist', $key, $conn->getServer(), $result );
} elseif ( $relative ) {
- $result = $conn->expire( $key, $this->convertToRelative( $exptime ) );
+ $result = $conn->expire( $key, $this->getExpirationAsTTL( $exptime ) );
$this->logRequest( 'expire', $key, $conn->getServer(), $result );
} else {
- $result = $conn->expireAt( $key, $this->convertToExpiry( $exptime ) );
+ $result = $conn->expireAt( $key, $this->getExpirationAsTimestamp( $exptime ) );
$this->logRequest( 'expireAt', $key, $conn->getServer(), $result );
}
} catch ( RedisException $e ) {
$this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
}
+ /**
+ * @param DatabaseDomain $domain
+ * @throws DBConnectionError
+ * @throws DBError
+ * @since 1.32
+ */
protected function doSelectDomain( DatabaseDomain $domain ) {
$this->currentDomain = $domain;
}
throw $this->newExceptionAfterConnectError( "DB path or directory required" );
}
- if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
+ // Check if the database file already exists but is non-readable
+ if (
+ !self::isProcessMemoryPath( $path ) &&
+ file_exists( $path ) &&
+ !is_readable( $path )
+ ) {
throw $this->newExceptionAfterConnectError( 'SQLite database file is not readable' );
} elseif ( !in_array( $this->trxMode, self::$VALID_TRX_MODES, true ) ) {
throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
}
try {
+ // Open the database file, creating it if it does not yet exist
$this->conn = new PDO( "sqlite:$path", null, null, $attributes );
} catch ( PDOException $e ) {
throw $this->newExceptionAfterConnectError( $e->getMessage() );
return false;
}
+ protected function doSelectDomain( DatabaseDomain $domain ) {
+ if ( $domain->getSchema() !== null ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+ );
+ }
+
+ $database = $domain->getDatabase();
+ // A null database means "don't care" so leave it as is and update the table prefix
+ if ( $database === null ) {
+ $this->currentDomain = new DatabaseDomain(
+ $this->currentDomain->getDatabase(),
+ null,
+ $domain->getTablePrefix()
+ );
+
+ return true;
+ }
+
+ if ( $database !== $this->getDBname() ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": cannot change database (got '$database')"
+ );
+ }
+
+ return true;
+ }
+
/**
* Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
*
}
public function serverIsReadOnly() {
+ $this->assertHasConnectionHandle();
+
$path = $this->getDbFilePath();
return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
*
* @param string $db
* @return bool True unless an exception was thrown
- * @throws DBConnectionError If databasesAreIndependent() is true and an error occurs
- * @throws DBError
+ * @throws DBConnectionError If databasesAreIndependent() is true and connection change fails
+ * @throws DBError On query error or if database changes are disallowed
* @deprecated Since 1.32 Use selectDomain() instead
*/
public function selectDB( $db );
* This should only be called by a load balancer or if the handle is not attached to one
*
* @param string|DatabaseDomain $domain
+ * @throws DBConnectionError If databasesAreIndependent() is true and connection change fails
+ * @throws DBError On query error, if domain changes are disallowed, or the domain is invalid
* @since 1.32
- * @throws DBConnectionError
*/
public function selectDomain( $domain );
protected $connFailureErrors = [];
/** @var int */
- private static $GARBAGE_COLLECT_DELAY_SEC = 1;
+ private static $GC_DELAY_SEC = 1;
/** @var string */
private static $OP_SET = 'set';
# Don't keep timing out trying to connect for each call if the DB is down
if (
isset( $this->connFailureErrors[$serverIndex] ) &&
- ( time() - $this->connFailureTimes[$serverIndex] ) < 60
+ ( $this->getCurrentTime() - $this->connFailureTimes[$serverIndex] ) < 60
) {
throw $this->connFailureErrors[$serverIndex];
}
$keysByTable[$serverIndex][$tableName][] = $key;
}
- $exptime = $this->convertToExpiry( $exptime );
+ $exptime = $this->getExpirationAsTimestamp( $exptime );
$result = true;
/** @noinspection PhpUnusedLocalVariableInspection */
protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $exptime = $this->convertToExpiry( $exptime );
+ $exptime = $this->getExpirationAsTimestamp( $exptime );
/** @noinspection PhpUnusedLocalVariableInspection */
$silenceScope = $this->silenceTransactionProfiler();
return $ok;
}
- protected function doChangeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
return $this->modifyMulti(
array_fill_keys( $keys, null ),
$exptime,
protected function isExpired( $db, $exptime ) {
return (
$exptime != $this->getMaxDateTime( $db ) &&
- wfTimestamp( TS_UNIX, $exptime ) < time()
+ wfTimestamp( TS_UNIX, $exptime ) < $this->getCurrentTime()
);
}
* @return string
*/
protected function getMaxDateTime( $db ) {
- if ( time() > 0x7fffffff ) {
+ if ( (int)$this->getCurrentTime() > 0x7fffffff ) {
return $db->timestamp( 1 << 62 );
} else {
return $db->timestamp( 0x7fffffff );
// Only purge on one in every $this->purgePeriod writes
mt_rand( 0, $this->purgePeriod - 1 ) == 0 &&
// Avoid repeating the delete within a few seconds
- ( time() - $this->lastGarbageCollect ) > self::$GARBAGE_COLLECT_DELAY_SEC
+ ( $this->getCurrentTime() - $this->lastGarbageCollect ) > self::$GC_DELAY_SEC
) {
$garbageCollector = function () use ( $db ) {
- $this->deleteServerObjectsExpiringBefore( $db, time(), null, $this->purgeLimit );
+ $this->deleteServerObjectsExpiringBefore(
+ $db, $this->getCurrentTime(),
+ null,
+ $this->purgeLimit
+ );
$this->lastGarbageCollect = time();
};
if ( $this->asyncHandler ) {
- $this->lastGarbageCollect = time(); // avoid duplicate enqueues
+ $this->lastGarbageCollect = $this->getCurrentTime(); // avoid duplicate enqueues
( $this->asyncHandler )( $garbageCollector );
} else {
$garbageCollector();
}
public function expireAll() {
- $this->deleteObjectsExpiringBefore( time() );
+ $this->deleteObjectsExpiringBefore( $this->getCurrentTime() );
}
public function deleteObjectsExpiringBefore(
protected function markServerDown( DBError $exception, $serverIndex ) {
unset( $this->conns[$serverIndex] ); // bug T103435
+ $now = $this->getCurrentTime();
if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
- if ( time() - $this->connFailureTimes[$serverIndex] >= 60 ) {
+ if ( $now - $this->connFailureTimes[$serverIndex] >= 60 ) {
unset( $this->connFailureTimes[$serverIndex] );
unset( $this->connFailureErrors[$serverIndex] );
} else {
return;
}
}
- $now = time();
$this->logger->info( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) );
$this->connFailureTimes[$serverIndex] = $now;
$this->connFailureErrors[$serverIndex] = $exception;
* Insert or update the redirect table entry for this page to indicate it redirects to $rt
* @param Title $rt Redirect target
* @param int|null $oldLatest Prior page_latest for check and set
+ * @return bool Success
*/
public function insertRedirectEntry( Title $rt, $oldLatest = null ) {
$dbw = wfGetDB( DB_MASTER );
],
__METHOD__
);
+ $success = true;
+ } else {
+ $success = false;
}
$dbw->endAtomic( __METHOD__ );
+
+ return $success;
}
/**
}
if ( $isRedirect ) {
- $this->insertRedirectEntry( $redirectTitle );
+ $success = $this->insertRedirectEntry( $redirectTitle );
} else {
// This is not a redirect, remove row from redirect table
$where = [ 'rd_from' => $this->getId() ];
$dbw->delete( 'redirect', $where, __METHOD__ );
+ $success = true;
}
if ( $this->getTitle()->getNamespace() == NS_FILE ) {
RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
}
- return ( $dbw->affectedRows() != 0 );
+ return $success;
}
/**
|| $this->dependencies
|| $this->messages
|| $this->skipFunction
+ || $this->packageFiles
);
return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
}
$namespaces = explode( ';', $opts[ 'namespace' ] );
if ( $opts[ 'associated' ] ) {
+ $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
$associatedNamespaces = array_map(
- function ( $ns ) {
- return MediaWikiServices::getInstance()->getNamespaceInfo()->
- getAssociated( $ns );
+ function ( $ns ) use ( $namespaceInfo ){
+ return $namespaceInfo->getAssociated( $ns );
},
- $namespaces
+ array_filter(
+ $namespaces,
+ function ( $ns ) use ( $namespaceInfo ) {
+ return $namespaceInfo->hasTalkNamespace( $ns );
+ }
+ )
);
$namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
}
'Mytalk' => \SpecialMytalk::class,
'Myuploads' => \SpecialMyuploads::class,
'AllMyUploads' => \SpecialAllMyUploads::class,
+ 'NewSection' => \SpecialNewSection::class,
'PermanentLink' => \SpecialPermanentLink::class,
'Redirect' => \SpecialRedirect::class,
'Revisiondelete' => \SpecialRevisionDelete::class,
--- /dev/null
+<?php
+/**
+ * Redirect from Special:NewSection/$1 to index.php?title=$1&action=edit§ion=new.
+ *
+ * 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 SpecialPage
+ */
+class SpecialNewSection extends RedirectSpecialPage {
+ public function __construct() {
+ parent::__construct( 'NewSection' );
+ $this->mAllowedRedirectParams = [ 'preloadtitle', 'nosummary', 'editintro',
+ 'preload', 'preloadparams[]', 'summary' ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRedirect( $subpage ) {
+ if ( $subpage === null || $subpage === '' ) {
+ return false;
+ }
+ $this->mAddedRedirectParams['title'] = $subpage;
+ $this->mAddedRedirectParams['action'] = 'edit';
+ $this->mAddedRedirectParams['section'] = 'new';
+ return true;
+ }
+
+ protected function showNoRedirectPage() {
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->showForm();
+ }
+
+ private function showForm() {
+ $form = HTMLForm::factory( 'ooui', [
+ 'page' => [
+ 'type' => 'text',
+ 'name' => 'page',
+ 'label-message' => 'newsection-page',
+ ],
+ ], $this->getContext(), 'newsection' );
+ $form->setSubmitTextMsg( 'newsection-submit' );
+ $form->setSubmitCallback( [ $this, 'onFormSubmit' ] );
+ $form->show();
+ }
+
+ public function onFormSubmit( $formData ) {
+ $page = $formData['page'];
+ $query = [ 'action' => 'edit', 'section' => 'new' ];
+ $url = $page->getFullUrlForRedirect( $query );
+ $this->getOutput()->redirect( $url );
+ }
+
+ public function isListed() {
+ return true;
+ }
+
+ protected function getGroupName() {
+ return 'redirects';
+ }
+}
$opts->add( 'feed', '' );
$opts->add( 'tagfilter', '' );
$opts->add( 'invert', false );
+ $opts->add( 'associated', false );
$opts->add( 'size-mode', 'max' );
$opts->add( 'size', 0 );
$username = $this->opts->consumeValue( 'username' );
$tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
$nsinvert = $this->opts->consumeValue( 'invert' );
+ $nsassociated = $this->opts->consumeValue( 'associated' );
$size = $this->opts->consumeValue( 'size' );
$max = $this->opts->consumeValue( 'size-mode' ) === 'max';
'default' => $nsinvert,
'tooltip' => 'invert',
],
+ 'nsassociated' => [
+ 'type' => 'check',
+ 'name' => 'associated',
+ 'label-message' => 'namespace_association',
+ 'default' => $nsassociated,
+ 'tooltip' => 'namespace_association',
+ ],
'tagFilter' => [
'type' => 'tagfilter',
'name' => 'tagfilter',
/**
* @ingroup Pager
*/
+use MediaWiki\MediaWikiServices;
+
class NewPagesPager extends ReverseChronologicalPager {
/**
$conds = [];
$conds['rc_new'] = 1;
- $namespace = $this->opts->getValue( 'namespace' );
- $namespace = ( $namespace === 'all' ) ? false : intval( $namespace );
-
$username = $this->opts->getValue( 'username' );
$user = Title::makeTitleSafe( NS_USER, $username );
}
}
- if ( $namespace !== false ) {
- if ( $this->opts->getValue( 'invert' ) ) {
- $conds[] = 'rc_namespace != ' . $this->mDb->addQuotes( $namespace );
- } else {
- $conds['rc_namespace'] = $namespace;
- }
- }
-
if ( $user ) {
$conds[] = ActorMigration::newMigration()->getWhere(
$this->mDb, 'rc_user', User::newFromName( $user->getText(), false ), false
$conds[] = ActorMigration::newMigration()->isAnon( $rcQuery['fields']['rc_user'] );
}
+ $conds = array_merge( $conds, $this->getNamespaceCond() );
+
# If this user cannot see patrolled edits or they are off, don't do dumb queries!
if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
$conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
return $info;
}
+ // Based on ContribsPager.php
+ function getNamespaceCond() {
+ $namespace = $this->opts->getValue( 'namespace' );
+ if ( $namespace === 'all' || $namespace === '' ) {
+ return [];
+ }
+
+ $namespace = intval( $namespace );
+ $invert = $this->opts->getValue( 'invert' );
+ $associated = $this->opts->getValue( 'associated' );
+
+ $eq_op = $invert ? '!=' : '=';
+ $bool_op = $invert ? 'AND' : 'OR';
+
+ if ( !$associated ) {
+ return [ "rc_namespace $eq_op " . $this->mDb->addQuotes( $namespace ) ];
+ }
+
+ $associatedNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociated( $namespace );
+ return [
+ "rc_namespace $eq_op " . $this->mDb->addQuotes( $namespace ) .
+ $bool_op .
+ " rc_namespace $eq_op " . $this->mDb->addQuotes( $associatedNS )
+ ];
+ }
+
function getIndexField() {
return 'rc_timestamp';
}
"permanentlink-revid": "Revision ID",
"permanentlink-submit": "Go to revision",
"permanentlink-summary": "",
+ "newsection": "New section",
+ "newsection-page": "Target page",
+ "newsection-submit": "Go to page",
+ "newsection-summary": "",
"dberr-problems": "Sorry! This site is experiencing technical difficulties.",
"dberr-again": "Try waiting a few minutes and reloading.",
"dberr-info": "(Cannot access the database: $1)",
"permanentlink-revid": "Label for the field for the revision ID in [[Special:PermanentLink]]\n{{Identical|Revision ID}}",
"permanentlink-submit": "Submit button on [[Special:PermanentLink]]",
"permanentlink-summary": "{{doc-specialpagesummary|permanentlink}}",
+ "newsection": "The title of [[Special:NewSection]]",
+ "newsection-page": "Label for the field for the target page in [[Special:NewSection]]",
+ "newsection-submit": "Submit button on [[Special:NewSection]]",
+ "newsection-summary": "{{doc-specialpagessummary|newsection}}",
"dberr-problems": "This message does not allow any wiki nor html markup.",
"dberr-again": "This message does not allow any wiki nor html markup.",
"dberr-info": "This message does not allow any wiki nor html markup. Parameters:\n* $1 - database server name\nSee also:\n* {{msg-mw|Dberr-info-hidden}} - hides database server name",
'Mytalk' => [ 'MyTalk' ],
'Myuploads' => [ 'MyUploads', 'MyFiles' ],
'Newimages' => [ 'NewFiles', 'NewImages' ],
+ 'NewSection' => [ 'NewSection', 'Newsection' ],
'Newpages' => [ 'NewPages' ],
'PagesWithProp' => [ 'PagesWithProp', 'Pageswithprop', 'PagesByProp', 'Pagesbyprop' ],
'PageData' => [ 'PageData' ],
$this->tablesUsed[] = 'archive';
}
- // TODO only modifyDisplayQuery and getSoftwareTags are tested, nothing else is
+ // TODO most methods are not tested
/** @dataProvider provideModifyDisplayQuery */
public function testModifyDisplayQuery( $origQuery, $filter_tag, $useTags, $modifiedQuery ) {
$this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
}
+ public function provideTags() {
+ $tags = [ 'tag 1', 'tag 2', 'tag 3' ];
+ $rcId = 123;
+ $revId = 456;
+ $logId = 789;
+
+ yield [ $tags, $rcId, null, null ];
+ yield [ $tags, null, $revId, null ];
+ yield [ $tags, null, null, $logId ];
+ yield [ $tags, $rcId, $revId, null ];
+ yield [ $tags, $rcId, null, $logId ];
+ yield [ $tags, $rcId, $revId, $logId ];
+ }
+
+ /**
+ * @dataProvider provideTags
+ */
+ public function testGetTags( array $tags, $rcId, $revId, $logId ) {
+ ChangeTags::addTags( $tags, $rcId, $revId, $logId );
+
+ $actualTags = ChangeTags::getTags( $this->db, $rcId, $revId, $logId );
+
+ $this->assertSame( $tags, $actualTags );
+ }
+
+ public function testGetTags_multiple_arguments() {
+ $rcId = 123;
+ $revId = 456;
+ $logId = 789;
+
+ ChangeTags::addTags( [ 'tag 1' ], $rcId );
+ ChangeTags::addTags( [ 'tag 2' ], $rcId, $revId );
+ ChangeTags::addTags( [ 'tag 3' ], $rcId, $revId, $logId );
+
+ $tags3 = [ 'tag 3' ];
+ $tags2 = array_merge( $tags3, [ 'tag 2' ] );
+ $tags1 = array_merge( $tags2, [ 'tag 1' ] );
+ $this->assertArrayEquals( $tags3, ChangeTags::getTags( $this->db, $rcId, $revId, $logId ) );
+ $this->assertArrayEquals( $tags2, ChangeTags::getTags( $this->db, $rcId, $revId ) );
+ $this->assertArrayEquals( $tags1, ChangeTags::getTags( $this->db, $rcId ) );
+ }
+
public function testTagUsageStatistics() {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'change_tag', '*' );
* @covers \Wikimedia\Rdbms\DatabasePostgres::selectDB
* @expectedException \Wikimedia\Rdbms\DBConnectionError
*/
- public function testInvalidSelectDBIndependant() {
+ public function testInvalidSelectDBIndependent() {
$dbname = 'unittest-domain'; // explodes if DB is selected
$factory = $this->newLBFactoryMulti(
[ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ],
[
- 'dbname' => 'do_not_select_me' // explodes if DB is selected
+ // Explodes with SQLite and Postgres during open/USE
+ 'dbname' => 'bad_dir/do_not_select_me'
]
);
$lb = $factory->getMainLB();
- if ( !wfGetDB( DB_MASTER )->databasesAreIndependent() ) {
+ if ( !$lb->getConnection( DB_MASTER )->databasesAreIndependent() ) {
$this->markTestSkipped( "Not applicable per databasesAreIndependent()" );
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::selectDB
* @covers \Wikimedia\Rdbms\DatabasePostgres::selectDB
- * @expectedException \Wikimedia\Rdbms\DBConnectionError
+ * @expectedException \Wikimedia\Rdbms\DBExpectedError
*/
- public function testInvalidSelectDBIndependant2() {
+ public function testInvalidSelectDBIndependent2() {
$dbname = 'unittest-domain'; // explodes if DB is selected
$factory = $this->newLBFactoryMulti(
[ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ],
[
- 'dbname' => 'do_not_select_me' // explodes if DB is selected
+ // Explodes with SQLite and Postgres during open/USE
+ 'dbname' => 'bad_dir/do_not_select_me'
]
);
$lb = $factory->getMainLB();
- if ( !wfGetDB( DB_MASTER )->databasesAreIndependent() ) {
+ if ( !$lb->getConnection( DB_MASTER )->databasesAreIndependent() ) {
$this->markTestSkipped( "Not applicable per databasesAreIndependent()" );
}
$db = $lb->getConnection( DB_MASTER );
- \Wikimedia\suppressWarnings();
$db->selectDB( 'garbage-db' );
- \Wikimedia\restoreWarnings();
}
/**
$key4 = $this->cache->makeKey( 'test-key4' );
// cleanup
- $this->cache->delete( $key1 );
- $this->cache->delete( $key2 );
- $this->cache->delete( $key3 );
- $this->cache->delete( $key4 );
+ $this->cache->deleteMulti( [ $key1, $key2, $key3, $key4 ] );
$ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], 30 );
$this->assertFalse( $ok, "No keys found" );
$this->assertFalse( $this->cache->get( $key3 ) );
$ok = $this->cache->setMulti( [ $key1 => 1, $key2 => 2, $key3 => 3 ] );
-
$this->assertTrue( $ok, "setMulti() succeeded" );
$this->assertEquals(
3,
$this->assertEquals( 2, $this->cache->get( $key2 ) );
$this->assertEquals( 3, $this->cache->get( $key3 ) );
- $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], $now + 86400 );
- $this->assertTrue( $ok, "Expiry set for all keys" );
-
$ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3, $key4 ], 300 );
$this->assertFalse( $ok, "One key missing" );
+ $this->assertEquals( 1, $this->cache->get( $key1 ), "Key still live" );
+
+ $now = microtime( true ); // real time
+ $ok = $this->cache->setMulti( [ $key1 => 1, $key2 => 2, $key3 => 3 ] );
+ $this->assertTrue( $ok, "setMulti() succeeded" );
+
+ $ok = $this->cache->changeTTLMulti( [ $key1, $key2, $key3 ], $now + 86400 );
+ $this->assertTrue( $ok, "Expiry set for all keys" );
+ $this->assertEquals( 1, $this->cache->get( $key1 ), "Key still live" );
$this->assertEquals( 2, $this->cache->incr( $key1 ) );
$this->assertEquals( 3, $this->cache->incr( $key2 ) );
$this->assertEquals( 4, $this->cache->incr( $key3 ) );
// cleanup
- $this->cache->delete( $key1 );
- $this->cache->delete( $key2 );
- $this->cache->delete( $key3 );
- $this->cache->delete( $key4 );
+ $this->cache->deleteMulti( [ $key1, $key2, $key3, $key4 ] );
}
/**
*/
public function testGetWithSetCallback() {
$now = 1563892142;
- $this->cache->setMockTime( $now );
- $key = $this->cache->makeKey( self::TEST_KEY );
+ $cache = new HashBagOStuff( [] );
+ $cache->setMockTime( $now );
+ $key = $cache->makeKey( self::TEST_KEY );
- $this->assertFalse( $this->cache->get( $key ), "No value" );
+ $this->assertFalse( $cache->get( $key ), "No value" );
- $value = $this->cache->getWithSetCallback(
+ $value = $cache->getWithSetCallback(
$key,
30,
function ( &$ttl ) {
);
$this->assertEquals( 'hello kitty', $value );
- $this->assertEquals( $value, $this->cache->get( $key ), "Value set" );
+ $this->assertEquals( $value, $cache->get( $key ), "Value set" );
$now += 11;
- $this->assertFalse( $this->cache->get( $key ), "Value expired" );
+ $this->assertFalse( $cache->get( $key ), "Value expired" );
}
/**
public function provideUpdateRedirectOn() {
yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ];
- yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, false, 1 ];
+ yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, true, 1 ];
yield [ 'SomeText', false, null, false, true, 0 ];
- yield [ 'SomeText', false, 'Foo', false, false, 1 ];
+ yield [ 'SomeText', false, 'Foo', false, true, 1 ];
}
/**
);
}
+ public function testRcNsFilterAssociatedSpecial() {
+ $this->assertConditions(
+ [ # expected
+ "rc_namespace IN ('-1','0','1')",
+ ],
+ [
+ 'namespace' => '1;-1',
+ 'associated' => 1,
+ ],
+ "rc conditions with associated and special namespace"
+ );
+ }
+
public function testRcNsFilterMultipleAssociatedInvert() {
$this->assertConditions(
[ # expected