Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.28% covered (success)
98.28%
57 / 58
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiWatchlistTrait
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
6 / 6
26
100.00% covered (success)
100.00%
1 / 1
 initServices
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getWatchlistParams
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 setWatch
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWatchlistValue
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 getExpiryFromParams
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 getWatchlistExpiry
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Api;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Page\PageIdentity;
7use MediaWiki\User\Options\UserOptionsLookup;
8use MediaWiki\User\User;
9use MediaWiki\User\UserIdentity;
10use MediaWiki\Watchlist\WatchedItemStoreInterface;
11use MediaWiki\Watchlist\WatchlistManager;
12use Wikimedia\ParamValidator\ParamValidator;
13use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
14
15/**
16 * An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to
17 * watch a page. This should ONLY be used in API modules that extend ApiBase.
18 * Also, it should not be used in ApiWatch, which has its own special handling.
19 *
20 * Note the class-level properties watchlistExpiryEnabled and watchlistMaxDuration must still be
21 * set in the API module's constructor.
22 *
23 * @ingroup API
24 * @since 1.35
25 */
26trait ApiWatchlistTrait {
27
28    /** @var bool Whether watchlist expiries are enabled. */
29    private $watchlistExpiryEnabled;
30
31    /** @var string Relative maximum expiry. */
32    private $watchlistMaxDuration;
33
34    private ?WatchlistManager $watchlistManager = null;
35    private WatchedItemStoreInterface $watchedItemStore;
36    private ?UserOptionsLookup $userOptionsLookup = null;
37
38    private function initServices() {
39        // This trait is used outside of core and therefor fallback to global state - T263904
40        $this->watchlistManager ??= MediaWikiServices::getInstance()->getWatchlistManager();
41        $this->watchedItemStore ??= MediaWikiServices::getInstance()->getWatchedItemStore();
42        $this->userOptionsLookup ??= MediaWikiServices::getInstance()->getUserOptionsLookup();
43    }
44
45    /**
46     * Get additional allow params specific to watchlisting.
47     * This should be merged in with the result of self::getAllowedParams().
48     *
49     * This purposefully does not include the deprecated 'watch' and 'unwatch'
50     * parameters that some APIs still accept.
51     *
52     * @param string[] $watchOptions
53     * @return array
54     */
55    protected function getWatchlistParams( array $watchOptions = [] ): array {
56        if ( !$watchOptions ) {
57            $watchOptions = [
58                'watch',
59                'unwatch',
60                'preferences',
61                'nochange',
62            ];
63        }
64
65        $result = [
66            'watchlist' => [
67                ParamValidator::PARAM_DEFAULT => 'preferences',
68                ParamValidator::PARAM_TYPE => $watchOptions,
69            ],
70        ];
71
72        if ( $this->watchlistExpiryEnabled ) {
73            $result['watchlistexpiry'] = [
74                ParamValidator::PARAM_TYPE => 'expiry',
75                ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
76                ExpiryDef::PARAM_USE_MAX => true,
77            ];
78        }
79
80        return $result;
81    }
82
83    /**
84     * Set a watch (or unwatch) based the based on a watchlist parameter.
85     * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
86     * @param PageIdentity $page The page to change
87     * @param User $user The user to set watch/unwatch for
88     * @param string|null $userOption The user option to consider when $watch=preferences
89     * @param string|null $expiry Optional expiry timestamp in any format acceptable to wfTimestamp(),
90     *   null will not create expiries, or leave them unchanged should they already exist.
91     */
92    protected function setWatch(
93        string $watch,
94        PageIdentity $page,
95        User $user,
96        ?string $userOption = null,
97        ?string $expiry = null
98    ): void {
99        $value = $this->getWatchlistValue( $watch, $page, $user, $userOption );
100        $this->watchlistManager->setWatch( $value, $user, $page, $expiry );
101    }
102
103    /**
104     * Return true if we're to watch the page, false if not.
105     * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
106     * @param PageIdentity $page The page under consideration
107     * @param User $user The user get the value for.
108     * @param string|null $userOption The user option to consider when $watchlist=preferences.
109     *    If not set will use watchdefault always and watchcreations if $page doesn't exist.
110     * @return bool
111     */
112    protected function getWatchlistValue(
113        string $watchlist,
114        PageIdentity $page,
115        User $user,
116        ?string $userOption = null
117    ): bool {
118        $this->initServices();
119        $userWatching = $this->watchlistManager->isWatchedIgnoringRights( $user, $page );
120
121        switch ( $watchlist ) {
122            case 'watch':
123                return true;
124
125            case 'unwatch':
126                return false;
127
128            case 'preferences':
129                // If the user is already watching, don't bother checking
130                if ( $userWatching ) {
131                    return true;
132                }
133                // If the user is a bot, act as 'nochange' to avoid big watchlists on single users
134                if ( $user->isBot() ) {
135                    return $userWatching;
136                }
137                // If no user option was passed, use watchdefault and watchcreations
138                if ( $userOption === null ) {
139                    return $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ||
140                        ( $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) && !$page->exists() );
141                }
142
143                // Watch the article based on the user preference
144                return $this->userOptionsLookup->getBoolOption( $user, $userOption );
145
146            // case 'nochange':
147            default:
148                return $userWatching;
149        }
150    }
151
152    /**
153     * Get formatted expiry from the given parameters. If no expiry was provided,
154     * return the current expiry if the user is watching the page, or check
155     * against user-preferred expiry for this action if they are not watching it.
156     *
157     * @param array $params Request parameters passed to the API.
158     * @param ?PageIdentity $page
159     * @param ?UserIdentity $user
160     * @param string $userOption The name of the watchlist preference for this action.
161     * @return string|null
162     */
163    protected function getExpiryFromParams(
164        array $params,
165        ?PageIdentity $page = null,
166        ?UserIdentity $user = null,
167        string $userOption = 'watchdefault-expiry'
168    ): ?string {
169        $watchlistExpiry = null;
170        if ( $this->watchlistExpiryEnabled ) {
171            // At this point, the ParamValidator has already normalized $params['watchlistexpiry'].
172            // Use the value provided in the API call, if any.
173            $watchlistExpiry = $params['watchlistexpiry'] ?? null;
174            // If expiry was not provided, and the user is already watching the page, keep the expiry value.
175            if ( $user && $page && $watchlistExpiry === null && $this->watchedItemStore->isWatched( $user, $page ) ) {
176                return $this->getWatchlistExpiry( $this->watchedItemStore, $page, $user );
177            }
178            // If the user is not watching the page, and expiry was not provided, use the default.
179            if ( $user && $watchlistExpiry === null ) {
180                $watchlistExpiry = ExpiryDef::normalizeExpiry(
181                    $this->userOptionsLookup->getOption( $user, $userOption )
182                );
183            }
184        }
185
186        return ApiResult::formatExpiry( $watchlistExpiry );
187    }
188
189    /**
190     * Get existing expiry from the database.
191     *
192     * @param WatchedItemStoreInterface $store
193     * @param PageIdentity $page
194     * @param UserIdentity $user The user to get the expiry for.
195     * @return string|null
196     */
197    protected function getWatchlistExpiry(
198        WatchedItemStoreInterface $store,
199        PageIdentity $page,
200        UserIdentity $user
201    ): ?string {
202        $watchedItem = $store->getWatchedItem( $user, $page );
203
204        if ( $watchedItem ) {
205            $expiry = $watchedItem->getExpiry();
206
207            if ( $expiry !== null ) {
208                return ApiResult::formatExpiry( $expiry );
209            }
210        }
211
212        return null;
213    }
214}
215
216/** @deprecated class alias since 1.43 */
217class_alias( ApiWatchlistTrait::class, 'ApiWatchlistTrait' );