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