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.
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.
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
19 * @ingroup SpecialPage
23 * Special page for adding and removing change tags to individual revisions.
24 * A lot of this is copied out of SpecialRevisiondelete.
26 * @ingroup SpecialPage
29 class SpecialEditTags
extends UnlistedSpecialPage
{
30 /** @var bool Was the DB modified in this request */
31 protected $wasSaved = false;
33 /** @var bool True if the submit button was clicked, and the form was posted */
34 private $submitClicked;
36 /** @var array Target ID list */
39 /** @var Title Title object for target parameter */
42 /** @var string Deletion type, may be revision or logentry */
45 /** @var ChangeTagsList Storing the list of items to be tagged */
48 /** @var bool Whether user is allowed to perform the action */
54 public function __construct() {
55 parent
::__construct( 'EditTags', 'changetags' );
58 public function execute( $par ) {
59 $this->checkPermissions();
60 $this->checkReadOnly();
62 $output = $this->getOutput();
63 $user = $this->getUser();
64 $request = $this->getRequest();
67 $this->outputHeader();
69 $this->getOutput()->addModules( array( 'mediawiki.special.edittags',
70 'mediawiki.special.edittags.styles' ) );
72 $this->submitClicked
= $request->wasPosted() && $request->getBool( 'wpSubmit' );
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 );
81 $this->ids
= array_keys( $request->getArray( 'ids', array() ) );
83 $this->ids
= array_unique( array_filter( $this->ids
) );
86 if ( count( $this->ids
) == 0 ) {
87 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
90 $this->typeName
= $request->getVal( 'type' );
91 $this->targetObj
= Title
::newFromText( $request->getText( 'target' ) );
93 // sanity check of parameter
94 switch ( $this->typeName
) {
97 $this->typeName
= 'logentry';
100 $this->typeName
= 'revision';
104 // Allow the list type to adjust the passed target
105 // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
107 $this->targetObj
= RevisionDeleter
::suggestTarget(
108 $this->typeName
=== 'revision' ?
'revision' : 'logging',
113 $this->isAllowed
= $user->isAllowed( 'changetags' );
115 $this->reason
= $request->getVal( 'wpReason' );
116 // We need a target page!
117 if ( is_null( $this->targetObj
) ) {
118 $output->addWikiMsg( 'undelete-header' );
121 // Give a link to the logs/hist for this page
122 $this->showConvenienceLinks();
124 // Either submit or create our form
125 if ( $this->isAllowed
&& $this->submitClicked
) {
126 $this->submit( $request );
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(
139 array( 'lim' => 25, 'conds' => array(), 'useMaster' => $this->wasSaved
)
144 * Show some useful links in the subtitle
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
);
153 $links[] = Linker
::linkKnown(
154 SpecialPage
::getTitleFor( 'Log' ),
155 $this->msg( 'viewpagelogs' )->escaped(),
158 'page' => $this->targetObj
->getPrefixedText(),
159 'hide_tag_log' => '0',
162 if ( !$this->targetObj
->isSpecialPage() ) {
163 // Give a link to the page history
164 $links[] = Linker
::linkKnown(
166 $this->msg( 'pagehist' )->escaped(),
168 array( 'action' => 'history' )
171 // Link to Special:Tags
172 $links[] = Linker
::linkKnown(
173 SpecialPage
::getTitleFor( 'Tags' ),
174 $this->msg( 'tags-edit-manage-link' )->escaped()
176 // Logs themselves don't have histories or archived revisions
177 $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
182 * Get the list object for this request
183 * @return ChangeTagsList
185 protected function getList() {
186 if ( is_null( $this->revList
) ) {
187 $this->revList
= ChangeTagsList
::factory( $this->typeName
, $this->getContext(),
188 $this->targetObj
, $this->ids
);
191 return $this->revList
;
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.
198 protected function showForm() {
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()
209 $out->addHelpLink( 'Help:Tags' );
210 $out->addHTML( "<ul>" );
214 $list = $this->getList();
215 // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
216 for ( $list->reset(); $list->current(); $list->next() ) {
217 // @codingStandardsIgnoreEnd
218 $item = $list->current();
220 $out->addHTML( $item->getHTML() );
223 if ( !$numRevisions ) {
224 throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
227 $out->addHTML( "</ul>" );
229 $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
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' ) .
241 '<td class="mw-label">' .
242 Xml
::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
244 '<td class="mw-input">' .
249 array( 'id' => 'wpReason', 'maxlength' => 100 )
254 '<td class="mw-submit">' .
255 Xml
::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
256 $numRevisions )->text(), array( 'name' => 'wpSubmit' ) ) .
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";
269 $out->addHTML( $form );
273 * @return string HTML
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 ) {
280 $tags = $list->current()->getTags();
282 $tags = explode( ',', $tags );
287 $html = '<table id="mw-edittags-tags-selector">';
288 $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
291 $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
293 $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
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>';
303 // Otherwise, use a multi-select field for adding tags, and a list of
304 // checkboxes for removing them
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 ) );
315 $tags = array_unique( $tags );
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(
328 'class' => 'mw-edittags-remove-checkbox',
331 $html .= '</td></tr></table>';
338 * Returns a <select multiple> element with a list of change tags that can be
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>
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
349 protected function getTagSelect( $selectedTags, $label ) {
351 $result[0] = Xml
::label( $label, 'mw-edittags-tag-list' );
352 $result[1] = Xml
::openElement( 'select', array(
353 'name' => 'wpTagList[]',
354 'id' => 'mw-edittags-tag-list',
355 'multiple' => 'multiple',
359 $tags = ChangeTags
::listExplicitlyDefinedTags();
360 $tags = array_unique( array_merge( $tags, $selectedTags ) );
361 foreach ( $tags as $tag ) {
362 $result[1] .= Xml
::option( $tag, $tag, in_array( $tag, $selectedTags ) );
365 $result[1] .= Xml
::closeElement( 'select' );
370 * UI entry point for form submission.
371 * @throws PermissionsError
374 protected function submit() {
375 // Check edit token on submission
376 $request = $this->getRequest();
377 $token = $request->getVal( 'wpEditToken' );
378 if ( $this->submitClicked
&& !$this->getUser()->matchEditToken( $token ) ) {
379 $this->getOutput()->addWikiMsg( 'sessionfailure' );
383 // Evaluate incoming request data
384 $tagList = $request->getArray( 'wpTagList' );
385 if ( is_null( $tagList ) ) {
388 $existingTags = $request->getVal( 'wpExistingTags' );
389 if ( is_null( $existingTags ) ||
$existingTags === '' ) {
390 $existingTags = array();
392 $existingTags = explode( ',', $existingTags );
395 if ( count( $this->ids
) > 1 ) {
396 // multiple revisions selected
397 $tagsToAdd = $tagList;
398 if ( $request->getBool( 'wpRemoveAllTags' ) ) {
399 $tagsToRemove = $existingTags;
401 $tagsToRemove = $request->getArray( 'wpTagsToRemove' );
404 // single revision selected
405 // The user tells us which tags they want associated to the revision.
406 // We have to figure out which ones to add, and which to remove.
407 $tagsToAdd = array_diff( $tagList, $existingTags );
408 $tagsToRemove = array_diff( $existingTags, $tagList );
411 if ( !$tagsToAdd && !$tagsToRemove ) {
412 $status = Status
::newFatal( 'tags-edit-none-selected' );
414 $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
415 $tagsToRemove, null, $this->reason
, $this->getUser() );
418 if ( $status->isGood() ) {
422 $this->failure( $status );
428 * Report that the submit operation succeeded
430 protected function success() {
431 $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
432 $this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>",
433 'tags-edit-success' );
434 $this->wasSaved
= true;
435 $this->revList
->reloadFromMaster();
436 $this->reason
= ''; // no need to spew the reason back at the user
441 * Report that the submit operation failed
442 * @param Status $status
444 protected function failure( $status ) {
445 $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
446 $this->getOutput()->addWikiText( '<div class="errorbox">' .
447 $status->getWikiText( 'tags-edit-failure' ) .
453 public function getDescription() {
454 return $this->msg( 'tags-edit-title' )->text();
457 protected function getGroupName() {