Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.59% covered (success)
92.59%
50 / 54
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiWatchlistTrait
94.34% covered (success)
94.34%
50 / 53
83.33% covered (warning)
83.33%
5 / 6
23.10
0.00% covered (danger)
0.00%
0 / 1
 initServices
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 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;
35    private UserOptionsLookup $userOptionsLookup;
36
37    private function initServices() {
38        // @phan-suppress-next-line PhanRedundantCondition Phan trusts the type hints too much
39        if ( isset( $this->watchlistManager ) && isset( $this->userOptionsLookup ) ) {
40            return;
41        }
42        // This trait is used outside of core and therefor fallback to global state - T263904
43        $services = MediaWikiServices::getInstance();
44        $this->watchlistManager ??= $services->getWatchlistManager();
45        $this->userOptionsLookup ??= $services->getUserOptionsLookup();
46    }
47
48    /**
49     * Get additional allow params specific to watchlisting.
50     * This should be merged in with the result of self::getAllowedParams().
51     *
52     * This purposefully does not include the deprecated 'watch' and 'unwatch'
53     * parameters that some APIs still accept.
54     *
55     * @param string[] $watchOptions
56     * @return array
57     */
58    protected function getWatchlistParams( array $watchOptions = [] ): array {
59        if ( !$watchOptions ) {
60            $watchOptions = [
61                'watch',
62                'unwatch',
63                'preferences',
64                'nochange',
65            ];
66        }
67
68        $result = [
69            'watchlist' => [
70                ParamValidator::PARAM_DEFAULT => 'preferences',
71                ParamValidator::PARAM_TYPE => $watchOptions,
72            ],
73        ];
74
75        if ( $this->watchlistExpiryEnabled ) {
76            $result['watchlistexpiry'] = [
77                ParamValidator::PARAM_TYPE => 'expiry',
78                ExpiryDef::PARAM_MAX => $this->watchlistMaxDuration,
79                ExpiryDef::PARAM_USE_MAX => true,
80            ];
81        }
82
83        return $result;
84    }
85
86    /**
87     * Set a watch (or unwatch) based the based on a watchlist parameter.
88     * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
89     * @param PageIdentity $page The page to change
90     * @param User $user The user to set watch/unwatch for
91     * @param string|null $userOption The user option to consider when $watch=preferences
92     * @param string|null $expiry Optional expiry timestamp in any format acceptable to wfTimestamp(),
93     *   null will not create expiries, or leave them unchanged should they already exist.
94     */
95    protected function setWatch(
96        string $watch,
97        PageIdentity $page,
98        User $user,
99        ?string $userOption = null,
100        ?string $expiry = null
101    ): void {
102        $value = $this->getWatchlistValue( $watch, $page, $user, $userOption );
103        $this->watchlistManager->setWatch( $value, $user, $page, $expiry );
104    }
105
106    /**
107     * Return true if we're to watch the page, false if not.
108     * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
109     * @param PageIdentity $page The page under consideration
110     * @param User $user The user get the value for.
111     * @param string|null $userOption The user option to consider when $watchlist=preferences.
112     *    If not set will use watchdefault always and watchcreations if $page doesn't exist.
113     * @return bool
114     */
115    protected function getWatchlistValue(
116        string $watchlist,
117        PageIdentity $page,
118        User $user,
119        ?string $userOption = null
120    ): bool {
121        $this->initServices();
122        $userWatching = $this->watchlistManager->isWatchedIgnoringRights( $user, $page );
123
124        switch ( $watchlist ) {
125            case 'watch':
126                return true;
127
128            case 'unwatch':
129                return false;
130
131            case 'preferences':
132                // If the user is already watching, don't bother checking
133                if ( $userWatching ) {
134                    return true;
135                }
136                // If the user is a bot, act as 'nochange' to avoid big watchlists on single users
137                if ( $user->isBot() ) {
138                    return $userWatching;
139                }
140                // If no user option was passed, use watchdefault and watchcreations
141                if ( $userOption === null ) {
142                    return $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ||
143                        ( $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) && !$page->exists() );
144                }
145
146                // Watch the article based on the user preference
147                return $this->userOptionsLookup->getBoolOption( $user, $userOption );
148
149            // case 'nochange':
150            default:
151                return $userWatching;
152        }
153    }
154
155    /**
156     * Get formatted expiry from the given parameters, or null if no expiry was provided.
157     * @param array $params Request parameters passed to the API.
158     * @return string|null
159     */
160    protected function getExpiryFromParams( array $params ): ?string {
161        $watchlistExpiry = null;
162        if ( $this->watchlistExpiryEnabled && isset( $params['watchlistexpiry'] ) ) {
163            $watchlistExpiry = ApiResult::formatExpiry( $params['watchlistexpiry'] );
164        }
165
166        return $watchlistExpiry;
167    }
168
169    /**
170     * Get existing expiry from the database.
171     *
172     * @param WatchedItemStoreInterface $store
173     * @param PageIdentity $page
174     * @param UserIdentity $user The user to get the expiry for.
175     * @return string|null
176     */
177    protected function getWatchlistExpiry(
178        WatchedItemStoreInterface $store,
179        PageIdentity $page,
180        UserIdentity $user
181    ): ?string {
182        $watchedItem = $store->getWatchedItem( $user, $page );
183
184        if ( $watchedItem ) {
185            $expiry = $watchedItem->getExpiry();
186
187            if ( $expiry !== null ) {
188                return ApiResult::formatExpiry( $expiry );
189            }
190        }
191
192        return null;
193    }
194}
195
196/** @deprecated class alias since 1.43 */
197class_alias( ApiWatchlistTrait::class, 'ApiWatchlistTrait' );