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