MediaWiki master
WatchAction.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Actions;
11
33use Wikimedia\Timestamp\TimestampFormat as TS;
34
40class WatchAction extends FormAction {
41
43 protected readonly bool $watchlistExpiry;
44
45 private readonly bool $enableWatchlistLabels;
46
48 protected $expiryFormFieldName = 'expiry';
49
51 protected $watchedItem = false;
52
56 public function __construct(
57 Article $article,
58 IContextSource $context,
59 private readonly WatchlistManager $watchlistManager,
60 private readonly WatchedItemStoreInterface $watchedItemStore,
61 protected readonly WatchlistLabelStore $watchlistLabelStore,
62 private readonly UserOptionsLookup $userOptionsLookup,
63 ) {
64 parent::__construct( $article, $context );
65 $this->watchlistExpiry = $this->getContext()->getConfig()->get( MainConfigNames::WatchlistExpiry );
66 $this->enableWatchlistLabels = $this->getContext()->getConfig()->get( MainConfigNames::EnableWatchlistLabels );
67 if ( $this->watchlistExpiry || $this->enableWatchlistLabels ) {
68 // The watchedItem is only used in this action's form if $wgWatchlistExpiry is enabled.
69 $this->watchedItem = $watchedItemStore->getWatchedItem(
70 $this->getUser(),
71 $this->getTitle()
72 );
73 }
74 }
75
77 public function getName() {
78 return 'watch';
79 }
80
82 public function requiresUnblock() {
83 return false;
84 }
85
87 protected function getDescription() {
88 return '';
89 }
90
92 public function onSubmit( $data ) {
93 // Even though we're never unwatching here, use WatchlistManager::setWatch()
94 // because it also checks for changed expiry.
95 $result = $this->watchlistManager->setWatch(
96 true,
97 $this->getAuthority(),
98 $this->getTitle(),
99 $this->getRequest()->getVal( 'wp' . $this->expiryFormFieldName ),
100 $this->getRequest()->getArray( 'wplabels', [] )
101 );
102
103 return Status::wrap( $result );
104 }
105
112 protected function checkCanExecute( User $user ) {
113 if ( !$user->isRegistered()
114 || ( $user->isTemp() && !$user->isAllowed( 'editmywatchlist' ) )
115 ) {
116 throw new UserNotLoggedIn( 'watchlistanontext', 'watchnologin' );
117 }
118
119 parent::checkCanExecute( $user );
120 }
121
123 public function getRestriction() {
124 return 'editmywatchlist';
125 }
126
128 protected function usesOOUI() {
129 return true;
130 }
131
133 protected function getFormFields() {
134 // If neither expiries or labels are enabled, return a simple confirmation message.
135 if ( !$this->watchlistExpiry && !$this->enableWatchlistLabels ) {
136 return [
137 'intro' => [
138 'type' => 'info',
139 'raw' => true,
140 'default' => $this->msg( 'confirm-watch-top' )->parse(),
141 ],
142 ];
143 }
144
145 $fields = [];
146
147 // Use a select-list of expiries, where the default is the user's
148 // preferred expiry time (or the existing watch duration if already temporarily watched).
149 if ( $this->watchlistExpiry ) {
150 $default = $this->userOptionsLookup->getOption( $this->getUser(), 'watchstar-expiry' );
151 $expiryOptions = static::getExpiryOptions( $this->getContext(), $this->watchedItem, $default );
153 'type' => 'select',
154 'label-message' => 'confirm-watch-label',
155 'options' => $expiryOptions['options'],
156 'default' => $expiryOptions['default'],
157 ];
158 }
159
160 // Show all of a user's labels as checkboxes.
161 if ( $this->enableWatchlistLabels ) {
162 $options = [];
163 foreach ( $this->watchlistLabelStore->loadAllForUser( $this->getUser() ) as $label ) {
164 $options[ htmlspecialchars( $label->getName() ) ] = $label->getId();
165 }
166 $default = $this->watchedItem instanceof WatchedItem
167 ? array_map( static fn ( WatchlistLabel $l ) => $l->getId(), $this->watchedItem->getLabels() )
168 : [];
169 $fields[ 'labels' ] = [
170 'label-message' => 'watchlistlabels-watchaction-label',
171 'type' => 'multiselect',
172 'options' => $options,
173 'default' => $default,
174 ];
175 }
176
177 return $fields;
178 }
179
192 public static function getExpiryOptions(
193 MessageLocalizer $msgLocalizer,
195 string $defaultExpiry = 'infinite'
196 ) {
197 $expiryOptions = self::getExpiryOptionsFromMessage( $msgLocalizer );
198
199 if ( !in_array( $defaultExpiry, $expiryOptions ) ) {
200 $expiryOptions = array_merge( [ $defaultExpiry => $defaultExpiry ], $expiryOptions );
201 }
202
203 if ( $watchedItem instanceof WatchedItem && $watchedItem->getExpiry() ) {
204 // If it's already being temporarily watched, add the existing expiry as an option in the dropdown.
205 $currentExpiry = $watchedItem->getExpiry( TS::ISO_8601 );
206 $daysLeft = $watchedItem->getExpiryInDaysText( $msgLocalizer, true );
207 $expiryOptions = array_merge( [ $daysLeft => $currentExpiry ], $expiryOptions );
208
209 // Always preselect the existing expiry.
210 $defaultExpiry = $currentExpiry;
211 }
212
213 return [
214 'options' => $expiryOptions,
215 'default' => $defaultExpiry,
216 ];
217 }
218
228 public static function getExpiryOptionsFromMessage(
229 MessageLocalizer $msgLocalizer, ?string $lang = null
230 ): array {
231 $expiryOptionsMsg = $msgLocalizer->msg( 'watchlist-expiry-options' );
232 $optionsText = !$lang ? $expiryOptionsMsg->text() : $expiryOptionsMsg->inLanguage( $lang )->text();
234 $optionsText
235 );
236
237 $expiryOptions = [];
238 foreach ( $options as $label => $value ) {
239 if ( strtotime( $value ) || wfIsInfinity( $value ) ) {
240 $expiryOptions[$label] = $value;
241 }
242 }
243
244 // If message options is invalid try to recover by returning
245 // english options (T267611)
246 if ( !$expiryOptions && $expiryOptionsMsg->getLanguage()->getCode() !== 'en' ) {
247 return self::getExpiryOptionsFromMessage( $msgLocalizer, 'en' );
248 }
249
250 return $expiryOptions;
251 }
252
253 protected function alterForm( HTMLForm $form ) {
254 $msg = $this->watchlistExpiry && $this->watchedItem ? 'updatewatchlist' : 'addwatch';
255 $form->setWrapperLegendMsg( $msg );
256 $submitMsg = $this->watchlistExpiry ? 'confirm-watch-button-expiry' : 'confirm-watch-button';
257 $form->setSubmitTextMsg( $submitMsg );
258 $form->setTokenSalt( 'watch' );
259 }
260
273 public function onSuccess() {
274 // Add success message for watching and (optionally) expiry.
275 $submittedExpiry = $this->getContext()->getRequest()->getText( 'wp' . $this->expiryFormFieldName );
276 $this->getOutput()->addWikiMsg( $this->makeSuccessMessage( $submittedExpiry ) );
277 // Also add a line for labels if any were saved.
278 $labelIds = $this->getRequest()->getArray( 'wplabels' );
279 if ( $labelIds ) {
280 $this->getOutput()->addWikiMsg(
281 'watchlistlabels-watchaction-success',
282 count( $labelIds ),
283 SpecialPage::getTitleFor( 'WatchlistLabels' )->getFullText()
284 );
285 }
286 }
287
288 protected function makeSuccessMessage( string $submittedExpiry ): MessageValue {
289 $msgKey = $this->getTitle()->isTalkPage() ? 'addedwatchtext-talk' : 'addedwatchtext';
290 $params = [];
291 if ( $submittedExpiry ) {
292 // We can't use $this->watchedItem to get the expiry because it's not been saved at this
293 // point in the request and so its values are those from before saving.
294 $expiry = ExpiryDef::normalizeExpiry( $submittedExpiry, TS::ISO_8601 );
295
296 // If the expiry label isn't one of the predefined ones in the dropdown, calculate 'x days'.
297 $expiryDays = WatchedItem::calculateExpiryInDays( $expiry );
298 $defaultLabels = static::getExpiryOptionsFromMessage( $this->getContext() );
299 $localizedExpiry = array_search( $submittedExpiry, $defaultLabels );
300
301 // Determine which message to use, depending on whether this is a talk page or not
302 // and whether an expiry was selected.
303 $isTalk = $this->getTitle()->isTalkPage();
304 if ( wfIsInfinity( $expiry ) ) {
305 $msgKey = $isTalk ? 'addedwatchindefinitelytext-talk' : 'addedwatchindefinitelytext';
306 } elseif ( $expiryDays >= 1 ) {
307 $msgKey = $isTalk ? 'addedwatchexpirytext-talk' : 'addedwatchexpirytext';
308 $params[] = $localizedExpiry === false
309 ? $this->getContext()->msg( 'days', $expiryDays )->text()
310 : $localizedExpiry;
311 } else {
312 // Less than one day.
313 $msgKey = $isTalk ? 'addedwatchexpiryhours-talk' : 'addedwatchexpiryhours';
314 }
315 }
316 return MessageValue::new( $msgKey )->params( $this->getTitle()->getPrefixedText(), ...$params );
317 }
318
320 public function doesWrites() {
321 return true;
322 }
323}
324
326class_alias( WatchAction::class, 'WatchAction' );
wfIsInfinity( $str)
Determine input string is represents as infinity.
getContext()
Get the IContextSource in use here.
Definition Action.php:102
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:132
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition Action.php:205
getTitle()
Shortcut to get the Title object from the page.
Definition Action.php:191
getRequest()
Get the WebRequest being used for this instance.
Definition Action.php:112
array $fields
The fields used to create the HTMLForm.
Definition Action.php:59
getAuthority()
Shortcut to get the Authority executing this instance.
Definition Action.php:142
An action which shows a form and does something based on the input from the form.
Page addition to a user's watchlist.
getName()
Return the name of the action this object responds to.1.17string Lowercase name
getRestriction()
Get the permission required to perform this action.Often, but not always, the same as the action name...
makeSuccessMessage(string $submittedExpiry)
__construct(Article $article, IContextSource $context, private readonly WatchlistManager $watchlistManager, private readonly WatchedItemStoreInterface $watchedItemStore, protected readonly WatchlistLabelStore $watchlistLabelStore, private readonly UserOptionsLookup $userOptionsLookup,)
Only public since 1.21.
onSubmit( $data)
Process the form on POST submission.If you don't want to do anything with the form,...
static getExpiryOptionsFromMessage(MessageLocalizer $msgLocalizer, ?string $lang=null)
Parse expiry options message.
readonly bool bool $watchlistExpiry
The value of the $wgWatchlistExpiry configuration variable.
onSuccess()
Show one of 8 possible success messages.
getFormFields()
Get an HTMLForm descriptor array.to override array
static getExpiryOptions(MessageLocalizer $msgLocalizer, $watchedItem, string $defaultExpiry='infinite')
Get options and default for a watchlist expiry select list.
usesOOUI()
Whether the form should use OOUI.to override bool
getDescription()
Returns the description that goes below the <h1> element.1.17 to override string HTML
requiresUnblock()
Whether this action can still be executed by a blocked user.Implementations of this methods must alwa...
alterForm(HTMLForm $form)
Play with the HTMLForm if you need to more substantially.
false WatchedItem $watchedItem
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Show an error when the user tries to do something whilst blocked.
Redirect a user to the login page or account creation page.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:214
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element.
setTokenSalt( $salt)
Set the salt for the edit token.
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
A class containing constants representing the names of configuration variables.
const EnableWatchlistLabels
Name constant for the EnableWatchlistLabels setting, for use with Config::get()
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:66
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:130
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2150
isTemp()
Is the user an autocreated temporary user?
Definition User.php:3419
isRegistered()
Get whether the user is registered.
Definition User.php:2091
Representation of a pair of user and title for watchlist entries.
static calculateExpiryInDays(?string $expiry)
Get the number of days remaining until the given expiry time.
getExpiry(int|TS|null $style=TS::MW)
When the watched item will expire.
getExpiryInDaysText(MessageLocalizer $msgLocalizer, $isDropdownOption=false)
Get days remaining until a watched item expires as a text.
Service class for storage of watchlist labels.
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:16
static parseOptionsMessage(string $msg)
Parse labels and values out of a comma- and colon-separated list of options, such as is used for expi...
Value object representing a message for i18n.
static new(string $key, array $params=[])
Static constructor for easier chaining of ->params() methods.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
Interface for objects which can provide a MediaWiki context on request.
Interface for localizing messages in MediaWiki.
getWatchedItem(UserIdentity $user, PageReference $target)
Get an item (may be cached)