]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - includes/specials/SpecialTags.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / specials / SpecialTags.php
index c2aecf4731f6dade593c65afaaf0225560a6b9d2..605ee008d857921afe4f2897b34eef562dd1ec2c 100644 (file)
@@ -21,9 +21,6 @@
  * @ingroup SpecialPage
  */
 
-if (!defined('MEDIAWIKI'))
-       die;
-
 /**
  * A special page that lists tags for edits
  *
@@ -31,67 +28,455 @@ if (!defined('MEDIAWIKI'))
  */
 class SpecialTags extends SpecialPage {
 
+       /**
+        * @var array List of explicitly defined tags
+        */
+       protected $explicitlyDefinedTags;
+
+       /**
+        * @var array List of software defined tags
+        */
+       protected $softwareDefinedTags;
+
+       /**
+        * @var array List of software activated tags
+        */
+       protected $softwareActivatedTags;
+
        function __construct() {
                parent::__construct( 'Tags' );
        }
 
        function execute( $par ) {
-               global $wgOut;
+               $this->setHeaders();
+               $this->outputHeader();
+
+               $request = $this->getRequest();
+               switch ( $par ) {
+                       case 'delete':
+                               $this->showDeleteTagForm( $request->getVal( 'tag' ) );
+                               break;
+                       case 'activate':
+                               $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
+                               break;
+                       case 'deactivate':
+                               $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
+                               break;
+                       case 'create':
+                               // fall through, thanks to HTMLForm's logic
+                       default:
+                               $this->showTagList();
+                               break;
+               }
+       }
 
-               $wgOut->setPageTitle( wfMsg( 'tags-title' ) );
-               $wgOut->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
+       function showTagList() {
+               $out = $this->getOutput();
+               $out->setPageTitle( $this->msg( 'tags-title' ) );
+               $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
 
-               // Write the headers
-               $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, wfMsgExt( 'tags-tag', 'parseinline' ) ) .
-                               Xml::tags( 'th', null, wfMsgExt( 'tags-display-header', 'parseinline' ) ) .
-                               Xml::tags( 'th', null, wfMsgExt( 'tags-description-header', 'parseinline' ) ) .
-                               Xml::tags( 'th', null, wfMsgExt( 'tags-hitcount-header', 'parseinline' ) )
-                       );
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select( 'change_tag', array( 'ct_tag', 'count(*) as hitcount' ), array(), __METHOD__, array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ) );
+               $user = $this->getUser();
+               $userCanManage = $user->isAllowed( 'managechangetags' );
+               $userCanDelete = $user->isAllowed( 'deletechangetags' );
+               $userCanEditInterface = $user->isAllowed( 'editinterface' );
 
-               foreach ( $res as $row ) {
-                       $html .= $this->doTagRow( $row->ct_tag, $row->hitcount );
-               }
+               // Show form to create a tag
+               if ( $userCanManage ) {
+                       $fields = [
+                               'Tag' => [
+                                       'type' => 'text',
+                                       'label' => $this->msg( 'tags-create-tag-name' )->plain(),
+                                       'required' => true,
+                               ],
+                               'Reason' => [
+                                       'type' => 'text',
+                                       'label' => $this->msg( 'tags-create-reason' )->plain(),
+                                       'size' => 50,
+                               ],
+                               'IgnoreWarnings' => [
+                                       'type' => 'hidden',
+                               ],
+                       ];
 
-               foreach( ChangeTags::listDefinedTags() as $tag ) {
-                       $html .= $this->doTagRow( $tag, 0 );
+                       $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
+                       $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
+                       $form->setWrapperLegendMsg( 'tags-create-heading' );
+                       $form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
+                       $form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
+                       $form->setSubmitTextMsg( 'tags-create-submit' );
+                       $form->show();
+
+                       // If processCreateTagForm generated a redirect, there's no point
+                       // continuing with this, as the user is just going to end up getting sent
+                       // somewhere else. Additionally, if we keep going here, we end up
+                       // populating the memcache of tag data (see ChangeTags::listDefinedTags)
+                       // with out-of-date data from the replica DB, because the replica DB hasn't caught
+                       // up to the fact that a new tag has been created as part of an implicit,
+                       // as yet uncommitted transaction on master.
+                       if ( $out->getRedirect() !== '' ) {
+                               return;
+                       }
                }
 
-               $wgOut->addHTML( Xml::tags( 'table', array( 'class' => 'wikitable mw-tags-table' ), $html ) );
-       }
+               // Used to get hitcounts for #doTagRow()
+               $tagStats = ChangeTags::tagUsageStatistics();
+
+               // Used in #doTagRow()
+               $this->explicitlyDefinedTags = array_fill_keys(
+                       ChangeTags::listExplicitlyDefinedTags(), true );
+               $this->softwareDefinedTags = array_fill_keys(
+                       ChangeTags::listSoftwareDefinedTags(), true );
 
