Merge "Add watchlist expiry support to applicable APIs"

This commit is contained in:
jenkins-bot 2020-07-14 04:37:44 +00:00 committed by Gerrit Code Review
commit e9ad97eead
22 changed files with 579 additions and 156 deletions

View file

@ -156,6 +156,7 @@ $wgAutoloadLocalClasses = [
'ApiUserrights' => __DIR__ . '/includes/api/ApiUserrights.php',
'ApiValidatePassword' => __DIR__ . '/includes/api/ApiValidatePassword.php',
'ApiWatch' => __DIR__ . '/includes/api/ApiWatch.php',
'ApiWatchlistTrait' => __DIR__ . '/includes/api/ApiWatchlistTrait.php',
'ArchivedFile' => __DIR__ . '/includes/filerepo/file/ArchivedFile.php',
'Argon2Password' => __DIR__ . '/includes/password/Argon2Password.php',
'ArrayDiffFormatter' => __DIR__ . '/includes/diff/ArrayDiffFormatter.php',

View file

@ -1021,18 +1021,13 @@ class EditPage implements IEditObject {
$this->minoredit = $request->getCheck( 'wpMinoredit' );
$this->watchthis = $request->getCheck( 'wpWatchthis' );
if ( $this->watchlistExpiryEnabled ) {
// Set the watchlistExpiry value: this will either be what's posted by the user,
// if we're saving the page; or what's already in the DB, if we're previewing.
// The posted value for previewing will be retrieved
// separately in $this->getCheckboxesDefinitionForWatchlist().
if ( $this->preview ) {
$watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
$this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
} else {
$expiry = ExpiryDef::normalizeExpiry( $request->getText( 'wpWatchlistExpiry' ) );
if ( $expiry !== false ) {
$this->watchlistExpiry = $expiry;
}
// This parsing of the user-posted expiry is done for both preview and saving. This
// is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
// only works because the unnormalized value is retrieved again below in
// getCheckboxesDefinitionForWatchlist().
$expiry = ExpiryDef::normalizeExpiry( $request->getText( 'wpWatchlistExpiry' ) );
if ( $expiry !== false ) {
$this->watchlistExpiry = $expiry;
}
}

View file

@ -52,6 +52,7 @@ use Wikimedia\Rdbms\IDatabase;
abstract class ApiBase extends ContextSource {
use ApiBlockInfoTrait;
use ApiWatchlistTrait;
/** @var HookContainer */
private $hookContainer;
@ -1039,46 +1040,6 @@ abstract class ApiBase extends ContextSource {
return $titleObj;
}
/**
* Return true if we're to watch the page, false if not, null if no change.
* @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
* @param Title $titleObj The page under consideration
* @param string|null $userOption The user option to consider when $watchlist=preferences.
* If not set will use watchdefault always and watchcreations if $titleObj doesn't exist.
* @return bool
*/
protected function getWatchlistValue( $watchlist, $titleObj, $userOption = null ) {
$userWatching = $this->getUser()->isWatched( $titleObj, User::IGNORE_USER_RIGHTS );
switch ( $watchlist ) {
case 'watch':
return true;
case 'unwatch':
return false;
case 'preferences':
# If the user is already watching, don't bother checking
if ( $userWatching ) {
return true;
}
# If no user option was passed, use watchdefault and watchcreations
if ( $userOption === null ) {
return $this->getUser()->getBoolOption( 'watchdefault' ) ||
$this->getUser()->getBoolOption( 'watchcreations' ) && !$titleObj->exists();
}
# Watch the article based on the user preference
return $this->getUser()->getBoolOption( $userOption );
case 'nochange':
return $userWatching;
default:
return $userWatching;
}
}
/**
* Using the settings determine the value for the given parameter
*
@ -1163,21 +1124,6 @@ abstract class ApiBase extends ContextSource {
* @{
*/
/**
* Set a watch (or unwatch) based the based on a watchlist parameter.
* @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
* @param Title $titleObj The article's title to change
* @param string|null $userOption The user option to consider when $watch=preferences
*/
protected function setWatch( $watch, $titleObj, $userOption = null ) {
$value = $this->getWatchlistValue( $watch, $titleObj, $userOption );
if ( $value === null ) {
return;
}
WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() );
}
/**
* Gets the user for whom to get the watchlist
*

View file

@ -29,6 +29,16 @@ use MediaWiki\MediaWikiServices;
* @ingroup API
*/
class ApiDelete extends ApiBase {
use ApiWatchlistTrait;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
/**
* Extracts the title and reason from the request parameters and invokes
* the local delete() function with these as arguments. It does not make use of
@ -91,12 +101,15 @@ class ApiDelete extends ApiBase {
} else {
$watch = $params['watchlist'];
}
$this->setWatch( $watch, $titleObj, 'watchdeletion' );
$watchlistExpiry = $this->getExpiryFromParams( $params );
$this->setWatch( $watch, $titleObj, 'watchdeletion', $watchlistExpiry );
$r = [
'title' => $titleObj->getPrefixedText(),
'reason' => $reason,
];
if ( $status->hasMessage( 'delete-scheduled' ) ) {
$r['scheduled'] = true;
}
@ -200,7 +213,7 @@ class ApiDelete extends ApiBase {
}
public function getAllowedParams() {
return [
$params = [
'title' => null,
'pageid' => [
ApiBase::PARAM_TYPE => 'integer'
@ -214,15 +227,13 @@ class ApiDelete extends ApiBase {
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_DEPRECATED => true,
],
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'unwatch',
'preferences',
'nochange'
],
],
];
// Params appear in the docs in the order they are defined,
// which is why this is here and not at the bottom.
$params += $this->getWatchlistParams();
return $params + [
'unwatch' => [
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_DEPRECATED => true,

View file

@ -35,6 +35,16 @@ use MediaWiki\Revision\SlotRecord;
* @ingroup API
*/
class ApiEditPage extends ApiBase {
use ApiWatchlistTrait;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
public function execute() {
$this->useTransactionalTimeLimit();
@ -357,6 +367,7 @@ class ApiEditPage extends ApiBase {
}
$watch = $this->getWatchlistValue( $params['watchlist'], $titleObj );
$watchlistExpiry = $params['watchlistexpiry'] ?? null;
// Deprecated parameters
if ( $params['watch'] ) {
@ -367,6 +378,10 @@ class ApiEditPage extends ApiBase {
if ( $watch ) {
$requestArray['wpWatchthis'] = '';
if ( $this->watchlistExpiryEnabled && $watchlistExpiry ) {
$requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
}
}
// Apply change tags
@ -463,6 +478,14 @@ class ApiEditPage extends ApiBase {
$r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
$pageObj->getTimestamp() );
}
if ( $watch ) {
$r['watched'] = $status->isOK();
if ( $this->watchlistExpiryEnabled ) {
$r['watchlistexpiry'] = ApiResult::formatExpiry( $watchlistExpiry );
}
}
break;
default:
@ -540,7 +563,7 @@ class ApiEditPage extends ApiBase {
}
public function getAllowedParams() {
return [
$params = [
'title' => [
ApiBase::PARAM_TYPE => 'string',
],
@ -582,15 +605,13 @@ class ApiEditPage extends ApiBase {
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_DEPRECATED => true,
],
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'unwatch',
'preferences',
'nochange'
],
],
];
// Params appear in the docs in the order they are defined,
// which is why this is here and not at the bottom.
$params += $this->getWatchlistParams();
return $params + [
'md5' => null,
'prependtext' => [
ApiBase::PARAM_TYPE => 'text',

View file

@ -28,6 +28,15 @@ use MediaWiki\MediaWikiServices;
*/
class ApiMove extends ApiBase {
use ApiWatchlistTrait;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
public function execute() {
$this->useTransactionalTimeLimit();
@ -157,10 +166,11 @@ class ApiMove extends ApiBase {
if ( isset( $params['watchlist'] ) ) {
$watch = $params['watchlist'];
}
$watchlistExpiry = $this->getExpiryFromParams( $params );
// Watch pages
$this->setWatch( $watch, $fromTitle, 'watchmoves' );
$this->setWatch( $watch, $toTitle, 'watchmoves' );
$this->setWatch( $watch, $fromTitle, 'watchmoves', $watchlistExpiry );
$this->setWatch( $watch, $toTitle, 'watchmoves', $watchlistExpiry );
$result->addValue( null, $this->getModuleName(), $r );
}
@ -238,7 +248,7 @@ class ApiMove extends ApiBase {
}
public function getAllowedParams() {
return [
$params = [
'from' => null,
'fromid' => [
ApiBase::PARAM_TYPE => 'integer'
@ -251,15 +261,13 @@ class ApiMove extends ApiBase {
'movetalk' => false,
'movesubpages' => false,
'noredirect' => false,
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'unwatch',
'preferences',
'nochange'
],
],
];
// Params appear in the docs in the order they are defined,
// which is why this is here and not at the bottom.
$params += $this->getWatchlistParams();
return $params + [
'ignorewarnings' => false,
'tags' => [
ApiBase::PARAM_TYPE => 'tags',

View file

@ -24,6 +24,16 @@
* @ingroup API
*/
class ApiProtect extends ApiBase {
use ApiWatchlistTrait;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
public function execute() {
$params = $this->extractRequestParams();
@ -102,7 +112,8 @@ class ApiProtect extends ApiBase {
$cascade = $params['cascade'];
$watch = $params['watch'] ? 'watch' : $params['watchlist'];
$this->setWatch( $watch, $titleObj, 'watchdefault' );
$watchlistExpiry = $this->getExpiryFromParams( $params );
$this->setWatch( $watch, $titleObj, 'watchdefault', $watchlistExpiry );
$status = $pageObj->doUpdateRestrictions(
$protections,
@ -164,16 +175,7 @@ class ApiProtect extends ApiBase {
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_DEPRECATED => true,
],
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'unwatch',
'preferences',
'nochange'
],
],
];
] + $this->getWatchlistParams();
}
public function needsToken() {

View file

@ -28,6 +28,15 @@ use MediaWiki\User\UserIdentity;
*/
class ApiRollback extends ApiBase {
use ApiWatchlistTrait;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
/**
* @var Title
*/
@ -82,9 +91,10 @@ class ApiRollback extends ApiBase {
}
$watch = $params['watchlist'] ?? 'preferences';
$watchlistExpiry = $this->getExpiryFromParams( $params );
// Watch pages
$this->setWatch( $watch, $titleObj, 'watchrollback' );
$this->setWatch( $watch, $titleObj, 'watchrollback', $watchlistExpiry );
$currentRevisionRecord = $details['current-revision-record'];
$targetRevisionRecord = $details['target-revision-record'];
@ -112,7 +122,7 @@ class ApiRollback extends ApiBase {
}
public function getAllowedParams() {
return [
$params = [
'title' => null,
'pageid' => [
ApiBase::PARAM_TYPE => 'integer'
@ -129,15 +139,13 @@ class ApiRollback extends ApiBase {
],
'summary' => '',
'markbot' => false,
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'unwatch',
'preferences',
'nochange'
],
],
];
// Params appear in the docs in the order they are defined,
// which is why this is here (we want it above the token param).
$params += $this->getWatchlistParams();
return $params + [
'token' => [
// Standard definition automatically inserted
ApiBase::PARAM_HELP_MSG_APPEND => [ 'api-help-param-token-webui' ],

View file

@ -25,6 +25,15 @@
*/
class ApiUndelete extends ApiBase {
use ApiWatchlistTrait;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
public function execute() {
$this->useTransactionalTimeLimit();
@ -83,7 +92,8 @@ class ApiUndelete extends ApiBase {
$this->getUser(), $params['reason'] );
}
$this->setWatch( $params['watchlist'], $titleObj );
$watchlistExpiry = $this->getExpiryFromParams( $params );
$this->setWatch( $params['watchlist'], $titleObj, null, $watchlistExpiry );
$info = [
'title' => $titleObj->getPrefixedText(),
@ -121,16 +131,7 @@ class ApiUndelete extends ApiBase {
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_ISMULTI => true,
],
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'unwatch',
'preferences',
'nochange'
],
],
];
] + $this->getWatchlistParams();
}
public function needsToken() {

View file

@ -24,11 +24,21 @@
* @ingroup API
*/
class ApiUpload extends ApiBase {
use ApiWatchlistTrait;
/** @var UploadBase|UploadFromChunks */
protected $mUpload = null;
protected $mParams;
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
}
public function execute() {
// Check whether upload is enabled
if ( !UploadBase::isEnabled() ) {
@ -825,6 +835,7 @@ class ApiUpload extends ApiBase {
$this->getWatchlistValue( 'preferences', $file->getTitle(), 'watchcreations' )
);
}
$watchlistExpiry = $this->getExpiryFromParams( $this->mParams );
// Deprecated parameters
if ( $this->mParams['watch'] ) {
@ -859,6 +870,7 @@ class ApiUpload extends ApiBase {
'tags' => $this->mParams['tags'],
'text' => $this->mParams['text'],
'watch' => $watch,
'watchlistexpiry' => $watchlistExpiry,
'session' => $this->getContext()->exportSession()
]
) );
@ -866,8 +878,14 @@ class ApiUpload extends ApiBase {
$result['stage'] = 'queued';
} else {
/** @var Status $status */
$status = $this->mUpload->performUpload( $this->mParams['comment'],
$this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
$status = $this->mUpload->performUpload(
$this->mParams['comment'],
$this->mParams['text'],
$watch,
$this->getUser(),
$this->mParams['tags'],
$watchlistExpiry
);
if ( !$status->isGood() ) {
$this->dieRecoverableError( $status->getErrors() );
@ -910,14 +928,17 @@ class ApiUpload extends ApiBase {
ApiBase::PARAM_DFLT => false,
ApiBase::PARAM_DEPRECATED => true,
],
'watchlist' => [
ApiBase::PARAM_DFLT => 'preferences',
ApiBase::PARAM_TYPE => [
'watch',
'preferences',
'nochange'
],
],
];
// Params appear in the docs in the order they are defined,
// which is why this is here and not at the bottom.
$params += $this->getWatchlistParams( [
'watch',
'preferences',
'nochange',
] );
$params += [
'ignorewarnings' => false,
'file' => [
ApiBase::PARAM_TYPE => 'upload',

View file

@ -0,0 +1,148 @@
<?php
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
/**
* An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to
* watch a page. This should ONLY be used in API modules that extend ApiBase.
* Also, it should not be used in ApiWatch, which has its own special handling.
*
* Note the class-level properties watchlistExpiryEnabled and watchlistMaxDuration must still be
* set in the API module's constructor.
*
* @ingroup API
* @since 1.35
*/
trait ApiWatchlistTrait {
/** @var bool Whether watchlist expiries are enabled. */
private $watchlistExpiryEnabled;
/** @var string Relative maximum expiry. */
private $watchlistMaxDuration;
/**
* Get additional allow params specific to watchlisting.
* This should be merged in with the result of self::getAllowedParams().
*
* This purposefully does not include the deprecated 'watch' and 'unwatch'
* parameters that some APIs still accept.
*
* @param string[] $watchOptions
* @return array
*/
protected function getWatchlistParams( array $watchOptions = [] ): array {
if ( !$watchOptions ) {
$watchOptions = [
'watch',
'unwatch',
'preferences',
'nochange',
];
}
$result = [
'watchlist' => [
ParamValidator::PARAM_DEFAULT => 'preferences',
ParamValidator::PARAM_TYPE => $watchOptions,
],
];
if ( $this->watchlistExpiryEnabled ) {
$result['watchlistexpiry'] = [
ParamValidator::PARAM_TYPE => 'expiry',
ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
ExpiryDef::PARAM_USE_MAX => true,
];
}
return $result;
}
/**
* Set a watch (or unwatch) based the based on a watchlist parameter.
* @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
* @param Title $titleObj The article's title to change
* @param string|null $userOption The user option to consider when $watch=preferences
* @param string|null $expiry Optional expiry timestamp in any format acceptable to wfTimestamp(),
* null will not create expiries, or leave them unchanged should they already exist.
*/
protected function setWatch(
string $watch,
Title $titleObj,
?string $userOption = null,
?string $expiry = null
): void {
$value = $this->getWatchlistValue( $watch, $titleObj, $userOption );
WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser(), $expiry );
}
/**
* Return true if we're to watch the page, false if not.
* @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
* @param Title $titleObj The page under consideration
* @param string|null $userOption The user option to consider when $watchlist=preferences.
* If not set will use watchdefault always and watchcreations if $titleObj doesn't exist.
* @return bool
*/
protected function getWatchlistValue(
string $watchlist,
Title $titleObj,
?string $userOption = null
): bool {
$user = $this->getUser();
$userWatching = $user->isWatched( $titleObj, User::IGNORE_USER_RIGHTS );
switch ( $watchlist ) {
case 'watch':
return true;
case 'unwatch':
return false;
case 'preferences':
// If the user is already watching, don't bother checking
if ( $userWatching ) {
return true;
}
// If no user option was passed, use watchdefault and watchcreations
if ( $userOption === null ) {
return $user->getBoolOption( 'watchdefault' ) ||
$user->getBoolOption( 'watchcreations' ) &&
!$titleObj->exists();
}
// Watch the article based on the user preference
return $user->getBoolOption( $userOption );
case 'nochange':
return $userWatching;
default:
return $userWatching;
}
}
/**
* Get formatted expiry from the given parameters, or null if no expiry was provided.
* @param array $params Request parameters passed to the API.
* @return string|null
*/
protected function getExpiryFromParams( array $params ): ?string {
$watchlistExpiry = null;
if ( $this->watchlistExpiryEnabled && isset( $params['watchlistexpiry'] ) ) {
$watchlistExpiry = ApiResult::formatExpiry( $params['watchlistexpiry'] );
}
return $watchlistExpiry;
}
/**
* Implemented by ApiBase. We do this because otherwise Phan would complain
* about calls to $this->getUser() in this trait, since it doesn't infer that
* classes using the trait are also extending ApiBase.
* @return User
*/
abstract protected function getUser();
}

View file

@ -128,6 +128,7 @@
"apihelp-delete-param-tags": "Change tags to apply to the entry in the deletion log.",
"apihelp-delete-param-watch": "Add the page to the current user's watchlist.",
"apihelp-delete-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-delete-param-watchlistexpiry": "Watchlist expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-delete-param-unwatch": "Remove the page from the current user's watchlist.",
"apihelp-delete-param-oldimage": "The name of the old image to delete as provided by [[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]].",
"apihelp-delete-example-simple": "Delete the <kbd>Main Page</kbd>.",
@ -155,6 +156,7 @@
"apihelp-edit-param-watch": "Add the page to the current user's watchlist.",
"apihelp-edit-param-unwatch": "Remove the page from the current user's watchlist.",
"apihelp-edit-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-edit-param-watchlistexpiry": "Watchlst expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-edit-param-md5": "The MD5 hash of the $1text parameter, or the $1prependtext and $1appendtext parameters concatenated. If set, the edit won't be done unless the hash is correct.",
"apihelp-edit-param-prependtext": "Add this text to the beginning of the page. Overrides $1text.",
"apihelp-edit-param-appendtext": "Add this text to the end of the page. Overrides $1text.\n\nUse $1section=new to append a new section, rather than this parameter.",
@ -322,6 +324,7 @@
"apihelp-move-param-watch": "Add the page and the redirect to the current user's watchlist.",
"apihelp-move-param-unwatch": "Remove the page and the redirect from the current user's watchlist.",
"apihelp-move-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-move-param-watchlistexpiry": "Watchlist expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-move-param-ignorewarnings": "Ignore any warnings.",
"apihelp-move-param-tags": "Change tags to apply to the entry in the move log and to the null revision on the destination page.",
"apihelp-move-example-move": "Move <kbd>Badtitle</kbd> to <kbd>Goodtitle</kbd> without leaving a redirect.",
@ -431,6 +434,7 @@
"apihelp-protect-param-cascade": "Enable cascading protection (i.e. protect transcluded templates and images used in this page). Ignored if none of the given protection levels support cascading.",
"apihelp-protect-param-watch": "If set, add the page being (un)protected to the current user's watchlist.",
"apihelp-protect-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-protect-param-watchlistexpiry": "Watchlist expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-protect-example-protect": "Protect a page.",
"apihelp-protect-example-unprotect": "Unprotect a page by setting restrictions to <kbd>all</kbd> (i.e. everyone is allowed to take the action).",
"apihelp-protect-example-unprotect2": "Unprotect a page by setting no restrictions.",
@ -1452,6 +1456,7 @@
"apihelp-rollback-param-summary": "Custom edit summary. If empty, default summary will be used.",
"apihelp-rollback-param-markbot": "Mark the reverted edits and the revert as bot edits.",
"apihelp-rollback-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-rollback-param-watchlistexpiry": "Watchlist expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-rollback-example-simple": "Roll back the last edits to page <kbd>Main Page</kbd> by user <kbd>Example</kbd>.",
"apihelp-rollback-example-summary": "Roll back the last edits to page <kbd>Main Page</kbd> by IP user <kbd>192.0.2.5</kbd> with summary <kbd>Reverting vandalism</kbd>, and mark those edits and the revert as bot edits.",
@ -1525,6 +1530,7 @@
"apihelp-undelete-param-timestamps": "Timestamps of the revisions to restore. If both <var>$1timestamps</var> and <var>$1fileids</var> are empty, all will be restored.",
"apihelp-undelete-param-fileids": "IDs of the file revisions to restore. If both <var>$1timestamps</var> and <var>$1fileids</var> are empty, all will be restored.",
"apihelp-undelete-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-undelete-param-watchlistexpiry": "Watchlist expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-undelete-example-page": "Undelete page <kbd>Main Page</kbd>.",
"apihelp-undelete-example-revisions": "Undelete two revisions of page <kbd>Main Page</kbd>.",
@ -1539,6 +1545,7 @@
"apihelp-upload-param-text": "Initial page text for new files.",
"apihelp-upload-param-watch": "Watch the page.",
"apihelp-upload-param-watchlist": "Unconditionally add or remove the page from the current user's watchlist, use preferences or do not change watch.",
"apihelp-upload-param-watchlistexpiry": "Watchlist expiry timestamp. Omit this parameter entirely to leave the current expiry unchanged.",
"apihelp-upload-param-ignorewarnings": "Ignore any warnings.",
"apihelp-upload-param-file": "File contents.",
"apihelp-upload-param-url": "URL to fetch the file from.",

View file

@ -129,6 +129,7 @@
"apihelp-delete-param-tags": "{{doc-apihelp-param|delete|tags}}",
"apihelp-delete-param-watch": "{{doc-apihelp-param|delete|watch}}",
"apihelp-delete-param-watchlist": "{{doc-apihelp-param|delete|watchlist}}",
"apihelp-delete-param-watchlistexpiry": "{{doc-apihelp-param|delete|watchlistexpiry}}",
"apihelp-delete-param-unwatch": "{{doc-apihelp-param|delete|unwatch}}",
"apihelp-delete-param-oldimage": "{{doc-apihelp-param|delete|oldimage}}",
"apihelp-delete-example-simple": "{{doc-apihelp-example|delete}}",
@ -154,6 +155,7 @@
"apihelp-edit-param-watch": "{{doc-apihelp-param|edit|watch}}",
"apihelp-edit-param-unwatch": "{{doc-apihelp-param|edit|unwatch}}",
"apihelp-edit-param-watchlist": "{{doc-apihelp-param|edit|watchlist}}",
"apihelp-edit-param-watchlistexpiry": "{{doc-apihelp-param|edit|watchlistexpiry}}",
"apihelp-edit-param-md5": "{{doc-apihelp-param|edit|md5}}",
"apihelp-edit-param-prependtext": "{{doc-apihelp-param|edit|prependtext}}",
"apihelp-edit-param-appendtext": "{{doc-apihelp-param|edit|appendtext}}",
@ -306,6 +308,7 @@
"apihelp-move-param-watch": "{{doc-apihelp-param|move|watch}}",
"apihelp-move-param-unwatch": "{{doc-apihelp-param|move|unwatch}}",
"apihelp-move-param-watchlist": "{{doc-apihelp-param|move|watchlist}}",
"apihelp-move-param-watchlistexpiry": "{{doc-apihelp-param|move|watchlistexpiry}}",
"apihelp-move-param-ignorewarnings": "{{doc-apihelp-param|move|ignorewarnings}}",
"apihelp-move-param-tags": "{{doc-apihelp-param|move|tags}}",
"apihelp-move-example-move": "{{doc-apihelp-example|move}}",
@ -409,6 +412,7 @@
"apihelp-protect-param-cascade": "{{doc-apihelp-param|protect|cascade}}",
"apihelp-protect-param-watch": "{{doc-apihelp-param|protect|watch}}",
"apihelp-protect-param-watchlist": "{{doc-apihelp-param|protect|watchlist}}",
"apihelp-protect-param-watchlistexpiry": "{{doc-apihelp-param|protect|watchlistexpiry}}",
"apihelp-protect-example-protect": "{{doc-apihelp-example|protect}}",
"apihelp-protect-example-unprotect": "{{doc-apihelp-example|protect}}",
"apihelp-protect-example-unprotect2": "{{doc-apihelp-example|protect}}",
@ -1360,6 +1364,7 @@
"apihelp-rollback-param-summary": "{{doc-apihelp-param|rollback|summary}}",
"apihelp-rollback-param-markbot": "{{doc-apihelp-param|rollback|markbot}}",
"apihelp-rollback-param-watchlist": "{{doc-apihelp-param|rollback|watchlist}}",
"apihelp-rollback-param-watchlistexpiry": "{{doc-apihelp-param|rollback|watchlistexpiry}}",
"apihelp-rollback-example-simple": "{{doc-apihelp-example|rollback}}",
"apihelp-rollback-example-summary": "{{doc-apihelp-example|rollback}}",
"apihelp-rsd-summary": "{{doc-apihelp-summary|rsd}}",
@ -1425,6 +1430,7 @@
"apihelp-undelete-param-timestamps": "{{doc-apihelp-param|undelete|timestamps}}",
"apihelp-undelete-param-fileids": "{{doc-apihelp-param|undelete|fileids}}",
"apihelp-undelete-param-watchlist": "{{doc-apihelp-param|undelete|watchlist}}",
"apihelp-undelete-param-watchlistexpiry": "{{doc-apihelp-param|undelete|watchlistexpiry}}",
"apihelp-undelete-example-page": "{{doc-apihelp-example|undelete}}",
"apihelp-undelete-example-revisions": "{{doc-apihelp-example|undelete}}",
"apihelp-unlinkaccount-summary": "{{doc-apihelp-summary|unlinkaccount}}",
@ -1437,6 +1443,7 @@
"apihelp-upload-param-text": "{{doc-apihelp-param|upload|text}}",
"apihelp-upload-param-watch": "{{doc-apihelp-param|upload|watch}}",
"apihelp-upload-param-watchlist": "{{doc-apihelp-param|upload|watchlist}}",
"apihelp-upload-param-watchlistexpiry": "{{doc-apihelp-param|upload|watchlistexpiry}}",
"apihelp-upload-param-ignorewarnings": "{{doc-apihelp-param|upload|ignorewarnings}}",
"apihelp-upload-param-file": "{{doc-apihelp-param|upload|file}}",
"apihelp-upload-param-url": "{{doc-apihelp-param|upload|url}}",

View file

@ -84,7 +84,8 @@ class PublishStashedFileJob extends Job {
$this->params['text'],
$this->params['watch'],
$user,
$this->params['tags'] ?? []
$this->params['tags'] ?? [],
$this->params['watchlistexpiry'] ?? null
);
if ( !$status->isGood() ) {
UploadBase::setSessionStatus(

View file

@ -919,9 +919,15 @@ abstract class UploadBase {
* @param User $user
* @param string[] $tags Change tags to add to the log entry and page revision.
* (This doesn't check $user's permissions.)
* @param string|null $watchlistExpiry Optional watchlist expiry timestamp in any format
* acceptable to wfTimestamp().
* @return Status Indicating the whether the upload succeeded.
*
* @since 1.35 Accepts $watchlistExpiry parameter.
*/
public function performUpload( $comment, $pageText, $watch, $user, $tags = [] ) {
public function performUpload(
$comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
) {
$this->getLocalFile()->load( File::READ_LATEST );
$props = $this->mFileProps;
@ -950,7 +956,8 @@ abstract class UploadBase {
WatchAction::doWatch(
$this->getLocalFile()->getTitle(),
$user,
User::IGNORE_USER_RIGHTS
User::IGNORE_USER_RIGHTS,
$watchlistExpiry
);
}
$this->getHookRunner()->onUploadComplete( $this );

View file

@ -17,8 +17,12 @@ class ApiDeleteTest extends ApiTestCase {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'change_tag', 'change_tag_def', 'logging' ]
[ 'change_tag', 'change_tag_def', 'logging', 'watchlist', 'watchlist_expiry' ]
);
$this->setMwGlobals( [
'wgWatchlistExpiry' => true,
] );
}
public function testDelete() {
@ -190,10 +194,17 @@ class ApiDeleteTest extends ApiTestCase {
$this->assertTrue( Title::newFromText( $name )->exists() );
$this->assertFalse( $user->isWatched( Title::newFromText( $name ) ) );
$this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name, 'watch' => '' ] );
$this->doApiRequestWithToken( [
'action' => 'delete',
'title' => $name,
'watch' => '',
'watchlistexpiry' => '99990123000000',
] );
$this->assertFalse( Title::newFromText( $name )->exists() );
$this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
$title = Title::newFromText( $name );
$this->assertFalse( $title->exists() );
$this->assertTrue( $user->isWatched( $title ) );
$this->assertTrue( $user->isTempWatched( $title ) );
}
public function testDeleteUnwatch() {
@ -205,7 +216,11 @@ class ApiDeleteTest extends ApiTestCase {
$user->addWatch( Title::newFromText( $name ) );
$this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
$this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name, 'unwatch' => '' ] );
$this->doApiRequestWithToken( [
'action' => 'delete',
'title' => $name,
'watchlist' => 'unwatch',
] );
$this->assertFalse( Title::newFromText( $name )->exists() );
$this->assertFalse( $user->isWatched( Title::newFromText( $name ) ) );

View file

@ -30,6 +30,8 @@ class ApiEditPageTest extends ApiTestCase {
12312 => 'testing',
12314 => 'testing-nontext',
],
'wgWatchlistExpiry' => true,
'wgWatchlistExpiryMaxDuration' => '6 months',
] );
$this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
'testing' => 'DummyContentHandlerForTesting',
@ -38,7 +40,7 @@ class ApiEditPageTest extends ApiTestCase {
] );
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'change_tag', 'change_tag_def', 'logging' ]
[ 'change_tag', 'change_tag_def', 'logging', 'watchlist', 'watchlist_expiry' ]
);
}
@ -1354,10 +1356,13 @@ class ApiEditPageTest extends ApiTestCase {
'title' => $name,
'text' => 'Some text',
'watch' => '',
'watchlistexpiry' => '99990123000000',
] );
$this->assertTrue( Title::newFromText( $name )->exists() );
$this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
$title = Title::newFromText( $name );
$this->assertTrue( $title->exists() );
$this->assertTrue( $user->isWatched( $title ) );
$this->assertTrue( $user->isTempWatched( $title ) );
}
public function testEditUnwatch() {

View file

@ -12,6 +12,20 @@ use MediaWiki\Revision\SlotRecord;
* @covers ApiMove
*/
class ApiMoveTest extends ApiTestCase {
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'watchlist', 'watchlist_expiry' ]
);
$this->setMwGlobals( [
'wgWatchlistExpiry' => true,
] );
}
/**
* @param string $from Prefixed name of source
* @param string $to Prefixed name of destination
@ -100,6 +114,24 @@ class ApiMoveTest extends ApiTestCase {
$this->assertArrayNotHasKey( 'warnings', $res[0] );
}
public function testMoveAndWatch(): void {
$name = ucfirst( __FUNCTION__ );
$this->createPage( $name );
$this->doApiRequestWithToken( [
'action' => 'move',
'from' => $name,
'to' => "$name 2",
'watchlist' => 'watch',
'watchlistexpiry' => '99990123000000',
] );
$title = Title::newFromText( $name );
$title2 = Title::newFromText( "$name 2" );
$this->assertTrue( $this->getTestSysop()->getUser()->isTempWatched( $title ) );
$this->assertTrue( $this->getTestSysop()->getUser()->isTempWatched( $title2 ) );
}
public function testMoveNonexistent() {
$this->expectException( ApiUsageException::class );
$this->expectExceptionMessage( "The page you specified doesn't exist." );

View file

@ -0,0 +1,49 @@
<?php
/**
* Tests for protect API.
*
* @group API
* @group Database
* @group medium
*
* @covers ApiProtect
*/
class ApiProtectTest extends ApiTestCase {
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'page_restrictions', 'logging', 'watchlist', 'watchlist_expiry' ]
);
$this->setMwGlobals( [
'wgWatchlistExpiry' => true,
] );
}
/**
* @covers ApiProtect::execute()
*/
public function testProtectWithWatch(): void {
$name = ucfirst( __FUNCTION__ );
$title = Title::newFromText( $name );
$this->editPage( $name, 'Some text' );
$apiResult = $this->doApiRequestWithToken( [
'action' => 'protect',
'title' => $name,
'protections' => 'edit=sysop',
'expiry' => '55550123000000',
'watchlist' => 'watch',
'watchlistexpiry' => '99990123000000',
] )[0];
$this->assertArrayHasKey( 'protect', $apiResult );
$this->assertSame( $name, $apiResult['protect']['title'] );
$this->assertTrue( $title->isProtected( 'edit' ) );
$this->assertTrue( $this->getTestSysop()->getUser()->isTempWatched( $title ) );
}
}

View file

@ -0,0 +1,70 @@
<?php
use MediaWiki\MediaWikiServices;
/**
* Tests for Rollback API.
*
* @group API
* @group Database
* @group medium
*
* @covers ApiRollback
*/
class ApiRollbackTest extends ApiTestCase {
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'watchlist', 'watchlist_expiry' ]
);
$this->setMwGlobals( [
'wgWatchlistExpiry' => true,
] );
}
/**
* @covers ApiRollback::execute()
*/
public function testProtectWithWatch(): void {
$name = ucfirst( __FUNCTION__ );
$title = Title::newFromText( $name );
$revLookup = MediaWikiServices::getInstance()
->getRevisionLookup();
$user = $this->getTestUser()->getUser();
$sysop = $this->getTestSysop()->getUser();
// Create page as sysop.
$this->editPage( $name, 'Some text', '', NS_MAIN, $sysop );
// Edit as non-sysop.
$this->editPage( $name, 'Vandalism', '', NS_MAIN, $user );
// Rollback as sysop.
$apiResult = $this->doApiRequestWithToken( [
'action' => 'rollback',
'title' => $name,
'user' => $user->getName(),
'watchlist' => 'watch',
'watchlistexpiry' => '99990123000000',
] )[0];
// Content of latest revision should match the initial.
$latestRev = $revLookup->getRevisionByTitle( $title );
$initialRev = $revLookup->getFirstRevision( $title );
$this->assertTrue( $latestRev->hasSameContent( $initialRev ) );
// ...but have different rev IDs.
$this->assertNotSame( $latestRev->getId(), $initialRev->getId() );
// Make sure the API response looks good.
$this->assertArrayHasKey( 'rollback', $apiResult );
$this->assertSame( $name, $apiResult['rollback']['title'] );
// And that the page was temporarily watched.
$this->assertTrue( $sysop->isTempWatched( $title ) );
}
}

View file

@ -0,0 +1,58 @@
<?php
/**
* Tests for Undelete API.
*
* @group API
* @group Database
* @group medium
*
* @covers ApiUndelete
*/
class ApiUndeleteTest extends ApiTestCase {
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'logging', 'watchlist', 'watchlist_expiry' ]
);
$this->setMwGlobals( [
'wgWatchlistExpiry' => true,
] );
}
/**
* @covers ApiUndelete::execute()
*/
public function testUndeleteWithWatch(): void {
$name = ucfirst( __FUNCTION__ );
$title = Title::newFromText( $name );
$sysop = $this->getTestSysop()->getUser();
// Create page.
$this->editPage( $name, 'Test' );
// Delete page.
$this->doApiRequestWithToken( [
'action' => 'delete',
'title' => $name,
] );
// For good measure.
$this->assertFalse( $title->exists() );
$this->assertFalse( $sysop->isWatched( $title ) );
// Restore page, and watch with expiry.
$this->doApiRequestWithToken( [
'action' => 'undelete',
'title' => $name,
'watchlist' => 'watch',
'watchlistexpiry' => '99990123000000',
] );
$this->assertTrue( $title->exists() );
$this->assertTrue( $sysop->isTempWatched( $title ) );
}
}

View file

@ -15,6 +15,7 @@ class ApiUploadTest extends ApiUploadTestCase {
protected function setUp() : void {
parent::setUp();
$this->tablesUsed[] = 'watchlist'; // This test might interfere with watchlists test.
$this->tablesUsed[] = 'watchlist_expiry';
$this->tablesUsed = array_merge( $this->tablesUsed, LocalFile::getQueryInfo()['tables'] );
$this->setService( 'RepoGroup', new RepoGroup(
[
@ -30,6 +31,10 @@ class ApiUploadTest extends ApiUploadTestCase {
null
) );
$this->resetServices();
$this->setMwGlobals( [
'wgWatchlistExpiry' => true,
] );
}
public function testUploadRequiresToken() {
@ -48,10 +53,12 @@ class ApiUploadTest extends ApiUploadTestCase {
], null, self::$users['uploader']->getUser() );
}
public function testUpload() {
public function testUploadWithWatch() {
$fileName = 'TestUpload.jpg';
$mimeType = 'image/jpeg';
$filePath = $this->filePath( 'yuv420.jpg' );
$title = Title::newFromText( $fileName, NS_FILE );
$user = self::$users['uploader']->getUser();
$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath );
list( $result ) = $this->doApiRequestWithToken( [
@ -60,12 +67,15 @@ class ApiUploadTest extends ApiUploadTestCase {
'file' => 'dummy content',
'comment' => 'dummy comment',
'text' => "This is the page text for $fileName",
], null, self::$users['uploader']->getUser() );
'watchlist' => 'watch',
'watchlistexpiry' => '99990123000000',
], null, $user );
$this->assertArrayHasKey( 'upload', $result );
$this->assertEquals( 'Success', $result['upload']['result'] );
$this->assertSame( filesize( $filePath ), (int)$result['upload']['imageinfo']['size'] );
$this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
$this->assertTrue( $user->isTempWatched( $title ) );
}
public function testUploadZeroLength() {