MediaWiki REL1_37
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 // 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" );
171 LogEventsList::showLogExtract(
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
190 $links = [];
191 $links[] = $linkRenderer->makeKnownLink(
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.
287 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
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}
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:38
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
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.
LinkRenderer null $linkRenderer
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.
Represents a title within MediaWiki.
Definition Title.php:48
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