Index: maintenance/archives/patch-user_watchlist_token.sql
===================================================================
--- maintenance/archives/patch-user_watchlist_token.sql	(revision 0)
+++ maintenance/archives/patch-user_watchlist_token.sql	(revision 0)
@@ -0,0 +1,10 @@
+--
+-- New user field for syndicated watchlists
+-- Wil Mahan  <wmahan at gmail dot com>, September 2006
+--
+
+ALTER TABLE /*$wgDBprefix*/user
+  -- Pseudorandomly generated token for watchlist feeds
+  -- Accounts contain NULL by default.
+  ADD user_watchlist_token CHAR(32) BINARY;
+
Index: maintenance/updaters.inc
===================================================================
--- maintenance/updaters.inc	(revision 16398)
+++ maintenance/updaters.inc	(working copy)
@@ -44,6 +44,7 @@
 	array( 'user',          'user_token',       'patch-user_token.sql' ),
 	array( 'user',          'user_email_token', 'patch-user_email_token.sql' ),
 	array( 'user',          'user_registration','patch-user_registration.sql' ),
+	array( 'user',          'user_watchlist_token','patch-user_watchlist_token.sql' ),
  	array( 'logging',       'log_params',       'patch-log_params.sql' ),
  	array( 'archive',       'ar_rev_id',        'patch-archive-rev_id.sql' ),
  	array( 'archive',       'ar_text_id',       'patch-archive-text_id.sql' ),
Index: maintenance/tables.sql
===================================================================
--- maintenance/tables.sql	(revision 16398)
+++ maintenance/tables.sql	(working copy)
@@ -110,6 +110,11 @@
   -- Accounts predating this schema addition may contain NULL.
   user_registration char(14) binary,
 
+  -- Initially NULL; when a user enables watchlist syndication,
+  -- this is set to a pseudorandomly generated token for
+  -- authorization
+  user_watchlist_token char(32) binary,
+
   PRIMARY KEY user_id (user_id),
   UNIQUE INDEX user_name (user_name),
   INDEX (user_email_token)
Index: includes/User.php
===================================================================
--- includes/User.php	(revision 16398)
+++ includes/User.php	(working copy)
@@ -44,6 +44,7 @@
 	var $mTouched;		//!<
 	var $mDatePreference; // !<
 	var $mVersion;		//!< serialized version
+	var $mWatchlistToken;   //!<
 	/**@}} */
 
 	/**
@@ -86,6 +87,7 @@
 		'numberheadings'	=> 0,
 		'uselivepreview'	=> 0,
 		'watchlistdays' 	=> 3.0,
+		'publicwatchlist' 	=> 0,
 	);
 
 	static public $mToggles = array(
@@ -122,6 +124,7 @@
 		'forceeditsummary',
 		'watchlisthideown',
 		'watchlisthidebots',
+		'publicwatchlist',
 	);		
 
 	/** Constructor using User:loadDefaults() */
@@ -211,6 +214,7 @@
 'mToken',
 'mTouched',
 'mVersion',
+'mWatchlistToken'
 );
 	}
 
@@ -434,6 +438,7 @@
 		$this->mBlockedby = -1; # Unset
 		$this->setToken(); # Random
 		$this->mHash = false;
+		$this->mWatchlistToken = null;
 
 		if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
 			$this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
@@ -828,7 +833,7 @@
 		$dbr =& wfGetDB( DB_SLAVE );
 		$s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
 		  'user_email_authenticated',
