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