-       function doTagRow( $tag, $hitcount ) {
-               static $sk=null, $doneTags=array();
-               if (!$sk) {
-                       global $wgUser;
-                       $sk = $wgUser->getSkin();
+               // List all defined tags, even if they were never applied
+               $definedTags = array_keys( $this->explicitlyDefinedTags + $this->softwareDefinedTags );
+
+               // Show header only if there exists atleast one tag
+               if ( !$tagStats && !$definedTags ) {
+                       return;
                }
 
-               if ( in_array( $tag, $doneTags ) ) {
-                       return '';
+               // Write the headers
+               $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
+                       Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
+                       Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
+                       Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
+                       Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
+                       Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
+                       ( ( $userCanManage || $userCanDelete ) ?
+                               Xml::tags( 'th', [ 'class' => 'unsortable' ],
+                                       $this->msg( 'tags-actions-header' )->parse() ) :
+                               '' )
+               );
+
+               // Used in #doTagRow()
+               $this->softwareActivatedTags = array_fill_keys(
+                       ChangeTags::listSoftwareActivatedTags(), true );
+
+               // Insert tags that have been applied at least once
+               foreach ( $tagStats as $tag => $hitcount ) {
+                       $html .= $this->doTagRow( $tag, $hitcount, $userCanManage,
+                               $userCanDelete, $userCanEditInterface );
+               }
+               // Insert tags defined somewhere but never applied
+               foreach ( $definedTags as $tag ) {
+                       if ( !isset( $tagStats[$tag] ) ) {
+                               $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
+                       }
                }
 
-               global $wgLang;
-               
+               $out->addHTML( Xml::tags(
+                       'table',
+                       [ 'class' => 'mw-datatable sortable mw-tags-table' ],
+                       $html
+               ) );
+       }
+
+       function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
                $newRow = '';
-               $newRow .= Xml::tags( 'td', null, Xml::element( 'tt', null, $tag ) );
+               $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
 
-               $disp = ChangeTags::tagDescription( $tag );
-               $disp .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsgHtml( 'tags-edit' ) ) . ')';
+               $linkRenderer = $this->getLinkRenderer();
+               $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
+               if ( $showEditLinks ) {
+                       $disp .= ' ';
+                       $editLink = $linkRenderer->makeLink(
+                               $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
+                               $this->msg( 'tags-edit' )->text()
+                       );
+                       $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
+               }
                $newRow .= Xml::tags( 'td', null, $disp );
 
-               $desc = wfMsgExt( "tag-$tag-description", 'parseinline' );
-               $desc = wfEmptyMsg( "tag-$tag-description", $desc ) ? '' : $desc;
-               $desc .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsgHtml( 'tags-edit' ) ) . ')';
+               $msg = $this->msg( "tag-$tag-description" );
+               $desc = !$msg->exists() ? '' : $msg->parse();
+               if ( $showEditLinks ) {
+                       $desc .= ' ';
+                       $editDescLink = $linkRenderer->makeLink(
+                               $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
+                               $this->msg( 'tags-edit' )->text()
+                       );
+                       $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
+               }
                $newRow .= Xml::tags( 'td', null, $desc );
 
