Reedy has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/170588

Change subject: Watchlist grouping rebase attempt
......................................................................

Watchlist grouping rebase attempt

Doesn't include Messages. Includes merge conflict in Special(Edit)?Watchlist.php

Based on I702b9a8f3cda39a49c625654ca10fdd292e1e74c

Change-Id: If87504366263964aafc5826be9c1752513d18c8f
---
M includes/AutoLoader.php
M includes/User.php
M includes/WatchedItem.php
A includes/WatchlistGroup.php
M includes/installer/MysqlUpdater.php
M includes/installer/PostgresUpdater.php
M includes/installer/SqliteUpdater.php
M includes/specialpage/SpecialPageFactory.php
M includes/specials/SpecialEditWatchlist.php
A includes/specials/SpecialEditWatchlistGroup.php
M includes/specials/SpecialWatchlist.php
A maintenance/archives/patch-watchlist_groups.sql
A maintenance/archives/patch-watchlist_groups_newfield.sql
A maintenance/postgres/archives/patch-watchlist_groups.sql
A maintenance/postgres/archives/patch-watchlist_groups_newfield.sql
M maintenance/postgres/tables.sql
A maintenance/sqlite/archives/patch-watchlist_groups_newfield.sql
M maintenance/tables.sql
18 files changed, 1,032 insertions(+), 24 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/88/170588/1

diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 36a0fcb..b94be90 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -177,6 +177,7 @@
        'UserArrayFromResult' => 'includes/UserArrayFromResult.php',
        'UserRightsProxy' => 'includes/UserRightsProxy.php',
        'WatchedItem' => 'includes/WatchedItem.php',
+       'WatchlistGroup' => 'includes/WatchlistGroup.php',
        'WebRequest' => 'includes/WebRequest.php',
        'WebRequestUpload' => 'includes/WebRequest.php',
        'WebResponse' => 'includes/WebResponse.php',
@@ -1045,6 +1046,7 @@
        'SpecialCreateAccount' => 'includes/specials/SpecialCreateAccount.php',
        'SpecialDiff' => 'includes/specials/SpecialDiff.php',
        'SpecialEditWatchlist' => 'includes/specials/SpecialEditWatchlist.php',
+       'SpecialEditWatchlistGroup' => 
'includes/specials/SpecialEditWatchlistGroup.php',
        'SpecialEmailUser' => 'includes/specials/SpecialEmailuser.php',
        'SpecialExpandTemplates' => 
'includes/specials/SpecialExpandTemplates.php',
        'SpecialExport' => 'includes/specials/SpecialExport.php',
diff --git a/includes/User.php b/includes/User.php
index 90d33fb..e2e2f66 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -3234,9 +3234,10 @@
         * @param Title $title Title of the article to look at
         * @param int $checkRights Whether to check 
'viewmywatchlist'/'editmywatchlist' rights.
         *     Pass WatchedItem::CHECK_USER_RIGHTS or 
WatchedItem::IGNORE_USER_RIGHTS.
+        * * @param int $group ID of the watchlist group
         */
