MediaWiki REL1_39
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 $reason;
52
54 private $permissionManager;
55
61 public function __construct( PermissionManager $permissionManager ) {
62 parent::__construct( 'EditTags', 'changetags' );
63
64 $this->permissionManager = $permissionManager;
65 }
66
67 public function doesWrites() {
68 return true;
69 }
70
71 public function execute( $par ) {
72 $this->checkPermissions();
73 $this->checkReadOnly();
74
75 $output = $this->getOutput();
76 $user = $this->getUser();
77 $request = $this->getRequest();
78
79 $this->setHeaders();
80 $this->outputHeader();
81
82 $output->addModules( [ 'mediawiki.special.edittags' ] );
83 $output->addModuleStyles( [
84 'mediawiki.interface.helpers.styles',
85 'mediawiki.special'
86 ] );
87
88 $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
89
90 // Handle our many different possible input types
91 $ids = $request->getVal( 'ids' );
92 if ( $ids !== null ) {
93 // Allow CSV from the form hidden field, or a single ID for show/hide links
94 $this->ids = explode( ',', $ids );
95 } else {
96 // Array input
97 $this->ids = array_keys( $request->getArray( 'ids', [] ) );
98 }
99 $this->ids = array_unique( array_filter( $this->ids ) );
100
101 // No targets?
102 if ( count( $this->ids ) == 0 ) {
103 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
104 }
105
106 $this->typeName = $request->getVal( 'type' );
107 $this->targetObj = Title::newFromText( $request->getText( 'target' ) );
108
109 switch ( $this->typeName ) {
110 case 'logentry':
111 case 'logging':
112 $this->typeName = 'logentry';
113 break;
114 default:
115 $this->typeName = 'revision';
116 break;
117 }
118
119 // Allow the list type to adjust the passed target
120 // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
121 // what we want
122 $this->targetObj = RevisionDeleter::suggestTarget(
123 $this->typeName === 'revision' ? 'revision' : 'logging',
124 $this->targetObj,
125 $this->ids
126 );
127
128 $this->reason = $request->getVal( 'wpReason', '' );
129 // We need a target page!
130 if ( $this->targetObj === null ) {
131 $output->addWikiMsg( 'undelete-header' );
132 return;
133 }
134
135 // Check blocks
136 $checkReplica = !$this->submitClicked;
137 if (
138 $this->permissionManager->isBlockedFrom(
139 $user,
140 $this->targetObj,
141 $checkReplica
142 )
143 ) {
144 throw new UserBlockedError(
145 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
146 $user->getBlock(),
147 $user,
148 $this->getLanguage(),
149 $request->getIP()
150 );
151 }
152
153 // Give a link to the logs/hist for this page
154 $this->showConvenienceLinks();
155
156 // Either submit or create our form
157 if ( $this->submitClicked ) {
158 $this->submit();
159 } else {
160 $this->showForm();
161 }
162
163 // Show relevant lines from the tag log
164 $tagLogPage = new LogPage( 'tag' );
165 $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
166 LogEventsList::showLogExtract(
167 $output,
168 'tag',
169 $this->targetObj,
170 '', /* user */
171 [ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
172 );
173 }
174
178 protected function showConvenienceLinks() {
179 // Give a link to the logs/hist for this page
180 if ( $this->targetObj ) {
181 // Also set header tabs to be for the target.
182 $this->getSkin()->setRelevantTitle( $this->targetObj );
183
184 $linkRenderer = $this->getLinkRenderer();
185 $links = [];
186 $links[] = $linkRenderer->makeKnownLink(
188 $this->msg( 'viewpagelogs' )->text(),
189 [],
190 [
191 'page' => $this->targetObj->getPrefixedText(),
192 'wpfilters' => [ 'tag' ],
193 ]
194 );
195 if ( !$this->targetObj->isSpecialPage() ) {
196 // Give a link to the page history
197 $links[] = $linkRenderer->makeKnownLink(
198 $this->targetObj,
199 $this->msg( 'pagehist' )->text(),
200 [],
201 [ 'action' => 'history' ]
202 );
203 }
204 // Link to Special:Tags
205 $links[] = $linkRenderer->makeKnownLink(
206 SpecialPage::getTitleFor( 'Tags' ),
207 $this->msg( 'tags-edit-manage-link' )->text()
208 );
209 // Logs themselves don't have histories or archived revisions
210 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
211 }
212 }
213
218 protected function getList() {
219 if ( $this->revList === null ) {
220 $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
221 $this->targetObj, $this->ids );
222 }
223
224 return $this->revList;
225 }
226
231 protected function showForm() {
232 $out = $this->getOutput();
233 // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
234 $out->wrapWikiMsg( "<strong>$1</strong>", [
235 "tags-edit-{$this->typeName}-selected",
236 $this->getLanguage()->formatNum( count( $this->ids ) ),
237 $this->targetObj->getPrefixedText()
238 ] );
239
240 $this->addHelpLink( 'Help:Tags' );
241 $out->addHTML( "<ul>" );
242
243 $numRevisions = 0;
244 // Live revisions...
245 $list = $this->getList();
246 for ( $list->reset(); $list->current(); $list->next() ) {
247 $item = $list->current();
248 if ( !$item->canView() ) {
249 throw new ErrorPageError( 'permissionserrors', 'tags-update-no-permission' );
250 }
251 $numRevisions++;
252 $out->addHTML( $item->getHTML() );
253 }
254
255 if ( !$numRevisions ) {
256 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
257 }
258
259 $out->addHTML( "</ul>" );
260 // Explanation text
261 $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
262
263 // Show form
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
299 $out->addHTML( $form );
300 }
301
305 protected function buildCheckBoxes() {
306 // If there is just one item, provide the user with a multi-select field
307 $list = $this->getList();
308 $tags = [];
309 if ( $list->length() == 1 ) {
310 $list->reset();
311 $tags = $list->current()->getTags();
312 if ( $tags ) {
313 $tags = explode( ',', $tags );
314 } else {
315 $tags = [];
316 }
317
318 $html = '<table id="mw-edittags-tags-selector">';
319 $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
320 '</td><td>';
321 if ( $tags ) {
322 $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
323 } else {
324 $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
325 }
326 $html .= '</td></tr>';
327 $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
328 $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
329 } else {
330 // Otherwise, use a multi-select field for adding tags, and a list of
331 // checkboxes for removing them
332
333 for ( $list->reset(); $list->current(); $list->next() ) {
334 $currentTags = $list->current()->getTags();
335 if ( $currentTags ) {
336 $tags = array_merge( $tags, explode( ',', $currentTags ) );
337 }
338 }
339 $tags = array_unique( $tags );
340
341 $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
342 $tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
343 $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
344 $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
345 $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
346 'wpRemoveAllTags', 'mw-edittags-remove-all' );
347 $i = 0; // used for generating checkbox IDs only
348 foreach ( $tags as $tag ) {
349 $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
350 'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
351 'value' => $tag,
352 'class' => 'mw-edittags-remove-checkbox',
353 ] );
354 }
355 }
356
357 // also output the tags currently applied as a hidden form field, so we
358 // know what to remove from the revision/log entry when the form is submitted
359 $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
360 $html .= '</td></tr></table>';
361
362 return $html;
363 }
364
377 protected function getTagSelect( $selectedTags, $label ) {
378 $result = [];
379 $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
380
381 $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
382 $select->setAttribute( 'multiple', 'multiple' );
383 $select->setAttribute( 'size', '8' );
384
386 $tags = array_unique( array_merge( $tags, $selectedTags ) );
387
388 // Values of $tags are also used as <option> labels
389 $select->addOptions( array_combine( $tags, $tags ) );
390
391 $result[1] = $select->getHTML();
392 return $result;
393 }
394
399 protected function submit() {
400 // Check edit token on submission
401 $request = $this->getRequest();
402 $token = $request->getVal( 'wpEditToken' );
403 if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
404 $this->getOutput()->addWikiMsg( 'sessionfailure' );
405 return false;
406 }
407
408 // Evaluate incoming request data
409 $tagList = $request->getArray( 'wpTagList' );
410 if ( $tagList === null ) {
411 $tagList = [];
412 }
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:39
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.
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.
Represents a title within MediaWiki.
Definition Title.php:49
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