MediaWiki  master
SpecialEditTags.php
Go to the documentation of this file.
1 <?php
23 
33  protected $wasSaved = false;
34 
36  private $submitClicked;
37 
39  private $ids;
40 
42  private $targetObj;
43 
45  private $typeName;
46 
48  private $revList;
49 
51  private $reason;
52 
54  private $permissionManager;
55 
61  public function __construct( PermissionManager $permissionManager ) {
62  parent::__construct( 'EditTags', 'changetags' );
63 
64  $this->permissionManager = $permissionManager;
65  }
66 
67  public function doesWrites() {
68  return true;
69  }
70 
71  public function execute( $par ) {
72  $this->checkPermissions();
73  $this->checkReadOnly();
74 
75  $output = $this->getOutput();
76  $user = $this->getUser();
77  $request = $this->getRequest();
78 
79  $this->setHeaders();
80  $this->outputHeader();
81 
82  $output->addModules( [ 'mediawiki.special.edittags' ] );
83  $output->addModuleStyles( [
84  'mediawiki.interface.helpers.styles',
85  'mediawiki.special'
86  ] );
87 
88  $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
89 
90  // Handle our many different possible input types
91  $ids = $request->getVal( 'ids' );
92  if ( $ids !== null ) {
93  // Allow CSV from the form hidden field, or a single ID for show/hide links
94  $this->ids = explode( ',', $ids );
95  } else {
96  // Array input
97  $this->ids = array_keys( $request->getArray( 'ids', [] ) );
98  }
99  $this->ids = array_unique( array_filter( $this->ids ) );
100 
101  // No targets?
102  if ( count( $this->ids ) == 0 ) {
103  throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
104  }
105 
106  $this->typeName = $request->getVal( 'type' );
107  $this->targetObj = Title::newFromText( $request->getText( 'target' ) );
108 
109  switch ( $this->typeName ) {
110  case 'logentry':
111  case 'logging':
112  $this->typeName = 'logentry';
113  break;
114  default:
115  $this->typeName = 'revision';
116  break;
117  }
118 
119  // Allow the list type to adjust the passed target
120  // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
121  // what we want
122  $this->targetObj = RevisionDeleter::suggestTarget(
123  $this->typeName === 'revision' ? 'revision' : 'logging',
124  $this->targetObj,
125  $this->ids
126  );
127 
128  $this->reason = $request->getVal( 'wpReason' );
129  // We need a target page!
130  if ( $this->targetObj === null ) {
131  $output->addWikiMsg( 'undelete-header' );
132  return;
133  }
134 
135  // Check blocks
136  $checkReplica = !$this->submitClicked;
137  if (
138  $this->permissionManager->isBlockedFrom(
139  $user,
140  $this->targetObj,
141  $checkReplica
142  )
143  ) {
144  throw new UserBlockedError(
145  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
146  $user->getBlock(),
147  $user,
148  $this->getLanguage(),
149  $request->getIP()
150  );
151  }
152 
153  // Give a link to the logs/hist for this page
154  $this->showConvenienceLinks();
155 
156  // Either submit or create our form
157  if ( $this->submitClicked ) {
158  $this->submit();
159  } else {
160  $this->showForm();
161  }
162 
163  // Show relevant lines from the tag log
164  $tagLogPage = new LogPage( 'tag' );
165  $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
167  $output,
168  'tag',
169  $this->targetObj,
170  '', /* user */
171  [ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
172  );
173  }
174 
178  protected function showConvenienceLinks() {
179  // Give a link to the logs/hist for this page
180  if ( $this->targetObj ) {
181  // Also set header tabs to be for the target.
182  $this->getSkin()->setRelevantTitle( $this->targetObj );
183 
184  $linkRenderer = $this->getLinkRenderer();
185  $links = [];
186  $links[] = $linkRenderer->makeKnownLink(
187  SpecialPage::getTitleFor( 'Log' ),
188  $this->msg( 'viewpagelogs' )->text(),
189  [],
190  [
191  'page' => $this->targetObj->getPrefixedText(),
192  'wpfilters' => [ 'tag' ],
193  ]
194  );
195  if ( !$this->targetObj->isSpecialPage() ) {
196  // Give a link to the page history
197  $links[] = $linkRenderer->makeKnownLink(
198  $this->targetObj,
199  $this->msg( 'pagehist' )->text(),
200  [],
201  [ 'action' => 'history' ]
202  );
203  }
204  // Link to Special:Tags
205  $links[] = $linkRenderer->makeKnownLink(
206  SpecialPage::getTitleFor( 'Tags' ),
207  $this->msg( 'tags-edit-manage-link' )->text()
208  );
209  // Logs themselves don't have histories or archived revisions
210  $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
211  }
212  }
213 
218  protected function getList() {
219  if ( $this->revList === null ) {
220  $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
221  $this->targetObj, $this->ids );
222  }
223 
224  return $this->revList;
225  }
226 
231  protected function showForm() {
232  $out = $this->getOutput();
233  // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
234  $out->wrapWikiMsg( "<strong>$1</strong>", [
235  "tags-edit-{$this->typeName}-selected",
236  $this->getLanguage()->formatNum( count( $this->ids ) ),
237  $this->targetObj->getPrefixedText()
238  ] );
239 
240  $this->addHelpLink( 'Help:Tags' );
241  $out->addHTML( "<ul>" );
242 
243  $numRevisions = 0;
244  // Live revisions...
245  $list = $this->getList();
246  for ( $list->reset(); $list->current(); $list->next() ) {
247  $item = $list->current();
248  if ( !$item->canView() ) {
249  throw new ErrorPageError( 'permissionserrors', 'tags-update-no-permission' );
250  }
251  $numRevisions++;
252  $out->addHTML( $item->getHTML() );
253  }
254 
255  if ( !$numRevisions ) {
256  throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
257  }
258 
259  $out->addHTML( "</ul>" );
260  // Explanation text
261  $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
262 
263  // Show form
264  $form = Xml::openElement( 'form', [ 'method' => 'post',
265  'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
266  'id' => 'mw-revdel-form-revisions' ] ) .
267  Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend",
268  count( $this->ids ) )->text() ) .
269  $this->buildCheckBoxes() .
270  Xml::openElement( 'table' ) .
271  "<tr>\n" .
272  '<td class="mw-label">' .
273  Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
274  '</td>' .
275  '<td class="mw-input">' .
276  Xml::input( 'wpReason', 60, $this->reason, [
277  'id' => 'wpReason',
278  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
279  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
280  // Unicode codepoints.
282  ] ) .
283  '</td>' .
284  "</tr><tr>\n" .
285  '<td></td>' .
286  '<td class="mw-submit">' .
287  Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
288  $numRevisions )->text(), [ 'name' => 'wpSubmit' ] ) .
289  '</td>' .
290  "</tr>\n" .
291  Xml::closeElement( 'table' ) .
292  Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
293  Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
294  Html::hidden( 'type', $this->typeName ) .
295  Html::hidden( 'ids', implode( ',', $this->ids ) ) .
296  Xml::closeElement( 'fieldset' ) . "\n" .
297  Xml::closeElement( 'form' ) . "\n";
298 
299  $out->addHTML( $form );
300  }
301 
305  protected function buildCheckBoxes() {
306  // If there is just one item, provide the user with a multi-select field
307  $list = $this->getList();
308  $tags = [];
309  if ( $list->length() == 1 ) {
310  $list->reset();
311  $tags = $list->current()->getTags();
312  if ( $tags ) {
313  $tags = explode( ',', $tags );
314  } else {
315  $tags = [];
316  }
317 
318  $html = '<table id="mw-edittags-tags-selector">';
319  $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
320  '</td><td>';
321  if ( $tags ) {
322  $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
323  } else {
324  $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
325  }
326  $html .= '</td></tr>';
327  $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
328  $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
329  } else {
330  // Otherwise, use a multi-select field for adding tags, and a list of
331  // checkboxes for removing them
332 
333  for ( $list->reset(); $list->current(); $list->next() ) {
334  $currentTags = $list->current()->getTags();
335  if ( $currentTags ) {
336  $tags = array_merge( $tags, explode( ',', $currentTags ) );
337  }
338  }
339  $tags = array_unique( $tags );
340 
341  $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
342  $tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
343  $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
344  $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
345  $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
346  'wpRemoveAllTags', 'mw-edittags-remove-all' );
347  $i = 0; // used for generating checkbox IDs only
348  foreach ( $tags as $tag ) {
349  $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
350  'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
351  'value' => $tag,
352  'class' => 'mw-edittags-remove-checkbox',
353  ] );
354  }
355  }
356 
357  // also output the tags currently applied as a hidden form field, so we
358  // know what to remove from the revision/log entry when the form is submitted
359  $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
360  $html .= '</td></tr></table>';
361 
362  return $html;
363  }
364 
377  protected function getTagSelect( $selectedTags, $label ) {
378  $result = [];
379  $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
380 
381  $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
382  $select->setAttribute( 'multiple', 'multiple' );
383  $select->setAttribute( 'size', '8' );
384 
386  $tags = array_unique( array_merge( $tags, $selectedTags ) );
387 
388  // Values of $tags are also used as <option> labels
389  $select->addOptions( array_combine( $tags, $tags ) );
390 
391  $result[1] = $select->getHTML();
392  return $result;
393  }
394 
399  protected function submit() {
400  // Check edit token on submission
401  $request = $this->getRequest();
402  $token = $request->getVal( 'wpEditToken' );
403  if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
404  $this->getOutput()->addWikiMsg( 'sessionfailure' );
405  return false;
406  }
407 
408  // Evaluate incoming request data
409  $tagList = $request->getArray( 'wpTagList' );
410  if ( $tagList === null ) {
411  $tagList = [];
412  }
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(
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.
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
An error page which can definitely be safely rendered using the OutputPage.
static successBox( $html, $className='')
Return a success box.
Definition: Html.php:800
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:851
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition: LogPage.php:40
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
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
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:373
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:26
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:121
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition: Xml.php:367
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:112
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:283
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:469
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:428
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:43
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:628