MediaWiki REL1_35
SpecialEditTags.php
Go to the documentation of this file.
1<?php
23
33 protected $wasSaved = false;
34
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',
130 );
131
132 $this->isAllowed = $this->permissionManager->userHasRight( $user, 'changetags' );
133
134 $this->reason = $request->getVal( 'wpReason', '' );
135
136 // We need a target page!
137 if ( $this->targetObj === null ) {
138 $output->addWikiMsg( 'undelete-header' );
139 return;
140 }
141
142 // Check blocks
143 if ( $this->permissionManager->isBlockedFrom( $user, $this->targetObj ) ) {
144 throw new UserBlockedError(
145 $user->getBlock(),
146 $user,
147 $this->getLanguage(),
148 $request->getIP()
149 );
150 }
151
152 // Give a link to the logs/hist for this page
153 $this->showConvenienceLinks();
154
155 // Either submit or create our form
156 if ( $this->isAllowed && $this->submitClicked ) {
157 $this->submit();
158 } else {
159 $this->showForm();
160 }
161
162 // Show relevant lines from the tag log
163 $tagLogPage = new LogPage( 'tag' );
164 $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
165 LogEventsList::showLogExtract(
166 $output,
167 'tag',
168 $this->targetObj,
169 '', /* user */
170 [ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
171 );
172 }
173
177 protected function showConvenienceLinks() {
178 // Give a link to the logs/hist for this page
179 if ( $this->targetObj ) {
180 // Also set header tabs to be for the target.
181 $this->getSkin()->setRelevantTitle( $this->targetObj );
182
184 $links = [];
185 $links[] = $linkRenderer->makeKnownLink(
187 $this->msg( 'viewpagelogs' )->text(),
188 [],
189 [
190 'page' => $this->targetObj->getPrefixedText(),
191 'wpfilters' => [ 'tag' ],
192 ]
193 );
194 if ( !$this->targetObj->isSpecialPage() ) {
195 // Give a link to the page history
196 $links[] = $linkRenderer->makeKnownLink(
197 $this->targetObj,
198 $this->msg( 'pagehist' )->text(),
199 [],
200 [ 'action' => 'history' ]
201 );
202 }
203 // Link to Special:Tags
204 $links[] = $linkRenderer->makeKnownLink(
205 SpecialPage::getTitleFor( 'Tags' ),
206 $this->msg( 'tags-edit-manage-link' )->text()
207 );
208 // Logs themselves don't have histories or archived revisions
209 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
210 }
211 }
212
217 protected function getList() {
218 if ( $this->revList === null ) {
219 $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
220 $this->targetObj, $this->ids );
221 }
222
223 return $this->revList;
224 }
225
230 protected function showForm() {
231 $out = $this->getOutput();
232 // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
233 $out->wrapWikiMsg( "<strong>$1</strong>", [
234 "tags-edit-{$this->typeName}-selected",
235 $this->getLanguage()->formatNum( count( $this->ids ) ),
236 $this->targetObj->getPrefixedText()
237 ] );
238
239 $this->addHelpLink( 'Help:Tags' );
240 $out->addHTML( "<ul>" );
241
242 $numRevisions = 0;
243 // Live revisions...
244 $list = $this->getList();
245 for ( $list->reset(); $list->current(); $list->next() ) {
246 $item = $list->current();
247 if ( !$item->canView() ) {
248 throw new ErrorPageError( 'permissionserrors', 'tags-update-no-permission' );
249 }
250 $numRevisions++;
251 $out->addHTML( $item->getHTML() );
252 }
253
254 if ( !$numRevisions ) {
255 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
256 }
257
258 $out->addHTML( "</ul>" );
259 // Explanation text
260 $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
261
262 // Show form if the user can submit
263 if ( $this->isAllowed ) {
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.
281 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
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 } else {
299 $form = '';
300 }
301 $out->addHTML( $form );
302 }
303
307 protected function buildCheckBoxes() {
308 // If there is just one item, provide the user with a multi-select field
309 $list = $this->getList();
310 $tags = [];
311 if ( $list->length() == 1 ) {
312 $list->reset();
313 $tags = $list->current()->getTags();
314 if ( $tags ) {
315 $tags = explode( ',', $tags );
316 } else {
317 $tags = [];
318 }
319
320 $html = '<table id="mw-edittags-tags-selector">';
321 $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
322 '</td><td>';
323 if ( $tags ) {
324 $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
325 } else {
326 $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
327 }
328 $html .= '</td></tr>';
329 $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
330 $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
331 } else {
332 // Otherwise, use a multi-select field for adding tags, and a list of
333 // checkboxes for removing them
334
335 for ( $list->reset(); $list->current(); $list->next() ) {
336 $currentTags = $list->current()->getTags();
337 if ( $currentTags ) {
338 $tags = array_merge( $tags, explode( ',', $currentTags ) );
339 }
340 }
341 $tags = array_unique( $tags );
342
343 $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
344 $tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
345 $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
346 $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
347 $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
348 'wpRemoveAllTags', 'mw-edittags-remove-all' );
349 $i = 0; // used for generating checkbox IDs only
350 foreach ( $tags as $tag ) {
351 $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
352 'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
353 'value' => $tag,
354 'class' => 'mw-edittags-remove-checkbox',
355 ] );
356 }
357 }
358
359 // also output the tags currently applied as a hidden form field, so we
360 // know what to remove from the revision/log entry when the form is submitted
361 $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
362 $html .= '</td></tr></table>';
363
364 return $html;
365 }
366
379 protected function getTagSelect( $selectedTags, $label ) {
380 $result = [];
381 $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
382
383 $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
384 $select->setAttribute( 'multiple', 'multiple' );
385 $select->setAttribute( 'size', '8' );
386
388 $tags = array_unique( array_merge( $tags, $selectedTags ) );
389
390 // Values of $tags are also used as <option> labels
391 $select->addOptions( array_combine( $tags, $tags ) );
392
393 $result[1] = $select->getHTML();
394 return $result;
395 }
396
401 protected function submit() {
402 // Check edit token on submission
403 $request = $this->getRequest();
404 $token = $request->getVal( 'wpEditToken' );
405 if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
406 $this->getOutput()->addWikiMsg( 'sessionfailure' );
407 return false;
408 }
409
410 // Evaluate incoming request data
411 $tagList = $request->getArray( 'wpTagList' );
412 if ( $tagList === null ) {
413 $tagList = [];
414 }
415 $existingTags = $request->getVal( 'wpExistingTags' );
416 if ( $existingTags === null || $existingTags === '' ) {
417 $existingTags = [];
418 } else {
419 $existingTags = explode( ',', $existingTags );
420 }
421
422 if ( count( $this->ids ) > 1 ) {
423 // multiple revisions selected
424 $tagsToAdd = $tagList;
425 if ( $request->getBool( 'wpRemoveAllTags' ) ) {
426 $tagsToRemove = $existingTags;
427 } else {
428 $tagsToRemove = $request->getArray( 'wpTagsToRemove' );
429 }
430 } else {
431 // single revision selected
432 // The user tells us which tags they want associated to the revision.
433 // We have to figure out which ones to add, and which to remove.
434 $tagsToAdd = array_diff( $tagList, $existingTags );
435 $tagsToRemove = array_diff( $existingTags, $tagList );
436 }
437
438 if ( !$tagsToAdd && !$tagsToRemove ) {
439 $status = Status::newFatal( 'tags-edit-none-selected' );
440 } else {
441 $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
442 $tagsToRemove, null, $this->reason, $this->getUser() );
443 }
444
445 if ( $status->isGood() ) {
446 $this->success();
447 return true;
448 } else {
449 $this->failure( $status );
450 return false;
451 }
452 }
453
457 protected function success() {
458 $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
459 $this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>",
460 'tags-edit-success' );
461 $this->wasSaved = true;
462 $this->revList->reloadFromMaster();
463 $this->reason = ''; // no need to spew the reason back at the user
464 $this->showForm();
465 }
466
471 protected function failure( $status ) {
472 $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
473 $this->getOutput()->wrapWikiTextAsInterface(
474 'errorbox', $status->getWikiText( 'tags-edit-failure', false, $this->getLanguage() )
475 );
476 $this->showForm();
477 }
478
479 public function getDescription() {
480 return $this->msg( 'tags-edit-title' )->text();
481 }
482
483 protected function getGroupName() {
484 return 'pagetools';
485 }
486}
static factory( $typeName, IContextSource $context, Title $title, 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:37
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.
bool $isAllowed
Whether user is allowed to perform the action.
failure( $status)
Report that the submit operation failed.
string $typeName
Deletion type, may be revision or logentry.
bool $wasSaved
Was the DB modified in this request.
ChangeTagsList $revList
Storing the list of items to be tagged.
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.
array $ids
Target ID list.
__construct(PermissionManager $permissionManager)
Title $targetObj
Title object for target parameter.
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...
PermissionManager $permissionManager
getList()
Get the list object for this request.
bool $submitClicked
True if the submit button was clicked, and the form was posted.
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.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
Represents a title within MediaWiki.
Definition Title.php:42
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