-		  'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
+		  'user_real_name','user_options','user_touched', 'user_token', 'user_registration', 'user_watchlist_token' ),
 		  array( 'user_id' => $this->mId ), $fname );
 
 		if ( $s !== false ) {
@@ -842,6 +847,7 @@
 			$this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
 			$this->mToken = $s->user_token;
 			$this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
+			$this->mWatchlistToken = $s->user_watchlist_token;
 
 			$res = $dbr->select( 'user_groups',
 				array( 'ug_group' ),
@@ -1556,7 +1562,8 @@
 		 		'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
 				'user_options' => $this->encodeOptions(),
 				'user_touched' => $dbw->timestamp($this->mTouched),
-				'user_token' => $this->mToken
+				'user_token' => $this->mToken,
+				'user_watchlist_token' => $this->mWatchlistToken,
 			), array( /* WHERE */
 				'user_id' => $this->mId
 			), $fname
@@ -1997,6 +2004,31 @@
 	}
 
 	/**
+	 * Generate, store, and return a new watchlist authentication
+	 * token.
+	 *
+	 * @return string
+	 * @public
+	 */
+	function resetWatchlistToken() {
+		$fname = 'User::watchlistToken';
+		$this->mWatchlistToken = $this->generateToken( $this->mId );
+		$this->invalidateCache();
+		$this->saveSettings();
+		return $this->mWatchlistToken;
+	}
+
+	/**
+	 * Return the watchlist authentication token for this user.
+	 *
+	 * @public
+	 */
+	function getWatchlistToken() {
+		$this->loadFromDatabase();
+		return $this->mWatchlistToken;
+	}
+
+	/**
 	 * @param array $groups list of groups
 	 * @return array list of permission key names for given groups combined
 	 * @static
Index: includes/Feed.php
===================================================================
--- includes/Feed.php	(revision 16398)
+++ includes/Feed.php	(working copy)
@@ -74,6 +74,29 @@
 	function getAuthor() { return $this->xmlEncode( $this->Author ); }
 	function getComments() { return $this->xmlEncode( $this->Comments ); }
 	/**#@-*/
