MediaWiki REL1_34
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 ( !is_null( $ids ) ) {
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 ( is_null( $this->targetObj ) ) {
137 $output->addWikiMsg( 'undelete-header' );
138 return;
139 }
140
141 // Check blocks
142 if ( $this->permissionManager->isBlockedFrom( $user, $this->targetObj ) ) {
143 throw new UserBlockedError( $user->getBlock() );
144 }
145
146 // Give a link to the logs/hist for this page
147 $this->showConvenienceLinks();
148
149 // Either submit or create our form
150 if ( $this->isAllowed && $this->submitClicked ) {
151 $this->submit();
152 } else {
153 $this->showForm();
154 }
155
156 // Show relevant lines from the tag log
157 $tagLogPage = new LogPage( 'tag' );
158 $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
160 $output,
161 'tag',
162 $this->targetObj,
163 '', /* user */
164 [ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
165 );
166 }
167
171 protected function showConvenienceLinks() {
172 // Give a link to the logs/hist for this page
173 if ( $this->targetObj ) {
174 // Also set header tabs to be for the target.
175 $this->getSkin()->setRelevantTitle( $this->targetObj );
176
178 $links = [];
179 $links[] = $linkRenderer->makeKnownLink(
181 $this->msg( 'viewpagelogs' )->text(),
182 [],
183 [
184 'page' => $this->targetObj->getPrefixedText(),
185 'wpfilters' => [ 'tag' ],
186 ]
187 );
188 if ( !$this->targetObj->isSpecialPage() ) {
189 // Give a link to the page history
190 $links[] = $linkRenderer->makeKnownLink(
191 $this->targetObj,
192 $this->msg( 'pagehist' )->text(),
193 [],
194 [ 'action' => 'history' ]
195 );
196 }
197 // Link to Special:Tags
198 $links[] = $linkRenderer->makeKnownLink(
199 SpecialPage::getTitleFor( 'Tags' ),
200 $this->msg( 'tags-edit-manage-link' )->text()
201 );
202 // Logs themselves don't have histories or archived revisions
203 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
204 }
205 }
206
211 protected function getList() {
212 if ( is_null( $this->revList ) ) {
213 $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
214 $this->targetObj, $this->ids );
215 }
216
217 return $this->revList;
218 }
219
224 protected function showForm() {
225 $out = $this->getOutput();
226 // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
227 $out->wrapWikiMsg( "<strong>$1</strong>", [
228 "tags-edit-{$this->typeName}-selected",
229 $this->getLanguage()->formatNum( count( $this->ids ) ),
230 $this->targetObj->getPrefixedText()
231 ] );
232
233 $this->addHelpLink( 'Help:Tags' );
234 $out->addHTML( "<ul>" );
235
236 $numRevisions = 0;
237 // Live revisions...
238 $list = $this->getList();
239 for ( $list->reset(); $list->current(); $list->next() ) {
240 $item = $list->current();
241 if ( !$item->canView() ) {
242 throw new ErrorPageError( 'permissionserrors', 'tags-update-no-permission' );
243 }
244 $numRevisions++;
245 $out->addHTML( $item->getHTML() );
246 }
247
248 if ( !$numRevisions ) {
249 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
250 }
251
252 $out->addHTML( "</ul>" );
253 // Explanation text
254 $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
255
256 // Show form if the user can submit
257 if ( $this->isAllowed ) {
258 $form = Xml::openElement( 'form', [ 'method' => 'post',
259 'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
260 'id' => 'mw-revdel-form-revisions' ] ) .
261 Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend",
262 count( $this->ids ) )->text() ) .
263 $this->buildCheckBoxes() .
264 Xml::openElement( 'table' ) .
265 "<tr>\n" .
266 '<td class="mw-label">' .
267 Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
268 '</td>' .
269 '<td class="mw-input">' .
270 Xml::input( 'wpReason', 60, $this->reason, [
271 'id' => 'wpReason',
272 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
273 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
274 // Unicode codepoints.
275 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
276 ] ) .
277 '</td>' .
278 "</tr><tr>\n" .
279 '<td></td>' .
280 '<td class="mw-submit">' .
281 Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
282 $numRevisions )->text(), [ 'name' => 'wpSubmit' ] ) .
283 '</td>' .
284 "</tr>\n" .
285 Xml::closeElement( 'table' ) .
286 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
287 Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
288 Html::hidden( 'type', $this->typeName ) .
289 Html::hidden( 'ids', implode( ',', $this->ids ) ) .
290 Xml::closeElement( 'fieldset' ) . "\n" .
291 Xml::closeElement( 'form' ) . "\n";
292 } else {
293 $form = '';
294 }
295 $out->addHTML( $form );
296 }
297
301 protected function buildCheckBoxes() {
302 // If there is just one item, provide the user with a multi-select field
303 $list = $this->getList();
304 $tags = [];
305 if ( $list->length() == 1 ) {
306 $list->reset();
307 $tags = $list->current()->getTags();
308 if ( $tags ) {
309 $tags = explode( ',', $tags );
310 } else {
311 $tags = [];
312 }
313
314 $html = '<table id="mw-edittags-tags-selector">';
315 $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
316 '</td><td>';
317 if ( $tags ) {
318 $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
319 } else {
320 $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
321 }
322 $html .= '</td></tr>';
323 $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
324 $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
325 } else {
326 // Otherwise, use a multi-select field for adding tags, and a list of
327 // checkboxes for removing them
328
329 for ( $list->reset(); $list->current(); $list->next() ) {
330 $currentTags = $list->current()->getTags();
331 if ( $currentTags ) {
332 $tags = array_merge( $tags, explode( ',', $currentTags ) );
333 }
334 }
335 $tags = array_unique( $tags );
336
337 $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
338 $tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
339 $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
340 $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
341 $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
342 'wpRemoveAllTags', 'mw-edittags-remove-all' );
343 $i = 0; // used for generating checkbox IDs only
344 foreach ( $tags as $tag ) {
345 $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
346 'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
347 'value' => $tag,
348 'class' => 'mw-edittags-remove-checkbox',
349 ] );
350 }
351 }
352
353 // also output the tags currently applied as a hidden form field, so we
354 // know what to remove from the revision/log entry when the form is submitted
355 $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
356 $html .= '</td></tr></table>';
357
358 return $html;
359 }
360
373 protected function getTagSelect( $selectedTags, $label ) {
374 $result = [];
375 $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
376
377 $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
378 $select->setAttribute( 'multiple', 'multiple' );
379 $select->setAttribute( 'size', '8' );
380
382 $tags = array_unique( array_merge( $tags, $selectedTags ) );
383
384 // Values of $tags are also used as <option> labels
385 $select->addOptions( array_combine( $tags, $tags ) );
386
387 $result[1] = $select->getHTML();
388 return $result;
389 }
390
395 protected function submit() {
396 // Check edit token on submission
397 $request = $this->getRequest();
398 $token = $request->getVal( 'wpEditToken' );
399 if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
400 $this->getOutput()->addWikiMsg( 'sessionfailure' );
401 return false;
402 }
403
404 // Evaluate incoming request data
405 $tagList = $request->getArray( 'wpTagList' );
406 if ( is_null( $tagList ) ) {
407 $tagList = [];
408 }
409 $existingTags = $request->getVal( 'wpExistingTags' );
410 if ( is_null( $existingTags ) || $existingTags === '' ) {
411 $existingTags = [];
412 } else {
413 $existingTags = explode( ',', $existingTags );
414 }
415
416 if ( count( $this->ids ) > 1 ) {
417 // multiple revisions selected
418 $tagsToAdd = $tagList;
419 if ( $request->getBool( 'wpRemoveAllTags' ) ) {
420 $tagsToRemove = $existingTags;
421 } else {
422 $tagsToRemove = $request->getArray( 'wpTagsToRemove' );
423 }
424 } else {
425 // single revision selected
426 // The user tells us which tags they want associated to the revision.
427 // We have to figure out which ones to add, and which to remove.
428 $tagsToAdd = array_diff( $tagList, $existingTags );
429 $tagsToRemove = array_diff( $existingTags, $tagList );
430 }
431
432 if ( !$tagsToAdd && !$tagsToRemove ) {
433 $status = Status::newFatal( 'tags-edit-none-selected' );
434 } else {
435 $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
436 $tagsToRemove, null, $this->reason, $this->getUser() );
437 }
438
439 if ( $status->isGood() ) {
440 $this->success();
441 return true;
442 } else {
443 $this->failure( $status );
444 return false;
445 }
446 }
447
451 protected function success() {
452 $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
453 $this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>",
454 'tags-edit-success' );
455 $this->wasSaved = true;
456 $this->revList->reloadFromMaster();
457 $this->reason = ''; // no need to spew the reason back at the user
458 $this->showForm();
459 }
460
465 protected function failure( $status ) {
466 $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
467 $this->getOutput()->wrapWikiTextAsInterface(
468 'errorbox', $status->getWikiText( 'tags-edit-failure', false, $this->getLanguage() )
469 );
470 $this->showForm();
471 }
472
473 public function getDescription() {
474 return $this->msg( 'tags-edit-title' )->text();
475 }
476
477 protected function getGroupName() {
478 return 'pagetools';
479 }
480}
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.
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:33
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