MediaWiki 1.40.4
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" );
169 LogEventsList::showLogExtract(
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(
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.
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.
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