+
+	/**
+ 	 * Get the URL for a syndicated feed
+ 	 *
+	 * @static
+ 	 * @param string $format The feed format, such as "rss" or "atom"
+ 	 * @return The URL for this page's feed
+ 	 */
+	function feedUrl( $format ) {
+		global $wgRequest, $wgTitle, $wgUser;
+
+		$isWatchlist = $wgTitle->equals( Title::makeTitle( NS_SPECIAL, 'Watchlist' ) );
+		if ($isWatchlist) {
+			$userName = wfUrlEncode( $wgUser->getTitlekey() );
+			$token = $wgUser->getWatchlistToken();
+			$limit = $wgUser->getOption( 'wllimit' );
+			$url = $wgTitle->getLocalUrl( "feed=$format&user=$userName&token=$token&limit=$limit" );
+		}
+		else {
+			$url = $wgRequest->escapeAppendQuery( "feed=$format" );
+		}
+		return $url;
+	}
 }
 
 /**
Index: includes/OutputPage.php
===================================================================
--- includes/OutputPage.php	(revision 16398)
+++ includes/OutputPage.php	(working copy)
@@ -1041,9 +1041,9 @@
 		}
 		if( $this->isSyndicated() ) {
 			# FIXME: centralize the mime-type and name information in Feed.php
-			$link = $wgRequest->escapeAppendQuery( 'feed=rss' );
+			$link = FeedItem::feedUrl( 'rss' );
 			$ret .= "<link rel='alternate' type='application/rss+xml' title='RSS 2.0' href='$link' />\n";
-			$link = $wgRequest->escapeAppendQuery( 'feed=atom' );
+			$link = FeedItem::feedUrl( 'atom' );
 			$ret .= "<link rel='alternate' type='application/atom+xml' title='Atom 1.0' href='$link' />\n";
 		}
 
Index: includes/SpecialWatchlist.php
===================================================================
--- includes/SpecialWatchlist.php	(revision 16398)
+++ includes/SpecialWatchlist.php	(working copy)
@@ -22,6 +22,16 @@
 	global $wgEnotifWatchlist;
 	$fname = 'wfSpecialWatchlist';
 
+	# Handle syndicated watchlists
+	if( wlHandleFeed( $wgOut, $wgRequest, $par ) ) {
+		return;
+	}
+
+	$syndicate = $wgUser->getOption( 'publicwatchlist' );
+	if( $syndicate ) {
+		$wgOut->setSyndicated( true );
+	}
+
 	$skin =& $wgUser->getSkin();
 	$specialTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' );
 	$wgOut->setRobotPolicy( 'noindex,nofollow' );
@@ -512,4 +522,163 @@
 		return( false );
 	}
 }
+
+/**
+ * Allow syndication of watchlists using "feed=..."
+ *
+ * @param $out Output object
+ * @param $request Request object
+ * @param $par Parameters passed to the watchlist page
+ * @return bool True if it's been taken care of; false indicates the watchlist
+ * 				code needs to do something further
+ */
+function wlHandleFeed( &$out, &$request, $par ) {
+	global $wgUser, $wgLang;
+	global $wgSyndicateWatchlists, $wgFeedClasses;
+	global $wgSitename, $wgShowUpdatedMarker;
+
+	$fname = 'wlHandleFeed';
+	if ( !$wgSyndicateWatchlists ) {
+		return false;
+	}
+
+	$feedFormat = $request->getVal( 'feed' );
+	if( is_null( $feedFormat ) || !isset( $wgFeedClasses[$feedFormat] ) ) {
+		return false;
+	}
+
+	$specialTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' );
+
+	$feedUserName = $request->getVal( 'user' );
+	if ( !is_null($feedUserName) ) {
+		$wlUser = User::newFromName( $feedUserName );
+		if( is_null( $wlUser ) ) {
+			# invalid user name for feed
+			$out->showErrorPage( 'noname', 'nosuchusershort' );
+			return true;
+		}
+
+		$public = $wlUser->getOption( 'publicwatchlist' );
+		if ( !$public ) {
+			$out->showErrorPage( 'badaccess', 'feedaccesdenied' );
+			return true;
+		}
+
+		$token = $request->getVal( 'token' );
+		if( !$token || $token !== $wlUser->getWatchlistToken() ) {
+			$out->showErrorPage( 'badaccess', 'feedacessdenied' );
+			return true;
+		}
+	}
+	else if ( !$wgUser->isAnon() ) {
+		# If no user is given, display feed for our own watchlist 
+		$wlUser = $wgUser;
+	}
+	else {
+		# Display "not logged in" error page
+		$skin =& $wgUser->getSkin();
+		$out->setPageTitle( wfMsg( 'watchnologin' ) );
+		$llink = $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
+		$out->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
+		return true;
+	}
+	$uid = $wlUser->getID();
+
+	# Set limit
+	$limit = intval( $wgUser->getOption( 'wllimit' ) );
+	if ( !$limit ) {
+		$limit = $wgUser->getDefaultOption('wllimit');
+	}
+	$limit = $request->getInt( 'limit', $limit );
+	global $wgFeedLimit;
+	if( $limit > $wgFeedLimit ) {
+		$limit = $wgFeedLimit;
+	}
+
+	$unseen = $request->getBool( 'unseen' );
+
+	# Force feeds to use the following settings
+	$cutoff = false;
+	$andLatest = '';
+	$andHideOwn = '';
+	$andHideBots = '';
+	$nameSpaceClause = '';
+	$limitWatchlist = "LIMIT $limit";
+	$npages = wfMsg( 'watchlistall1' );
+
+	$dbr =& wfGetDB( DB_SLAVE );
+	extract( $dbr->tableNames( 'page', 'revision', 'watchlist', 'recentchanges' ) );
+
+	# Get the number of pages found
+	$sql = "SELECT COUNT(*) AS n FROM $watchlist WHERE wl_user=$uid";
+	$res = $dbr->query( $sql, $fname );
+	$s = $dbr->fetchObject( $res );
+	$nitems = floor($s->n / 2);
+
+	$sql = "SELECT
+	  rc_namespace AS page_namespace, rc_title AS page_title,
+	  rc_comment AS rev_comment, rc_cur_id AS page_id,
+	  rc_user AS rev_user, rc_user_text AS rev_user_text,
+	  rc_timestamp AS rev_timestamp, rc_minor AS rev_minor_edit,
+	  rc_this_oldid AS rev_id,
+	  rc_last_oldid, rc_id, rc_patrolled,
+	  rc_new AS page_is_new,wl_notificationtimestamp
+	  FROM $watchlist,$recentchanges,$page
+	  WHERE wl_user=$uid
+	  AND wl_namespace=rc_namespace
+	  AND wl_title=rc_title
+	  AND rc_timestamp > '$cutoff'
+	  AND rc_cur_id=page_id
+	  $andLatest
+	  $andHideOwn
+	  $andHideBots
+	  $nameSpaceClause
+	  ORDER BY rc_timestamp DESC
+	  $limitWatchlist";
+
+	$res = $dbr->query( $sql, $fname );
+
+	$feed = new $wgFeedClasses[$feedFormat]
+	  (
+	   $wgSitename . ' - ' . wfMsg( 'watchlist' ) . ' ' . wfMsgWikiHtml( 'watchlistfor', htmlspecialchars( $wlUser->getName() ) ),
+	   wfMsgWikiHtml( "watchdetails",
+		  $wgLang->formatNum( $nitems ), 
+		  $wgLang->formatNum( $npages ), 
+		  '',
+		  $specialTitle->escapeLocalUrl( )
+		  )
+	   ,
+	   $specialTitle->getFullUrl() 
+	   );
+	$feed->outHeader();
+		
+	while ( $obj = $dbr->fetchObject( $res ) ) {
+		if ( $wgShowUpdatedMarker ) {
+			$updated = $obj->wl_notificationtimestamp;
+		} else {
+			$updated = true;
+		}
+		# With the "unseen" option, skip pages that weren't updated
+		if ( $unseen && !$updated ) {
+			continue;
+		}
+		$title = Title::makeTitle( $obj->page_namespace, $obj->page_title );
+		$talkpage = $title->getTalkPage();
+		$item = new FeedItem(
+			$title->getPrefixedText(),
+			htmlspecialchars( $obj->rev_comment ),
+			$title->getFullURL(),
+			$obj->rev_timestamp,
+			$obj->rev_user_text,
+			$talkpage->getFullURL()
+			);
+		$feed->outItem( $item );
+	}
+	$feed->outFooter();
+
+	$dbr->freeResult( $res );
+
+	return true;
+}
+
 ?>
