MediaWiki  1.28.1
SpecialEditTags.php
Go to the documentation of this file.
1 <?php
31  protected $wasSaved = false;
32 
34  private $submitClicked;
35 
37  private $ids;
38 
40  private $targetObj;
41 
43  private $typeName;
44 
46  private $revList;
47 
49  private $isAllowed;
50 
52  private $reason;
53 
54  public function __construct() {
55  parent::__construct( 'EditTags', 'changetags' );
56  }
57 
58  public function doesWrites() {
59  return true;
60  }
61 
62  public function execute( $par ) {
63  $this->checkPermissions();
64  $this->checkReadOnly();
65 
66  $output = $this->getOutput();
67  $user = $this->getUser();
68  $request = $this->getRequest();
69 
70  // Check blocks
71  if ( $user->isBlocked() ) {
72  throw new UserBlockedError( $user->getBlock() );
73  }
74 
75  $this->setHeaders();
76  $this->outputHeader();
77 
78  $this->getOutput()->addModules( [ 'mediawiki.special.edittags',
79  'mediawiki.special.edittags.styles' ] );
80 
81  $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
82 
83  // Handle our many different possible input types
84  $ids = $request->getVal( 'ids' );
85  if ( !is_null( $ids ) ) {
86  // Allow CSV from the form hidden field, or a single ID for show/hide links
87  $this->ids = explode( ',', $ids );
88  } else {
89  // Array input
90  $this->ids = array_keys( $request->getArray( 'ids', [] ) );
91  }
92  $this->ids = array_unique( array_filter( $this->ids ) );
93 
94  // No targets?
95  if ( count( $this->ids ) == 0 ) {
96  throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
97  }
98 
99  $this->typeName = $request->getVal( 'type' );
100  $this->targetObj = Title::newFromText( $request->getText( 'target' ) );
101 
102  // sanity check of parameter
103  switch ( $this->typeName ) {
104  case 'logentry':
105  case 'logging':
106  $this->typeName = 'logentry';
107  break;
108  default:
109  $this->typeName = 'revision';
110  break;
111  }
112 
113  // Allow the list type to adjust the passed target
114  // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
115  // what we want
116  $this->targetObj = RevisionDeleter::suggestTarget(
117  $this->typeName === 'revision' ? 'revision' : 'logging',
118  $this->targetObj,
119  $this->ids
120  );
121 
122  $this->isAllowed = $user->isAllowed( 'changetags' );
123 
124  $this->reason = $request->getVal( 'wpReason' );
125  // We need a target page!
126  if ( is_null( $this->targetObj ) ) {
127  $output->addWikiMsg( 'undelete-header' );
128  return;
129  }
130  // Give a link to the logs/hist for this page
131  $this->showConvenienceLinks();
132 
133  // Either submit or create our form
134  if ( $this->isAllowed && $this->submitClicked ) {
135  $this->submit();
136  } else {
137  $this->showForm();
138  }
139 
140  // Show relevant lines from the tag log
141  $tagLogPage = new LogPage( 'tag' );
142  $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
144  $output,
145  'tag',
146  $this->targetObj,
147  '', /* user */
148  [ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
149  );
150  }
151 
155  protected function showConvenienceLinks() {
156  // Give a link to the logs/hist for this page
157  if ( $this->targetObj ) {
158  // Also set header tabs to be for the target.
159  $this->getSkin()->setRelevantTitle( $this->targetObj );
160 
161  $links = [];
162  $links[] = Linker::linkKnown(
163  SpecialPage::getTitleFor( 'Log' ),
164  $this->msg( 'viewpagelogs' )->escaped(),
165  [],
166  [
167  'page' => $this->targetObj->getPrefixedText(),
168  'hide_tag_log' => '0',
169  ]
170  );
171  if ( !$this->targetObj->isSpecialPage() ) {
172  // Give a link to the page history
173  $links[] = Linker::linkKnown(
174  $this->targetObj,
175  $this->msg( 'pagehist' )->escaped(),
176  [],
177  [ 'action' => 'history' ]
178  );
179  }
180  // Link to Special:Tags
181  $links[] = Linker::linkKnown(
182  SpecialPage::getTitleFor( 'Tags' ),
183  $this->msg( 'tags-edit-manage-link' )->escaped()
184  );
185  // Logs themselves don't have histories or archived revisions
186  $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
187  }
188  }
189 
194  protected function getList() {
195  if ( is_null( $this->revList ) ) {
196  $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
197  $this->targetObj, $this->ids );
198  }
199 
200  return $this->revList;
201  }
202 
207  protected function showForm() {
208  $userAllowed = true;
209 
210  $out = $this->getOutput();
211  // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
212  $out->wrapWikiMsg( "<strong>$1</strong>", [
213  "tags-edit-{$this->typeName}-selected",
214  $this->getLanguage()->formatNum( count( $this->ids ) ),
215  $this->targetObj->getPrefixedText()
216  ] );
217 
218  $this->addHelpLink( 'Help:Tags' );
219  $out->addHTML( "<ul>" );
220 
221  $numRevisions = 0;
222  // Live revisions...
223  $list = $this->getList();
224  // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
225  for ( $list->reset(); $list->current(); $list->next() ) {
226  // @codingStandardsIgnoreEnd
227  $item = $list->current();
228  $numRevisions++;
229  $out->addHTML( $item->getHTML() );
230  }
231 
232  if ( !$numRevisions ) {
233  throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
234  }
235 
236  $out->addHTML( "</ul>" );
237  // Explanation text
238  $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
239 
240  // Show form if the user can submit
241  if ( $this->isAllowed ) {
242  $form = Xml::openElement( 'form', [ 'method' => 'post',
243  'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
244  'id' => 'mw-revdel-form-revisions' ] ) .
245  Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend",
246  count( $this->ids ) )->text() ) .
247  $this->buildCheckBoxes() .
248  Xml::openElement( 'table' ) .
249  "<tr>\n" .
250  '<td class="mw-label">' .
251  Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
252  '</td>' .
253  '<td class="mw-input">' .
254  Xml::input(
255  'wpReason',
256  60,
257  $this->reason,
258  [ 'id' => 'wpReason', 'maxlength' => 100 ]
259  ) .
260  '</td>' .
261  "</tr><tr>\n" .
262  '<td></td>' .
263  '<td class="mw-submit">' .
264  Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
265  $numRevisions )->text(), [ 'name' => 'wpSubmit' ] ) .
266  '</td>' .
267  "</tr>\n" .
268  Xml::closeElement( 'table' ) .
269  Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
270  Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
271  Html::hidden( 'type', $this->typeName ) .
272  Html::hidden( 'ids', implode( ',', $this->ids ) ) .
273  Xml::closeElement( 'fieldset' ) . "\n" .
274  Xml::closeElement( 'form' ) . "\n";
275  } else {
276  $form = '';
277  }
278  $out->addHTML( $form );
279  }
280 
284  protected function buildCheckBoxes() {
285  // If there is just one item, provide the user with a multi-select field
286  $list = $this->getList();
287  $tags = [];
288  if ( $list->length() == 1 ) {
289  $list->reset();
290  $tags = $list->current()->getTags();
291  if ( $tags ) {
292  $tags = explode( ',', $tags );
293  } else {
294  $tags = [];
295  }
296 
297  $html = '<table id="mw-edittags-tags-selector">';
298  $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
299  '</td><td>';
300  if ( $tags ) {
301  $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
302  } else {
303  $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
304  }
305  $html .= '</td></tr>';
306  $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
307  $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
308  } else {
309  // Otherwise, use a multi-select field for adding tags, and a list of
310  // checkboxes for removing them
311 
312  // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
313  for ( $list->reset(); $list->current(); $list->next() ) {
314  // @codingStandardsIgnoreEnd
315  $currentTags = $list->current()->getTags();
316  if ( $currentTags ) {
317  $tags = array_merge( $tags, explode( ',', $currentTags ) );
318  }
319  }
320  $tags = array_unique( $tags );
321 
322  $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
323  $tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
324  $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
325  $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
326  $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
327  'wpRemoveAllTags', 'mw-edittags-remove-all' );
328  $i = 0; // used for generating checkbox IDs only
329  foreach ( $tags as $tag ) {
330  $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
331  'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
332  'value' => $tag,
333  'class' => 'mw-edittags-remove-checkbox',
334  ] );
335  }
336  }
337 
338  // also output the tags currently applied as a hidden form field, so we
339  // know what to remove from the revision/log entry when the form is submitted
340  $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
341  $html .= '</td></tr></table>';
342 
343  return $html;
344  }
345 
358  protected function getTagSelect( $selectedTags, $label ) {
359  $result = [];
360  $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
361 
362  $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
363  $select->setAttribute( 'multiple', 'multiple' );
364  $select->setAttribute( 'size', '8' );
365 
367  $tags = array_unique( array_merge( $tags, $selectedTags ) );
368 
369  // Values of $tags are also used as <option> labels
370  $select->addOptions( array_combine( $tags, $tags ) );
371 
372  $result[1] = $select->getHTML();
373  return $result;
374  }
375 
381  protected function submit() {
382  // Check edit token on submission
383  $request = $this->getRequest();
384  $token = $request->getVal( 'wpEditToken' );
385  if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
386  $this->getOutput()->addWikiMsg( 'sessionfailure' );
387  return false;
388  }
389 
390  // Evaluate incoming request data
391  $tagList = $request->getArray( 'wpTagList' );
392  if ( is_null( $tagList ) ) {
393  $tagList = [];
394  }
395  $existingTags = $request->getVal( 'wpExistingTags' );
396  if ( is_null( $existingTags ) || $existingTags === '' ) {
397  $existingTags = [];
398  } else {
399  $existingTags = explode( ',', $existingTags );
400  }
401 
402  if ( count( $this->ids ) > 1 ) {
403  // multiple revisions selected
404  $tagsToAdd = $tagList;
405  if ( $request->getBool( 'wpRemoveAllTags' ) ) {
406  $tagsToRemove = $existingTags;
407  } else {
408  $tagsToRemove = $request->getArray( 'wpTagsToRemove' );
409  }
410  } else {
411  // single revision selected
412  // The user tells us which tags they want associated to the revision.
413  // We have to figure out which ones to add, and which to remove.
414  $tagsToAdd = array_diff( $tagList, $existingTags );
415  $tagsToRemove = array_diff( $existingTags, $tagList );
416  }
417 
418  if ( !$tagsToAdd && !$tagsToRemove ) {
419  $status = Status::newFatal( 'tags-edit-none-selected' );
420  } else {
421  $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
422  $tagsToRemove, null, $this->reason, $this->getUser() );
423  }
424 
425  if ( $status->isGood() ) {
426  $this->success();
427  return true;
428  } else {
429  $this->failure( $status );
430  return false;
431  }
432  }
433 
437  protected function success() {
438  $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
439  $this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>",
440  'tags-edit-success' );
441  $this->wasSaved = true;
442  $this->revList->reloadFromMaster();
443  $this->reason = ''; // no need to spew the reason back at the user
444  $this->showForm();
445  }
446 
451  protected function failure( $status ) {
452  $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
453  $this->getOutput()->addWikiText( '<div class="errorbox">' .
454  $status->getWikiText( 'tags-edit-failure' ) .
455  '</div>'
456  );
457  $this->showForm();
458  }
459 
460  public function getDescription() {
461  return $this->msg( 'tags-edit-title' )->text();
462  }
463 
464  protected function getGroupName() {
465  return 'pagetools';
466  }
467 }
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1936
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:802
either a plain
Definition: hooks.txt:1987
Shortcut to construct a special page which is unlisted by default.
string $typeName
Deletion type, may be revision or logentry.
getContext()
Gets the context this SpecialPage is executed in.
static element($element, $attribs=null, $contents= '', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:39
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...
Definition: SpecialPage.php:82
static newFatal($message)
Factory function for fatal errors.
Definition: StatusValue.php:63
getList()
Get the list object for this request.
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:758
Class for generating HTML =""> element with a list of change tags that can be applied by users...
ChangeTagsList $revList
Storing the list of items to be tagged.
getPageTitle($subpage=false)
Get a self-referential title object.