Add opt-in RSS feed for watchlist
authorAryeh Gregor <simetrical@users.mediawiki.org>
Fri, 24 Jul 2009 01:22:06 +0000 (01:22 +0000)
committerAryeh Gregor <simetrical@users.mediawiki.org>
Fri, 24 Jul 2009 01:22:06 +0000 (01:22 +0000)
Authentication is via a token entered in preferences, if not blank.  If
you set a token in your preferences, the following sort of link will
generate the RSS feed:

api.php?action=feedwatchlist&list=watchlist&wluser=Simetrical&wltoken=91c1ef18279f9c24ccf67a79e899ae4d2a3201bc

I haven't actually added the <link> tag to Special:Watchlist, since I've
done enough coding for one night.  Someone else can feel free to do
that (otherwise people might get kind of confused :) ).

An auto-generated random token is suggested to the user on the pref page
so that they don't have to be too creative.  Pref help text is rather
underemphasized in the default style, though.

It would be worth considering making this opt-out instead of opt-in,
but that would require some voodoo magic to get the default prefs to
work right (since we'd need a different value for each user).  We might
set the default to some function of user id + secret site-specific value
to avoid having to store the values in the database.

Since the feature is implemented via the API, it only works if the API
is enabled.  Some API people might want to review my code for sanity.

Bug: 471

RELEASE-NOTES
includes/HTMLForm.php
includes/Preferences.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiQueryWatchlist.php
languages/messages/MessagesEn.php
maintenance/language/messages.inc

index 24b342a..950508c 100644 (file)
@@ -160,6 +160,7 @@ this. Was used when mwEmbed was going to be an extension.
   when the Go button is pressed in addition to the main namespace.
 * (bug 19900) The "listgrouprights-key" message is now wrapped in a div with
   class "mw-listgrouprights-key"
+* (bug 471) Allow RSS feeds for watchlist, using an opt-in security token
 
 === Bug fixes in 1.16 ===
 
index 9cf212e..53cf698 100644 (file)
@@ -462,20 +462,23 @@ abstract class HTMLFormField {
                $html = Xml::tags( 'tr', array( 'class' => "mw-htmlform-field-$fieldType" ),
                                                        $html ) . "\n";
 
-               // Help text
+               $helptext = null;
                if ( isset( $this->mParams['help-message'] ) ) {
                        $msg = $this->mParams['help-message'];
-
-                       $text = wfMsgExt( $msg, 'parseinline' );
-
-                       if( !wfEmptyMsg( $msg, $text ) ) {
-                               $row = Xml::tags( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
-                                                       $text );
-
-                               $row = Xml::tags( 'tr', null, $row );
-
-                               $html .= "$row\n";
+                       $helptext = wfMsgExt( $msg, 'parseinline' );
+                       if ( wfEmptyMsg( $msg, $helptext ) ) {
+                               # Never mind
+                               $helptext = null;
                        }
+               } elseif ( isset( $this->mParams['help'] ) ) {
+                       $helptext = $this->mParams['help'];
+               }
+
+               if ( !is_null( $helptext ) ) {
+                       $row = Xml::tags( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
+                               $helptext );
+                       $row = Xml::tags( 'tr', null, $row );
+                       $html .= "$row\n";
                }
 
                return $html;
index 2728461..80e4ebd 100644 (file)
@@ -746,7 +746,7 @@ class Preferences {
        }
 
        static function watchlistPreferences( $user, &$defaultPreferences ) {
-               global $wgUseRCPatrol;
+               global $wgUseRCPatrol, $wgEnableAPI;
                ## Watchlist #####################################
                $defaultPreferences['watchlistdays'] =
                                array(
@@ -800,6 +800,17 @@ class Preferences {
                                        'section' => 'watchlist/advancedwatchlist',
                                        'label-message' => 'tog-watchlisthideliu',
                                );
+               if ( $wgEnableAPI ) {
+                       # Some random gibberish as a proposed default
+                       $hash = sha1( mt_rand() . microtime( true ) );
+                       $defaultPreferences['watchlisttoken'] =
+                                       array(
+                                               'type' => 'text',
+                                               'section' => 'watchlist/advancedwatchlist',
+                                               'label-message' => 'prefs-watchlist-token',
+                                               'help' => wfMsgHtml( 'prefs-help-watchlist-token', $hash )
+                                       );
+               }
 
                if ( $wgUseRCPatrol ) {
                        $defaultPreferences['watchlisthidepatrolled'] =
index 5a3cfdb..a26184c 100644 (file)
@@ -75,6 +75,11 @@ class ApiFeedWatchlist extends ApiBase {
                                'wllimit' => (50 > $wgFeedLimit) ? $wgFeedLimit : 50
                        );
 
+                       if (!is_null($params['wluser']))
+                               $fauxReqArr['wluser'] = $params['wluser'];
+                       if (!is_null($params['wltoken']))
+                               $fauxReqArr['wltoken'] = $params['wltoken'];
+
                        // Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
                        if ( ! is_null ( $params['allrev'] ) )  $fauxReqArr['wlallrev'] = '';
 
@@ -152,7 +157,13 @@ class ApiFeedWatchlist extends ApiBase {
                                ApiBase :: PARAM_MIN => 1,
                                ApiBase :: PARAM_MAX => 72,
                        ),
-                       'allrev' => null
+                       'allrev' => null,
+                       'wluser' => array (
+                               ApiBase :: PARAM_TYPE => 'user'
+                       ),
+                       'wltoken' => array (
+                               ApiBase :: PARAM_TYPE => 'string'
+                       )
                );
        }
 
@@ -160,7 +171,9 @@ class ApiFeedWatchlist extends ApiBase {
                return array (
                        'feedformat' => 'The format of the feed',
                        'hours'      => 'List pages modified within this many hours from now',
-                       'allrev'     => 'Include multiple revisions of the same page within given timeframe.'
+                       'allrev'     => 'Include multiple revisions of the same page within given timeframe.',
+                       'wluser'     => "The user whose watchlist you want (must be accompanied by wltoken if it's not you)",
+                       'wltoken'    => 'Security token that requested user set in their preferences'
                );
        }
 
index 3d73175..6d317aa 100644 (file)
@@ -56,11 +56,20 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
 
                $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist');
 
-               if (!$wgUser->isLoggedIn())
-                       $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
-
                $params = $this->extractRequestParams();
 
+               if (!is_null($params['user']) && !is_null($params['token'])) {
+                       $user = User::newFromName($params['user']);
+                       $token = $user->getOption('watchlisttoken');
+                       if ($token == '' || $token != $params['token']) {
+                               $this->dieUsage('Incorrect watchlist token provided', 'bad_wltoken');
+                       }
+               } elseif (!$wgUser->isLoggedIn()) {
+                       $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
+               } else {
+                       $user = $wgUser;
+               }
+
                if (!is_null($params['prop']) && is_null($resultPageSet)) {
 
                        $prop = array_flip($params['prop']);
@@ -122,7 +131,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        'recentchanges'
                ));
 
-               $userId = $wgUser->getId();
+               $userId = $user->getId();
                $this->addWhere(array (
                        'wl_namespace = rc_namespace',
                        'wl_title = rc_title',
@@ -147,7 +156,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                                $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
                        }
                        
-                       // Check permissions
+                       // Check permissions.  FIXME: should this check $user instead of
+                       // $wgUser?
                        global $wgUser;
                        if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
                                $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
@@ -162,13 +172,17 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled']));
                        $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled']));                      
                }
-               
-               if(!is_null($params['user']) && !is_null($params['excludeuser']))
-                       $this->dieUsage('user and excludeuser cannot be used together', 'user-excludeuser');
-               if(!is_null($params['user']))
-                       $this->addWhereFld('rc_user_text', $params['user']);
-               if(!is_null($params['excludeuser']))
-                       $this->addWhere('rc_user_text != ' . $this->getDB()->addQuotes($params['excludeuser']));
+
+               # Ignore extra user conditions if we're using token mode, since the
+               # user was already manually specified.
+               if(is_null($params['user']) || is_null($params['token'])) {
+                       if(!is_null($params['user']) && !is_null($params['excludeuser']))
+                               $this->dieUsage('user and excludeuser cannot be used together', 'user-excludeuser');
+                       if(!is_null($params['user']))
+                               $this->addWhereFld('rc_user_text', $params['user']);
+                       if(!is_null($params['excludeuser']))
+                               $this->addWhere('rc_user_text != ' . $this->getDB()->addQuotes($params['excludeuser']));
+               }
 
 
                # This is an index optimization for mysql, as done in the Special:Watchlist page
@@ -321,6 +335,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                                        'patrolled',
                                        '!patrolled',
                                )
+                       ),
+                       'token' => array (
+                               ApiBase :: PARAM_TYPE => 'string'
                        )
                );
        }