Index: includes/SkinTemplate.php
===================================================================
--- includes/SkinTemplate.php	(revision 16398)
+++ includes/SkinTemplate.php	(working copy)
@@ -222,7 +222,7 @@
 			foreach( $wgFeedClasses as $format => $class ) {
 				$feeds[$format] = array(
 					'text' => $format,
-					'href' => $wgRequest->appendQuery( "feed=$format" )
+					'href' => FeedItem::feedUrl( $format )
 				);
 			}
 			$tpl->setRef( 'feeds', $feeds );
Index: includes/SpecialPreferences.php
===================================================================
--- includes/SpecialPreferences.php	(revision 16398)
+++ includes/SpecialPreferences.php	(working copy)
@@ -28,6 +28,7 @@
 	var $mSearch, $mRecent, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
 	var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize;
 	var $mUnderline, $mWatchlistEdits;
+	var $mResetWatchlistToken;
 
 	/**
 	 * Constructor
@@ -66,6 +67,7 @@
 		$this->mSuccess = $request->getCheck( 'success' );
 		$this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
 		$this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
+		$this->mResetWatchlistToken = $request->getCheck( 'wpOpresetwatchlisttoken' );
 
 		$this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
 			$this->mPosted &&
@@ -208,6 +210,7 @@
 		global $wgEnableUserEmail, $wgEnableEmail;
 		global $wgEmailAuthentication, $wgMinimalPasswordLength;
 		global $wgAuth;
+		global $wgSyndicateWatchlists;
 
 
 		if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
@@ -326,6 +329,13 @@
 				$wgUser->saveSettings();
 			}
 		}
+		
+		if( $wgSyndicateWatchlists && $wgUser->getOption( 'publicwatchlist' ) ) {
+			if( is_null( $wgUser->getWatchlistToken() ) || $this->mResetWatchlistToken ) {
+				# generate a new watchlist token
+				$wgUser->resetWatchlistToken();
+			}
+		}
 
 		if( $needRedirect && $error === false ) {
 			$title =& Title::makeTitle( NS_SPECIAL, "Preferences" );
@@ -869,6 +879,29 @@
 		$wgOut->addHTML( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'extendwatchlist' ) ) );
 		$wgOut->addHTML( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ),
 			'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) );
+		global $wgSyndicateWatchlists;
+		if( $wgSyndicateWatchlists ) {
+			$wgOut->addHTML( '<br /><br />' ); # Spacing
+			$wgOut->addHTML( $this->getToggles( array( 'publicwatchlist' ) ) );
+			if( $wgUser->getOption( 'publicwatchlist' ) ) {
+				$skin = $wgUser->getSkin();
+				$token = $wgUser->getWatchlistToken();
+				$userName = wfUrlencode( $wgUser->getTitlekey() );
+				$limit = $wgUser->getOption( 'wllimit' );
+				$specialTitle = Title::makeTitle( NS_SPECIAL, 'Watchlist' );
+				global $wgFeedClasses;
+				$s = '';
+				foreach( $wgFeedClasses as $format => $class ) {
+					$s .= $skin->makeKnownLinkObj( $specialTitle, $format, "feed=$format&user=$userName&token=$token&limit=$limit" ) . ' ';
+				}
+				$wgOut->addHTML( wfMsgWikiHtml( 'syndicatedwatchlistwarning', $s ) );
+				$tname = 'resetwatchlisttoken';
+				$ttext = wfMsg( 'resetwatchlisttoken' );
+				$wgOut->addHTML( 
+					"<div class='toggle'><input type='checkbox' value='1' id=\"$tname\" name=\"wpOp$tname\" />" .
+					" <span class='toggletext'><label for=\"$tname\">$ttext</label></span></div>\n" );
+			}
+		}
 
 		$wgOut->addHTML( '</fieldset>' );
 
Index: includes/DefaultSettings.php
===================================================================
--- includes/DefaultSettings.php	(revision 16398)
+++ includes/DefaultSettings.php	(working copy)
@@ -1671,7 +1671,10 @@
  * pages larger than this size. */
 $wgFeedDiffCutoff = 32768;
 
