Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.34% covered (success)
94.34%
50 / 53
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
3use MediaWiki\MediaWikiServices;
4use MediaWiki\Title\Title;
5use MediaWiki\User\Options\UserOptionsLookup;
6use MediaWiki\User\User;
7use MediaWiki\User\UserIdentity;
8use MediaWiki\Watchlist\WatchlistManager;
9use Wikimedia\ParamValidator\ParamValidator;
10use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
11
12/**
13 * An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to
14 * watch a page. This should ONLY be used in API modules that extend ApiBase.
15 * Also, it should not be used in ApiWatch, which has its own special handling.
16 *
17 * Note the class-level properties watchlistExpiryEnabled and watchlistMaxDuration must still be
18 * set in the API module's constructor.
19 *
20 * @ingroup API
21 * @since 1.35
22 */
23trait ApiWatchlistTrait {
24
25    /** @var bool Whether watchlist expiries are enabled. */
26    private $watchlistExpiryEnabled;
27
28    /** @var string Relative maximum expiry. */
29    private $watchlistMaxDuration;
30
31    private WatchlistManager $watchlistManager;
32    private UserOptionsLookup $userOptionsLookup;
33
34    private function initServices() {
35        // @phan-suppress-next-line PhanRedundantCondition Phan trusts the type hints too much
36        if ( isset( $this->watchlistManager ) && isset( $this->userOptionsLookup ) ) {
37            return;
38        }
39        // This trait is used outside of core and therefor fallback to global state - T263904
40        $services = MediaWikiServices::getInstance();
41        $this->watchlistManager ??= $services->getWatchlistManager();
42        $this->userOptionsLookup ??= $services->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 Title $title The article's title 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        Title $title,
95        User $user,
96        ?string $userOption = null,
97        ?string $expiry = null
98    ): void {
99        $value = $this->getWatchlistValue( $watch, $title, $user, $userOption );
100        $this->watchlistManager->setWatch( $value, $user, $title, $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 Title $title 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 $title doesn't exist.
110     * @return bool
111     */
112    protected function getWatchlistValue(
113        string $watchlist,
114        Title $title,
115        User $user,
116        ?string $userOption = null
117    ): bool {
118        $this->initServices();
119        $userWatching = $this->watchlistManager->isWatchedIgnoringRights( $user, $title );
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' ) && !$title->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, or null if no expiry was provided.
154     * @param array $params Request parameters passed to the API.
155     * @return string|null
156     */
157    protected function getExpiryFromParams( array $params ): ?string {
158        $watchlistExpiry = null;
159        if ( $this->watchlistExpiryEnabled && isset( $params['watchlistexpiry'] ) ) {
160            $watchlistExpiry = ApiResult::formatExpiry( $params['watchlistexpiry'] );
161        }
162
163        return $watchlistExpiry;
164    }
165
166    /**
167     * Get existing expiry from the database.
168     *
169     * @param WatchedItemStoreInterface $store
170     * @param Title $title
171     * @param UserIdentity $user The user to get the expiry for.
172     * @return string|null
173     */
174    protected function getWatchlistExpiry(
175        WatchedItemStoreInterface $store,
176        Title $title,
177        UserIdentity $user
178    ): ?string {
179        $watchedItem = $store->getWatchedItem( $user, $title );
180
181        if ( $watchedItem ) {
182            $expiry = $watchedItem->getExpiry();
183
184            if ( $expiry !== null ) {
185                return ApiResult::formatExpiry( $expiry );
186            }
187        }
188
189        return null;
190    }
191}