From: This, that and the other Date: Wed, 15 Apr 2015 01:31:53 +0000 (+1000) Subject: UI for adding and removing change tags on revisions and log entries X-Git-Tag: 1.31.0-rc.0~11704 X-Git-Url: http://git.heureux-cyclage.org/?a=commitdiff_plain;h=5c4681012e78a8d5004eea917eba90d448d7e0f3;p=lhc%2Fweb%2Fwiklou.git UI for adding and removing change tags on revisions and log entries There is a new special page, Special:EditTags, which is very similar to Special:RevisionDelete in a lot of ways. In fact, the SpecialEditTags class started off as a copy-paste of SpecialRevisiondelete. You invoke this special page by going to an article history page, checking some revisions, and clicking "Edit tags of selected revisions". Then you pick the modifications you want to make and click "Apply". Very much like the revision deletion workflow. I had to restructure some of the Action routing code, which was only designed to handle revision deletion. Also removing some code from SpecialRevisiondelete which didn't work as advertised in the first place, and definitely doesn't work now. Change-Id: I7d3ef927b5686f6211bc5817776286ead19d916b --- diff --git a/autoload.php b/autoload.php index 2fe805f287..92d60145d8 100644 --- a/autoload.php +++ b/autoload.php @@ -200,6 +200,11 @@ $wgAutoloadLocalClasses = array( 'CgzCopyTransaction' => __DIR__ . '/maintenance/storage/recompressTracked.php', 'ChangePassword' => __DIR__ . '/maintenance/changePassword.php', 'ChangeTags' => __DIR__ . '/includes/ChangeTags.php', + 'ChangeTagsList' => __DIR__ . '/includes/changetags/ChangeTagsList.php', + 'ChangeTagsLogItem' => __DIR__ . '/includes/changetags/ChangeTagsLogItem.php', + 'ChangeTagsLogList' => __DIR__ . '/includes/changetags/ChangeTagsLogList.php', + 'ChangeTagsRevisionItem' => __DIR__ . '/includes/changetags/ChangeTagsRevisionItem.php', + 'ChangeTagsRevisionList' => __DIR__ . '/includes/changetags/ChangeTagsRevisionList.php', 'ChangesFeed' => __DIR__ . '/includes/changes/ChangesFeed.php', 'ChangesList' => __DIR__ . '/includes/changes/ChangesList.php', 'ChangesListSpecialPage' => __DIR__ . '/includes/specialpage/ChangesListSpecialPage.php', @@ -1103,6 +1108,7 @@ $wgAutoloadLocalClasses = array( 'SpecialContributions' => __DIR__ . '/includes/specials/SpecialContributions.php', 'SpecialCreateAccount' => __DIR__ . '/includes/specials/SpecialCreateAccount.php', 'SpecialDiff' => __DIR__ . '/includes/specials/SpecialDiff.php', + 'SpecialEditTags' => __DIR__ . '/includes/specials/SpecialEditTags.php', 'SpecialEditWatchlist' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php', 'SpecialEmailUser' => __DIR__ . '/includes/specials/SpecialEmailuser.php', 'SpecialExpandTemplates' => __DIR__ . '/includes/specials/SpecialExpandTemplates.php', @@ -1126,6 +1132,7 @@ $wgAutoloadLocalClasses = array( 'SpecialNewFiles' => __DIR__ . '/includes/specials/SpecialNewimages.php', 'SpecialNewpages' => __DIR__ . '/includes/specials/SpecialNewpages.php', 'SpecialPage' => __DIR__ . '/includes/specialpage/SpecialPage.php', + 'SpecialPageAction' => __DIR__ . '/includes/actions/SpecialPageAction.php', 'SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php', 'SpecialPageLanguage' => __DIR__ . '/includes/specials/SpecialPageLanguage.php', 'SpecialPagesWithProp' => __DIR__ . '/includes/specials/SpecialPagesWithProp.php', diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index a097cd6fab..3103edd8e0 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -18,6 +18,7 @@ * http://www.gnu.org/copyleft/gpl.html * * @file + * @ingroup Change tagging */ class ChangeTags { diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 5c0bcff227..d5801cba86 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -6774,6 +6774,7 @@ $wgActions = array( 'credits' => true, 'delete' => true, 'edit' => true, + 'editchangetags' => 'SpecialPageAction', 'history' => true, 'info' => true, 'markpatrolled' => true, @@ -6782,7 +6783,7 @@ $wgActions = array( 'raw' => true, 'render' => true, 'revert' => true, - 'revisiondelete' => true, + 'revisiondelete' => 'SpecialPageAction', 'rollback' => true, 'submit' => true, 'unprotect' => true, diff --git a/includes/RevisionList.php b/includes/RevisionList.php index 0f77111992..1cb43f7501 100644 --- a/includes/RevisionList.php +++ b/includes/RevisionList.php @@ -317,7 +317,7 @@ class RevisionItem extends RevisionItemBase { } public function getAuthorNameField() { - return 'user_name'; // see Revision::selectUserFields() + return 'rev_user_text'; } public function canView() { @@ -334,16 +334,19 @@ class RevisionItem extends RevisionItemBase { /** * Get the HTML link to the revision text. - * Overridden by RevDelArchiveItem. + * @todo Essentially a copy of RevDelRevisionItem::getRevisionLink. That class + * should inherit from this one, and implement an appropriate interface instead + * of extending RevDelItem * @return string */ protected function getRevisionLink() { $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( $this->revision->getTimestamp(), $this->list->getUser() ) ); + if ( $this->isDeleted() && !$this->canViewContent() ) { return $date; } - return Linker::link( + return Linker::linkKnown( $this->list->title, $date, array(), @@ -356,30 +359,34 @@ class RevisionItem extends RevisionItemBase { /** * Get the HTML link to the diff. - * Overridden by RevDelArchiveItem + * @todo Essentially a copy of RevDelRevisionItem::getDiffLink. That class + * should inherit from this one, and implement an appropriate interface instead + * of extending RevDelItem * @return string */ protected function getDiffLink() { if ( $this->isDeleted() && !$this->canViewContent() ) { return $this->context->msg( 'diff' )->escaped(); } else { - return Linker::link( + return Linker::linkKnown( $this->list->title, - $this->context->msg( 'diff' )->escaped(), + $this->list->msg( 'diff' )->escaped(), array(), array( 'diff' => $this->revision->getId(), 'oldid' => 'prev', 'unhide' => 1 - ), - array( - 'known', - 'noclasses' ) ); } } + /** + * @todo Essentially a copy of RevDelRevisionItem::getHTML. That class + * should inherit from this one, and implement an appropriate interface instead + * of extending RevDelItem + * @return string + */ public function getHTML() { $difflink = $this->context->msg( 'parentheses' ) ->rawParams( $this->getDiffLink() )->escaped(); diff --git a/includes/actions/Action.php b/includes/actions/Action.php index 8d11d901a2..aca4363fce 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -132,6 +132,8 @@ abstract class Action { if ( $actionName === 'historysubmit' ) { if ( $request->getBool( 'revisiondelete' ) ) { $actionName = 'revisiondelete'; + } elseif ( $request->getBool( 'editchangetags' ) ) { + $actionName = 'editchangetags'; } else { $actionName = 'view'; } diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 85ff9944ba..669317809d 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -484,6 +484,7 @@ class HistoryPager extends ReverseChronologicalPager { 'id' => 'mw-history-compare' ) ) . "\n"; $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n"; $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; + $s .= Html::hidden( 'type', 'revision' ) . "\n"; // Button container stored in $this->buttons for re-use in getEndBody() $this->buttons = '
'; @@ -494,8 +495,17 @@ class HistoryPager extends ReverseChronologicalPager { $attrs ) . "\n"; - if ( $this->getUser()->isAllowed( 'deleterevision' ) ) { - $this->buttons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); + $user = $this->getUser(); + $actionButtons = ''; + if ( $user->isAllowed( 'deleterevision' ) ) { + $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); + } + if ( $user->isAllowed( 'changetags' ) ) { + $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' ); + } + if ( $actionButtons ) { + $this->buttons .= Xml::tags( 'div', array( 'class' => + 'mw-history-revisionactions' ), $actionButtons ); } $this->buttons .= '
'; @@ -616,11 +626,15 @@ class HistoryPager extends ReverseChronologicalPager { $del = ''; $user = $this->getUser(); - // Show checkboxes for each revision - if ( $user->isAllowed( 'deleterevision' ) ) { + $canRevDelete = $user->isAllowed( 'deleterevision' ); + $canModifyTags = $user->isAllowed( 'changetags' ); + // Show checkboxes for each revision, to allow for revision deletion and + // change tags + if ( $canRevDelete || $canModifyTags ) { $this->preventClickjacking(); - // If revision was hidden from sysops, disable the checkbox - if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { + // If revision was hidden from sysops and we don't need the checkbox + // for anything else, disable it + if ( !$canModifyTags && !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { $del = Xml::check( 'deleterevisions', false, array( 'disabled' => 'disabled' ) ); // Otherwise, enable the checkbox... } else { diff --git a/includes/actions/RevisiondeleteAction.php b/includes/actions/RevisiondeleteAction.php index b6eeb7b4f6..6c84bbd643 100644 --- a/includes/actions/RevisiondeleteAction.php +++ b/includes/actions/RevisiondeleteAction.php @@ -27,8 +27,14 @@ * An action that just pass the request to Special:RevisionDelete * * @ingroup Actions + * @deprecated since 1.26 This class has been replaced by SpecialPageAction, but + * you really shouldn't have been using it outside core in the first place */ class RevisiondeleteAction extends FormlessAction { + public function __construct( Page $page, IContextSource $context = null ) { + wfDeprecated( 'RevisiondeleteAction class', '1.26' ); + parent::__construct( $page, $context ); + } public function getName() { return 'revisiondelete'; diff --git a/includes/actions/SpecialPageAction.php b/includes/actions/SpecialPageAction.php new file mode 100644 index 0000000000..3c8a21ef0e --- /dev/null +++ b/includes/actions/SpecialPageAction.php @@ -0,0 +1,79 @@ + 'Revisiondelete', + 'editchangetags' => 'EditTags', + ); + + public function getName() { + $request = $this->getRequest(); + $actionName = $request->getVal( 'action', 'view' ); + // TODO: Shouldn't need to copy-paste this code from Action::getActionName! + if ( $actionName === 'historysubmit' ) { + if ( $request->getBool( 'revisiondelete' ) ) { + $actionName = 'revisiondelete'; + } elseif ( $request->getBool( 'editchangetags' ) ) { + $actionName = 'editchangetags'; + } + } + + if ( isset( self::$actionToSpecialPageMapping[$actionName] ) ) { + return $actionName; + } + return 'nosuchaction'; + } + + public function requiresUnblock() { + return false; + } + + public function getDescription() { + return ''; + } + + public function onView() { + return ''; + } + + public function show() { + $action = self::getName(); + if ( $action === 'nosuchaction' ) { + throw new ErrorPageError( $this->msg( 'nosuchaction' ), $this->msg( 'nosuchactiontext' ) ); + } + + // map actions to (whitelisted) special pages + $special = SpecialPageFactory::getPage( self::$actionToSpecialPageMapping[$action] ); + $special->setContext( $this->getContext() ); + $special->getContext()->setTitle( $special->getPageTitle() ); + $special->run( '' ); + } +} diff --git a/includes/changetags/ChangeTagsList.php b/includes/changetags/ChangeTagsList.php new file mode 100644 index 0000000000..dd8bab9878 --- /dev/null +++ b/includes/changetags/ChangeTagsList.php @@ -0,0 +1,77 @@ +ids = $ids; + } + + /** + * Creates a ChangeTags*List of the requested type. + * + * @param string $typeName 'revision' or 'logentry' + * @param IContextSource $context + * @param Title $title + * @param array $ids + * @return ChangeTagsList An instance of the requested subclass + * @throws Exception If you give an unknown $typeName + */ + public static function factory( $typeName, IContextSource $context, + Title $title, array $ids ) { + + switch ( $typeName ) { + case 'revision': + $className = 'ChangeTagsRevisionList'; + break; + case 'logentry': + $className = 'ChangeTagsLogList'; + break; + default: + throw new Exception( "Class $className requested, but does not exist" ); + } + return new $className( $context, $title, $ids ); + } + + /** + * Reload the list data from the master DB. + */ + function reloadFromMaster() { + $dbw = wfGetDB( DB_MASTER ); + $this->res = $this->doQuery( $dbw ); + } + + /** + * Add/remove change tags from all the items in the list. + * + * @param array $tagsToAdd + * @param array $tagsToRemove + * @param array $params + * @param string $reason + * @param User $user + * @return Status + */ + abstract function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, + $reason, $user ); +} diff --git a/includes/changetags/ChangeTagsLogItem.php b/includes/changetags/ChangeTagsLogItem.php new file mode 100644 index 0000000000..b648ce0b5d --- /dev/null +++ b/includes/changetags/ChangeTagsLogItem.php @@ -0,0 +1,100 @@ +row, Revision::DELETED_RESTRICTED, $this->list->getUser() ); + } + + public function canViewContent() { + return true; // none + } + + /** + * @return string Comma-separated list of tags + */ + public function getTags() { + return $this->row->ts_tags; + } + + /** + * @return string A HTML
  • element representing this revision, showing + * change tags and everything + */ + public function getHTML() { + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->row->log_timestamp, $this->list->getUser() ) ); + $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title ); + $formatter = LogFormatter::newFromRow( $this->row ); + $formatter->setContext( $this->list->getContext() ); + $formatter->setAudience( LogFormatter::FOR_THIS_USER ); + + // Log link for this page + $loglink = Linker::link( + SpecialPage::getTitleFor( 'Log' ), + $this->list->msg( 'log' )->escaped(), + array(), + array( 'page' => $title->getPrefixedText() ) + ); + $loglink = $this->list->msg( 'parentheses' )->rawParams( $loglink )->escaped(); + // User links and action text + $action = $formatter->getActionText(); + // Comment + $comment = $this->list->getLanguage()->getDirMark() . + $formatter->getComment(); + + if ( LogEventsList::isDeleted( $this->row, LogPage::DELETED_COMMENT ) ) { + $comment = '' . $comment . ''; + } + + $content = "$loglink $date $action $comment"; + $attribs = array(); + $tags = $this->getTags(); + if ( $tags ) { + list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow( $tags, 'edittags' ); + $content .= " $tagSummary"; + $attribs['class'] = implode( ' ', $classes ); + } + return Xml::tags( 'li', $attribs, $content ); + } +} diff --git a/includes/changetags/ChangeTagsLogList.php b/includes/changetags/ChangeTagsLogList.php new file mode 100644 index 0000000000..ad274d99d5 --- /dev/null +++ b/includes/changetags/ChangeTagsLogList.php @@ -0,0 +1,89 @@ +ids ); + $queryInfo = DatabaseLogEntry::getSelectQueryData(); + $queryInfo['conds'] += array( 'log_id' => $ids ); + $queryInfo['options'] += array( 'ORDER BY' => 'log_id DESC' ); + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'] + ); + return $db->select( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + __METHOD__, + $queryInfo['options'], + $queryInfo['join_conds'] + ); + } + + public function newItem( $row ) { + return new ChangeTagsLogItem( $this, $row ); + } + + /** + * Add/remove change tags from all the log entries in the list. + * + * @param array $tagsToAdd + * @param array $tagsToRemove + * @param array $params + * @param string $reason + * @param User $user + * @return Status + */ + public function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, + $reason, $user ) { + + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed + for ( $this->reset(); $this->current(); $this->next() ) { + // @codingStandardsIgnoreEnd + $item = $this->current(); + $status = ChangeTags::updateTagsWithChecks( $tagsToAdd, $tagsToRemove, + null, null, $item->getId(), $params, $reason, $user ); + // Should only fail on second and subsequent times if the user trips + // the rate limiter + if ( !$status->isOK() ) { + break; + } + } + + return $status; + } +} diff --git a/includes/changetags/ChangeTagsRevisionItem.php b/includes/changetags/ChangeTagsRevisionItem.php new file mode 100644 index 0000000000..8225be4b6f --- /dev/null +++ b/includes/changetags/ChangeTagsRevisionItem.php @@ -0,0 +1,58 @@ +row->ts_tags; + } + + /** + * @return string A HTML
  • element representing this revision, showing + * change tags and everything + */ + public function getHTML() { + $difflink = $this->list->msg( 'parentheses' ) + ->rawParams( $this->getDiffLink() )->escaped(); + $revlink = $this->getRevisionLink(); + $userlink = Linker::revUserLink( $this->revision ); + $comment = Linker::revComment( $this->revision ); + if ( $this->isDeleted() ) { + $revlink = "$revlink"; + } + + $content = "$difflink $revlink $userlink $comment"; + $attribs = array(); + $tags = $this->getTags(); + if ( $tags ) { + list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow( $tags, 'edittags' ); + $content .= " $tagSummary"; + $attribs['class'] = implode( ' ', $classes ); + } + return Xml::tags( 'li', $attribs, $content ); + } +} diff --git a/includes/changetags/ChangeTagsRevisionList.php b/includes/changetags/ChangeTagsRevisionList.php new file mode 100644 index 0000000000..c51d605a51 --- /dev/null +++ b/includes/changetags/ChangeTagsRevisionList.php @@ -0,0 +1,99 @@ +ids ); + $queryInfo = array( + 'tables' => array( 'revision', 'user' ), + 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ), + 'conds' => array( + 'rev_page' => $this->title->getArticleID(), + 'rev_id' => $ids, + ), + 'options' => array( 'ORDER BY' => 'rev_id DESC' ), + 'join_conds' => array( + 'page' => Revision::pageJoinCond(), + 'user' => Revision::userJoinCond(), + ), + ); + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'] + ); + return $db->select( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + __METHOD__, + $queryInfo['options'], + $queryInfo['join_conds'] + ); + } + + public function newItem( $row ) { + return new ChangeTagsRevisionItem( $this, $row ); + } + + /** + * Add/remove change tags from all the revisions in the list. + * + * @param array $tagsToAdd + * @param array $tagsToRemove + * @param array $params + * @param string $reason + * @param User $user + * @return Status + */ + public function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, + $reason, $user ) { + + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed + for ( $this->reset(); $this->current(); $this->next() ) { + // @codingStandardsIgnoreEnd + $item = $this->current(); + $status = ChangeTags::updateTagsWithChecks( $tagsToAdd, $tagsToRemove, + null, $item->getId(), null, $params, $reason, $user ); + // Should only fail on second and subsequent times if the user trips + // the rate limiter + if ( !$status->isOK() ) { + break; + } + } + + return $status; + } +} diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index c2625191ae..dedfcb6afc 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -159,6 +159,7 @@ class SpecialPageFactory { 'ApiHelp' => 'SpecialApiHelp', 'Blankpage' => 'SpecialBlankpage', 'Diff' => 'SpecialDiff', + 'EditTags' => 'SpecialEditTags', 'Emailuser' => 'SpecialEmailUser', 'Movepage' => 'MovePageForm', 'Mycontributions' => 'SpecialMycontributions', diff --git a/includes/specials/SpecialEditTags.php b/includes/specials/SpecialEditTags.php new file mode 100644 index 0000000000..4b1fcaea88 --- /dev/null +++ b/includes/specials/SpecialEditTags.php @@ -0,0 +1,459 @@ +checkPermissions(); + $this->checkReadOnly(); + + $output = $this->getOutput(); + $user = $this->getUser(); + $request = $this->getRequest(); + + $this->setHeaders(); + $this->outputHeader(); + + $this->getOutput()->addModules( array( 'mediawiki.special.edittags', + 'mediawiki.special.edittags.styles' ) ); + + $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' ); + + // Handle our many different possible input types + $ids = $request->getVal( 'ids' ); + if ( !is_null( $ids ) ) { + // Allow CSV from the form hidden field, or a single ID for show/hide links + $this->ids = explode( ',', $ids ); + } else { + // Array input + $this->ids = array_keys( $request->getArray( 'ids', array() ) ); + } + $this->ids = array_unique( array_filter( $this->ids ) ); + + // No targets? + if ( count( $this->ids ) == 0 ) { + throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' ); + } + + $this->typeName = $request->getVal( 'type' ); + $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); + + // sanity check of parameter + switch ( $this->typeName ) { + case 'logentry': + case 'logging': + $this->typeName = 'logentry'; + break; + default: + $this->typeName = 'revision'; + break; + } + + // Allow the list type to adjust the passed target + // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly + // what we want + $this->targetObj = RevisionDeleter::suggestTarget( + $this->typeName === 'revision' ? 'revision' : 'logging', + $this->targetObj, + $this->ids + ); + + $this->isAllowed = $user->isAllowed( 'changetags' ); + + $this->reason = $request->getVal( 'wpReason' ); + // We need a target page! + if ( is_null( $this->targetObj ) ) { + $output->addWikiMsg( 'undelete-header' ); + return; + } + // Give a link to the logs/hist for this page + $this->showConvenienceLinks(); + + // Either submit or create our form + if ( $this->isAllowed && $this->submitClicked ) { + $this->submit( $request ); + } else { + $this->showForm(); + } + + // Show relevant lines from the tag log + $tagLogPage = new LogPage( 'tag' ); + $output->addHTML( "

    " . $tagLogPage->getName()->escaped() . "

    \n" ); + LogEventsList::showLogExtract( + $output, + 'tag', + $this->targetObj, + '', /* user */ + array( 'lim' => 25, 'conds' => array(), 'useMaster' => $this->wasSaved ) + ); + } + + /** + * Show some useful links in the subtitle + */ + protected function showConvenienceLinks() { + // Give a link to the logs/hist for this page + if ( $this->targetObj ) { + // Also set header tabs to be for the target. + $this->getSkin()->setRelevantTitle( $this->targetObj ); + + $links = array(); + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Log' ), + $this->msg( 'viewpagelogs' )->escaped(), + array(), + array( + 'page' => $this->targetObj->getPrefixedText(), + 'hide_tag_log' => '0', + ) + ); + if ( !$this->targetObj->isSpecialPage() ) { + // Give a link to the page history + $links[] = Linker::linkKnown( + $this->targetObj, + $this->msg( 'pagehist' )->escaped(), + array(), + array( 'action' => 'history' ) + ); + } + // Link to Special:Tags + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Tags' ), + $this->msg( 'tags-edit-manage-link' )->escaped() + ); + // Logs themselves don't have histories or archived revisions + $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); + } + } + + /** + * Get the list object for this request + * @return ChangeTagsList + */ + protected function getList() { + if ( is_null( $this->revList ) ) { + $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(), + $this->targetObj, $this->ids ); + } + + return $this->revList; + } + + /** + * Show a list of items that we will operate on, and show a form which allows + * the user to modify the tags applied to those items. + */ + protected function showForm() { + $userAllowed = true; + + $out = $this->getOutput(); + // Messages: tags-edit-revision-selected, tags-edit-logentry-selected + $out->wrapWikiMsg( "$1", array( + "tags-edit-{$this->typeName}-selected", + $this->getLanguage()->formatNum( count( $this->ids ) ), + $this->targetObj->getPrefixedText() + ) ); + + $out->addHelpLink( 'Help:Tags' ); + $out->addHTML( "" ); + // Explanation text + $out->wrapWikiMsg( '

    $1

    ', "tags-edit-{$this->typeName}-explanation" ); + + // Show form if the user can submit + if ( $this->isAllowed ) { + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ), + 'id' => 'mw-revdel-form-revisions' ) ) . + Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend", + count( $this->ids ) )->text() ) . + $this->buildCheckBoxes() . + Xml::openElement( 'table' ) . + "\n" . + '' . + Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) . + '' . + '' . + Xml::input( + 'wpReason', + 60, + $this->reason, + array( 'id' => 'wpReason', 'maxlength' => 100 ) + ) . + '' . + "\n" . + '' . + '' . + Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit", + $numRevisions )->text(), array( 'name' => 'wpSubmit' ) ) . + '' . + "\n" . + Xml::closeElement( 'table' ) . + Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . + Html::hidden( 'target', $this->targetObj->getPrefixedText() ) . + Html::hidden( 'type', $this->typeName ) . + Html::hidden( 'ids', implode( ',', $this->ids ) ) . + Xml::closeElement( 'fieldset' ) . "\n" . + Xml::closeElement( 'form' ) . "\n"; + } else { + $form = ''; + } + $out->addHTML( $form ); + } + + /** + * @return string HTML + */ + protected function buildCheckBoxes() { + // If there is just one item, provide the user with a multi-select field + $list = $this->getList(); + if ( $list->length() == 1 ) { + $list->reset(); + $tags = $list->current()->getTags(); + if ( $tags ) { + $tags = explode( ',', $tags ); + } else { + $tags = array(); + } + + $html = ''; + $html .= ''; + $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() ); + $html .= '
    ' . $this->msg( 'tags-edit-existing-tags' )->escaped() . + ''; + if ( $tags ) { + $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) ); + } else { + $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse(); + } + $html .= '
    ' . $tagSelect[0] . '' . $tagSelect[1]; + // also output the tags currently applied as a hidden form field, so we + // know what to remove from the revision/log entry when the form is submitted + $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) ); + $html .= '
    '; + } else { + // Otherwise, use a multi-select field for adding tags, and a list of + // checkboxes for removing them + $tags = array(); + + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed + for ( $list->reset(); $list->current(); $list->next() ) { + // @codingStandardsIgnoreEnd + $currentTags = $list->current()->getTags(); + if ( $currentTags ) { + $tags = array_merge( $tags, explode( ',', $currentTags ) ); + } + } + $tags = array_unique( $tags ); + + $html = '
    '; + $tagSelect = $this->getTagSelect( array(), $this->msg( 'tags-edit-add' )->plain() ); + $html .= '

    ' . $tagSelect[0] . '

    ' . $tagSelect[1] . '
    '; + $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() ); + $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(), + 'wpRemoveAllTags', 'mw-edittags-remove-all' ); + $i = 0; // used for generating checkbox IDs only + foreach ( $tags as $tag ) { + $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag, + 'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, array( + 'value' => $tag, + 'class' => 'mw-edittags-remove-checkbox', + ) ); + } + $html .= '
    '; + } + + return $html; + } + + /** + * Returns a + * element. + * @param string $label The text of a