-       public function addWatch( $title, $checkRights = 
WatchedItem::CHECK_USER_RIGHTS ) {
-               $this->getWatchedItem( $title, $checkRights )->addWatch();
+       public function addWatch( $title, $checkRights = 
WatchedItem::CHECK_USER_RIGHTS, $group = 0 ) {
+               $this->getWatchedItem( $title, $checkRights )->setGroup( $group 
)->addWatch();
                $this->invalidateCache();
        }
 
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index ab136b8..b84a661 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -60,21 +60,36 @@
        private $timestamp;
 
        /**
-        * Create a WatchedItem object with the given user and title
+        * Internal identifier for the group,
+        * default: WatchlistGroup::DEFAULT_GROUP
+        *
+        * @var Integer
+        */
+       protected $group = WatchlistGroup::DEFAULT_GROUP;
+
+       /**
+        * Create a WatchedItem object with the given user and title.
+        *
         * @since 1.22 $checkRights parameter added
+        *
         * @param User $user The user to use for (un)watching
         * @param Title $title The title we're going to (un)watch
         * @param int $checkRights Whether to check the 'viewmywatchlist' and 
'editmywatchlist' rights.
         *     Pass either WatchedItem::IGNORE_USER_RIGHTS or 
WatchedItem::CHECK_USER_RIGHTS.
+        * @param $group Int: (default WatchlistGroup::DEFAULT_GROUP)
         * @return WatchedItem
         */
-       public static function fromUserTitle( $user, $title,
-               $checkRights = WatchedItem::CHECK_USER_RIGHTS
+       public static function fromUserTitle(
+               $user,
+               $title,
+               $checkRights = WatchedItem::CHECK_USER_RIGHTS,
+               $group = WatchlistGroup::DEFAULT_GROUP
        ) {
                $wl = new WatchedItem;
                $wl->mUser = $user;
                $wl->mTitle = $title;
                $wl->mCheckRights = $checkRights;
+               $wl->setGroup( $group );
 
                return $wl;
        }
@@ -271,10 +286,22 @@
        }
 
        /**
+        * Sets the group of the watched item.
+        * @return WatchedItem
+        */
+       public function setGroup( $group ) {
+               $isGroup = is_int( $group );
+               if( $isGroup ){
+                       $this->group = $group;
+               }
+               return $this;
+       }
+
+       /**
         * @param WatchedItem[] $items
         * @return bool
         */
-       public static function batchAddWatch( array $items ) {
+       public function batchAddWatch( array $items ) {
                $section = new ProfileSection( __METHOD__ );
 
                if ( wfReadOnly() ) {
@@ -289,6 +316,7 @@
                        }
                        $rows[] = array(
                                'wl_user' => $item->getUserId(),
+                               'wl_group' => $this->group,
                                'wl_namespace' => MWNamespace::getSubject( 
$item->getTitleNs() ),
                                'wl_title' => $item->getTitleDBkey(),
                                'wl_notificationtimestamp' => null,
@@ -297,6 +325,7 @@
                        // namespace:page and namespace_talk:page need separate 
entries:
                        $rows[] = array(
                                'wl_user' => $item->getUserId(),
+                               'wl_group' => $this->group,
                                'wl_namespace' => MWNamespace::getTalk( 
$item->getTitleNs() ),
                                'wl_title' => $item->getTitleDBkey(),
                                'wl_notificationtimestamp' => null
@@ -323,7 +352,7 @@
         * @return bool
         */
        public function addWatch() {
-               return self::batchAddWatch( array( $this ) );
+               return $this->batchAddWatch( array( $this ) );
        }
 
        /**
@@ -432,4 +461,4 @@
 
                return true;
        }
-}
+}
\ No newline at end of file
diff --git a/includes/WatchlistGroup.php b/includes/WatchlistGroup.php
new file mode 100644
index 0000000..c9f1f2d
--- /dev/null
+++ b/includes/WatchlistGroup.php
@@ -0,0 +1,285 @@
+<?php
+/**
+ * Accessor and mutator for watchlist groups.
+ *
+ * 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
+ */
+
+/**
+ * Class representing whatchlist groups.
+ *
+ * @note mutators require a User object to check permissions, while accessors
+ * just take user IDs.
+ *
+*/
+class WatchlistGroup {
+
+       /**
+        * User to whom belong the whatched group of items
+        */
+       protected $user;
+
+       /**
+        * Id of the user
+        */
+       protected $id;
+
+       /**
+        * Array representing this user watchlists.
+        * Key is the groupID, the value contains the group name and
+        * the permission.
+        *
+        */
+       protected $groups = array();
+
+       /**
+        * Constant representing the ID of the general group which is used
+        * whenever an item is watched but not actually assigned to a specifc
+        * group.
+        */
+       const DEFAULT_GROUP = 0;
+
+       /**
+        * Create a WatchlistGroup object with the given user and title
+        * @param $user User: the user that owns the watchlist
+        * @return WatchlistGroup object
+        */
+       public static function newFromUser( $user ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'watchlist_groups',
+                       array( 'wg_id', 'wg_name', 'wg_perm' ),
+                       array( 'wg_user' => $user->getId() ), __METHOD__
+               );
+               $groups = array();
+               foreach ( $res as $s ) {
+                       $groups[$s->wg_id] = array( $s->wg_name, $s->wg_perm );
+               }
+
+               # Instantiate a represent the whatchlist
+               $wg = new WatchlistGroup;
+               $wg->user = $user;
+               $wg->id = $user->getId();
+               $wg->groups = $groups;
+
+               return $wg;
+       }
+
+       /**
+        * Returns a two dimensional array containing user's watchlist groups in
+        * the following format:
+        *
+        * @code
+        * array(
+        *     id => array( <group name>, <permission> ),
+        * )
+        * @endcode
+        *
+        * @see $groups
+        *
+        * @param bool $include_nogroup (default false)
+        * @return array of groups
+        */
+       public function getGroups( $include_nogroup = false ) {
+
+               $groups = $this->groups;
+
+               // Include the "ungrouped" group
+               if( $include_nogroup ) {
+                       $groups[WatchlistGroup::DEFAULT_GROUP] = array(
+                               wfMsg( 'watchlistedit-nogroup' ),
+                               0 /** permission */
+                       );
+               }
+               return $groups;
+       }
+
+       /**
+        * Gets the group ID for a group name in a user's watchlist
+        *
+        * @param $groupName string
+        * @return bool|string False on failure
+        */
+       public function getGroupFromName( $groupName ) {
+               if( $groupName == 'none' ){
+                       return WatchlistGroup::DEFAULT_GROUP;
+               }
+               foreach ($this->groups as $gid => $ginfo) {
+                       if( $groupName == $ginfo[0] ) {
+                               return intval( $gid );
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Gets the group name for a group ID in a user's watchlist
+        *
+        * @param $groupId int
+        *
+        * @return String
+        */
+       public function getGroupNameFromID( $groupId ) {
+               return $this->groups[$groupId][0];
+       }
+
+       /**
+        * Checks a group name and returns the name without invalid characters 
if
+        * it is otherwise okay.
+        * Returns false if is a reserved keyword.
+        *
+        * @param $groupName string: the group name
+        *
+        * @return bool|string False on failure
+        */
+       public static function checkValidGroupName( $groupName ) {
+               # FIXME this is most probably wrong
+               $groupName = preg_replace( '/[^a-zA-Z0-9]+/', '', $groupName );
+               if( $groupName == '' || $groupName == 'clear' || $groupName == 
'raw'
+                       || $groupName == 'edit' || $groupName == '0' || 
$groupName == '1'
+                       || $groupName == '2'
+               ) {
+                       return false;
+               }
+               return $groupName;
+       }
+
+       /**
+        * Existence/permission-checking method for watchlist groups.
+        *
+        * @param $groupId int: the group ID
+        * @param $perm bool: if true, the group must also be viewable by the 
given
+        * user to return true
+        *
+        * @return bool
+        */
+       public function isGroup( $groupId, $perm = false ) {
+               $dbw = wfGetDB( DB_SLAVE );
+               $res = $dbw->selectRow( 'watchlist_groups',
+                       array( 'wg_name', 'wg_user', 'wg_perm' ),
+                       array( 'wg_id' => $groupId, ),
+                       __METHOD__
+               );
+
+               # FIXME this need to be made easier to understand
+               return $res && (
+                       // we're not supposed to check permissions:
+                       !$perm ||
+                       // the user has permission:
+                       ( $perm && $res->wg_perm ) ||
+                       // users can access their own watchlists:
+                       $this->id == $res->wg_user
+               );
+       }
+
+       /**
+        * Mutators
+        **/
+
+       /**
+        * Changes the group associated with titles in a watchlist
+        *
+        * @param $titles array: titles to be regrouped
+        * @param $group int: the group ID of the new desired group
+        *
+        * @return bool
+        */
+       public function regroupTitles( $titles, $group ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $lb = new LinkBatch( $titles );
+               $where_titles = $lb->constructSet( 'wl', $dbw );
+               $res = $dbw->update( 'watchlist',
+                       array( 'wl_group' => $group ),
+                       array( 'wl_user' => $this->id,  $where_titles ),
+                       __METHOD__
+               );
+               return $res;
+       }
+
+       /**
+        * Create a watchlist group.
+        *
+        * @param $groupName string: the name of the new group
+        *
+        * @return bool
+        */
+       public function createGroup( $groupName ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $res = $dbw->insert( 'watchlist_groups',
+                       array( 'wg_name' => $groupName, 'wg_user' => $this->id 
),
+                       __METHOD__
+               );
+               return $res;
+       }
+
+       /**
+        * Rename a watchlist group.
+        *
+        * @param $group int: the group ID
+        * @param $newName string: the new name of the group
+        *
+        * @return bool
+        */
+       public function renameGroup( $groupId, $newName ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $res = $dbw->update( 'watchlist_groups',
+                       array( 'wg_name' => $newName ),
+                       array( 'wg_id' => $groupId, 'wg_user' => $this->id ),
+                       __METHOD__
+               );
+               return $res;
+       }
+
+       /**
+        * Change the permissions for a watchlist group
+        *
+        * @param $groupId int: the group ID
+        * @param $perm int: 0 (private) or 1 (public)
+        *
+        * @return bool
+        */
+       public function changePerm( $groupId, $perm ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $res = $dbw->update( 'watchlist_groups',
+                       array( 'wg_perm' => $perm ),
+                       array( 'wg_id' => $groupId, 'wg_user' => $this->id ),
+                       __METHOD__
+               );
+               return $res;
+       }
+
+       /**
+        * Delete a watchlist group
+        *
+        * @param $groupId int: the group ID
+        *
+        * @return bool
+        */
+       public function deleteGroup( $groupId ) {
+               $dbw = wfGetDB( DB_MASTER );
+               # FIXME: this should be wrapped in a transaction
+               $dbw->update( 'watchlist',
+                       array( 'wl_group' => 0 ), array( 'wl_group' => $groupId 
),
+                       __METHOD__
+               );
+               $res = $dbw->delete( 'watchlist_groups',
+                       array( 'wg_id' => $groupId, 'wg_user' => $this->id),
+                       __METHOD__
+               );
+               return $res;
+       }
+}
diff --git a/includes/installer/MysqlUpdater.php 
b/includes/installer/MysqlUpdater.php
index 990b5b0..6dfe8d2 100644
--- a/includes/installer/MysqlUpdater.php
+++ b/includes/installer/MysqlUpdater.php
@@ -266,6 +266,10 @@
                                'patch-oi_major_mime-chemical.sql' ),
                        array( 'modifyField', 'filearchive', 'fa_major_mime',
                                'patch-fa_major_mime-chemical.sql' ),
+
+                       // 1.25
+                       array( 'addTable', 'watchlist_groups', 
'patch-watchlist_groups.sql' ),
+                       array( 'addField', 'watchlist', 'wl_group', 
'patch-watchlist_groups_newfield.sql' ),
                );
        }
 
diff --git a/includes/installer/PostgresUpdater.php 
b/includes/installer/PostgresUpdater.php
index 9e8ee94..4af71ca 100644
--- a/includes/installer/PostgresUpdater.php
+++ b/includes/installer/PostgresUpdater.php
@@ -91,6 +91,7 @@
                        array( 'addTable', 'uploadstash', 
'patch-uploadstash.sql' ),
                        array( 'addTable', 'user_former_groups', 
'patch-user_former_groups.sql' ),
                        array( 'addTable', 'sites', 'patch-sites.sql' ),
+                       array( 'addTable', 'watchlist_groups',  
'patch-watchlist_groups.sql' ),
 
                        # Needed before new field
                        array( 'convertArchive2' ),
@@ -169,6 +170,7 @@
                        array( 'addPgField', 'externallinks', 'el_id',
                                "INTEGER NOT NULL PRIMARY KEY DEFAULT 
nextval('externallinks_el_id_seq')" ),
                        array( 'addPgField', 'uploadstash', 'us_props', "BYTEA" 
),
+                       array( 'addPgField', 'watchlist', 'wl_group', "INTEGER 
NOT NULL DEFAULT 0" ),
 
                        # type changes
                        array( 'changeField', 'archive', 'ar_deleted', 
'smallint', '' ),
diff --git a/includes/installer/SqliteUpdater.php 
b/includes/installer/SqliteUpdater.php
index ab5ab7d..ce6b0cc 100644
--- a/includes/installer/SqliteUpdater.php
+++ b/includes/installer/SqliteUpdater.php
@@ -137,6 +137,10 @@
                        array( 'addField', 'pagelinks', 'pl_from_namespace', 
'patch-pl_from_namespace.sql' ),
                        array( 'addField', 'templatelinks', 
'tl_from_namespace', 'patch-tl_from_namespace.sql' ),
                        array( 'addField', 'imagelinks', 'il_from_namespace', 
'patch-il_from_namespace.sql' ),
+
+                       // 1.25
+                       array( 'addTable', 'watchlist_groups', 
'patch-watchlist_groups.sql' ),
+                       array( 'addField', 'watchlist', 'wl_group', 
'patch-watchlist_groups_newfield.sql' ),
                );
        }
 
diff --git a/includes/specialpage/SpecialPageFactory.php 
b/includes/specialpage/SpecialPageFactory.php
index b110bda..4c2787d 100644
--- a/includes/specialpage/SpecialPageFactory.php
+++ b/includes/specialpage/SpecialPageFactory.php
@@ -101,6 +101,7 @@
                'Listbots' => 'SpecialListBots',
                'Userrights' => 'UserrightsPage',
                'EditWatchlist' => 'SpecialEditWatchlist',
+               'EditWatchlistGroup' => 'SpecialEditWatchlistGroup',
 
                // Recent changes and logs
                'Newimages' => 'SpecialNewFiles',
diff --git a/includes/specials/SpecialEditWatchlist.php 
b/includes/specials/SpecialEditWatchlist.php
index bc63e99..7bd1615 100644
--- a/includes/specials/SpecialEditWatchlist.php
+++ b/includes/specials/SpecialEditWatchlist.php
@@ -48,6 +48,8 @@
        protected $toc;
 
        private $badItems = array();
+       private $wg_obj;
+       private $groupName = '';
 
        public function __construct() {
                parent::__construct( 'EditWatchlist', 'editmywatchlist' );
@@ -56,9 +58,25 @@
        /**
         * Main execution point
         *
+<<<<<<< HEAD
         * @param int $mode
+=======
+        * @param $par string: This could be the mode OR a watchlist query by 
user/group
+>>>>>>> 4e55649... Watchlist grouping
         */
-       public function execute( $mode ) {
+       public function execute( $par ) {
+               $this->wg_obj = WatchlistGroup::newFromUser( $this->getUser() );
+               // Set the mode
+               if( $par == 'clear' || $par == 'raw' || $par == 'edit'
+                       || $par == '0' || $par == '1' || $par == '2' ) {
+                       $mode = $par;
+               } else {
+                       $mode = '';
+                       if( $this->wg_obj->isGroup( 
$this->wg_obj->getGroupFromName( $par ) ) ) {
+                               $this->groupName = $par;
+                       }
+               }
+
                $this->setHeaders();
 
                # Anons don't get a watchlist
@@ -91,12 +109,30 @@
                                        $out->addReturnTo( 
SpecialPage::getTitleFor( 'Watchlist' ) );
                                }
                                break;
+<<<<<<< HEAD
                        case self::EDIT_CLEAR:
                                $out->setPageTitle( $this->msg( 
'watchlistedit-clear-title' ) );
                                $form = $this->getClearForm();
                                if ( $form->show() ) {
                                        $out->addHTML( $this->successMessage );
                                        $out->addReturnTo( 
SpecialPage::getTitleFor( 'Watchlist' ) );
+=======
+
+                       case self::EDIT_NORMAL:
+                       default:
+                               if( $this->getWatchlistInfo() == array() ) {
+                                       $out->setPageTitle( $this->msg( 
'watchlistedit-normal-title' ) );
+                                       $out->addWikiMsg( 
'watchlistedit-noitems' );
+                               } else {
+                                       $out->setPageTitle( $this->msg( 
'watchlistedit-normal-title' ) );
+                                       $form = $this->getNormalForm();
+                                       if( $form->show() ) {
+                                               $out->addHTML( 
$this->successMessage );
+                                               $out->addReturnTo( 
SpecialPage::getTitleFor( 'Watchlist' ) );
+                                       } elseif ( $this->toc !== false ) {
+                                               $out->prependHTML( $this->toc );
+                                       }
+>>>>>>> 4e55649... Watchlist grouping
                                }
                                break;
 
@@ -161,17 +197,23 @@
         * @return array
         */
        private function extractTitles( $list ) {
+               $titles = array();
                $list = explode( "\n", trim( $list ) );
                if ( !is_array( $list ) ) {
                        return array();
                }
+<<<<<<< HEAD
 
                $titles = array();
 
                foreach ( $list as $text ) {
+=======
+               foreach( $list as $text ) {
+>>>>>>> 4e55649... Watchlist grouping
                        $text = trim( $text );
                        if ( strlen( $text ) > 0 ) {
                                $title = Title::newFromText( $text );
+<<<<<<< HEAD
                                if ( $title instanceof Title && 
$title->isWatchable() ) {
                                        $titles[] = $title;
                                }
@@ -187,11 +229,20 @@
                }
 
                return array_unique( $list );
+=======
+                               if( $title instanceof Title && 
$title->isWatchable() ) {
+                                       $titles[] = $title->getPrefixedText();
+                               }
+                       }
+               }
+               return array_unique( $titles );
+>>>>>>> 4e55649... Watchlist grouping
        }
 
        public function submitRaw( $data ) {
-               $wanted = $this->extractTitles( $data['Titles'] );
+               $wanted = array();
                $current = $this->getWatchlist();
+<<<<<<< HEAD
 
                if ( count( $wanted ) > 0 ) {
                        $toWatch = array_diff( $wanted, $current );
@@ -201,11 +252,41 @@
                        $this->getUser()->invalidateCache();
 
                        if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) 
{
+=======
+               $allWatched = array();
+               $allUnwatched = array();
+               foreach( $data as $key => $d ) {
+                       if( substr( $key, 0, 12 ) == 'Titles_group' ) {
+                               $group = intval( substr( $key, 12, 13 ) );
+                               $wanted[$group] = $this->extractTitles( 
$data['Titles_group' . $group] );
+                       }
+               }
+               $groups = array_unique( array_keys( $wanted ) + array_keys( 
$current ) );
+               foreach( $wanted as $gid => $titles ) {
+                       if( count( $wanted ) > 0 ) {
+                               $toUnwatch = array_diff( $current[$gid], 
$wanted[$gid] );
+                               $this->unwatchTitles( $toUnwatch );
+                               $this->getUser()->invalidateCache();
+                               $allUnwatched += $toUnwatch;
+                       }
+               }
+               foreach( $wanted as $gid => $titles ) {
+                       if( count( $wanted ) > 0 ) {
+                               $toWatch = array_diff( $wanted[$gid], 
$current[$gid] );
+                               $this->watchTitles( $toWatch, $gid );
+                               $this->getUser()->invalidateCache();
+                               $allWatched += $toWatch;
+                       }
+               }
+               if( count( $wanted, 1 ) > count( $wanted ) ) {
+                       if( count( $allWatched ) > 0 || count( $allUnwatched ) 
> 0 ) {
+>>>>>>> 4e55649... Watchlist grouping
                                $this->successMessage = $this->msg( 
'watchlistedit-raw-done' )->parse();
                        } else {
                                return false;
                        }
 
+<<<<<<< HEAD
                        if ( count( $toWatch ) > 0 ) {
                                $this->successMessage .= ' ' . $this->msg( 
'watchlistedit-raw-added' )
                                        ->numParams( count( $toWatch ) 
)->parse();
@@ -216,12 +297,28 @@
                                $this->successMessage .= ' ' . $this->msg( 
'watchlistedit-raw-removed' )
                                        ->numParams( count( $toUnwatch ) 
)->parse();
                                $this->showTitles( $toUnwatch, 
$this->successMessage );
+=======
+                       if( count( $allWatched ) > 0 ) {
+                               $this->successMessage .= ' ' . $this->msg( 
'watchlistedit-raw-added'
+                                       )->numParams( count( $allWatched ) 
)->parse();
+                               $this->showTitles( $allWatched, 
$this->successMessage );
+                       }
+
+                       if( count( $allUnwatched ) > 0 ) {
+                               $this->successMessage .= ' ' . $this->msg( 
'watchlistedit-raw-removed'
+                                       )->numParams( count( $allUnwatched ) 
)->parse();
+                               $this->showTitles( $allUnwatched, 
$this->successMessage );
+>>>>>>> 4e55649... Watchlist grouping
                        }
                } else {
                        $this->clearWatchlist();
                        $this->getUser()->invalidateCache();
 
+<<<<<<< HEAD
                        if ( count( $current ) > 0 ) {
+=======
+                       if( count( $current, 1 ) > count( $current ) ) {
+>>>>>>> 4e55649... Watchlist grouping
                                $this->successMessage = $this->msg( 
'watchlistedit-raw-done' )->parse();
                        } else {
                                return false;
@@ -309,31 +406,38 @@
                $res = $dbr->select(
                        'watchlist',
                        array(
-                               'wl_namespace', 'wl_title'
+                               'wl_group', 'wl_namespace', 'wl_title'
                        ), array(
                                'wl_user' => $this->getUser()->getId(),
                        ),
                        __METHOD__
                );
+<<<<<<< HEAD
 
                if ( $res->numRows() > 0 ) {
                        $titles = array();
+=======
+               if( $res->numRows() > 0 ) {
+>>>>>>> 4e55649... Watchlist grouping
                        foreach ( $res as $row ) {
                                $title = Title::makeTitleSafe( 
$row->wl_namespace, $row->wl_title );
 
                                if ( $this->checkTitle( $title, 
$row->wl_namespace, $row->wl_title )
                                        && !$title->isTalkPage()
                                ) {
-                                       $titles[] = $title;
+                                       $list[$row->wl_group][] = 
$title->getPrefixedText();
                                }
                        }
                        $res->free();
+<<<<<<< HEAD
 
                        GenderCache::singleton()->doTitlesArray( $titles );
 
                        foreach ( $titles as $title ) {
                                $list[] = $title->getPrefixedText();
                        }
+=======
+>>>>>>> 4e55649... Watchlist grouping
                }
 
                $this->cleanupWatchlist();
@@ -349,12 +453,15 @@
         */
        protected function getWatchlistInfo() {
                $titles = array();
+               $where = array( 'wl_user' => $this->getUser()->getId() );
+               if ( $this->groupName != '' ) {
+                       $where['wl_group'] = $this->wg_obj->getGroupFromName( 
$this->groupName );
+               }
                $dbr = wfGetDB( DB_MASTER );
-
                $res = $dbr->select(
                        array( 'watchlist' ),
-                       array( 'wl_namespace', 'wl_title' ),
-                       array( 'wl_user' => $this->getUser()->getId() ),
+                       array( 'wl_namespace', 'wl_group', 'wl_title' ),
+                       $where,
                        __METHOD__,
                        array( 'ORDER BY' => array( 'wl_namespace', 'wl_title' 
) )
                );
@@ -364,7 +471,7 @@
                foreach ( $res as $row ) {
                        $lb->add( $row->wl_namespace, $row->wl_title );
                        if ( !MWNamespace::isTalk( $row->wl_namespace ) ) {
-                               $titles[$row->wl_namespace][$row->wl_title] = 1;
+                               $titles[$row->wl_namespace][$row->wl_title] = 
intval( $row->wl_group );
                        }
                }
 
@@ -452,7 +559,7 @@
         *
         * @param array $titles Array of strings, or Title objects
         */
-       private function watchTitles( $titles ) {
+       private function watchTitles( $titles, $group = 0 ) {
                $dbw = wfGetDB( DB_MASTER );
                $rows = array();
 
@@ -464,12 +571,14 @@
                        if ( $title instanceof Title ) {
                                $rows[] = array(
                                        'wl_user' => $this->getUser()->getId(),
+                                       'wl_group' => $group,
                                        'wl_namespace' => 
MWNamespace::getSubject( $title->getNamespace() ),
                                        'wl_title' => $title->getDBkey(),
                                        'wl_notificationtimestamp' => null,
                                );
                                $rows[] = array(
                                        'wl_user' => $this->getUser()->getId(),
+                                       'wl_group' => $group,
                                        'wl_namespace' => MWNamespace::getTalk( 
$title->getNamespace() ),
                                        'wl_title' => $title->getDBkey(),
                                        'wl_notificationtimestamp' => null,
@@ -526,7 +635,31 @@
        public function submitNormal( $data ) {
                $removed = array();
 
+<<<<<<< HEAD
                foreach ( $data as $titles ) {
+=======
+               // Regrouping submission
+               $group = intval( $data['group'] );
+               unset($data['group']);
+               if( $group > -1 ) {
+                       foreach( $data as $ns => $title_strings ) {
+                               $nsid = intval( str_replace( 'TitlesNs', '', 
$ns ) );
+                               $titles = array();
+                               foreach( $title_strings as $t ) {
+                                       $title = Title::newFromText( $t );
+                                       $titles[] = $title;
+                                       $titles[] = $title->getTalkPage();
+                               }
+                               if( count( $titles ) > 0 ) {
+                                       $this->wg_obj->regroupTitles( $titles, 
$group );
+                               }
+                       }
+                       $this->successMessage = $this->msg( 
'watchlistedit-normal-donegrouping' )->escaped();
+                       return true;
+               }
+
+               foreach( $data as $titles ) {
+>>>>>>> 4e55649... Watchlist grouping
                        $this->unwatchTitles( $titles );
                        $removed = array_merge( $removed, $titles );
                }
@@ -551,6 +684,7 @@
                global $wgContLang;
 
                $fields = array();
+
                $count = 0;
 
                // Allow subscribers to manipulate the list of watched pages 
(or use it
@@ -564,12 +698,20 @@
                foreach ( $watchlistInfo as $namespace => $pages ) {
                        $options = array();
 
+<<<<<<< HEAD
                        foreach ( array_keys( $pages ) as $dbkey ) {
                                $title = Title::makeTitleSafe( $namespace, 
$dbkey );
 
                                if ( $this->checkTitle( $title, $namespace, 
$dbkey ) ) {
                                        $text = $this->buildRemoveLine( $title 
);
                                        $options[$text] = 
$title->getPrefixedText();
+=======
+                       foreach( $pages as $dbkey => $group ) {
+                               $title = Title::makeTitleSafe( $namespace, 
$dbkey );
+                               if ( $this->checkTitle( $title, $namespace, 
$dbkey )  ) {
+                                       $text = $this->buildRemoveLine( $title, 
$group );
+                                       
$fields['TitlesNs'.$namespace]['options'][$text] = htmlspecialchars( 
$title->getPrefixedText() );
+>>>>>>> 4e55649... Watchlist grouping
                                        $count++;
                                }
                        }
@@ -599,15 +741,38 @@
                                $this->toc .= Linker::tocLine( 
"editwatchlist-{$data['section']}", $nsText,
                                        $this->getLanguage()->formatNum( 
++$tocLength ), 1 ) . Linker::tocLineEnd();
                        }
+<<<<<<< HEAD
 
                        $this->toc = Linker::tocList( $this->toc );
+=======
+                       //$this->toc = Linker::tocList( $this->toc );
+>>>>>>> 4e55649... Watchlist grouping
                } else {
                        $this->toc = false;
                }
 
+<<<<<<< HEAD
                $context = new DerivativeContext( $this->getContext() );
                $context->setTitle( $this->getPageTitle() ); // Remove subpage
                $form = new EditWatchlistNormalHTMLForm( $fields, $context );
+=======
+               $groups = $this->wg_obj->getGroups();
+               foreach( $groups as &$g ) {
+                       $g = $this->msg( 'watchlistedit-normal-change' 
)->rawParams( $g )->parse();
+               }
+
+               $gsOptions = array_merge( array( $this->msg( 
'watchlistedit-normal-remove' )->escaped() => -1,
+                                               $this->msg( 
'watchlistedit-normal-ungroup' )->escaped() => 0 ), array_flip( $groups ) );
+
+               $fields['group'] = array(
+                       'type' => 'select',
+                       'options' => $gsOptions,
+                       'label' => $this->msg( 'watchlistedit-normal-action' 
)->escaped()
+               );
+
+               $form = new EditWatchlistNormalHTMLForm( $fields, 
$this->getContext()->getUser()->getId() );
+               $form->setTitle( $this->getTitle() );
+>>>>>>> 4e55649... Watchlist grouping
                $form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
                $form->setSubmitDestructive();
                # Used message keys:
@@ -615,6 +780,9 @@
                $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
                $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
                $form->addHeaderText( $this->msg( 
'watchlistedit-normal-explain' )->parse() );
+               if( $this->groupName != '' ) {
+                       $form->addHeaderText( '<p>' . $this->msg( 
'watchlistedit-normal-onlygroup' )->rawParams( $this->groupName )->parse() . 
'</p>' );
+               }
                $form->setSubmitCallback( array( $this, 'submitNormal' ) );
 
                return $form;
@@ -623,10 +791,15 @@
        /**
         * Build the label for a checkbox, with a link to the title, and 
various additional bits
         *
+<<<<<<< HEAD
         * @param Title $title
+=======
+        * @param $title Title
+        * @param $group int
+>>>>>>> 4e55649... Watchlist grouping
         * @return string
         */
-       private function buildRemoveLine( $title ) {
+       private function buildRemoveLine( $title, $gid ) {
                $link = Linker::link( $title );
 
                $tools['talk'] = Linker::link(
@@ -650,6 +823,7 @@
                        );
                }
 
+<<<<<<< HEAD
                wfRunHooks(
                        'WatchlistEditorBuildRemoveLine',
                        array( &$tools, $title, $title->isRedirect(), 
$this->getSkin(), &$link )
@@ -659,8 +833,20 @@
                        // Linker already makes class mw-redirect, so this is 
redundant
                        $link = '<span class="watchlistredir">' . $link . 
'</span>';
                }
+=======
+               $groups = $this->wg_obj->getGroups( true );
+               $wgroup = $groups[$gid];
+               if( $gid == 0 ){
+                       $url = 'none';
+               } else {
+                       $url = $wgroup[0];
+               }
+               $wgrouplink = Linker::link( SpecialPage::getTitleFor( 
'EditWatchlist', $url ), $wgroup[0] );
 
-               return $link . " (" . $this->getLanguage()->pipeList( $tools ) 
. ")";
+               wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, 
$title, $title->isRedirect(), $this->getSkin() ) );
+>>>>>>> 4e55649... Watchlist grouping
+
+               return $link . " (" . $this->getLanguage()->pipeList( $tools ) 
. ") (" . $wgrouplink . ")";
        }
 
        /**
@@ -669,6 +855,7 @@
         * @return HTMLForm
         */
        protected function getRawForm() {
+<<<<<<< HEAD
                $titles = implode( $this->getWatchlist(), "\n" );
                $fields = array(
                        'Titles' => array(
@@ -680,6 +867,27 @@
                $context = new DerivativeContext( $this->getContext() );
                $context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset 
subpage
                $form = new HTMLForm( $fields, $context );
+=======
+               $titles = $this->getWatchlist();
+               $fields = array();
+               $groups = array( 0 => array( $this->msg( 
'watchlistedit-nogroup' )->parse(), 0 ) );
+               $groups = $groups + $this->wg_obj->getGroups();
+               foreach( $groups as $gid => $g ) {
+                       if( !empty( $titles[$gid] ) ) {
+                               $fields['Titles_group' . $gid] = array(
+                                       //'type' => 'textarea',
+                                       'class' => 'HTMLTextAreaField',
+                                       'section' => $g[0],
+                                       'cssclass' => 'mw-collapsible 
mw-collapsed',
+                                       'rows' => 16,
+                                       'label-message' => 
'watchlistedit-raw-titles',
+                                       'default' => implode( $titles[$gid], 
"\n" ),
+                               );
+                       }
+               }
+               $form = new EditWatchlistRawHTMLForm( $fields, 
$this->getContext() );
+               $form->setTitle( $this->getTitle( 'raw' ) );
+>>>>>>> 4e55649... Watchlist grouping
                $form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
                # Used message keys: 'accesskey-watchlistedit-raw-submit', 
'tooltip-watchlistedit-raw-submit'
                $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
@@ -750,7 +958,11 @@
                        'view' => array( 'Watchlist', false ),
                        'edit' => array( 'EditWatchlist', false ),
                        'raw' => array( 'EditWatchlist', 'raw' ),
+<<<<<<< HEAD
                        'clear' => array( 'EditWatchlist', 'clear' ),
+=======
+                       'group' => array( 'EditWatchlistGroup', false )
+>>>>>>> 4e55649... Watchlist grouping
                );
 
                foreach ( $modes as $mode => $arr ) {
@@ -786,6 +998,12 @@
        }
 }
 
+class EditWatchlistRawHTMLForm extends HTMLForm {
+       public function getLegend( $group ) {
+               return $group;
+       }
+}
+
 class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
        /**
         * HTMLMultiSelectField throws validation errors if we get input data
diff --git a/includes/specials/SpecialEditWatchlistGroup.php 
b/includes/specials/SpecialEditWatchlistGroup.php
new file mode 100644
index 0000000..a1d0ac4
--- /dev/null
+++ b/includes/specials/SpecialEditWatchlistGroup.php
@@ -0,0 +1,197 @@
+<?php
+/**
+ * Implements Special:EditWatchlistGroup.
+ *
+ * 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
+ * @ingroup Watchlist
+ */
+
+/**
+ * Provides the UI through which users can modify their watchlist groups.
+ *
+ * @ingroup SpecialPage
+ * @ingroup Watchlist
+ * @author Aaron Pramana <[email protected]>
+ */
+class SpecialEditWatchlistGroup extends UnlistedSpecialPage {
+
+       /**
+        * Holds a WatchlistGroup object
+        */
+       protected $wg_obj;
+
+       public function __construct() {
+               parent::__construct( 'EditWatchlistGroup' );
+       }
+
+       /**
+        * Main execution point.
+        */
+       public function execute($subPage) {
+               $out = $this->getOutput();
+
+               # Anons do not have a watchlist
+               if( $this->getUser()->isAnon() ) {
+                       $out->setPageTitle( $this->msg( 'watchnologin' ) );
+                       $llink = Linker::linkKnown(
+                               SpecialPage::getTitleFor( 'Userlogin' ),
+                               $this->msg( 'loginreqlink' )->escaped(),
+                               array(),
+                               array( 'returnto' => 
$this->getTitle()->getPrefixedText() )
+                       );
+                       $out->addHTML( $this->msg( 'watchlistanontext' 
)->rawParams( $llink )->parse() );
+                       return;
+               }
+
+               $this->wg_obj = WatchlistGroup::newFromUser( $this->getUser() );
+
+               $this->setHeaders();
+               $this->outputHeader();
+
+               $out->addSubtitle( $this->msg( 'watchlistfor2', 
$this->getUser()->getName()
+                       )->rawParams( SpecialEditWatchlist::buildTools( null ) 
) );
+
+               $form = $this->getForm();
+               if( $form->show() ) {
+                       $out->addHTML( $this->successMessage );
+                       $out->returnToMain();
+               }
+       }
+
+       /**
+        * Get a form for editing watchlist groups
+        *
+        * @return HTMLForm
+        */
+       protected function getForm() {
+               $fields = array();
+               $groups = $this->wg_obj->getGroups();
+
+               // fields for each of the existing groups
+               foreach( $groups as $id => $info ) {
+
+                       $fields['groupaction_' . $id] = array(
+                               'type' => 'select',
+                               'options' => array( $this->msg( 
'wlgroup-noaction' )->escaped() => 0,
+                                       $this->msg( 'wlgroup-rename' 
)->escaped() => 1,
+                                       $this->msg( 'wlgroup-changeperm' 
)->escaped() => 2,
+                                       $this->msg( 'wlgroup-delete' 
)->escaped() => -1 ),
+                               'label' => $this->msg( 'actions' )->parse(),
+                               'section' => $info[0]
+                       );
+
+                       $fields['grouprename_' . $id] = array(
+                               'type' => 'text',
+                               'label' => $this->msg( 'wlgroup-renameto' 
)->rawParams( $info[0] )->parse(),
+                               'size' => '15',
+                               'section' => $info[0]
+                       );
+
+                       $fields['groupperm_' . $id] = array(
+                               'type' => 'select',
+                               'options' => array( $this->msg( 
'wlgroup-permprivate' )->parse() => 0,
+                                       $this->msg( 'wlgroup-permpublic' 
)->parse() => 1 ),
+                               'default' => $info[1],
+                               'label' => $this->msg( 'wlgroup-perm' 
)->parse(),
+                               'section' => $info[0]
+                       );
+               }
+
+               // field used to add a new group
+               $fields['groupnew'] = array(
+                       'type' => 'text',
+                       'label' => $this->msg( 'wlgroup-createnew' ),
+                       'size' => '15'
+               );
+
+               $form = new EditWatchlistGroupHTMLForm( $fields, 
$this->getContext() );
+               $form->setTitle( $this->getTitle() )
+                       ->setSubmitTextMsg( 'wlgroup-submit' )
+                       ->setSubmitTooltip('wlgroup-submit')
+                       ->setWrapperLegendMsg( 'wlgroup-legend' )
+                       ->addHeaderText( $this->msg( 'wlgroup-explain' 
)->parse() )
+                       ->setSubmitCallback( array( $this, 'submit' ) )
+               ;
+
+               return $form;
+       }
+
+       /**
+        * The callback function for the watchlist group editing form
+        *
+        * @param $data array
+        */
+       public function submit( $data ) {
+               $wg = 
WatchlistGroup::newFromUser($this->getContext()->getUser());
+               $status = true;
+               // create a new group if requested
+               if( $data['groupnew'] != '' ) {
+                       // for now, group names are limited to a-z, 0-9 - 
discuss tech. restrictions
+                       $name = WatchlistGroup::checkValidGroupName( 
$data['groupnew'] );
+                       if( $name ) {
+                               $status = $wg->createGroup( $name );
+                       } else {
+                               $status = false;
+                       }
+               }
+
+               foreach( $data as $key => $val ) {
+                       // rename, change permissions, or delete groups if 
requested
+                       if( substr( $key, 0, 12 ) == 'groupaction_' && intval( 
$val ) != 0 ) {
+                               $group = intval( substr( $key, 12 ) );
+                               if( intval( $val ) === 1 ) {
+                                       // rename
+                                       $name = 
WatchlistGroup::checkValidGroupName( $data['grouprename_' . $group] );
+                                       if($name) {
+                                               $status = $wg->renameGroup( 
$group, $name );
+                                       } else {
+                                               $status = false;
+                                       }
+                               }
+                               if( intval( $val ) === 2 ) {
+                                       // change perms
+                                       $permval = intval( $data['groupperm_' . 
$group] );
+                                       if( $permval === 0 || $permval === 1 ) {
+                                               $status = $wg->changePerm( 
$group, $permval );
+                                       } else {
+                                               $status = false;
+                                       }
+                               } elseif( intval( $val ) === -1 ) {
+                                       // delete
+                                       $status = $wg->deleteGroup( $group );
+                               }
+                       }
+               }
+               if( $status ) {
+                       $this->successMessage = $this->msg( 'wlgroup-success' 
)->escaped();
+                       return true;
+               }
+               return false;
+       }
+}
+
+class EditWatchlistGroupHTMLForm extends HTMLForm {
+
+       protected $mSubSectionBeforeFields = false;
+
+       public function getLegend( $group ) {
+               return $group;
+       }
+
+}
diff --git a/includes/specials/SpecialWatchlist.php 
b/includes/specials/SpecialWatchlist.php
index 421840f..af49f14 100644
--- a/includes/specials/SpecialWatchlist.php
+++ b/includes/specials/SpecialWatchlist.php
@@ -44,6 +44,7 @@
                $output = $this->getOutput();
                $request = $this->getRequest();
 
+<<<<<<< HEAD
                $mode = SpecialEditWatchlist::getMode( $request, $subpage );
                if ( $mode !== false ) {
                        if ( $mode === SpecialEditWatchlist::EDIT_RAW ) {
@@ -53,6 +54,21 @@
                        } else {
                                $title = SpecialPage::getTitleFor( 
'EditWatchlist' );
                        }
+=======
+               # Anons don't get a watchlist - but let them through for public 
watchlists
+               if( $user->isAnon() ) {
+                       $output->setPageTitle( $this->msg( 'watchnologin' ) );
+                       $output->setRobotPolicy( 'noindex,nofollow' );
+                       $llink = Linker::linkKnown(
+                               SpecialPage::getTitleFor( 'Userlogin' ),
+                               $this->msg( 'loginreqlink' )->escaped(),
+                               array(),
+                               array( 'returnto' => 
$this->getTitle()->getPrefixedText() )
+                       );
+                       $output->addHTML( $this->msg( 'watchlistanontext' 
)->rawParams( $llink )->parse() );
+                       return;
+               }
+>>>>>>> 4e55649... Watchlist grouping
 
                        $output->redirect( $title->getLocalURL() );
 
@@ -78,6 +94,7 @@
                parent::execute( $subpage );
        }
 
+<<<<<<< HEAD
        /**
         * Return an array of subpages beginning with $search that this special 
page will accept.
         *
@@ -95,9 +112,29 @@
                                'edit',
                                'raw',
                        )
+=======
+               // @TODO: use FormOptions!
+               $defaults = array(
+               /* float */ 'days'      => floatval( $user->getOption( 
'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+               /* bool  */ 'hideMinor' => (int)$user->getBoolOption( 
'watchlisthideminor' ),
+               /* bool  */ 'hideBots'  => (int)$user->getBoolOption( 
'watchlisthidebots' ),
+               /* bool  */ 'hideAnons' => (int)$user->getBoolOption( 
'watchlisthideanons' ),
+               /* bool  */ 'hideLiu'   => (int)$user->getBoolOption( 
'watchlisthideliu' ),
+               /* bool  */ 'hidePatrolled' => (int)$user->getBoolOption( 
'watchlisthidepatrolled' ),
+               /* bool  */ 'hideOwn'   => (int)$user->getBoolOption( 
'watchlisthideown' ),
+               /* bool  */ 'extended'   => (int)$user->getBoolOption( 
'extendwatchlist' ),
+               /* ?     */ 'namespace' => '', //means all
+               /* ?     */ 'invert'    => false,
+               /* bool  */ 'associated' => false,
+               /* int    */ 'user_id'          => $user->getId(),
+               /* string */ 'user'     => $user->getName(),
+               /* int    */ 'group'         => null,
+               /* string */ 'group_name'    => null,
+>>>>>>> 4e55649... Watchlist grouping
                );
        }
 
+<<<<<<< HEAD
        /**
         * Get a FormOptions object containing the default options
         *
@@ -106,6 +143,48 @@
        public function getDefaultOptions() {
                $opts = parent::getDefaultOptions();
                $user = $this->getUser();
+=======
+               # Extract variables from the request, falling back to user 
preferences or
+               # other default values if these don't exist
+               $values = array();
+               $values['user'] = $request->getText( 'user', $defaults['user'] 
);
+               if( empty( $values['user'] ) ) {
+                       $values['user'] = $user->getName();
+               }
+
+               $values['group_name'] = $request->getText( 'group', 
$defaults['group'] );
+
+               // Look for subpage parameters passed in the URL
+               $subpages = explode( '/', $par );
+
+               if( !empty( $subpages[0] ) ) {
+                       $values['user'] = $subpages[0];
+               }
+               if( isset( $subpages[1] ) && !empty( $subpages[1] ) ) {
+                       $values['group_name'] = $subpages[1];
+               }
+
+               $user_obj = User::newFromName( $values['user'] );
+               $wg_obj = WatchlistGroup::newFromUser( $user_obj );
+               $values['user_id'] = $user_obj->getId();
+               $values['group'] = $wg_obj->getGroupFromName( 
$values['group_name'] );
+               // for non existing group set the name to default
+               if ( !$values['group'] ) {
+                       $values['group_name'] = $defaults['group'];
+               }
+               $values['days']          = $request->getVal( 'days', 
$prefs['days'] );
+               $values['hideMinor']     = (int)$request->getBool( 'hideMinor', 
$prefs['hideminor'] );
+               $values['hideBots']      = (int)$request->getBool( 'hideBots' , 
$prefs['hidebots'] );
+               $values['hideAnons']     = (int)$request->getBool( 'hideAnons', 
$prefs['hideanons'] );
+               $values['hideLiu']       = (int)$request->getBool( 'hideLiu'  , 
$prefs['hideliu'] );
+               $values['hideOwn']       = (int)$request->getBool( 'hideOwn'  , 
$prefs['hideown'] );
+               $values['hidePatrolled'] = (int)$request->getBool( 
'hidePatrolled', $prefs['hidepatrolled'] );
+               $values['extended'] = (int)$request->getBool( 'extended', 
$defaults['extended'] );
+
+               foreach( $this->customFilters as $key => $params ) {
+                       $values[$key] = (int)$request->getBool( $key );
+               }
+>>>>>>> 4e55649... Watchlist grouping
 
                $opts->add( 'days', $user->getOption( 'watchlistdays' ), 
FormOptions::FLOAT );
 
@@ -121,6 +200,7 @@
                return $opts;
        }
 
+<<<<<<< HEAD
        /**
         * Get custom show/hide filters
         *
@@ -130,6 +210,16 @@
                if ( $this->customFilters === null ) {
                        $this->customFilters = parent::getCustomFilters();
                        wfRunHooks( 'SpecialWatchlistFilters', array( $this, 
&$this->customFilters ), '1.23' );
+=======
+               // Backup conditions to restrict access
+               $conds[] = 'wl_group = 0 OR wg_perm = 1 OR (wg_perm = 0 AND 
wg_user = ' . $user->getId() . ')';
+               if( intval( $values['group'] ) > 0 || $values['group'] === 0 ) {
+                       $conds[] = 'wl_group = '. intval( $values['group'] );
+               }
+
+               if( $values['days'] > 0 ) {
+                       $conds[] = 'rc_timestamp > ' . $dbr->addQuotes( 
$dbr->timestamp( time() - intval( $values['days'] * 86400 ) ) );
+>>>>>>> 4e55649... Watchlist grouping
                }
 
                return $this->customFilters;
@@ -221,21 +311,79 @@
                        $usePage = true;
                }
 
+<<<<<<< HEAD
                $tables = array( 'recentchanges', 'watchlist' );
                $fields = RecentChange::selectFields();
                $query_options = array( 'ORDER BY' => 'rc_timestamp DESC' );
+=======
+               # Show a message about slave lag, if applicable
+               $lag = wfGetLB()->safeGetLag( $dbr );
+               if( $lag > 0 ) {
+                       $output->showLagWarning( $lag );
+               }
+
+               # ADD USER WATCHLIST/GROUP SELECTION
+               $fields['user'] = array(
+                       'type' => 'text',
+                       'label' => $this->msg( 'watchlist-user' )->escaped(),
+                       'value' => $values['user_id']
+               );
+               $fields['group'] = array(
+                       'type' => 'text',
+                       'label' => $this->msg( 'watchlist-group' )->escaped(),
+                       'value' => $values['group']
+               );
+
+               # Create output form
+               $form = Xml::fieldset( $this->msg( 'watchlist-options' 
)->text(), false, array( 'id' => 'mw-watchlist-options' ) );
+
+               # Show watchlist header
+               $form .= $this->msg( 'watchlist-details' )->numParams( $nitems 
)->parse() . "\n";
+
+               if( $user->getOption( 'enotifwatchlistpages' ) && 
$wgEnotifWatchlist) {
+                       $form .= $this->msg( 'wlheader-enotif' 
)->parseAsBlock() . "\n";
+               }
+               if( $wgShowUpdatedMarker ) {
+                       $form .= Xml::openElement( 'form', array( 'method' => 
'post',
+                                               'action' => 
$this->getTitle()->getLocalUrl(),
+                                               'id' => 
'mw-watchlist-resetbutton' ) ) . "\n" .
+                                       $this->msg( 'wlheader-showupdated' 
)->parse() .
+                                       Xml::submitButton( $this->msg( 
'enotif_reset' )->text(), array( 'name' => 'dummy' ) ) . "\n" .
+                                       Html::hidden( 'reset', 'all' ) . "\n";
+                                       foreach ( $nondefaults as $key => 
$value ) {
+                                               $form .= Html::hidden( $key, 
$value ) . "\n";
+                                       }
+                                       $form .= Xml::closeElement( 'form' ) . 
"\n";
+               }
+               $form .= "<hr />\n";
+
+               $tables = array( 'recentchanges', 'watchlist', 
'watchlist_groups' );
+               $fields = array_merge( RecentChange::selectFields(), array( 
$dbr->tableName( 'watchlist_groups' ) . '.*' ) );
+
+>>>>>>> 4e55649... Watchlist grouping
                $join_conds = array(
                        'watchlist' => array(
                                'INNER JOIN',
                                array(
-                                       'wl_user' => $user->getId(),
+                                       'wl_user' => $values['user_id'],
                                        'wl_namespace=rc_namespace',
                                        'wl_title=rc_title'
                                ),
                        ),
+                       'watchlist_groups' => array(
+                               'LEFT JOIN',
+                               array(
+                                       'wg_id=wl_group'
+                               ),
+                       )
                );
 
+<<<<<<< HEAD
                if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
+=======
+               $options = array( 'ORDER BY' => 'rc_timestamp DESC' );
+               if( $wgShowUpdatedMarker ) {
+>>>>>>> 4e55649... Watchlist grouping
                        $fields[] = 'wl_notificationtimestamp';
                }
                if ( $limitWatchlist ) {
@@ -481,13 +629,68 @@
                        $opts['associated'],
                        array( 'title' => $this->msg( 
'tooltip-namespace_association' )->text() )
                ) . '&#160;';
+
+               $form .= '</p><p>';
+               $form .= Xml::openElement( 'label', array( 'for' => 
'user_search' ) ) . $this->msg( 'watchlist-user' )->escaped() . 
Xml::closeElement( 'label' );
+               $form .= Html::input( 'user', $values['user'], 'text', array( 
'id' => 'user_search' ) ) . '&#160;';
+               $form .= Xml::openElement( 'label', array( 'for' => 
'group_search' ) ) . $this->msg( 'watchlist-group' )->escaped() . 
Xml::closeElement( 'label' );
+               $form .= Html::input( 'group', $values['group_name'], 'text', 
array( 'id' => 'group_search' ) ) . '&#160;';
+
                $form .= Xml::submitButton( $this->msg( 'allpagessubmit' 
)->text() ) . "</p>\n";
-               foreach ( $hiddenFields as $key => $value ) {
-                       $form .= Html::hidden( $key, $value ) . "\n";
+               $form .= Html::hidden( 'days', $values['days'] ) . "\n";
+               foreach ( $filters as $key => $msg ) {
+                       if ( $values[$key] ) {
+                               $form .= Html::hidden( $key, 1 ) . "\n";
+                       }
                }
                $form .= Xml::closeElement( 'fieldset' ) . "\n";
+<<<<<<< HEAD
                $form .= Xml::closeElement( 'form' ) . "\n";
                $this->getOutput()->addHTML( $form );
+=======
+
+               $output->addHTML( $form );
+
+               if( $values['user_id'] == 0 ) {
+                       $output->addWikiMsg( 'wlfilter-nouser' );
+                       return;
+               } else {
+                       // Check permissions
+                       $hasPerm = ( $values['user_id'] == $user->getId() ) // 
if the user is self
+                                 || $wg_obj->isGroup( $values['group'], true );
+                       // If the user doesn't have permission or there's 
nothing to show, stop here
+                       if( !$hasPerm ) {
+                               $output->addWikiMsg( 'wlfilter-permdenied' );
+                               return;
+                       }
+                       // No changes for the given criteria
+                       if( $numRows == 0 ) {
+                               $output->addWikiMsg( 'watchnochange' );
+                               return;
+                       }
+               }
+
+               // The filter message might be repetitive since this info is 
already available in the filter form fields.
+               /*$filter_status = '<p>';
+               if( isset( $values['user'] ) ) {
+                       $filter_status .= $this->msg( 'wlfilter' )->rawParams( 
$values['user'] )->escaped();
+               }
+               if( isset( $values['group_name'] ) ) {
+                   $filter_status .= ' ' . $this->msg( 'wlfilter-group' 
)->rawParams( $values['group_name'] )->escaped();
+               }
+               $filter_status .= '</p>';
+               $output->addHTML( $filter_status );*/
+               /* End bottom header */
+
+               /* Do link batch query */
+               $linkBatch = new LinkBatch;
+               foreach ( $res as $row ) {
+                       $userNameUnderscored = str_replace( ' ', '_', 
$row->rc_user_text );
+                       if ( $row->rc_user != 0 ) {
+                               $linkBatch->add( NS_USER, $userNameUnderscored 
);
+                       }
+                       $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
+>>>>>>> 4e55649... Watchlist grouping
 
                $this->setBottomText( $opts );
        }
diff --git a/maintenance/archives/patch-watchlist_groups.sql 
b/maintenance/archives/patch-watchlist_groups.sql
new file mode 100644
index 0000000..2a63a04
--- /dev/null
+++ b/maintenance/archives/patch-watchlist_groups.sql
@@ -0,0 +1,13 @@
+CREATE TABLE /*_*/watchlist_groups (
+  wg_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  wg_name varchar(255) binary NOT NULL,
+
+  -- Key to user.user_id for owner of the watchlist
+  wg_user int unsigned NOT NULL,
+
+  -- Permissions bool
+  wg_perm tinyint NOT NULL default 0
+
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/wg_user ON /*_*/watchlist_groups (wg_id, wg_name, 
wg_user);
\ No newline at end of file
diff --git a/maintenance/archives/patch-watchlist_groups_newfield.sql 
b/maintenance/archives/patch-watchlist_groups_newfield.sql
new file mode 100644
index 0000000..1e663df
--- /dev/null
+++ b/maintenance/archives/patch-watchlist_groups_newfield.sql
@@ -0,0 +1,4 @@
+ALTER TABLE /*$wgDBprefix*/watchlist ADD wl_group int unsigned NOT NULL 
default 0;
+
+DROP INDEX /*i*/wl_user ON /*_*/watchlist;
+CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_group, 
wl_namespace, wl_title);
\ No newline at end of file
diff --git a/maintenance/postgres/archives/patch-watchlist_groups.sql 
b/maintenance/postgres/archives/patch-watchlist_groups.sql
new file mode 100644
index 0000000..bf68648
--- /dev/null
+++ b/maintenance/postgres/archives/patch-watchlist_groups.sql
@@ -0,0 +1,9 @@
+CREATE SEQUENCE 'watchlist_groups_wg_id_seq';
+CREATE TABLE watchlist_groups (
+  wg_id     INTEGER  NOT NULL  PRIMARY KEY DEFAULT 
nextval('watchlist_groups_wg_id_seq'),
+  wg_name   TEXT     NOT NULL,
+  wg_user   INTEGER  NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE 
DEFERRABLE INITIALLY DEFERRED,
+  wg_perm   SMALLINT NOT NULL  DEFAULT 0,
+);
+
+CREATE UNIQUE INDEX wg_id_name_user_perm ON watchlist (wg_id, wg_name, 
wg_user, wg_perm);
\ No newline at end of file
diff --git a/maintenance/postgres/archives/patch-watchlist_groups_newfield.sql 
b/maintenance/postgres/archives/patch-watchlist_groups_newfield.sql
new file mode 100644
index 0000000..67637c0
--- /dev/null
+++ b/maintenance/postgres/archives/patch-watchlist_groups_newfield.sql
@@ -0,0 +1,4 @@
+ALTER TABLE watchlist ADD COLUMN wl_group INTEGER NOT NULL DEFAULT 0;
+
+DROP INDEX wl_group_user_namespace_title;
+CREATE UNIQUE INDEX wl_group_user_namespace_title ON watchlist (wl_user, 
wl_group, wl_namespace, wl_title);
\ No newline at end of file
diff --git a/maintenance/postgres/tables.sql b/maintenance/postgres/tables.sql
index 400050e..dc39de7 100644
--- a/maintenance/postgres/tables.sql
+++ b/maintenance/postgres/tables.sql
@@ -446,15 +446,26 @@
 
 CREATE TABLE watchlist (
   wl_user                   INTEGER     NOT NULL  REFERENCES mwuser(user_id) 
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+  wl_group                  INTEGER     NOT NULL  DEFAULT 0,
   wl_namespace              SMALLINT    NOT NULL  DEFAULT 0,
   wl_title                  TEXT        NOT NULL,
   wl_notificationtimestamp  TIMESTAMPTZ
 );
-CREATE UNIQUE INDEX wl_user_namespace_title ON watchlist (wl_namespace, 
wl_title, wl_user);
+CREATE UNIQUE INDEX wl_group_user_namespace_title ON watchlist (wl_user, 
wl_group, wl_namespace, wl_title);
 CREATE INDEX wl_user ON watchlist (wl_user);
 CREATE INDEX wl_user_notificationtimestamp ON watchlist (wl_user, 
wl_notificationtimestamp);
 
 
+CREATE SEQUENCE 'watchlist_groups_wg_id_seq';
+CREATE TABLE watchlist_groups (
+  wg_id     INTEGER  NOT NULL  PRIMARY KEY DEFAULT 
nextval('watchlist_groups_wg_id_seq'),
+  wg_name   TEXT     NOT NULL,
+  wg_user   INTEGER  NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE 
DEFERRABLE INITIALLY DEFERRED,
+  wg_perm   SMALLINT NOT NULL  DEFAULT 0,
+);
+CREATE UNIQUE INDEX wg_id_name_user_perm ON watchlist (wg_id, wg_name, 
wg_user, wg_perm);
+
+
 CREATE TABLE interwiki (
   iw_prefix  TEXT      NOT NULL  UNIQUE,
   iw_url     TEXT      NOT NULL,
diff --git a/maintenance/sqlite/archives/patch-watchlist_groups_newfield.sql 
b/maintenance/sqlite/archives/patch-watchlist_groups_newfield.sql
new file mode 100644
index 0000000..1a9aea0
--- /dev/null
+++ b/maintenance/sqlite/archives/patch-watchlist_groups_newfield.sql
@@ -0,0 +1,4 @@
+ALTER TABLE /*$wgDBprefix*/watchlist ADD wl_group int unsigned NOT NULL 
default 0;
+
+DROP INDEX wl_user;
+CREATE UNIQUE INDEX wl_user ON /*_*/watchlist (wl_user, wl_group, 
wl_namespace, wl_title);
\ No newline at end of file
diff --git a/maintenance/tables.sql b/maintenance/tables.sql
index 0228684..7b28594 100644
--- a/maintenance/tables.sql
+++ b/maintenance/tables.sql
@@ -1127,6 +1127,9 @@
   -- Key to user.user_id
   wl_user int unsigned NOT NULL,
 
+  -- Key to watchlist_groups.wg_id
+  wl_group int unsigned NOT NULL default 0,
+
   -- Key to page_namespace/page_title
   -- Note that users may watch pages which do not exist yet,
   -- or existed in the past but have been deleted.
@@ -1140,11 +1143,25 @@
 
 ) /*$wgDBTableOptions*/;
 
-CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, 
wl_title);
+CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_group, 
wl_namespace, wl_title);
 CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title);
 CREATE INDEX /*i*/wl_user_notificationtimestamp ON /*_*/watchlist (wl_user, 
wl_notificationtimestamp);
 
 
+CREATE TABLE /*_*/watchlist_groups (
+  wg_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  wg_name varchar(255) binary NOT NULL,
+
+  -- Key to user.user_id for owner of the watchlist
+  wg_user int unsigned NOT NULL,
+
+  -- Permissions bool
+  wg_perm tinyint NOT NULL default 0
+
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/wg_user ON /*_*/watchlist_groups (wg_id, wg_name, 
wg_user, wg_perm);
+
 --
 -- When using the default MySQL search backend, page titles
 -- and text are munged to strip markup, do Unicode case folding,

-- 
To view, visit https://gerrit.wikimedia.org/r/170588
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: If87504366263964aafc5826be9c1752513d18c8f
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Reedy <[email protected]>
Gerrit-Reviewer: Aaron Pramana <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to