Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.95% |
79 / 93 |
|
80.00% |
12 / 15 |
CRAP | |
0.00% |
0 / 1 |
WatchAction | |
85.87% |
79 / 92 |
|
80.00% |
12 / 15 |
44.51 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
requiresUnblock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDescription | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onSubmit | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
checkCanExecute | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
getRestriction | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
usesOOUI | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormFields | |
44.44% |
8 / 18 |
|
0.00% |
0 / 1 |
2.69 | |||
getExpiryOptions | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
getExpiryOptionsFromMessage | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
alterForm | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
onSuccess | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeSuccessMessage | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
9.02 | |||
doesWrites | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Performs the watch actions on a page |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License |
16 | * along with this program; if not, write to the Free Software |
17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
18 | * |
19 | * @file |
20 | * @ingroup Actions |
21 | */ |
22 | |
23 | namespace MediaWiki\Actions; |
24 | |
25 | use MediaWiki\Context\IContextSource; |
26 | use MediaWiki\Exception\PermissionsError; |
27 | use MediaWiki\Exception\ReadOnlyError; |
28 | use MediaWiki\Exception\UserBlockedError; |
29 | use MediaWiki\Exception\UserNotLoggedIn; |
30 | use MediaWiki\HTMLForm\HTMLForm; |
31 | use MediaWiki\MainConfigNames; |
32 | use MediaWiki\Page\Article; |
33 | use MediaWiki\Status\Status; |
34 | use MediaWiki\User\User; |
35 | use MediaWiki\User\UserOptionsLookup; |
36 | use MediaWiki\Watchlist\WatchedItem; |
37 | use MediaWiki\Watchlist\WatchedItemStoreInterface; |
38 | use MediaWiki\Watchlist\WatchlistManager; |
39 | use MediaWiki\Xml\XmlSelect; |
40 | use MessageLocalizer; |
41 | use Wikimedia\Message\MessageValue; |
42 | use Wikimedia\ParamValidator\TypeDef\ExpiryDef; |
43 | |
44 | /** |
45 | * Page addition to a user's watchlist |
46 | * |
47 | * @ingroup Actions |
48 | */ |
49 | class WatchAction extends FormAction { |
50 | |
51 | /** @var bool The value of the $wgWatchlistExpiry configuration variable. */ |
52 | protected $watchlistExpiry; |
53 | |
54 | /** @var string */ |
55 | protected $expiryFormFieldName = 'expiry'; |
56 | |
57 | /** @var false|WatchedItem */ |
58 | protected $watchedItem = false; |
59 | |
60 | private WatchlistManager $watchlistManager; |
61 | private UserOptionsLookup $userOptionsLookup; |
62 | |
63 | /** |
64 | * Only public since 1.21 |
65 | * |
66 | * @param Article $article |
67 | * @param IContextSource $context |
68 | * @param WatchlistManager $watchlistManager |
69 | * @param WatchedItemStoreInterface $watchedItemStore |
70 | * @param UserOptionsLookup $userOptionsLookup |
71 | */ |
72 | public function __construct( |
73 | Article $article, |
74 | IContextSource $context, |
75 | WatchlistManager $watchlistManager, |
76 | WatchedItemStoreInterface $watchedItemStore, |
77 | UserOptionsLookup $userOptionsLookup |
78 | ) { |
79 | parent::__construct( $article, $context ); |
80 | $this->watchlistExpiry = $this->getContext()->getConfig()->get( MainConfigNames::WatchlistExpiry ); |
81 | if ( $this->watchlistExpiry ) { |
82 | // The watchedItem is only used in this action's form if $wgWatchlistExpiry is enabled. |
83 | $this->watchedItem = $watchedItemStore->getWatchedItem( |
84 | $this->getUser(), |
85 | $this->getTitle() |
86 | ); |
87 | } |
88 | $this->watchlistManager = $watchlistManager; |
89 | $this->userOptionsLookup = $userOptionsLookup; |
90 | } |
91 | |
92 | /** @inheritDoc */ |
93 | public function getName() { |
94 | return 'watch'; |
95 | } |
96 | |
97 | /** @inheritDoc */ |
98 | public function requiresUnblock() { |
99 | return false; |
100 | } |
101 | |
102 | /** @inheritDoc */ |
103 | protected function getDescription() { |
104 | return ''; |
105 | } |
106 | |
107 | /** @inheritDoc */ |
108 | public function onSubmit( $data ) { |
109 | // Even though we're never unwatching here, use WatchlistManager::setWatch() |
110 | // because it also checks for changed expiry. |
111 | $result = $this->watchlistManager->setWatch( |
112 | true, |
113 | $this->getAuthority(), |
114 | $this->getTitle(), |
115 | $this->getRequest()->getVal( 'wp' . $this->expiryFormFieldName ) |
116 | ); |
117 | |
118 | return Status::wrap( $result ); |
119 | } |
120 | |
121 | /** |
122 | * @throws UserNotLoggedIn |
123 | * @throws PermissionsError |
124 | * @throws ReadOnlyError |
125 | * @throws UserBlockedError |
126 | */ |
127 | protected function checkCanExecute( User $user ) { |
128 | if ( !$user->isRegistered() |
129 | || ( $user->isTemp() && !$user->isAllowed( 'editmywatchlist' ) ) |
130 | ) { |
131 | throw new UserNotLoggedIn( 'watchlistanontext', 'watchnologin' ); |
132 | } |
133 | |
134 | parent::checkCanExecute( $user ); |
135 | } |
136 | |
137 | /** @inheritDoc */ |
138 | public function getRestriction() { |
139 | return 'editmywatchlist'; |
140 | } |
141 | |
142 | /** @inheritDoc */ |
143 | protected function usesOOUI() { |
144 | return true; |
145 | } |
146 | |
147 | /** @inheritDoc */ |
148 | protected function getFormFields() { |
149 | // If watchlist expiry is not enabled, return a simple confirmation message. |
150 | if ( !$this->watchlistExpiry ) { |
151 | return [ |
152 | 'intro' => [ |
153 | 'type' => 'info', |
154 | 'raw' => true, |
155 | 'default' => $this->msg( 'confirm-watch-top' )->parse(), |
156 | ], |
157 | ]; |
158 | } |
159 | |
160 | // Otherwise, use a select-list of expiries, where the default is the user's |
161 | // preferred expiry time (or the existing watch duration if already temporarily watched). |
162 | $default = $this->userOptionsLookup->getOption( $this->getUser(), 'watchstar-expiry' ); |
163 | $expiryOptions = static::getExpiryOptions( $this->getContext(), $this->watchedItem, $default ); |
164 | return [ |
165 | $this->expiryFormFieldName => [ |
166 | 'type' => 'select', |
167 | 'label-message' => 'confirm-watch-label', |
168 | 'options' => $expiryOptions['options'], |
169 | 'default' => $expiryOptions['default'], |
170 | ] |
171 | ]; |
172 | } |
173 | |
174 | /** |
175 | * Get options and default for a watchlist expiry select list. If an expiry time is provided, it |
176 | * will be added to the top of the list as 'x days left'. |
177 | * |
178 | * @since 1.35 |
179 | * @todo Move this somewhere better when it's being used in more than just this action. |
180 | * |
181 | * @param MessageLocalizer $msgLocalizer |
182 | * @param WatchedItem|false $watchedItem |
183 | * @param string $defaultExpiry The default expiry time to use if $watchedItem isn't already on a watchlist. |
184 | * @return mixed[] With keys `options` (string[]) and `default` (string). |
185 | */ |
186 | public static function getExpiryOptions( |
187 | MessageLocalizer $msgLocalizer, |
188 | $watchedItem, |
189 | string $defaultExpiry = 'infinite' |
190 | ) { |
191 | $expiryOptions = self::getExpiryOptionsFromMessage( $msgLocalizer ); |
192 | |
193 | if ( !in_array( $defaultExpiry, $expiryOptions ) ) { |
194 | $expiryOptions = array_merge( [ $defaultExpiry => $defaultExpiry ], $expiryOptions ); |
195 | } |
196 | |
197 | if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() ) { |
198 | // If it's already being temporarily watched, add the existing expiry as an option in the dropdown. |
199 | $currentExpiry = $watchedItem->getExpiry( TS_ISO_8601 ); |
200 | $daysLeft = $watchedItem->getExpiryInDaysText( $msgLocalizer, true ); |
201 | $expiryOptions = array_merge( [ $daysLeft => $currentExpiry ], $expiryOptions ); |
202 | |
203 | // Always preselect the existing expiry. |
204 | $defaultExpiry = $currentExpiry; |
205 | } |
206 | |
207 | return [ |
208 | 'options' => $expiryOptions, |
209 | 'default' => $defaultExpiry, |
210 | ]; |
211 | } |
212 | |
213 | /** |
214 | * Parse expiry options message. Fallback to english options |
215 | * if translated options are invalid or broken |
216 | * |
217 | * @param MessageLocalizer $msgLocalizer |
218 | * @param string|null $lang |
219 | * @return string[] |
220 | * @since 1.45 Method is public |
221 | */ |
222 | public static function getExpiryOptionsFromMessage( |
223 | MessageLocalizer $msgLocalizer, ?string $lang = null |
224 | ): array { |
225 | $expiryOptionsMsg = $msgLocalizer->msg( 'watchlist-expiry-options' ); |
226 | $optionsText = !$lang ? $expiryOptionsMsg->text() : $expiryOptionsMsg->inLanguage( $lang )->text(); |
227 | $options = XmlSelect::parseOptionsMessage( |
228 | $optionsText |
229 | ); |
230 | |
231 | $expiryOptions = []; |
232 | foreach ( $options as $label => $value ) { |
233 | if ( strtotime( $value ) || wfIsInfinity( $value ) ) { |
234 | $expiryOptions[$label] = $value; |
235 | } |
236 | } |
237 | |
238 | // If message options is invalid try to recover by returning |
239 | // english options (T267611) |
240 | if ( !$expiryOptions && $expiryOptionsMsg->getLanguage()->getCode() !== 'en' ) { |
241 | return self::getExpiryOptionsFromMessage( $msgLocalizer, 'en' ); |
242 | } |
243 | |
244 | return $expiryOptions; |
245 | } |
246 | |
247 | protected function alterForm( HTMLForm $form ) { |
248 | $msg = $this->watchlistExpiry && $this->watchedItem ? 'updatewatchlist' : 'addwatch'; |
249 | $form->setWrapperLegendMsg( $msg ); |
250 | $submitMsg = $this->watchlistExpiry ? 'confirm-watch-button-expiry' : 'confirm-watch-button'; |
251 | $form->setSubmitTextMsg( $submitMsg ); |
252 | $form->setTokenSalt( 'watch' ); |
253 | } |
254 | |
255 | /** |
256 | * Show one of 8 possible success messages. |
257 | * The messages are: |
258 | * 1. addedwatchtext |
259 | * 2. addedwatchtext-talk |
260 | * 3. addedwatchindefinitelytext |
261 | * 4. addedwatchindefinitelytext-talk |
262 | * 5. addedwatchexpirytext |
263 | * 6. addedwatchexpirytext-talk |
264 | * 7. addedwatchexpiryhours |
265 | * 8. addedwatchexpiryhours-talk |
266 | */ |
267 | public function onSuccess() { |
268 | $submittedExpiry = $this->getContext()->getRequest()->getText( 'wp' . $this->expiryFormFieldName ); |
269 | $this->getOutput()->addWikiMsg( $this->makeSuccessMessage( $submittedExpiry ) ); |
270 | } |
271 | |
272 | protected function makeSuccessMessage( string $submittedExpiry ): MessageValue { |
273 | $msgKey = $this->getTitle()->isTalkPage() ? 'addedwatchtext-talk' : 'addedwatchtext'; |
274 | $params = []; |
275 | if ( $submittedExpiry ) { |
276 | // We can't use $this->watchedItem to get the expiry because it's not been saved at this |
277 | // point in the request and so its values are those from before saving. |
278 | $expiry = ExpiryDef::normalizeExpiry( $submittedExpiry, TS_ISO_8601 ); |
279 | |
280 | // If the expiry label isn't one of the predefined ones in the dropdown, calculate 'x days'. |
281 | $expiryDays = WatchedItem::calculateExpiryInDays( $expiry ); |
282 | $defaultLabels = static::getExpiryOptionsFromMessage( $this->getContext() ); |
283 | $localizedExpiry = array_search( $submittedExpiry, $defaultLabels ); |
284 | |
285 | // Determine which message to use, depending on whether this is a talk page or not |
286 | // and whether an expiry was selected. |
287 | $isTalk = $this->getTitle()->isTalkPage(); |
288 | if ( wfIsInfinity( $expiry ) ) { |
289 | $msgKey = $isTalk ? 'addedwatchindefinitelytext-talk' : 'addedwatchindefinitelytext'; |
290 | } elseif ( $expiryDays >= 1 ) { |
291 | $msgKey = $isTalk ? 'addedwatchexpirytext-talk' : 'addedwatchexpirytext'; |
292 | $params[] = $localizedExpiry === false |
293 | ? $this->getContext()->msg( 'days', $expiryDays )->text() |
294 | : $localizedExpiry; |
295 | } else { |
296 | // Less than one day. |
297 | $msgKey = $isTalk ? 'addedwatchexpiryhours-talk' : 'addedwatchexpiryhours'; |
298 | } |
299 | } |
300 | return MessageValue::new( $msgKey )->params( $this->getTitle()->getPrefixedText(), ...$params ); |
301 | } |
302 | |
303 | /** @inheritDoc */ |
304 | public function doesWrites() { |
305 | return true; |
306 | } |
307 | } |
308 | |
309 | /** @deprecated class alias since 1.44 */ |
310 | class_alias( WatchAction::class, 'WatchAction' ); |