MediaWiki  master
SpecialEditTags.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Specials;
23 
24 use ChangeTagsList;
25 use ErrorPageError;
26 use LogEventsList;
27 use LogPage;
36 use RevisionDeleter;
38 use Xml;
39 use XmlSelect;
40 
50  protected $wasSaved = false;
51 
53  private $submitClicked;
54 
56  private $ids;
57 
59  private $targetObj;
60 
62  private $typeName;
63 
65  private $revList;
66 
68  private $reason;
69 
70  private PermissionManager $permissionManager;
71  private ChangeTagsStore $changeTagsStore;
72 
78  public function __construct( PermissionManager $permissionManager, ChangeTagsStore $changeTagsStore ) {
79  parent::__construct( 'EditTags', 'changetags' );
80 
81  $this->permissionManager = $permissionManager;
82  $this->changeTagsStore = $changeTagsStore;
83  }
84 
85  public function doesWrites() {
86  return true;
87  }
88 
89  public function execute( $par ) {
90  $this->checkPermissions();
91  $this->checkReadOnly();
92 
93  $output = $this->getOutput();
94  $user = $this->getUser();
95  $request = $this->getRequest();
96 
97  $this->setHeaders();
98  $this->outputHeader();
99 
100  $output->addModules( [ 'mediawiki.misc-authed-curate' ] );
101  $output->addModuleStyles( [
102  'mediawiki.interface.helpers.styles',
103  'mediawiki.special'
104  ] );
105 
106  $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
107 
108  // Handle our many different possible input types
109  $ids = $request->getVal( 'ids' );
110  if ( $ids !== null ) {
111  // Allow CSV from the form hidden field, or a single ID for show/hide links
112  $this->ids = explode( ',', $ids );
113  } else {
114  // Array input
115  $this->ids = array_keys( $request->getArray( 'ids', [] ) );
116  }
117  $this->ids = array_unique( array_filter( $this->ids ) );
118 
119  // No targets?
120  if ( count( $this->ids ) == 0 ) {
121  throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
122  }
123 
124  $this->typeName = $request->getVal( 'type' );
125  $this->targetObj = Title::newFromText( $request->getText( 'target' ) );
126 
127  switch ( $this->typeName ) {
128  case 'logentry':
129  case 'logging':
130  $this->typeName = 'logentry';
131  break;
132  default:
133  $this->typeName = 'revision';
134  break;
135  }
136 
137  // Allow the list type to adjust the passed target
138  // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
139  // what we want
140  $this->targetObj = RevisionDeleter::suggestTarget(
141  $this->typeName === 'revision' ? 'revision' : 'logging',
142  $this->targetObj,
143  $this->ids
144  );
145 
146  $this->reason = $request->getVal( 'wpReason', '' );
147  // We need a target page!
148  if ( $this->targetObj === null ) {
149  $output->addWikiMsg( 'undelete-header' );
150  return;
151  }
152 
153  // Check blocks
154  $checkReplica = !$this->submitClicked;
155  if (
156  $this->permissionManager->isBlockedFrom(
157  $user,
158  $this->targetObj,
159  $checkReplica
160  )
161  ) {
162  throw new UserBlockedError(
163  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
164  $user->getBlock(),
165  $user,
166  $this->getLanguage(),
167  $request->getIP()
168  );
169  }
170 
171  // Give a link to the logs/hist for this page
172  $this->showConvenienceLinks();
173 
174  // Either submit or create our form
175  if ( $this->submitClicked ) {
176  $this->submit();
177  } else {
178  $this->showForm();
179  }
180 
181  // Show relevant lines from the tag log
182  $tagLogPage = new LogPage( 'tag' );
183  $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
185  $output,
186  'tag',
187  $this->targetObj,
188  '', /* user */
189  [ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
190  );
191  }
192 
196  protected function showConvenienceLinks() {
197  // Give a link to the logs/hist for this page
198  if ( $this->targetObj ) {
199  // Also set header tabs to be for the target.
200  $this->getSkin()->setRelevantTitle( $this->targetObj );
201 
202  $linkRenderer = $this->getLinkRenderer();
203  $links = [];
204  $links[] = $linkRenderer->makeKnownLink(
205  SpecialPage::getTitleFor( 'Log' ),
206  $this->msg( 'viewpagelogs' )->text(),
207  [],
208  [
209  'page' => $this->targetObj->getPrefixedText(),
210  'wpfilters' => [ 'tag' ],
211  ]
212  );
213  if ( !$this->targetObj->isSpecialPage() ) {
214  // Give a link to the page history
215  $links[] = $linkRenderer->makeKnownLink(
216  $this->targetObj,
217  $this->msg( 'pagehist' )->text(),
218  [],
219  [ 'action' => 'history' ]
220  );
221  }
222  // Link to Special:Tags
223  $links[] = $linkRenderer->makeKnownLink(
224  SpecialPage::getTitleFor( 'Tags' ),
225  $this->msg( 'tags-edit-manage-link' )->text()
226  );
227  // Logs themselves don't have histories or archived revisions
228  $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
229  }
230  }
231 
236  protected function getList() {
237  if ( $this->revList === null ) {
238  $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
239  $this->targetObj, $this->ids );
240  }
241 
242  return $this->revList;
243  }
244 
249  protected function showForm() {
250  $out = $this->getOutput();
251  // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
252  $out->wrapWikiMsg( "<strong>$1</strong>", [
253  "tags-edit-{$this->typeName}-selected",
254  $this->getLanguage()->formatNum( count( $this->ids ) ),
255  $this->targetObj->getPrefixedText()
256  ] );
257 
258  $this->addHelpLink( 'Help:Tags' );
259  $out->addHTML( "<ul>" );
260 
261  $numRevisions = 0;
262  // Live revisions...
263  $list = $this->getList();
264  for ( $list->reset(); $list->current(); $list->next() ) {
265  $item = $list->current();
266  if ( !$item->canView() ) {
267  throw new ErrorPageError( 'permissionserrors', 'tags-update-no-permission' );
268  }
269  $numRevisions++;
270  $out->addHTML( $item->getHTML() );
271  }
272 
273  if ( !$numRevisions ) {
274  throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
275  }
276 
277  $out->addHTML( "</ul>" );
278  // Explanation text
279  $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
280 
281  // Show form
282  $form = Xml::openElement( 'form', [ 'method' => 'post',
283  'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
284  'id' => 'mw-revdel-form-revisions' ] ) .
285  Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend",
286  count( $this->ids ) )->text() ) .
287  $this->buildCheckBoxes() .
288  Xml::openElement( 'table' ) .
289  "<tr>\n" .
290  '<td class="mw-label">' .
291  Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
292  '</td>' .
293  '<td class="mw-input">' .
294  Xml::input( 'wpReason', 60, $this->reason, [
295  'id' => 'wpReason',
296  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
297  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
298  // Unicode codepoints.
300  ] ) .
301  '</td>' .
302  "</tr><tr>\n" .
303  '<td></td>' .
304  '<td class="mw-submit">' .
305  Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
306  $numRevisions )->text(), [ 'name' => 'wpSubmit' ] ) .
307  '</td>' .
308  "</tr>\n" .
309  Xml::closeElement( 'table' ) .
310  Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
311  Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
312  Html::hidden( 'type', $this->typeName ) .
313  Html::hidden( 'ids', implode( ',', $this->ids ) ) .
314  Xml::closeElement( 'fieldset' ) . "\n" .
315  Xml::closeElement( 'form' ) . "\n";
316 
317  $out->addHTML( $form );
318  }
319 
323  protected function buildCheckBoxes() {
324  // If there is just one item, provide the user with a multi-select field
325  $list = $this->getList();
326  $tags = [];
327  if ( $list->length() == 1 ) {
328  $list->reset();
329  $tags = $list->current()->getTags();
330  if ( $tags ) {
331  $tags = explode( ',', $tags );
332  } else {
333  $tags = [];
334  }
335 
336  $html = '<table id="mw-edittags-tags-selector">';
337  $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
338  '</td><td>';
339  if ( $tags ) {
340  $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
341  } else {
342  $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
343  }
344  $html .= '</td></tr>';
345  $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
346  $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
347  } else {
348  // Otherwise, use a multi-select field for adding tags, and a list of
349  // checkboxes for removing them
350 
351  for ( $list->reset(); $list->current(); $list->next() ) {
352  $currentTags = $list->current()->getTags();
353  if ( $currentTags ) {
354  $tags = array_merge( $tags, explode( ',', $currentTags ) );
355  }
356  }
357  $tags = array_unique( $tags );
358 
359  $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
360  $tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
361  $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
362  $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
363  $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
364  'wpRemoveAllTags', 'mw-edittags-remove-all' );
365  $i = 0; // used for generating checkbox IDs only
366  foreach ( $tags as $tag ) {
367  $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
368  'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
369  'value' => $tag,
370  'class' => 'mw-edittags-remove-checkbox',
371  ] );
372  }
373  }
374 
375  // also output the tags currently applied as a hidden form field, so we
376  // know what to remove from the revision/log entry when the form is submitted
377  $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
378  $html .= '</td></tr></table>';
379 
380  return $html;
381  }
382 
395  protected function getTagSelect( $selectedTags, $label ) {
396  $result = [];
397  $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
398 
399  $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
400  $select->setAttribute( 'multiple', 'multiple' );
401  $select->setAttribute( 'size', '8' );
402 
403  $tags = $this->changeTagsStore->listExplicitlyDefinedTags();
404  $tags = array_unique( array_merge( $tags, $selectedTags ) );
405 
406  // Values of $tags are also used as <option> labels
407  $select->addOptions( array_combine( $tags, $tags ) );
408 
409  $result[1] = $select->getHTML();
410  return $result;
411  }
412 
417  protected function submit() {
418  // Check edit token on submission
419  $request = $this->getRequest();
420  $token = $request->getVal( 'wpEditToken' );
421  if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
422  $this->getOutput()->addWikiMsg( 'sessionfailure' );
423  return false;
424  }
425 
426  // Evaluate incoming request data
427  $tagList = $request->getArray( 'wpTagList' ) ?? [];
428  $existingTags = $request->getVal( 'wpExistingTags' );
429  if ( $existingTags === null || $existingTags === '' ) {
430  $existingTags = [];
431  } else {
432  $existingTags = explode( ',', $existingTags );
433  }
434 
435  if ( count( $this->ids ) > 1 ) {
436  // multiple revisions selected
437  $tagsToAdd = $tagList;
438  if ( $request->getBool( 'wpRemoveAllTags' ) ) {
439  $tagsToRemove = $existingTags;
440  } else {
441  $tagsToRemove = $request->getArray( 'wpTagsToRemove', [] );
442  }
443  } else {
444  // single revision selected
445  // The user tells us which tags they want associated to the revision.
446  // We have to figure out which ones to add, and which to remove.
447  $tagsToAdd = array_diff( $tagList, $existingTags );
448  $tagsToRemove = array_diff( $existingTags, $tagList );
449  }
450 
451  if ( !$tagsToAdd && !$tagsToRemove ) {
452  $status = Status::newFatal( 'tags-edit-none-selected' );
453  } else {
454  $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
455  $tagsToRemove, null, $this->reason, $this->getAuthority() );
456  }
457 
458  if ( $status->isGood() ) {
459  $this->success();
460  return true;
461  } else {
462  $this->failure( $status );
463  return false;
464  }
465  }
466 
470  protected function success() {
471  $out = $this->getOutput();
472  $out->setPageTitleMsg( $this->msg( 'actioncomplete' ) );
473  $out->addHTML(
474  Html::successBox( $out->msg( 'tags-edit-success' )->parse() )
475  );
476  $this->wasSaved = true;
477  $this->revList->reloadFromPrimary();
478  $this->reason = ''; // no need to spew the reason back at the user
479  $this->showForm();
480  }
481 
486  protected function failure( $status ) {
487  $out = $this->getOutput();
488  $out->setPageTitleMsg( $this->msg( 'actionfailed' ) );
489  $out->addHTML(
491  $out->parseAsContent(
492  $status->getWikiText( 'tags-edit-failure', false, $this->getLanguage() )
493  )
494  )
495  );
496  $this->showForm();
497  }
498 
499  public function getDescription() {
500  return $this->msg( 'tags-edit-title' );
501  }
502 
503  protected function getGroupName() {
504  return 'pagetools';
505  }
506 }
507 
511 class_alias( SpecialEditTags::class, 'SpecialEditTags' );
static factory( $typeName, IContextSource $context, PageIdentity $page, array $ids)
Creates a ChangeTags*List of the requested type.
An error page which can definitely be safely rendered using the OutputPage.
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition: LogPage.php:43
Gateway class for change_tags table.
Handle database storage of comments such as edit summaries and log reasons.
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
static successBox( $html, $className='')
Return a success box.
Definition: Html.php:836
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:822
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:889
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Parent class for all special pages.
Definition: SpecialPage.php:66
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
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,...
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Shortcut to construct a special page which is unlisted by default.
Special page for adding and removing change tags to individual revisions.
success()
Report that the submit operation succeeded.
showConvenienceLinks()
Show some useful links in the subtitle.
failure( $status)
Report that the submit operation failed.
getTagSelect( $selectedTags, $label)
Returns a <select multiple> element with a list of change tags that can be applied by users.
showForm()
Show a list of items that we will operate on, and show a form which allows the user to modify the tag...
execute( $par)
Default execute method Checks user permissions.
doesWrites()
Indicates whether this special page may perform database writes.
bool $wasSaved
Was the DB modified in this request.
getList()
Get the list object for this request.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getDescription()
Returns the name that goes in the <h1> in the special page itself, and also the name that will be l...
__construct(PermissionManager $permissionManager, ChangeTagsStore $changeTagsStore)
submit()
UI entry point for form submission.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Represents a title within MediaWiki.
Definition: Title.php:76
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:400
General controller for RevDel, used by both SpecialRevisiondelete and ApiRevisionDelete.
static suggestTarget( $typeName, $target, array $ids)
Suggest a target for the revision deletion.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
Show an error when the user tries to do something whilst blocked.
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:28
Module of static functions for generating XML.
Definition: Xml.php:33
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:120
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition: Xml.php:363
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:111
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:279
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:465
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:424
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:46
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:624