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