Merge "Remove gen from RawAction."
[lhc/web/wiklou.git] / includes / specials / SpecialEditTags.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup SpecialPage
20 */
21
22 /**
23 * Special page for adding and removing change tags to individual revisions.
24 * A lot of this is copied out of SpecialRevisiondelete.
25 *
26 * @ingroup SpecialPage
27 * @since 1.25
28 */
29 class SpecialEditTags extends UnlistedSpecialPage {
30 /** @var bool Was the DB modified in this request */
31 protected $wasSaved = false;
32
33 /** @var bool True if the submit button was clicked, and the form was posted */
34 private $submitClicked;
35
36 /** @var array Target ID list */
37 private $ids;
38
39 /** @var Title Title object for target parameter */
40 private $targetObj;
41
42 /** @var string Deletion type, may be revision or logentry */
43 private $typeName;
44
45 /** @var ChangeTagsList Storing the list of items to be tagged */
46 private $revList;
47
48 /** @var bool Whether user is allowed to perform the action */
49 private $isAllowed;
50
51 /** @var string */
52 private $reason;
53
54 public function __construct() {
55 parent::__construct( 'EditTags', 'changetags' );
56 }
57
58 public function execute( $par ) {
59 $this->checkPermissions();
60 $this->checkReadOnly();
61
62 $output = $this->getOutput();
63 $user = $this->getUser();
64 $request = $this->getRequest();
65
66 // Check blocks
67 if ( $user->isBlocked() ) {
68 throw new UserBlockedError( $user->getBlock() );
69 }
70
71 $this->setHeaders();
72 $this->outputHeader();
73
74 $this->getOutput()->addModules( array( 'mediawiki.special.edittags',
75 'mediawiki.special.edittags.styles' ) );
76
77 $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
78
79 // Handle our many different possible input types
80 $ids = $request->getVal( 'ids' );
81 if ( !is_null( $ids ) ) {
82 // Allow CSV from the form hidden field, or a single ID for show/hide links
83 $this->ids = explode( ',', $ids );
84 } else {
85 // Array input
86 $this->ids = array_keys( $request->getArray( 'ids', array() ) );
87 }
88 $this->ids = array_unique( array_filter( $this->ids ) );
89
90 // No targets?
91 if ( count( $this->ids ) == 0 ) {
92 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
93 }
94
95 $this->typeName = $request->getVal( 'type' );
96 $this->targetObj = Title::newFromText( $request->getText( 'target' ) );
97
98 // sanity check of parameter
99 switch ( $this->typeName ) {
100 case 'logentry':
101 case 'logging':
102 $this->typeName = 'logentry';
103 break;
104 default:
105 $this->typeName = 'revision';
106 break;
107 }
108
109 // Allow the list type to adjust the passed target
110 // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
111 // what we want
112 $this->targetObj = RevisionDeleter::suggestTarget(
113 $this->typeName === 'revision' ? 'revision' : 'logging',
114 $this->targetObj,
115 $this->ids
116 );
117
118 $this->isAllowed = $user->isAllowed( 'changetags' );
119
120 $this->reason = $request->getVal( 'wpReason' );
121 // We need a target page!
122 if ( is_null( $this->targetObj ) ) {
123 $output->addWikiMsg( 'undelete-header' );
124 return;
125 }
126 // Give a link to the logs/hist for this page
127 $this->showConvenienceLinks();
128
129 // Either submit or create our form
130 if ( $this->isAllowed && $this->submitClicked ) {
131 $this->submit();
132 } else {
133 $this->showForm();
134 }
135
136 // Show relevant lines from the tag log
137 $tagLogPage = new LogPage( 'tag' );
138 $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
139 LogEventsList::showLogExtract(
140 $output,
141 'tag',
142 $this->targetObj,
143 '', /* user */
144 array( 'lim' => 25, 'conds' => array(), 'useMaster' => $this->wasSaved )
145 );
146 }
147
148 /**
149 * Show some useful links in the subtitle
150 */
151 protected function showConvenienceLinks() {
152 // Give a link to the logs/hist for this page
153 if ( $this->targetObj ) {
154 // Also set header tabs to be for the target.
155 $this->getSkin()->setRelevantTitle( $this->targetObj );
156
157 $links = array();
158 $links[] = Linker::linkKnown(
159 SpecialPage::getTitleFor( 'Log' ),
160 $this->msg( 'viewpagelogs' )->escaped(),
161 array(),
162 array(
163 'page' => $this->targetObj->getPrefixedText(),
164 'hide_tag_log' => '0',
165 )
166 );
167 if ( !$this->targetObj->isSpecialPage() ) {
168 // Give a link to the page history
169 $links[] = Linker::linkKnown(
170 $this->targetObj,
171 $this->msg( 'pagehist' )->escaped(),
172 array(),
173 array( 'action' => 'history' )
174 );
175 }
176 // Link to Special:Tags
177 $links[] = Linker::linkKnown(
178 SpecialPage::getTitleFor( 'Tags' ),
179 $this->msg( 'tags-edit-manage-link' )->escaped()
180 );
181 // Logs themselves don't have histories or archived revisions
182 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
183 }
184 }
185
186 /**
187 * Get the list object for this request
188 * @return ChangeTagsList
189 */
190 protected function getList() {
191 if ( is_null( $this->revList ) ) {
192 $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
193 $this->targetObj, $this->ids );
194 }
195
196 return $this->revList;
197 }
198
199 /**
200 * Show a list of items that we will operate on, and show a form which allows
201 * the user to modify the tags applied to those items.
202 */
203 protected function showForm() {
204 $userAllowed = true;
205
206 $out = $this->getOutput();
207 // Messages: tags-edit-revision-selected, tags-edit-logentry-selected
208 $out->wrapWikiMsg( "<strong>$1</strong>", array(
209 "tags-edit-{$this->typeName}-selected",
210 $this->getLanguage()->formatNum( count( $this->ids ) ),
211 $this->targetObj->getPrefixedText()
212 ) );
213
214 $this->addHelpLink( 'Help:Tags' );
215 $out->addHTML( "<ul>" );
216
217 $numRevisions = 0;
218 // Live revisions...
219 $list = $this->getList();
220 // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
221 for ( $list->reset(); $list->current(); $list->next() ) {
222 // @codingStandardsIgnoreEnd
223 $item = $list->current();
224 $numRevisions++;
225 $out->addHTML( $item->getHTML() );
226 }
227
228 if ( !$numRevisions ) {
229 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
230 }
231
232 $out->addHTML( "</ul>" );
233 // Explanation text
234 $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
235
236 // Show form if the user can submit
237 if ( $this->isAllowed ) {
238 $form = Xml::openElement( 'form', array( 'method' => 'post',
239 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ),
240 'id' => 'mw-revdel-form-revisions' ) ) .
241 Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend",
242 count( $this->ids ) )->text() ) .
243 $this->buildCheckBoxes() .
244 Xml::openElement( 'table' ) .
245 "<tr>\n" .
246 '<td class="mw-label">' .
247 Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
248 '</td>' .
249 '<td class="mw-input">' .
250 Xml::input(
251 'wpReason',
252 60,
253 $this->reason,
254 array( 'id' => 'wpReason', 'maxlength' => 100 )
255 ) .
256 '</td>' .
257 "</tr><tr>\n" .
258 '<td></td>' .
259 '<td class="mw-submit">' .
260 Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
261 $numRevisions )->text(), array( 'name' => 'wpSubmit' ) ) .
262 '</td>' .
263 "</tr>\n" .
264 Xml::closeElement( 'table' ) .
265 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
266 Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
267 Html::hidden( 'type', $this->typeName ) .
268 Html::hidden( 'ids', implode( ',', $this->ids ) ) .
269 Xml::closeElement( 'fieldset' ) . "\n" .
270 Xml::closeElement( 'form' ) . "\n";
271 } else {
272 $form = '';
273 }
274 $out->addHTML( $form );
275 }
276
277 /**
278 * @return string HTML
279 */
280 protected function buildCheckBoxes() {
281 // If there is just one item, provide the user with a multi-select field
282 $list = $this->getList();
283 $tags = array();
284 if ( $list->length() == 1 ) {
285 $list->reset();
286 $tags = $list->current()->getTags();
287 if ( $tags ) {
288 $tags = explode( ',', $tags );
289 } else {
290 $tags = array();
291 }
292
293 $html = '<table id="mw-edittags-tags-selector">';
294 $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
295 '</td><td>';
296 if ( $tags ) {
297 $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
298 } else {
299 $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
300 }
301 $html .= '</td></tr>';
302 $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
303 $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
304 } else {
305 // Otherwise, use a multi-select field for adding tags, and a list of
306 // checkboxes for removing them
307
308 // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
309 for ( $list->reset(); $list->current(); $list->next() ) {
310 // @codingStandardsIgnoreEnd
311 $currentTags = $list->current()->getTags();
312 if ( $currentTags ) {
313 $tags = array_merge( $tags, explode( ',', $currentTags ) );
314 }
315 }
316 $tags = array_unique( $tags );
317
318 $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
319 $tagSelect = $this->getTagSelect( array(), $this->msg( 'tags-edit-add' )->plain() );
320 $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
321 $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
322 $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
323 'wpRemoveAllTags', 'mw-edittags-remove-all' );
324 $i = 0; // used for generating checkbox IDs only
325 foreach ( $tags as $tag ) {
326 $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
327 'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, array(
328 'value' => $tag,
329 'class' => 'mw-edittags-remove-checkbox',
330 ) );
331 }
332 }
333
334 // also output the tags currently applied as a hidden form field, so we
335 // know what to remove from the revision/log entry when the form is submitted
336 $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
337 $html .= '</td></tr></table>';
338
339 return $html;
340 }
341
342 /**
343 * Returns a <select multiple> element with a list of change tags that can be
344 * applied by users.
345 *
346 * @param array $selectedTags The tags that should be preselected in the
347 * list. Any tags in this list, but not in the list returned by
348 * ChangeTags::listExplicitlyDefinedTags, will be appended to the <select>
349 * element.
350 * @param string $label The text of a <label> to precede the <select>
351 * @return array HTML <label> element at index 0, HTML <select> element at
352 * index 1
353 */
354 protected function getTagSelect( $selectedTags, $label ) {
355 $result = array();
356 $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
357
358 $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
359 $select->setAttribute( 'multiple', 'multiple' );
360 $select->setAttribute( 'size', '8' );
361
362 $tags = ChangeTags::listExplicitlyDefinedTags();
363 $tags = array_unique( array_merge( $tags, $selectedTags ) );
364
365 // Values of $tags are also used as <option> labels
366 $select->addOptions( array_combine( $tags, $tags ) );
367
368 $result[1] = $select->getHTML();
369 return $result;
370 }
371
372 /**
373 * UI entry point for form submission.
374 * @throws PermissionsError
375 * @return bool
376 */
377 protected function submit() {
378 // Check edit token on submission
379 $request = $this->getRequest();
380 $token = $request->getVal( 'wpEditToken' );
381 if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
382 $this->getOutput()->addWikiMsg( 'sessionfailure' );
383 return false;
384 }
385
386 // Evaluate incoming request data
387 $tagList = $request->getArray( 'wpTagList' );
388 if ( is_null( $tagList ) ) {
389 $tagList = array();
390 }
391 $existingTags = $request->getVal( 'wpExistingTags' );
392 if ( is_null( $existingTags ) || $existingTags === '' ) {
393 $existingTags = array();
394 } else {
395 $existingTags = explode( ',', $existingTags );
396 }
397
398 if ( count( $this->ids ) > 1 ) {
399 // multiple revisions selected
400 $tagsToAdd = $tagList;
401 if ( $request->getBool( 'wpRemoveAllTags' ) ) {
402 $tagsToRemove = $existingTags;
403 } else {
404 $tagsToRemove = $request->getArray( 'wpTagsToRemove' );
405 }
406 } else {
407 // single revision selected
408 // The user tells us which tags they want associated to the revision.
409 // We have to figure out which ones to add, and which to remove.
410 $tagsToAdd = array_diff( $tagList, $existingTags );
411 $tagsToRemove = array_diff( $existingTags, $tagList );
412 }
413
414 if ( !$tagsToAdd && !$tagsToRemove ) {
415 $status = Status::newFatal( 'tags-edit-none-selected' );
416 } else {
417 $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
418 $tagsToRemove, null, $this->reason, $this->getUser() );
419 }
420
421 if ( $status->isGood() ) {
422 $this->success();
423 return true;
424 } else {
425 $this->failure( $status );
426 return false;
427 }
428 }
429
430 /**
431 * Report that the submit operation succeeded
432 */
433 protected function success() {
434 $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
435 $this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>",
436 'tags-edit-success' );
437 $this->wasSaved = true;
438 $this->revList->reloadFromMaster();
439 $this->reason = ''; // no need to spew the reason back at the user
440 $this->showForm();
441 }
442
443 /**
444 * Report that the submit operation failed
445 * @param Status $status
446 */
447 protected function failure( $status ) {
448 $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
449 $this->getOutput()->addWikiText( '<div class="errorbox">' .
450 $status->getWikiText( 'tags-edit-failure' ) .
451 '</div>'
452 );
453 $this->showForm();
454 }
455
456 public function getDescription() {
457 return $this->msg( 'tags-edit-title' )->text();
458 }
459
460 protected function getGroupName() {
461 return 'pagetools';
462 }
463 }