X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/specials/SpecialTags.php diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index c2aecf47..605ee008 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -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( "
", 'tags-intro' ); + function showTagList() { + $out = $this->getOutput(); + $out->setPageTitle( $this->msg( 'tags-title' ) ); + $out->wrapWikiMsg( " ", '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( "