Anomie has uploaded a new change for review.
https://gerrit.wikimedia.org/r/204342
Change subject: UI for adding and removing change tags on revisions and log
entries
......................................................................
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
(cherry picked from commit 5c4681012e78a8d5004eea917eba90d448d7e0f3)
---
M autoload.php
M includes/ChangeTags.php
M includes/DefaultSettings.php
M includes/RevisionList.php
M includes/actions/Action.php
M includes/actions/HistoryAction.php
M includes/actions/RevisiondeleteAction.php
A includes/actions/SpecialPageAction.php
A includes/changetags/ChangeTagsList.php
A includes/changetags/ChangeTagsLogItem.php
A includes/changetags/ChangeTagsLogList.php
A includes/changetags/ChangeTagsRevisionItem.php
A includes/changetags/ChangeTagsRevisionList.php
M includes/specialpage/SpecialPageFactory.php
A includes/specials/SpecialEditTags.php
M includes/specials/SpecialLog.php
M includes/specials/SpecialRevisiondelete.php
M languages/i18n/en.json
M languages/i18n/qqq.json
M resources/Resources.php
M resources/src/mediawiki.action/mediawiki.action.history.js
M resources/src/mediawiki.legacy/shared.css
A resources/src/mediawiki.special/mediawiki.special.edittags.css
A resources/src/mediawiki.special/mediawiki.special.edittags.js
M tests/phpunit/includes/actions/ActionTest.php
25 files changed, 1,165 insertions(+), 49 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/42/204342/1
diff --git a/autoload.php b/autoload.php
index 2fe805f..92d6014 100644
--- a/autoload.php
+++ b/autoload.php
@@ -200,6 +200,11 @@
'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 @@
'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 @@
'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 5506cb4..71771ba 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 90f2ba9..11096be 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -6781,6 +6781,7 @@
'credits' => true,
'delete' => true,
'edit' => true,
+ 'editchangetags' => 'SpecialPageAction',
'history' => true,
'info' => true,
'markpatrolled' => true,
@@ -6789,7 +6790,7 @@
'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 d10b541..6844dad 100644
--- a/includes/RevisionList.php
+++ b/includes/RevisionList.php
@@ -317,7 +317,7 @@
}
public function getAuthorNameField() {
- return 'user_name'; // see Revision::selectUserFields()
+ return 'rev_user_text';
}
public function canView() {
@@ -334,15 +334,18 @@
/**
* 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 = $this->list->getLanguage()->timeanddate(
$this->revision->getTimestamp(), true );
+
if ( $this->isDeleted() && !$this->canViewContent() ) {
return $date;
}
- return Linker::link(
+ return Linker::linkKnown(
$this->list->title,
$date,
array(),
@@ -355,30 +358,34 @@
/**
* 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 8d11d90..aca4363 100644
--- a/includes/actions/Action.php
+++ b/includes/actions/Action.php
@@ -132,6 +132,8 @@
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 7189372..f38bc50 100644
--- a/includes/actions/HistoryAction.php
+++ b/includes/actions/HistoryAction.php
@@ -479,6 +479,7 @@
'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 = '<div>';
@@ -489,8 +490,17 @@
$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 .= '</div>';
@@ -606,11 +616,15 @@
$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 b6eeb7b..dbcb848 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.25 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.25' );
+ 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 0000000..9b72163
--- /dev/null
+++ b/includes/actions/SpecialPageAction.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
USA
+ *
+ * @file
+ * @ingroup Actions
+ */
+
+/**
+ * An action that just passes the request to the relevant special page
+ *
+ * @ingroup Actions
+ * @since 1.25
+ */
+class SpecialPageAction extends FormlessAction {
+
+ /**
+ * @var array A mapping of action names to special page names.
+ */
+ public static $actionToSpecialPageMapping = array(
+ 'revisiondelete' => '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 0000000..dd8bab9
--- /dev/null
+++ b/includes/changetags/ChangeTagsList.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Change tagging
+ */
+
+/**
+ * Generic list for change tagging.
+ */
+abstract class ChangeTagsList extends RevisionListBase {
+ function __construct( IContextSource $context, Title $title, array $ids
) {
+ parent::__construct( $context, $title );
+ $this->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 0000000..565d159
--- /dev/null
+++ b/includes/changetags/ChangeTagsLogItem.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Change tagging
+ */
+
+/**
+ * Item class for a logging table row with its associated change tags.
+ * @todo Abstract out a base class for this and RevDelLogItem, similar to the
+ * RevisionItem class but specifically for log items.
+ * @since 1.25
+ */
+class ChangeTagsLogItem extends RevisionItemBase {
+ public function getIdField() {
+ return 'log_id';
+ }
+
+ public function getTimestampField() {
+ return 'log_timestamp';
+ }
+
+ public function getAuthorIdField() {
+ return 'log_user';
+ }
+
+ public function getAuthorNameField() {
+ return 'log_user_text';
+ }
+
+ public function canView() {
+ return LogEventsList::userCan( $this->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 <li> 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 = '<span class="history-deleted">' . $comment
. '</span>';
+ }
+
+ $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 0000000..fe80695
--- /dev/null
+++ b/includes/changetags/ChangeTagsLogList.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Change tagging
+ */
+
+/**
+ * Stores a list of taggable log entries.
+ * @since 1.25
+ */
+class ChangeTagsLogList extends ChangeTagsList {
+ public function getType() {
+ return 'logentry';
+ }
+
+ /**
+ * @param DatabaseBase $db
+ * @return mixed
+ */
+ public function doQuery( $db ) {
+ $ids = array_map( 'intval', $this->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 0000000..e90a1b4
--- /dev/null
+++ b/includes/changetags/ChangeTagsRevisionItem.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Change tagging
+ */
+
+/**
+ * Item class for a live revision table row with its associated change tags.
+ * @since 1.25
+ */
+class ChangeTagsRevisionItem extends RevisionItem {
+ /**
+ * @return string Comma-separated list of tags
+ */
+ public function getTags() {
+ return $this->row->ts_tags;
+ }
+
+ /**
+ * @return string A HTML <li> 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 = "<span
class=\"history-deleted\">$revlink</span>";
+ }
+
+ $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 0000000..842d327
--- /dev/null
+++ b/includes/changetags/ChangeTagsRevisionList.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Change tagging
+ */
+
+/**
+ * Stores a list of taggable revisions.
+ * @since 1.25
+ */
+class ChangeTagsRevisionList extends ChangeTagsList {
+ public function getType() {
+ return 'revision';
+ }
+
+ /**
+ * @param DatabaseBase $db
+ * @return mixed
+ */
+ public function doQuery( $db ) {
+ $ids = array_map( 'intval', $this->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 c262519..dedfcb6 100644
--- a/includes/specialpage/SpecialPageFactory.php
+++ b/includes/specialpage/SpecialPageFactory.php
@@ -159,6 +159,7 @@
'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 0000000..14850ff
--- /dev/null
+++ b/includes/specials/SpecialEditTags.php
@@ -0,0 +1,459 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page for adding and removing change tags to individual revisions.
+ * A lot of this is copied out of SpecialRevisiondelete.
+ *
+ * @ingroup SpecialPage
+ * @since 1.25
+ */
+class SpecialEditTags extends UnlistedSpecialPage {
+ /** @var bool Was the DB modified in this request */
+ protected $wasSaved = false;
+
+ /** @var bool True if the submit button was clicked, and the form was
posted */
+ private $submitClicked;
+
+ /** @var array Target ID list */
+ private $ids;
+
+ /** @var Title Title object for target parameter */
+ private $targetObj;
+
+ /** @var string Deletion type, may be revision or logentry */
+ private $typeName;
+
+ /** @var ChangeTagsList Storing the list of items to be tagged */
+ private $revList;
+
+ /** @var bool Whether user is allowed to perform the action */
+ private $isAllowed;
+
+ /** @var string */
+ private $reason;
+
+ public function __construct() {
+ parent::__construct( 'EditTags', 'changetags' );
+ }
+
+ public function execute( $par ) {
+ $this->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( "<h2>" . $tagLogPage->getName()->escaped() .
"</h2>\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( "<strong>$1</strong>", array(
+ "tags-edit-{$this->typeName}-selected",
+ $this->getLanguage()->formatNum( count( $this->ids ) ),
+ $this->targetObj->getPrefixedText()
+ ) );
+
+ $out->addHelpLink( 'Help:Tags' );
+ $out->addHTML( "<ul>" );
+
+ $numRevisions = 0;
+ // Live revisions...
+ $list = $this->getList();
+ // @codingStandardsIgnoreStart
Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ for ( $list->reset(); $list->current(); $list->next() ) {
+ // @codingStandardsIgnoreEnd
+ $item = $list->current();
+ $numRevisions++;
+ $out->addHTML( $item->getHTML() );
+ }
+
+ if ( !$numRevisions ) {
+ throw new ErrorPageError( 'tags-edit-nooldid-title',
'tags-edit-nooldid-text' );
+ }
+
+ $out->addHTML( "</ul>" );
+ // Explanation text
+ $out->wrapWikiMsg( '<p>$1</p>',
"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' ) .
+ "<tr>\n" .
+ '<td class="mw-label">' .
+ Xml::label( $this->msg(
'tags-edit-reason' )->text(), 'wpReason' ) .
+ '</td>' .
+ '<td class="mw-input">' .
+ Xml::input(
+ 'wpReason',
+ 60,
+ $this->reason,
+ array( 'id' =>
'wpReason', 'maxlength' => 100 )
+ ) .
+ '</td>' .
+ "</tr><tr>\n" .
+ '<td></td>' .
+ '<td class="mw-submit">' .
+ Xml::submitButton( $this->msg(
"tags-edit-{$this->typeName}-submit",
+ $numRevisions
)->text(), array( 'name' => 'wpSubmit' ) ) .
+ '</td>' .
+ "</tr>\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 = '<table id="mw-edittags-tags-selector">';
+ $html .= '<tr><td>' . $this->msg(
'tags-edit-existing-tags' )->escaped() .
+ '</td><td>';
+ if ( $tags ) {
+ $html .= $this->getLanguage()->commaList(
array_map( 'htmlspecialchars', $tags ) );
+ } else {
+ $html .= $this->msg(
'tags-edit-existing-tags-none' )->parse();
+ }
+ $html .= '</td></tr>';
+ $tagSelect = $this->getTagSelect( $tags, $this->msg(
'tags-edit-new-tags' )->plain() );
+ $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' .
$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 .= '</td></tr></table>';
+ } 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 = '<table
id="mw-edittags-tags-selector-multi"><tr><td>';
+ $tagSelect = $this->getTagSelect( array(), $this->msg(
'tags-edit-add' )->plain() );
+ $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1]
. '</td><td>';
+ $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 .= '</td></tr></table>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Returns a <select multiple> element with a list of change tags that
can be
+ * applied by users.
+ *
+ * @param array $selectedTags The tags that should be preselected in the
+ * list. Any tags in this list, but not in the list returned by
+ * ChangeTags::listExplicitlyDefinedTags, will be appended to the
<select>
+ * element.
+ * @param string $label The text of a <label> to precede the <select>
+ * @return array HTML <label> element at index 0, HTML <select> element
at
+ * index 1
+ */
+ protected function getTagSelect( $selectedTags, $label ) {
+ $result = array();
+ $result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
+ $result[1] = Xml::openElement( 'select', array(
+ 'name' => 'wpTagList[]',
+ 'id' => 'mw-edittags-tag-list',
+ 'multiple' => 'multiple',
+ 'size' => '8',
+ ) );
+
+ $tags = ChangeTags::listExplicitlyDefinedTags();
+ $tags = array_unique( array_merge( $tags, $selectedTags ) );
+ foreach ( $tags as $tag ) {
+ $result[1] .= Xml::option( $tag, $tag, in_array( $tag,
$selectedTags ) );
+ }
+
+ $result[1] .= Xml::closeElement( 'select' );
+ return $result;
+ }
+
+ /**
+ * UI entry point for form submission.
+ * @throws PermissionsError
+ * @return bool
+ */
+ protected function submit() {
+ // Check edit token on submission
+ $request = $this->getRequest();
+ $token = $request->getVal( 'wpEditToken' );
+ if ( $this->submitClicked && !$this->getUser()->matchEditToken(
$token ) ) {
+ $this->getOutput()->addWikiMsg( 'sessionfailure' );
+ return false;
+ }
+
+ // Evaluate incoming request data
+ $tagList = $request->getArray( 'wpTagList' );
+ if ( is_null( $tagList ) ) {
+ $tagList = array();
+ }
+ $existingTags = $request->getVal( 'wpExistingTags' );
+ if ( is_null( $existingTags ) || $existingTags === '' ) {
+ $existingTags = array();
+ } else {
+ $existingTags = explode( ',', $existingTags );
+ }
+
+ if ( count( $this->ids ) > 1 ) {
+ // multiple revisions selected
+ $tagsToAdd = $tagList;
+ if ( $request->getBool( 'wpRemoveAllTags' ) ) {
+ $tagsToRemove = $existingTags;
+ } else {
+ $tagsToRemove = $request->getArray(
'wpTagsToRemove' );
+ }
+ } else {
+ // single revision selected
+ // The user tells us which tags they want associated to
the revision.
+ // We have to figure out which ones to add, and which
to remove.
+ $tagsToAdd = array_diff( $tagList, $existingTags );
+ $tagsToRemove = array_diff( $existingTags, $tagList );
+ }
+
+ //var_dump( array( 'add' => $tagsToAdd, 'remove' =>
$tagsToRemove ) );
+
+ if ( !$tagsToAdd && !$tagsToRemove ) {
+ $status = Status::newFatal( 'tags-edit-none-selected' );
+ } else {
+ $status = $this->getList()->updateChangeTagsOnAll(
$tagsToAdd,
+ $tagsToRemove, null, $this->reason,
$this->getUser() );
+ }
+
+ if ( $status->isGood() ) {
+ $this->success();
+ return true;
+ } else {
+ $this->failure( $status );
+ return false;
+ }
+ }
+
+ /**
+ * Report that the submit operation succeeded
+ */
+ protected function success() {
+ $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete'
) );
+ $this->getOutput()->wrapWikiMsg( "<span
class=\"success\">\n$1\n</span>",
+ 'tags-edit-success' );
+ $this->wasSaved = true;
+ $this->revList->reloadFromMaster();
+ $this->reason = ''; // no need to spew the reason back at the
user
+ $this->showForm();
+ }
+
+ /**
+ * Report that the submit operation failed
+ * @param Status $status
+ */
+ protected function failure( $status ) {
+ $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' )
);
+ $this->getOutput()->addWikiText( $status->getWikiText(
'tags-edit-failure' ) );
+ $this->showForm();
+ }
+
+ public function getDescription() {
+ return $this->msg( 'tags-edit-title' )->text();
+ }
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
+}
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
index 88184f9..f16e5ba 100644
--- a/includes/specials/SpecialLog.php
+++ b/includes/specials/SpecialLog.php
@@ -203,7 +203,7 @@
if ( $logBody ) {
$this->getOutput()->addHTML(
$pager->getNavigationBar() .
- $this->getRevisionButton(
+ $this->getActionButtons(
$loglist->beginLogEventsList() .
$logBody .
$loglist->endLogEventsList()
@@ -215,30 +215,50 @@
}
}
- private function getRevisionButton( $formcontents ) {
- # If the user doesn't have the ability to delete log entries,
- # don't bother showing them the button.
- if ( !$this->getUser()->isAllowedAll( 'deletedhistory',
'deletelogentry' ) ) {
+ private function getActionButtons( $formcontents ) {
+ $user = $this->getUser();
+ $canRevDelete = $user->isAllowedAll( 'deletedhistory',
'deletelogentry' );
+ $canModifyTags = $user->isAllowed( 'changetags' );
+ # If the user doesn't have the ability to delete log entries
nor edit tags,
+ # don't bother showing them the button(s).
+ if ( !$canRevDelete && !$canModifyTags ) {
return $formcontents;
}
- # Show button to hide log entries
+ # Show button to hide log entries and/or edit change tags
$s = Html::openElement(
'form',
array( 'action' => wfScript(), 'id' =>
'mw-log-deleterevision-submit' )
) . "\n";
- $s .= Html::hidden( 'title', SpecialPage::getTitleFor(
'Revisiondelete' ) ) . "\n";
- $s .= Html::hidden( 'target', SpecialPage::getTitleFor( 'Log' )
) . "\n";
+ $s .= Html::hidden( 'action', 'historysubmit' ) . "\n";
$s .= Html::hidden( 'type', 'logging' ) . "\n";
- $button = Html::element(
- 'button',
- array(
- 'type' => 'submit',
- 'class' => "deleterevision-log-submit
mw-log-deleterevision-button"
- ),
- $this->msg( 'showhideselectedlogentries' )->text()
- ) . "\n";
- $s .= $button . $formcontents . $button;
+
+ $buttons = '';
+ if ( $canRevDelete ) {
+ $buttons .= Html::element(
+ 'button',
+ array(
+ 'type' => 'submit',
+ 'name' => 'revisiondelete',
+ 'value' => '1',
+ 'class' => "deleterevision-log-submit
mw-log-deleterevision-button"
+ ),
+ $this->msg( 'showhideselectedlogentries'
)->text()
+ ) . "\n";
+ }
+ if ( $canModifyTags ) {
+ $buttons .= Html::element(
+ 'button',
+ array(
+ 'type' => 'submit',
+ 'name' => 'editchangetags',
+ 'value' => '1',
+ 'class' => "editchangetags-log-submit
mw-log-editchangetags-button"
+ ),
+ $this->msg( 'log-edit-tags' )->text()
+ ) . "\n";
+ }
+ $s .= $buttons . $formcontents . $buttons;
$s .= Html::closeElement( 'form' );
return $s;
diff --git a/includes/specials/SpecialRevisiondelete.php
b/includes/specials/SpecialRevisiondelete.php
index e0a964e..bdfe911 100644
--- a/includes/specials/SpecialRevisiondelete.php
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -132,18 +132,8 @@
// $this->ids = array_map( 'intval', $this->ids );
$this->ids = array_unique( array_filter( $this->ids ) );
- if ( $request->getVal( 'action' ) == 'historysubmit'
- || $request->getVal( 'action' ) == 'revisiondelete'
- ) {
- // For show/hide form submission from history page
- // Since we are access through
index.php?title=XXX&action=historysubmit
- // getFullTitle() will contain the target title and not
our title
- $this->targetObj = $this->getFullTitle();
- $this->typeName = 'revision';
- } else {
- $this->typeName = $request->getVal( 'type' );
- $this->targetObj = Title::newFromText(
$request->getText( 'target' ) );
- }
+ $this->typeName = $request->getVal( 'type' );
+ $this->targetObj = Title::newFromText( $request->getText(
'target' ) );
# For reviewing deleted files...
$this->archiveName = $request->getVal( 'file' );
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 003ee91..e56aad7 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -765,6 +765,7 @@
"history-feed-description": "Revision history for this page on the
wiki",
"history-feed-item-nocomment": "$1 at $2",
"history-feed-empty": "The requested page does not exist.\nIt may have
been deleted from the wiki, or renamed.\nTry [[Special:Search|searching on the
wiki]] for relevant new pages.",
+ "history-edit-tags": "Edit tags of selected revisions",
"rev-deleted-comment": "(edit summary removed)",
"rev-deleted-user": "(username removed)",
"rev-deleted-event": "(log details removed)",
@@ -1721,6 +1722,7 @@
"logempty": "No matching items in log.",
"log-title-wildcard": "Search titles starting with this text",
"showhideselectedlogentries": "Change visibility of selected log
entries",
+ "log-edit-tags": "Edit tags of selected log entries",
"allpages": "All pages",
"allpages-summary": "",
"nextpage": "Next page ($1)",
@@ -3431,6 +3433,30 @@
"tags-update-add-not-allowed-multi": "The following {{PLURAL:$2|tag
is|tags are}} not allowed to be manually added: $1",
"tags-update-remove-not-allowed-one": "The tag \"$1\" is not allowed to
be removed.",
"tags-update-remove-not-allowed-multi": "The following {{PLURAL:$2|tag
is|tags are}} not allowed to be manually removed: $1",
+ "tags-edit-title": "Edit tags",
+ "tags-edit-manage-link": "Manage tags",
+ "tags-edit-revision-selected": "{{PLURAL:$1|Selected revision|Selected
revisions}} of [[:$2]]:",
+ "tags-edit-logentry-selected": "{{PLURAL:$1|Selected log event|Selected
log events}}:",
+ "tags-edit-revision-explanation": "",
+ "tags-edit-logentry-explanation": "",
+ "tags-edit-revision-legend": "Add or remove tags from {{PLURAL:$1|this
revision|all $1 revisions}}",
+ "tags-edit-logentry-legend": "Add or remove tags from {{PLURAL:$1|this
log entry|all $1 log entries}}",
+ "tags-edit-existing-tags": "Existing tags:",
+ "tags-edit-existing-tags-none": "''None''",
+ "tags-edit-new-tags": "New tags:",
+ "tags-edit-add": "Add these tags:",
+ "tags-edit-remove": "Remove these tags:",
+ "tags-edit-remove-all-tags": "(remove all tags)",
+ "tags-edit-chosen-placeholder": "Select some tags",
+ "tags-edit-chosen-no-results": "No tags found that match",
+ "tags-edit-reason": "Reason:",
+ "tags-edit-revision-submit": "Apply changes to {{PLURAL:$1|this
revision|$1 revisions}}",
+ "tags-edit-logentry-submit": "Apply changes to {{PLURAL:$1|this log
entry|$1 log entries}}",
+ "tags-edit-success": "<strong>The changes were successfully
applied.</strong>",
+ "tags-edit-failure": "<strong>The changes could not be
applied:</strong>\n$1",
+ "tags-edit-nooldid-title": "Invalid target revision",
+ "tags-edit-nooldid-text": "You have either not specified a target
revision(s) to perform this function, or the specified revision does not
exist.",
+ "tags-edit-none-selected": "Please select at least one tag to add or
remove.",
"comparepages": "Compare pages",
"comparepages-summary": "",
"compare-page1": "Page 1",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 39dc6ca..db30115 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -931,6 +931,7 @@
"history-feed-description": "Used as subtitle (description) of the
RSS/Atom feed for a page history. See
[{{canonicalurl:Main_Page|feed=atom&action=history}} example].",
"history-feed-item-nocomment": "Title for each revision when viewing
the RSS/Atom feed for a page history.\n\nParameters:\n* $1 - username\n* $2 -
date/time\n* $3 - (Optional) date\n* $4 - (Optional) time",
"history-feed-empty": "Used as summary of the RSS/Atom feed for a page
history when the feed is empty.\nSee
[{{canonicalurl:x|feed=atom&action=history}} example].",
+ "history-edit-tags": "Text of button used to access change tagging
interface. For more information on tags see [[mw:Manual:Tags]].",
"rev-deleted-comment": "Apparently this can also be about the reason of
a log action, not only an edit summary. See
also:\n*{{msg-mw|revdelete-hide-comment}}",
"rev-deleted-user": "See also:\n* {{msg-mw|Rev-deleted-event}}",
"rev-deleted-event": "See also:\n* {{msg-mw|Rev-deleted-user}}",
@@ -1887,6 +1888,7 @@
"logempty": "Used as warning when there are no items to show.",
"log-title-wildcard": "* Appears in: [[Special:Log]]\n* Description: A
check box to enable prefix search option",
"showhideselectedlogentries": "Text of the button which brings up the
[[mw:RevisionDelete|RevisionDelete]] menu on [[Special:Log]].",
+ "log-edit-tags": "Text of button used to access change tagging
interface. For more information on tags see [[mw:Manual:Tags]].",
"allpages": "{{doc-special|AllPages}}\nFirst part of the navigation bar for the
special page [[Special:AllPages]] and [[Special:PrefixIndex]].\nThe other parts
are {{msg-mw|Prevpage}} and {{msg-mw|Nextpage}}.\n{{Identical|All pages}}",
"allpages": "{{doc-special|AllPages}}\nFirst part of the navigation bar
for the special page [[Special:AllPages]] and [[Special:PrefixIndex]].\nThe
other parts are {{msg-mw|Prevpage}} and {{msg-mw|Nextpage}}.\n{{Identical|All
pages}}",
"allpages-summary": "{{doc-specialpagesummary|allpages}}",
"nextpage": "Third part of the navigation bar for the special page
[[Special:AllPages]] and [[Special:PrefixIndex]]. $1 is a page title. The other
parts are {{msg-mw|Allpages}} and {{msg-mw|Prevpage}}.\n\n{{Identical|Next
page}}",
@@ -3597,6 +3599,30 @@
"tags-update-add-not-allowed-multi": "Error message seen via the API
when a user tries to add more than one tag that is not properly
defined.\n\nParameters:\n* $1 - comma-separated list of tag names\n* $2 -
number of tags",
"tags-update-remove-not-allowed-one": "Error message seen via the API
when a user tries to remove a single tag that is not properly defined. This
message is only ever used in the case of 1 tag.\n\nParameters:\n* $1 - tag
name",
"tags-update-remove-not-allowed-multi": "Error message seen via the API
when a user tries to remove more than one tag that is not properly
defined.\n\nParameters:\n* $1 - comma-separated list of tag names\n* $2 -
number of tags",
+ "tags-edit-title": "The title of a page where tags can be added or
removed from selected revisions or log entries.\nFor more information on tags
see [[mw:Manual:Tags]].",
+ "tags-edit-manage-link": "Text of a link to [[Special:Tags]], in
imperative mood. Refers to the same thing as {{msg-mw|log-name-managetags}}.",
+ "tags-edit-revision-selected":
"{{Identical|revdelete-selected-text}}\n\nSee also:\n*
{{msg-mw|tags-edit-logentry-selected}}",
+ "tags-edit-logentry-selected": "{{Identical|logdelete-selected}}\n\nSee
also:\n* {{msg-mw|tags-edit-revision-selected}}",
+ "tags-edit-revision-explanation": "Leave blank.\n\nSee also:\n*
{{msg-mw|tags-edit-logentry-explanation}}",
+ "tags-edit-logentry-explanation": "Leave blank.\n\nSee also:\n*
{{msg-mw|tags-edit-revision-explanation}}",
+ "tags-edit-revision-legend": "Form legend.\n\nSee also:\n*
{{msg-mw|tags-edit-logentry-legend}}",
+ "tags-edit-logentry-legend": "Form legend.\n\nSee also:\n*
{{msg-mw|tags-edit-revision-legend}}",
+ "tags-edit-existing-tags": "Heading beneath which a list of tags
already applied to the revision or log entry is presented.",
+ "tags-edit-existing-tags-none": "Shown when no tags are applied. Should
be formatted differently (italicised or parenthesised).",
+ "tags-edit-new-tags": "Heading beneath which the user chooses which
tags should be attached to the revision or log entry. They may add or remove
tags.",
+ "tags-edit-add": "Heading beneath which the user picks which tags to
add to the revision or log entry.",
+ "tags-edit-remove": "Heading beneath which the user picks which tags to
remove from the revision or log entry.",
+ "tags-edit-remove-all-tags": "Check box label that the user selects
when they want to remove all the tags from the revision or log entry.",
+ "tags-edit-reason": "{{Identical|Reason}}",
+ "tags-edit-revision-submit": "Text of the submission button of the edit
tag form for revisions.\n\nSee also:\n* {{msg-mw|tags-edit-logentry-submit}}",
+ "tags-edit-logentry-submit": "Text of the submission button of the edit
tag form for log entries.\n\nSee also:\n* {{msg-mw|tags-edit-revision-submit}}",
+ "tags-edit-success": "Success message for the edit tag form.",
+ "tags-edit-failure": "Error message wrapper for the edit tag
form.\n\nParameters:\n* $1 - additional error messages",
+ "tags-edit-nooldid-title": "Title for an error message
({{msg-mw|tags-edit-nooldid-text}}) for the edit tag form.",
+ "tags-edit-nooldid-text": "Error message for the edit tag form.\n\nSee
also:\n* {{msg-mw|tags-edit-nooldid-title}}",
+ "tags-edit-none-selected": "Error message for the edit tag form.",
+ "tags-edit-chosen-placeholder": "Placeholder text on the jQuery Chosen
input box where users can select zero or more tags.",
+ "tags-edit-chosen-no-results": "Message displayed by the jQuery Chosen
input box when the user enters a string which doesn't match a known tag.\n\nDue
to technical limitations, the user's input is not passed as a parameter to this
message. The string the user entered is wrapped in quotation marks (\") and
appended to the end of this string.",
"comparepages": "The title of [[Special:ComparePages]]",
"comparepages-summary": "{{doc-specialpagesummary|comparepages}}",
"compare-page1": "Label for the field of the 1st page in the comparison
for [[Special:ComparePages]]\n{{Identical|Page}}",
diff --git a/resources/Resources.php b/resources/Resources.php
index e56d557..a17b678 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1401,6 +1401,20 @@
'mediawiki.special.changeslist.enhanced' => array(
'styles' =>
'resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css',
),
+ 'mediawiki.special.edittags' => array(
+ 'scripts' =>
'resources/src/mediawiki.special/mediawiki.special.edittags.js',
+ 'dependencies' => array(
+ 'jquery.chosen',
+ ),
+ 'messages' => array(
+ 'tags-edit-chosen-placeholder',
+ 'tags-edit-chosen-no-results',
+ ),
+ ),
+ 'mediawiki.special.edittags.styles' => array(
+ 'styles' =>
'resources/src/mediawiki.special/mediawiki.special.edittags.css',
+ 'position' => 'top',
+ ),
'mediawiki.special.import' => array(
'scripts' =>
'resources/src/mediawiki.special/mediawiki.special.import.js',
),
diff --git a/resources/src/mediawiki.action/mediawiki.action.history.js
b/resources/src/mediawiki.action/mediawiki.action.history.js
index ac48c59..2ebfe92 100644
--- a/resources/src/mediawiki.action/mediawiki.action.history.js
+++ b/resources/src/mediawiki.action/mediawiki.action.history.js
@@ -85,7 +85,8 @@
$copyForm.find( 'input[name^="ids["]:checked'
).prop( 'checked', false );
// Remove diff=&oldid=, change action=historysubmit to
revisiondelete, remove revisiondelete
- } else if ( $historySubmitter.hasClass(
'mw-history-revisiondelete-button' ) ) {
+ } else if ( $historySubmitter.hasClass(
'mw-history-revisiondelete-button' ) ||
+ $historySubmitter.hasClass(
'mw-history-editchangetags-button' ) ) {
$copyRadios.remove();
$copyAction.val( $historySubmitter.attr( 'name'
) );
$copyForm.find( ':submit' ).remove();
diff --git a/resources/src/mediawiki.legacy/shared.css
b/resources/src/mediawiki.legacy/shared.css
index e526d47..3657b12 100644
--- a/resources/src/mediawiki.legacy/shared.css
+++ b/resources/src/mediawiki.legacy/shared.css
@@ -426,7 +426,7 @@
border: 1px dashed #aaa;
}
-.mw-history-revisiondelete-button, #mw-fileduplicatesearch-icon {
+.mw-history-revisionactions, #mw-fileduplicatesearch-icon {
float: right;
}
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.css
b/resources/src/mediawiki.special/mediawiki.special.edittags.css
new file mode 100644
index 0000000..204009c
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.edittags.css
@@ -0,0 +1,15 @@
+/*!
+ * Styling for Special:EditTags and action=editchangetags
+ */
+#mw-edittags-tags-selector td {
+ vertical-align: top;
+}
+
+#mw-edittags-tags-selector-multi td {
+ vertical-align: top;
+ padding-right: 1.5em;
+}
+
+#mw-edittags-tag-list {
+ min-width: 20em;
+}
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.js
b/resources/src/mediawiki.special/mediawiki.special.edittags.js
new file mode 100644
index 0000000..69a2a67
--- /dev/null
+++ b/resources/src/mediawiki.special/mediawiki.special.edittags.js
@@ -0,0 +1,24 @@
+/*!
+ * JavaScript for Special:EditTags
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $tagList = $( '#mw-edittags-tag-list' );
+ if ( $tagList.length ) {
+ $tagList.chosen( {
+ /*jscs:disable
requireCamelCaseOrUpperCaseIdentifiers */
+ placeholder_text_multiple: mw.msg(
'tags-edit-chosen-placeholder' ),
+ no_results_text: mw.msg(
'tags-edit-chosen-no-results' )
+ } );
+ }
+
+ $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
+ $( '.mw-edittags-remove-checkbox' ).prop( 'checked',
e.target.checked );
+ } );
+ $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e
) {
+ if ( !e.target.checked ) {
+ $( '#mw-edittags-remove-all' ).prop( 'checked',
false );
+ }
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/tests/phpunit/includes/actions/ActionTest.php
b/tests/phpunit/includes/actions/ActionTest.php
index 83f5922..3babb97 100644
--- a/tests/phpunit/includes/actions/ActionTest.php
+++ b/tests/phpunit/includes/actions/ActionTest.php
@@ -19,7 +19,7 @@
'disabled' => false,
'view' => true,
'edit' => true,
- 'revisiondelete' => true,
+ 'revisiondelete' => 'SpecialPageAction',
'dummy' => true,
'string' => 'NamedDummyAction',
'declared' => 'NonExistingClassName',
--
To view, visit https://gerrit.wikimedia.org/r/204342
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I7d3ef927b5686f6211bc5817776286ead19d916b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: REL1_25
Gerrit-Owner: Anomie <[email protected]>
Gerrit-Reviewer: TTO <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits