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 , 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 .= "\n"; - $link = $wgRequest->escapeAppendQuery( 'feed=atom' ); + $link = FeedItem::feedUrl( 'atom' ); $ret .= "\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( '

' ); # 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( + "
" . + "
\n" ); + } + } $wgOut->addHTML( '' ); 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 = "" . wfMsg( 'printableversion' ) . ''; if( $wgOut->isSyndicated() ) { foreach( $wgFeedClasses as $format => $class ) { - $feedurl = $wgRequest->escapeAppendQuery( "feed=$format" ); + $feedurl = wfFeedUrl( $format ); $s .= " | {$format}"; } } 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',