Switch some HTMLForms in special pages to OOUI
[lhc/web/wiklou.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 extension defined tags
38 */
39 protected $extensionDefinedTags;
40
41 /**
42 * @var array List of extension activated tags
43 */
44 protected $extensionActivatedTags;
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 $userCanEditInterface = $user->isAllowed( 'editinterface' );
81
82 // Show form to create a tag
83 if ( $userCanManage ) {
84 $fields = array(
85 'Tag' => array(
86 'type' => 'text',
87 'label' => $this->msg( 'tags-create-tag-name' )->plain(),
88 'required' => true,
89 ),
90 'Reason' => array(
91 'type' => 'text',
92 'label' => $this->msg( 'tags-create-reason' )->plain(),
93 'size' => 50,
94 ),
95 'IgnoreWarnings' => array(
96 'type' => 'hidden',
97 ),
98 );
99
100 $form = new HTMLForm( $fields, $this->getContext() );
101 $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
102 $form->setWrapperLegendMsg( 'tags-create-heading' );
103 $form->setHeaderText( $this->msg( 'tags-create-explanation' )->plain() );
104 $form->setSubmitCallback( array( $this, 'processCreateTagForm' ) );
105 $form->setSubmitTextMsg( 'tags-create-submit' );
106 $form->show();
107
108 // If processCreateTagForm generated a redirect, there's no point
109 // continuing with this, as the user is just going to end up getting sent
110 // somewhere else. Additionally, if we keep going here, we end up
111 // populating the memcache of tag data (see ChangeTags::listDefinedTags)
112 // with out-of-date data from the slave, because the slave hasn't caught
113 // up to the fact that a new tag has been created as part of an implicit,
114 // as yet uncommitted transaction on master.
115 if ( $out->getRedirect() !== '' ) {
116 return;
117 }
118 }
119
120 // Used to get hitcounts for #doTagRow()
121 $tagStats = ChangeTags::tagUsageStatistics();
122
123 // Used in #doTagRow()
124 $this->explicitlyDefinedTags = array_fill_keys(
125 ChangeTags::listExplicitlyDefinedTags(), true );
126 $this->extensionDefinedTags = array_fill_keys(
127 ChangeTags::listExtensionDefinedTags(), true );
128
129 // List all defined tags, even if they were never applied
130 $definedTags = array_keys( array_merge(
131 $this->explicitlyDefinedTags, $this->extensionDefinedTags ) );
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 ?
146 Xml::tags( 'th', array( 'class' => 'unsortable' ),
147 $this->msg( 'tags-actions-header' )->parse() ) :
148 '' )
149 );
150
151 // Used in #doTagRow()
152 $this->extensionActivatedTags = array_fill_keys(
153 ChangeTags::listExtensionActivatedTags(), 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, $userCanEditInterface );
158 }
159 // Insert tags defined somewhere but never applied
160 foreach ( $definedTags as $tag ) {
161 if ( !isset( $tagStats[$tag] ) ) {
162 $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanEditInterface );
163 }
164 }
165
166 $out->addHTML( Xml::tags(
167 'table',
168 array( 'class' => 'mw-datatable sortable mw-tags-table' ),
169 $html
170 ) );
171 }
172
173 function doTagRow( $tag, $hitcount, $showActions, $showEditLinks ) {
174 $newRow = '';
175 $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
176
177 $disp = ChangeTags::tagDescription( $tag );
178 if ( $showEditLinks ) {
179 $disp .= ' ';
180 $editLink = Linker::link(
181 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
182 $this->msg( 'tags-edit' )->escaped()
183 );
184 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
185 }
186 $newRow .= Xml::tags( 'td', null, $disp );
187
188 $msg = $this->msg( "tag-$tag-description" );
189 $desc = !$msg->exists() ? '' : $msg->parse();
190 if ( $showEditLinks ) {
191 $desc .= ' ';
192 $editDescLink = Linker::link(
193 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
194 $this->msg( 'tags-edit' )->escaped()
195 );
196 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
197 }
198 $newRow .= Xml::tags( 'td', null, $desc );
199
200 $sourceMsgs = array();
201 $isExtension = isset( $this->extensionDefinedTags[$tag] );
202 $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
203 if ( $isExtension ) {
204 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
205 }
206 if ( $isExplicit ) {
207 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
208 }
209 if ( !$sourceMsgs ) {
210 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
211 }
212 $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
213
214 $isActive = $isExplicit || isset( $this->extensionActivatedTags[$tag] );
215 $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
216 $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
217
218 $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped();
219 $hitcountLink = Linker::link(
220 SpecialPage::getTitleFor( 'Recentchanges' ),
221 $hitcountLabel,
222 array(),
223 array( 'tagfilter' => $tag )
224 );
225
226 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
227 $newRow .= Xml::tags( 'td', array( 'data-sort-value' => $hitcount ), $hitcountLink );
228
229 // actions
230 if ( $showActions ) { // we've already checked that the user had the requisite userright
231 $actionLinks = array();
232
233 // delete
234 if ( ChangeTags::canDeleteTag( $tag )->isOK() ) {
235 $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ),
236 $this->msg( 'tags-delete' )->escaped(),
237 array(),
238 array( 'tag' => $tag ) );
239 }
240
241 // activate
242 if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
243 $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'activate' ),
244 $this->msg( 'tags-activate' )->escaped(),
245 array(),
246 array( 'tag' => $tag ) );
247 }
248
249 // deactivate
250 if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
251 $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'deactivate' ),
252 $this->msg( 'tags-deactivate' )->escaped(),
253 array(),
254 array( 'tag' => $tag ) );
255 }
256
257 $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
258 }
259
260 return Xml::tags( 'tr', null, $newRow ) . "\n";
261 }
262
263 public function processCreateTagForm( array $data, HTMLForm $form ) {
264 $context = $form->getContext();
265 $out = $context->getOutput();
266
267 $tag = trim( strval( $data['Tag'] ) );
268 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
269 $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
270 $context->getUser(), $ignoreWarnings );
271
272 if ( $status->isGood() ) {
273 $out->redirect( $this->getPageTitle()->getLocalURL() );
274 return true;
275 } elseif ( $status->isOK() ) {
276 // we have some warnings, so we show a confirmation form
277 $fields = array(
278 'Tag' => array(
279 'type' => 'hidden',
280 'default' => $data['Tag'],
281 ),
282 'Reason' => array(
283 'type' => 'hidden',
284 'default' => $data['Reason'],
285 ),
286 'IgnoreWarnings' => array(
287 'type' => 'hidden',
288 'default' => '1',
289 ),
290 );
291
292 // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
293 // we get into an infinite loop!
294 $context->getRequest()->unsetVal( 'wpEditToken' );
295
296 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
297 count( $status->getWarningsArray() ) )->parseAsBlock() .
298 $out->parse( $status->getWikitext() ) .
299 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
300
301 $subform = new HTMLForm( $fields, $this->getContext() );
302 $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
303 $subform->setWrapperLegendMsg( 'tags-create-heading' );
304 $subform->setHeaderText( $headerText );
305 $subform->setSubmitCallback( array( $this, 'processCreateTagForm' ) );
306 $subform->setSubmitTextMsg( 'htmlform-yes' );
307 $subform->show();
308
309 $out->addBacklinkSubtitle( $this->getPageTitle() );
310 return true;
311 } else {
312 $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
313 "\n</div>" );
314 return false;
315 }
316 }
317
318 protected function showDeleteTagForm( $tag ) {
319 $user = $this->getUser();
320 if ( !$user->isAllowed( 'managechangetags' ) ) {
321 throw new PermissionsError( 'managechangetags' );
322 }
323
324 $out = $this->getOutput();
325 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
326 $out->addBacklinkSubtitle( $this->getPageTitle() );
327
328 // is the tag actually able to be deleted?
329 $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
330 if ( !$canDeleteResult->isGood() ) {
331 $out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() .
332 "\n</div>" );
333 if ( !$canDeleteResult->isOK() ) {
334 return;
335 }
336 }
337
338 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
339 $tagUsage = ChangeTags::tagUsageStatistics();
340 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
341 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
342 $tagUsage[$tag] )->parseAsBlock();
343 }
344 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
345
346 // see if the tag is in use
347 $this->extensionActivatedTags = array_fill_keys(
348 ChangeTags::listExtensionActivatedTags(), true );
349 if ( isset( $this->extensionActivatedTags[$tag] ) ) {
350 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
351 }
352
353 $fields = array();
354 $fields['Reason'] = array(
355 'type' => 'text',
356 'label' => $this->msg( 'tags-delete-reason' )->plain(),
357 'size' => 50,
358 );
359 $fields['HiddenTag'] = array(
360 'type' => 'hidden',
361 'name' => 'tag',
362 'default' => $tag,
363 'required' => true,
364 );
365
366 $form = new HTMLForm( $fields, $this->getContext() );
367 $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
368 $form->tagAction = 'delete'; // custom property on HTMLForm object
369 $form->setSubmitCallback( array( $this, 'processTagForm' ) );
370 $form->setSubmitTextMsg( 'tags-delete-submit' );
371 $form->setSubmitDestructive(); // nasty!
372 $form->addPreText( $preText );
373 $form->show();
374 }
375
376 protected function showActivateDeactivateForm( $tag, $activate ) {
377 $actionStr = $activate ? 'activate' : 'deactivate';
378
379 $user = $this->getUser();
380 if ( !$user->isAllowed( 'managechangetags' ) ) {
381 throw new PermissionsError( 'managechangetags' );
382 }
383
384 $out = $this->getOutput();
385 // tags-activate-title, tags-deactivate-title
386 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
387 $out->addBacklinkSubtitle( $this->getPageTitle() );
388
389 // is it possible to do this?
390 $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
391 $result = ChangeTags::$func( $tag, $user );
392 if ( !$result->isGood() ) {
393 $out->wrapWikiMsg( "<div class=\"error\">\n$1" . $result->getWikiText() .
394 "\n</div>" );
395 if ( !$result->isOK() ) {
396 return;
397 }
398 }
399
400 // tags-activate-question, tags-deactivate-question
401 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
402
403 $fields = array();
404 // tags-activate-reason, tags-deactivate-reason
405 $fields['Reason'] = array(
406 'type' => 'text',
407 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
408 'size' => 50,
409 );
410 $fields['HiddenTag'] = array(
411 'type' => 'hidden',
412 'name' => 'tag',
413 'default' => $tag,
414 'required' => true,
415 );
416
417 $form = new HTMLForm( $fields, $this->getContext() );
418 $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
419 $form->tagAction = $actionStr;
420 $form->setSubmitCallback( array( $this, 'processTagForm' ) );
421 // tags-activate-submit, tags-deactivate-submit
422 $form->setSubmitTextMsg( "tags-$actionStr-submit" );
423 $form->addPreText( $preText );
424 $form->show();
425 }
426
427 public function processTagForm( array $data, HTMLForm $form ) {
428 $context = $form->getContext();
429 $out = $context->getOutput();
430
431 $tag = $data['HiddenTag'];
432 $status = call_user_func( array( 'ChangeTags', "{$form->tagAction}TagWithChecks" ),
433 $tag, $data['Reason'], $context->getUser(), true );
434
435 if ( $status->isGood() ) {
436 $out->redirect( $this->getPageTitle()->getLocalURL() );
437 return true;
438 } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
439 // deletion succeeded, but hooks raised a warning
440 $out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag,
441 count( $status->getWarningsArray() ) )->text() . "\n" .
442 $status->getWikitext() );
443 $out->addReturnTo( $this->getPageTitle() );
444 return true;
445 } else {
446 $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
447 "\n</div>" );
448 return false;
449 }
450 }
451
452 protected function getGroupName() {
453 return 'changes';
454 }
455 }