@@ -339,7 +356,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        'show' => array (
                                'Show only items that meet this criteria.',
                                'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
-                       )
+                       ),
+                       'token' => "Give a security token (settable in preferences) to allow access to another user's watchlist"
                );
        }
 
index 06448bd..7b6821a 100644 (file)
@@ -1700,6 +1700,7 @@ Note that their indexes of {{SITENAME}} content may be out of date.',
 'prefs-watchlist-days-max'      => '(maximum 7 days)',
 'prefs-watchlist-edits'         => 'Maximum number of changes to show in expanded watchlist:',
 'prefs-watchlist-edits-max'     => '(maximum number: 1000)',
+'prefs-watchlist-token'         => 'Watchlist token',
 'prefs-misc'                    => 'Misc',
 'prefs-resetpass'               => 'Change password',
 'prefs-email'                   => 'E-mail options',
@@ -1720,6 +1721,9 @@ Note that their indexes of {{SITENAME}} content may be out of date.',
 'recentchangesdays-max'         => '(maximum $1 {{PLURAL:$1|day|days}})',
 'recentchangescount'            => 'Number of edits to show by default:',
 'prefs-help-recentchangescount' => 'This includes recent changes, page histories, and logs.',
+'prefs-help-watchlist-token'    => "Filling in this field with a secret key will generate an RSS feed for your watchlist.
+Anyone who knows the key in this field will be able to read your watchlist, so choose a secure value.
+Here's a randomly-generated value you can use: $1",
 'savedprefs'                    => 'Your preferences have been saved.',
 'timezonelegend'                => 'Time zone:',
 'localtime'                     => 'Local time:',
index 244bcd0..f8d32df 100644 (file)
@@ -898,6 +898,7 @@ $wgMessageStructure = array(
                'prefs-watchlist-days-max',
                'prefs-watchlist-edits',
                'prefs-watchlist-edits-max',
+               'prefs-watchlist-token',
                'prefs-misc', // continue checking if used from here on (r49916)
                'prefs-resetpass',
                'prefs-email',
@@ -918,6 +919,7 @@ $wgMessageStructure = array(
                'recentchangesdays-max',
                'recentchangescount',
                'prefs-help-recentchangescount',
+               'prefs-help-watchlist-token',
                'savedprefs',
                'timezonelegend',
                'localtime',