]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/specials/SpecialTags.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / includes / specials / SpecialTags.php
1 <?php
2 /**
3  * Implements Special:Tags
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup SpecialPage
22  */
23
24 /**
25  * A special page that lists tags for edits
26  *
27  * @ingroup SpecialPage
28  */
29 class SpecialTags extends SpecialPage {
30
31         /**
32          * @var array List of explicitly defined tags
33          */
34         protected $explicitlyDefinedTags;
35
36         /**
37          * @var array List of software defined tags
38          */
39         protected $softwareDefinedTags;
40
41         /**
42          * @var array List of software activated tags
43          */
44         protected $softwareActivatedTags;
45
46         function __construct() {
47                 parent::__construct( 'Tags' );
48         }
49
50         function execute( $par ) {
51                 $this->setHeaders();
52                 $this->outputHeader();
53
54                 $request = $this->getRequest();
55                 switch ( $par ) {
56                         case 'delete':
57                                 $this->showDeleteTagForm( $request->getVal( 'tag' ) );
58                                 break;
59                         case 'activate':
60                                 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
61                                 break;
62                         case 'deactivate':
63                                 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
64                                 break;
65                         case 'create':
66                                 // fall through, thanks to HTMLForm's logic
67                         default:
68                                 $this->showTagList();
69                                 break;
70                 }
71         }
72
73         function showTagList() {
74                 $out = $this->getOutput();
75                 $out->setPageTitle( $this->msg( 'tags-title' ) );
76                 $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
77
78                 $user = $this->getUser();
79                 $userCanManage = $user->isAllowed( 'managechangetags' );
80                 $userCanDelete = $user->isAllowed( 'deletechangetags' );
81                 $userCanEditInterface = $user->isAllowed( 'editinterface' );
82
83                 // Show form to create a tag
84                 if ( $userCanManage ) {
85                         $fields = [
86                                 'Tag' => [
87                                         'type' => 'text',
88                                         'label' => $this->msg( 'tags-create-tag-name' )->plain(),
89                                         'required' => true,
90                                 ],
91                                 'Reason' => [
92                                         'type' => 'text',
93                                         'label' => $this->msg( 'tags-create-reason' )->plain(),
94                                         'size' => 50,
95                                 ],
96                                 'IgnoreWarnings' => [
97                                         'type' => 'hidden',
98                                 ],
99                         ];
100
101                         $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
102                         $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
103                         $form->setWrapperLegendMsg( 'tags-create-heading' );
104                         $form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
105                         $form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
106                         $form->setSubmitTextMsg( 'tags-create-submit' );
107                         $form->show();
108
109                         // If processCreateTagForm generated a redirect, there's no point
110                         // continuing with this, as the user is just going to end up getting sent
111                         // somewhere else. Additionally, if we keep going here, we end up
112                         // populating the memcache of tag data (see ChangeTags::listDefinedTags)
113                         // with out-of-date data from the replica DB, because the replica DB hasn't caught
114                         // up to the fact that a new tag has been created as part of an implicit,
115                         // as yet uncommitted transaction on master.
116                         if ( $out->getRedirect() !== '' ) {
117                                 return;
118                         }
119                 }
120
121                 // Used to get hitcounts for #doTagRow()
122                 $tagStats = ChangeTags::tagUsageStatistics();
123
124                 // Used in #doTagRow()
125                 $this->explicitlyDefinedTags = array_fill_keys(
126                         ChangeTags::listExplicitlyDefinedTags(), true );
127                 $this->softwareDefinedTags = array_fill_keys(
128                         ChangeTags::listSoftwareDefinedTags(), true );
129
130                 // List all defined tags, even if they were never applied
131                 $definedTags = array_keys( $this->explicitlyDefinedTags + $this->softwareDefinedTags );
132
133                 // Show header only if there exists atleast one tag
134                 if ( !$tagStats && !$definedTags ) {
135                         return;
136                 }
137
138                 // Write the headers
139                 $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
140                         Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
141                         Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
142                         Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
143                         Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
144                         Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
145                         ( ( $userCanManage || $userCanDelete ) ?
146                                 Xml::tags( 'th', [ 'class' => 'unsortable' ],
147                                         $this->msg( 'tags-actions-header' )->parse() ) :
148                                 '' )
149                 );
150
151                 // Used in #doTagRow()
152                 $this->softwareActivatedTags = array_fill_keys(
153                         ChangeTags::listSoftwareActivatedTags(), true );
154
155                 // Insert tags that have been applied at least once
156                 foreach ( $tagStats as $tag => $hitcount ) {
157                         $html .= $this->doTagRow( $tag, $hitcount, $userCanManage,
158                                 $userCanDelete, $userCanEditInterface );
159                 }
160                 // Insert tags defined somewhere but never applied
161                 foreach ( $definedTags as $tag ) {
162                         if ( !isset( $tagStats[$tag] ) ) {
163                                 $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
164                         }
165                 }
166
167                 $out->addHTML( Xml::tags(
168                         'table',
169                         [ 'class' => 'mw-datatable sortable mw-tags-table' ],
170                         $html
171                 ) );
172         }
173
174         function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
175                 $newRow = '';
176                 $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
177
178                 $linkRenderer = $this->getLinkRenderer();
179                 $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
180                 if ( $showEditLinks ) {
181                         $disp .= ' ';
182                         $editLink = $linkRenderer->makeLink(
183                                 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
184                                 $this->msg( 'tags-edit' )->text()
185                         );
186                         $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
187                 }
188                 $newRow .= Xml::tags( 'td', null, $disp );
189
190                 $msg = $this->msg( "tag-$tag-description" );
191                 $desc = !$msg->exists() ? '' : $msg->parse();
192                 if ( $showEditLinks ) {
193                         $desc .= ' ';
194                         $editDescLink = $linkRenderer->makeLink(
195                                 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
196                                 $this->msg( 'tags-edit' )->text()
197                         );
198                         $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
199                 }
200                 $newRow .= Xml::tags( 'td', null, $desc );
201
202                 $sourceMsgs = [];
203                 $isSoftware = isset( $this->softwareDefinedTags[$tag] );
204                 $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
205                 if ( $isSoftware ) {
206                         // TODO: Rename this message
207                         $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
208                 }
209                 if ( $isExplicit ) {
210                         $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
211                 }
212                 if ( !$sourceMsgs ) {
213                         $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
214                 }
215                 $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
216
217                 $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
218                 $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
219                 $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
220
221                 $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
222                 if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
223                         $hitcountLabel = $linkRenderer->makeLink(
224                                 SpecialPage::getTitleFor( 'Recentchanges' ),
225                                 $hitcountLabelMsg->text(),
226                                 [],
227                                 [ 'tagfilter' => $tag ]
228                         );
229                 } else {
230                         $hitcountLabel = $hitcountLabelMsg->escaped();
231                 }
232
233                 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
234                 $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
235
236                 // actions
237                 $actionLinks = [];
238
239                 // delete
240                 if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
241                         $actionLinks[] = $linkRenderer->makeKnownLink(
242                                 $this->getPageTitle( 'delete' ),
243                                 $this->msg( 'tags-delete' )->text(),
244                                 [],
245                                 [ 'tag' => $tag ] );
246                 }
247
248                 if ( $showManageActions ) { // we've already checked that the user had the requisite userright
249                         // activate
250                         if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
251                                 $actionLinks[] = $linkRenderer->makeKnownLink(
252                                         $this->getPageTitle( 'activate' ),
253                                         $this->msg( 'tags-activate' )->text(),
254                                         [],
255                                         [ 'tag' => $tag ] );
256                         }
257
258                         // deactivate
259                         if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
260                                 $actionLinks[] = $linkRenderer->makeKnownLink(
261                                         $this->getPageTitle( 'deactivate' ),
262                                         $this->msg( 'tags-deactivate' )->text(),
263                                         [],
264                                         [ 'tag' => $tag ] );
265                         }
266                 }
267
268                 if ( $showDeleteActions || $showManageActions ) {
269                         $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
270                 }
271
272                 return Xml::tags( 'tr', null, $newRow ) . "\n";
273         }
274
275         public function processCreateTagForm( array $data, HTMLForm $form ) {
276                 $context = $form->getContext();
277                 $out = $context->getOutput();
278
279                 $tag = trim( strval( $data['Tag'] ) );
280                 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
281                 $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
282                         $context->getUser(), $ignoreWarnings );
283
284                 if ( $status->isGood() ) {
285                         $out->redirect( $this->getPageTitle()->getLocalURL() );
286                         return true;
287                 } elseif ( $status->isOK() ) {
288                         // we have some warnings, so we show a confirmation form
289                         $fields = [
290                                 'Tag' => [
291                                         'type' => 'hidden',
292                                         'default' => $data['Tag'],
293                                 ],
294                                 'Reason' => [
295                                         'type' => 'hidden',
296                                         'default' => $data['Reason'],
297                                 ],
298                                 'IgnoreWarnings' => [
299                                         'type' => 'hidden',
300                                         'default' => '1',
301                                 ],
302                         ];
303
304                         // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
305                         // we get into an infinite loop!
306                         $context->getRequest()->unsetVal( 'wpEditToken' );
307
308                         $headerText = $this->msg( 'tags-create-warnings-above', $tag,
309                                 count( $status->getWarningsArray() ) )->parseAsBlock() .
310                                 $out->parse( $status->getWikiText() ) .
311                                 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
312
313                         $subform = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
314                         $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
315                         $subform->setWrapperLegendMsg( 'tags-create-heading' );
316                         $subform->setHeaderText( $headerText );
317                         $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
318                         $subform->setSubmitTextMsg( 'htmlform-yes' );
319                         $subform->show();
320
321                         $out->addBacklinkSubtitle( $this->getPageTitle() );
322                         return true;
323                 } else {
324                         $out->addWikiText( "<div class=\"error\">\n" . $status->getWikiText() .
325                                 "\n</div>" );
326                         return false;
327                 }
328         }
329
330         protected function showDeleteTagForm( $tag ) {
331                 $user = $this->getUser();
332                 if ( !$user->isAllowed( 'deletechangetags' ) ) {
333                         throw new PermissionsError( 'deletechangetags' );
334                 }
335
336                 $out = $this->getOutput();
337                 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
338                 $out->addBacklinkSubtitle( $this->getPageTitle() );
339
340                 // is the tag actually able to be deleted?
341                 $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
342                 if ( !$canDeleteResult->isGood() ) {
343                         $out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() .
344                                 "\n</div>" );
345                         if ( !$canDeleteResult->isOK() ) {
346                                 return;
347                         }
348                 }
349
350                 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
351                 $tagUsage = ChangeTags::tagUsageStatistics();
352                 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
353                         $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
354                                 $tagUsage[$tag] )->parseAsBlock();
355                 }
356                 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
357
358                 // see if the tag is in use
359                 $this->softwareActivatedTags = array_fill_keys(
360                         ChangeTags::listSoftwareActivatedTags(), true );
361                 if ( isset( $this->softwareActivatedTags[$tag] ) ) {
362                         $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
363                 }
364
365                 $fields = [];
366                 $fields['Reason'] = [
367                         'type' => 'text',
368                         'label' => $this->msg( 'tags-delete-reason' )->plain(),
369                         'size' => 50,
370                 ];
371                 $fields['HiddenTag'] = [
372                         'type' => 'hidden',
373                         'name' => 'tag',
374                         'default' => $tag,
375                         'required' => true,
376                 ];
377
378                 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
379                 $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
380                 $form->tagAction = 'delete'; // custom property on HTMLForm object
381                 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
382                 $form->setSubmitTextMsg( 'tags-delete-submit' );
383                 $form->setSubmitDestructive(); // nasty!
384                 $form->addPreText( $preText );
385                 $form->show();
386         }
387
388         protected function showActivateDeactivateForm( $tag, $activate ) {
389                 $actionStr = $activate ? 'activate' : 'deactivate';
390
391                 $user = $this->getUser();
392                 if ( !$user->isAllowed( 'managechangetags' ) ) {
393                         throw new PermissionsError( 'managechangetags' );
394                 }
395
396                 $out = $this->getOutput();
397                 // tags-activate-title, tags-deactivate-title
398                 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
399                 $out->addBacklinkSubtitle( $this->getPageTitle() );
400
401                 // is it possible to do this?
402                 $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
403                 $result = ChangeTags::$func( $tag, $user );
404                 if ( !$result->isGood() ) {
405                         $out->addWikiText( "<div class=\"error\">\n" . $result->getWikiText() .
406                                 "\n</div>" );
407                         if ( !$result->isOK() ) {
408                                 return;
409                         }
410                 }
411
412                 // tags-activate-question, tags-deactivate-question
413                 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
414
415                 $fields = [];
416                 // tags-activate-reason, tags-deactivate-reason
417                 $fields['Reason'] = [
418                         'type' => 'text',
419                         'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
420                         'size' => 50,
421                 ];
422                 $fields['HiddenTag'] = [
423                         'type' => 'hidden',
424                         'name' => 'tag',
425                         'default' => $tag,
426                         'required' => true,
427                 ];
428
429                 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
430                 $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
431                 $form->tagAction = $actionStr;
432                 $form->setSubmitCallback( [ $this, 'processTagForm' ] );
433                 // tags-activate-submit, tags-deactivate-submit
434                 $form->setSubmitTextMsg( "tags-$actionStr-submit" );
435                 $form->addPreText( $preText );
436                 $form->show();
437         }
438
439         public function processTagForm( array $data, HTMLForm $form ) {
440                 $context = $form->getContext();
441                 $out = $context->getOutput();
442
443                 $tag = $data['HiddenTag'];
444                 $status = call_user_func( [ 'ChangeTags', "{$form->tagAction}TagWithChecks" ],
445                         $tag, $data['Reason'], $context->getUser(), true );
446
447                 if ( $status->isGood() ) {
448                         $out->redirect( $this->getPageTitle()->getLocalURL() );
449                         return true;
450                 } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
451                         // deletion succeeded, but hooks raised a warning
452                         $out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag,
453                                 count( $status->getWarningsArray() ) )->text() . "\n" .
454                                 $status->getWikitext() );
455                         $out->addReturnTo( $this->getPageTitle() );
456                         return true;
457                 } else {
458                         $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
459                                 "\n</div>" );
460                         return false;
461                 }
462         }
463
464         /**
465          * Return an array of subpages that this special page will accept.
466          *
467          * @return string[] subpages
468          */
469         public function getSubpagesForPrefixSearch() {
470                 // The subpages does not have an own form, so not listing it at the moment
471                 return [
472                         // 'delete',
473                         // 'activate',
474                         // 'deactivate',
475                         // 'create',
476                 ];
477         }
478
479         protected function getGroupName() {
480                 return 'changes';
481         }
482 }