-               $hitcount = wfMsgExt( 'tags-hitcount', array( 'parsemag' ), $wgLang->formatNum( $hitcount ) );
-               $hitcount = $sk->link( SpecialPage::getTitleFor( 'Recentchanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) );
-               $newRow .= Xml::tags( 'td', null, $hitcount );
+               $sourceMsgs = [];
+               $isSoftware = isset( $this->softwareDefinedTags[$tag] );
+               $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
+               if ( $isSoftware ) {
+                       // TODO: Rename this message
+                       $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
+               }
+               if ( $isExplicit ) {
+                       $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
+               }
+               if ( !$sourceMsgs ) {
+                       $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
+               }
+               $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
+
+               $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
+               $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
+               $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
+
+               $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
+               if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
+                       $hitcountLabel = $linkRenderer->makeLink(
+                               SpecialPage::getTitleFor( 'Recentchanges' ),
+                               $hitcountLabelMsg->text(),
+                               [],
+                               [ 'tagfilter' => $tag ]
+                       );
+               } else {
+                       $hitcountLabel = $hitcountLabelMsg->escaped();
+               }
+
+               // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
+               $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
+
+               // actions
+               $actionLinks = [];
+
+               // delete
+               if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
+                       $actionLinks[] = $linkRenderer->makeKnownLink(
+                               $this->getPageTitle( 'delete' ),
+                               $this->msg( 'tags-delete' )->text(),
+                               [],
+                               [ 'tag' => $tag ] );
+               }
+
+               if ( $showManageActions ) { // we've already checked that the user had the requisite userright
+                       // activate
+                       if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
+                               $actionLinks[] = $linkRenderer->makeKnownLink(
+                                       $this->getPageTitle( 'activate' ),
+                                       $this->msg( 'tags-activate' )->text(),
+                                       [],
+                                       [ 'tag' => $tag ] );
+                       }
 
-               $doneTags[] = $tag;
+                       // deactivate
+                       if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
+                               $actionLinks[] = $linkRenderer->makeKnownLink(
+                                       $this->getPageTitle( 'deactivate' ),
+                                       $this->msg( 'tags-deactivate' )->text(),
+                                       [],
+                                       [ 'tag' => $tag ] );
+                       }
+               }
+
+               if ( $showDeleteActions || $showManageActions ) {
+                       $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
+               }
 
                return Xml::tags( 'tr', null, $newRow ) . "\n";
        }
