aboutsummaryrefslogtreecommitdiffstats
path: root/includes/api/ApiWatchlistTrait.php
blob: 62a68fc42da07f7c983244bb189019df801af233 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<?php

use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserOptionsLookup;
use MediaWiki\Watchlist\WatchlistManager;
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;

	private WatchlistManager $watchlistManager;
	private UserOptionsLookup $userOptionsLookup;

	private function initServices() {
		// @phan-suppress-next-line PhanRedundantCondition Phan trusts the type hints too much
		if ( isset( $this->watchlistManager ) && isset( $this->userOptionsLookup ) ) {
			return;
		}
		// This trait is used outside of core and therefor fallback to global state - T263904
		$services = MediaWikiServices::getInstance();
		$this->watchlistManager ??= $services->getWatchlistManager();
		$this->userOptionsLookup ??= $services->getUserOptionsLookup();
	}

	/**
	 * 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 $title The article's title to change
	 * @param User $user The user to set watch/unwatch for
	 * @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 $title,
		User $user,
		?string $userOption = null,
		?string $expiry = null
	): void {
		$value = $this->getWatchlistValue( $watch, $title, $user, $userOption );
		$this->watchlistManager->setWatch( $value, $user, $title, $expiry );
	}

	/**
	 * Return true if we're to watch the page, false if not.
	 * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
	 * @param Title $title The page under consideration
	 * @param User $user The user get the value for.
	 * @param string|null $userOption The user option to consider when $watchlist=preferences.
	 *    If not set will use watchdefault always and watchcreations if $title doesn't exist.
	 * @return bool
	 */
	protected function getWatchlistValue(
		string $watchlist,
		Title $title,
		User $user,
		?string $userOption = null
	): bool {
		$this->initServices();
		$userWatching = $this->watchlistManager->isWatchedIgnoringRights( $user, $title );

		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 the user is a bot, act as 'nochange' to avoid big watchlists on single users
				if ( $user->isBot() ) {
					return $userWatching;
				}
				// If no user option was passed, use watchdefault and watchcreations
				if ( $userOption === null ) {
					return $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ||
						$this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) &&
						!$title->exists();
				}

				// Watch the article based on the user preference
				return $this->userOptionsLookup->getBoolOption( $user, $userOption );

			// case 'nochange':
			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;
	}

	/**
	 * Get existing expiry from the database.
	 *
	 * @param WatchedItemStoreInterface $store
	 * @param Title $title
	 * @param UserIdentity $user The user to get the expiry for.
	 * @return string|null
	 */
	protected function getWatchlistExpiry(
		WatchedItemStoreInterface $store,
		Title $title,
		UserIdentity $user
	): ?string {
		$watchedItem = $store->getWatchedItem( $user, $title );

		if ( $watchedItem ) {
			$expiry = $watchedItem->getExpiry();

			if ( $expiry !== null ) {
				return ApiResult::formatExpiry( $expiry );
			}
		}

		return null;
	}
}