MediaWiki master
SpecialEditTags.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Specials;
23
27use LogPage;
38use Xml;
39use 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" );
184 LogEventsList::showLogExtract(
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(
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.
299 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
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(
490 Html::errorBox(
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
511class_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.
Class to simplify the use of log pages.
Definition LogPage.php:44
Gateway class for change_tags table.
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:56
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Parent class for all special pages.
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:54
Represents a title within MediaWiki.
Definition Title.php:78
General controller for RevDel, used by both SpecialRevisiondelete and ApiRevisionDelete.
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
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...