+
+       public function processCreateTagForm( array $data, HTMLForm $form ) {
+               $context = $form->getContext();
+               $out = $context->getOutput();
+
+               $tag = trim( strval( $data['Tag'] ) );
+               $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
+               $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
+                       $context->getUser(), $ignoreWarnings );
+
+               if ( $status->isGood() ) {
+                       $out->redirect( $this->getPageTitle()->getLocalURL() );
+                       return true;
+               } elseif ( $status->isOK() ) {
+                       // we have some warnings, so we show a confirmation form
+                       $fields = [
+                               'Tag' => [
+                                       'type' => 'hidden',
+                                       'default' => $data['Tag'],
+                               ],
+                               'Reason' => [
+                                       'type' => 'hidden',
+                                       'default' => $data['Reason'],
+                               ],
+                               'IgnoreWarnings' => [
+                                       'type' => 'hidden',
+                                       'default' => '1',
+                               ],
+                       ];
+
+                       // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
+                       // we get into an infinite loop!
+                       $context->getRequest()->unsetVal( 'wpEditToken' );
+
+                       $headerText = $this->msg( 'tags-create-warnings-above', $tag,
+                               count( $status->getWarningsArray() ) )->parseAsBlock() .
+                               $out->parse( $status->getWikiText() ) .
+                               $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
+
+                       $subform = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
+                       $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
+                       $subform->setWrapperLegendMsg( 'tags-create-heading' );
+                       $subform->setHeaderText( $headerText );
+                       $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
+                       $subform->setSubmitTextMsg( 'htmlform-yes' );
+                       $subform->show();
+
+                       $out->addBacklinkSubtitle( $this->getPageTitle() );
+                       return true;
+               } else {
+                       $out->addWikiText( "<div class=\"error\">\n" . $status->getWikiText() .
+                               "\n</div>" );
+                       return false;
+               }
+       }
+
+       protected function showDeleteTagForm( $tag ) {
+               $user = $this->getUser();
+               if ( !$user->isAllowed( 'deletechangetags' ) ) {
+                       throw new PermissionsError( 'deletechangetags' );
+               }
+
+               $out = $this->getOutput();
+               $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
+               $out->addBacklinkSubtitle( $this->getPageTitle() );
+
+               // is the tag actually able to be deleted?
+               $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
+               if ( !$canDeleteResult->isGood() ) {
+                       $out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() .
+                               "\n</div>" );
+                       if ( !$canDeleteResult->isOK() ) {
+                               return;
+                       }
+               }
+
+               $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
+               $tagUsage = ChangeTags::tagUsageStatistics();
+               if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
+                       $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
+                               $tagUsage[$tag] )->parseAsBlock();
+               }
+               $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
+
+               // see if the tag is in use
+               $this->softwareActivatedTags = array_fill_keys(
+                       ChangeTags::listSoftwareActivatedTags(), true );
+               if ( isset( $this->softwareActivatedTags[$tag] ) ) {
+                       $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
+               }
+
+               $fields = [];
+               $fields['Reason'] = [
+                       'type' => 'text',
+                       'label' => $this->msg( 'tags-delete-reason' )->plain(),
+                       'size' => 50,
+               ];
+               $fields['HiddenTag'] = [
+                       'type' => 'hidden',
+                       'name' => 'tag',
+                       'default' => $tag,
+                       'required' => true,
+               ];
+
+               $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
+               $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
+               $form->tagAction = 'delete'; // custom property on HTMLForm object
+               $form->setSubmitCallback( [ $this, 'processTagForm' ] );
+               $form->setSubmitTextMsg( 'tags-delete-submit' );
+               $form->setSubmitDestructive(); // nasty!
+               $form->addPreText( $preText );
+               $form->show();
+       }
+
+       protected function showActivateDeactivateForm( $tag, $activate ) {
+               $actionStr = $activate ? 'activate' : 'deactivate';
+
+               $user = $this->getUser();
+               if ( !$user->isAllowed( 'managechangetags' ) ) {
+                       throw new PermissionsError( 'managechangetags' );
+               }
+
+               $out = $this->getOutput();
+               // tags-activate-title, tags-deactivate-title
+               $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
+               $out->addBacklinkSubtitle( $this->getPageTitle() );
+
+               // is it possible to do this?
+               $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
+               $result = ChangeTags::$func( $tag, $user );
+               if ( !$result->isGood() ) {
+                       $out->addWikiText( "<div class=\"error\">\n" . $result->getWikiText() .
+                               "\n</div>" );
+                       if ( !$result->isOK() ) {
+                               return;
+                       }
+               }
+
+               // tags-activate-question, tags-deactivate-question
+               $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
+
+               $fields = [];
+               // tags-activate-reason, tags-deactivate-reason
+               $fields['Reason'] = [
+                       'type' => 'text',
+                       'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
+                       'size' => 50,
+               ];
+               $fields['HiddenTag'] = [
+                       'type' => 'hidden',
+                       'name' => 'tag',
+                       'default' => $tag,
+                       'required' => true,
+               ];
+
+               $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
+               $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
+               $form->tagAction = $actionStr;
+               $form->setSubmitCallback( [ $this, 'processTagForm' ] );
+               // tags-activate-submit, tags-deactivate-submit
+               $form->setSubmitTextMsg( "tags-$actionStr-submit" );
+               $form->addPreText( $preText );
+               $form->show();
+       }
+
+       public function processTagForm( array $data, HTMLForm $form ) {
+               $context = $form->getContext();
+               $out = $context->getOutput();
+
+               $tag = $data['HiddenTag'];
+               $status = call_user_func( [ 'ChangeTags', "{$form->tagAction}TagWithChecks" ],
+                       $tag, $data['Reason'], $context->getUser(), true );
+
+               if ( $status->isGood() ) {
+                       $out->redirect( $this->getPageTitle()->getLocalURL() );
+                       return true;
+               } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
+                       // deletion succeeded, but hooks raised a warning
+                       $out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag,
+                               count( $status->getWarningsArray() ) )->text() . "\n" .
+                               $status->getWikitext() );
+                       $out->addReturnTo( $this->getPageTitle() );
+                       return true;
+               } else {
+                       $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
+                               "\n</div>" );
+                       return false;
+               }
+       }
+
+       /**
+        * Return an array of subpages that this special page will accept.
+        *
+        * @return string[] subpages
+        */
+       public function getSubpagesForPrefixSearch() {
+               // The subpages does not have an own form, so not listing it at the moment
+               return [
+                       // 'delete',
+                       // 'activate',
+                       // 'deactivate',
+                       // 'create',
+               ];
+       }
+
+       protected function getGroupName() {
+               return 'changes';
+       }
 }