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