+/** Allow users to share their watchlists in Atom or RSS feeds */
+$wgSyndicateWatchlists         = true;
 
+
 /**
  * Additional namespaces. If the namespaces defined in Language.php and
  * Namespace.php are insufficient, you can create new ones here, for example,
Index: includes/Skin.php
===================================================================
--- includes/Skin.php	(revision 16398)
+++ includes/Skin.php	(working copy)
@@ -752,7 +752,7 @@
 		$s = "<a href=\"$printurl\">" . wfMsg( 'printableversion' ) . '</a>';
 		if( $wgOut->isSyndicated() ) {
 			foreach( $wgFeedClasses as $format => $class ) {
-				$feedurl = $wgRequest->escapeAppendQuery( "feed=$format" );
+				$feedurl = wfFeedUrl( $format );
 				$s .= " | <a href=\"$feedurl\">{$format}</a>";
 			}
 		}
Index: languages/MessagesEn.php
===================================================================
--- languages/MessagesEn.php	(revision 16398)
+++ languages/MessagesEn.php	(working copy)
@@ -364,7 +364,12 @@
 'tog-forceeditsummary' => 'Prompt me when entering a blank edit summary',
 'tog-watchlisthideown' => 'Hide my edits from the watchlist',
 'tog-watchlisthidebots' => 'Hide bot edits from the watchlist',
+'tog-publicwatchlist' => 'Allow sharing my watchlist',
 
+'syndicatedwatchlistwarning' => "My watchlist feeds: $1 ('''''Note''': anyone who knows these URLs can see your watchlist.)''",
+'resetwatchlisttoken' => 'Reset my watchlist feed URLs now',
+'feedacessdenied' => 'You do not have permission to access this feed.',
+
 'underline-always' => 'Always',
 'underline-never' => 'Never',
 'underline-default' => 'Browser default',
