r68422 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r68421‎ | r68422 | r68423 >
Date:18:04, 22 June 2010
Author:tisane
Status:deferred (Comments)
Tags:
Comment:
Add interwiki watchlist feature. The implementation doesn't yet use all the interwiki links that it should (e.g. for user page, user talk page, use contribs, etc.), duplicates a lot of core code and table fields, and will be completely revamped in the coming weeks, but it's better than nothing. It's mostly a proof-of-concept for the interwiki watchlist idea.
Modified paths:
  • /trunk/extensions/InterwikiIntegration/InterwikiIntegration.body.php (added) (history)
  • /trunk/extensions/InterwikiIntegration/InterwikiIntegration.hooks.php (modified) (history)
  • /trunk/extensions/InterwikiIntegration/InterwikiIntegration.i18n.php (modified) (history)
  • /trunk/extensions/InterwikiIntegration/InterwikiIntegration.php (modified) (history)
  • /trunk/extensions/InterwikiIntegration/InterwikiIntegrationChangesList.php (added) (history)
  • /trunk/extensions/InterwikiIntegration/InterwikiIntegrationRecentChange.php (added) (history)
  • /trunk/extensions/InterwikiIntegration/SpecialInterwikiIntegration.php (modified) (history)
  • /trunk/extensions/InterwikiIntegration/SpecialInterwikiWatchlist.php (added) (history)
  • /trunk/extensions/InterwikiIntegration/interwikiintegration-iwlinks.sql (modified) (history)
  • /trunk/extensions/InterwikiIntegration/interwikiintegration-page.sql (added) (history)
  • /trunk/extensions/InterwikiIntegration/interwikiintegration-prefix.sql (modified) (history)
  • /trunk/extensions/InterwikiIntegration/interwikiintegration-recentchanges.sql (modified) (history)
  • /trunk/extensions/InterwikiIntegration/interwikiintegration-watchlist.sql (modified) (history)

Diff [purge]

Index: trunk/extensions/InterwikiIntegration/InterwikiIntegration.php
@@ -29,7 +29,7 @@
3030 'name' => 'Interwiki Integration',
3131 'author' => 'Tisane',
3232 'url' => 'http://www.mediawiki.org/wiki/Extension:InterwikiIntegration',
33 - 'descriptionmsg' => 'interwikiintegration-desc',
 33+ 'descriptionmsg' => 'integration-desc',
3434 'version' => '1.0.4',
3535 );
3636 $dir = dirname( __FILE__ ) . '/';
@@ -37,10 +37,15 @@
3838 $wgAutoloadClasses['PopulateInterwikiIntegrationTable'] = "$dir/SpecialInterwikiIntegration.php";
3939 $wgAutoloadClasses['PopulateInterwikiWatchlistTable'] = "$dir/SpecialInterwikiIntegration.php";
4040 $wgAutoloadClasses['PopulateInterwikiRecentChangesTable'] = "$dir/SpecialInterwikiIntegration.php";
41 -#$wgAutoloadClasses['InterwikiWatchlist'] = "$dir/SpecialInterwikiWatchlist.php";
42 -#$wgAutoloadClasses['InterwikiRecentChanges'] = "$dir/SpecialInterwikiRecentChanges.php";
 41+$wgAutoloadClasses['PopulateInterwikiPageTable'] = "$dir/SpecialInterwikiIntegration.php";
 42+$wgAutoloadClasses['InterwikiWatchlist'] = "$dir/SpecialInterwikiWatchlist.php";
 43+$wgAutoloadClasses['InterwikiRecentChanges'] = "$dir/SpecialInterwikiRecentChanges.php";
 44+$wgAutoloadClasses['InterwikiIntegrationFunctions'] = "$dir/InterwikiIntegration.body.php";
 45+$wgAutoloadClasses['InterwikiIntegrationRecentChange'] = "$dir/InterwikiIntegrationRecentChange.php";
 46+$wgAutoloadClasses['InterwikiIntegrationChangesList'] = "$dir/InterwikiIntegrationChangesList.php";
 47+$wgAutoloadClasses['EnhancedInterwikiIntegrationChangesList'] = "$dir/InterwikiIntegrationChangesList.php";
 48+$wgAutoloadClasses['OldInterwikiIntegrationChangesList'] = "$dir/InterwikiIntegrationChangesList.php";
4349 $wgExtensionMessagesFiles['InterwikiIntegration'] = $dir . 'InterwikiIntegration.i18n.php';
44 -$wgExtensionAliasesFiles['InterwikiIntegration'] = $dir . 'InterwikiIntegration.alias.php';
4550 $wgHooks['LoadExtensionSchemaUpdates'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationCreateTable';
4651 $wgHooks['ArticleEditUpdates'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationArticleEditUpdates';
4752 $wgHooks['LinkBegin'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationLink';
@@ -54,18 +59,25 @@
5560 $wgHooks['RecentChange_save'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationRecentChange_save';
5661 $wgHooks['WatchArticleComplete'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationWatchArticleComplete';
5762 $wgHooks['UnwatchArticleComplete'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationUnwatchArticleComplete';
 63+$wgHooks['ArticleSaveComplete'][] = 'InterwikiIntegrationHooks::InterwikiIntegrationArticleSaveComplete';
5864 $wgSpecialPages['PopulateInterwikiIntegrationTable'] = 'PopulateInterwikiIntegrationTable';
5965 $wgSpecialPages['PopulateInterwikiWatchlistTable'] = 'PopulateInterwikiWatchlistTable';
6066 $wgSpecialPages['PopulateInterwikiRecentChangesTable'] = 'PopulateInterwikiRecentChangesTable';
61 -#$wgSpecialPages['InterwikiWatchlist'] = 'InterwikiWatchlist';
62 -#$wgSpecialPages['InterwikiRecentChanges'] = 'InterwikiRecentChanges';
 67+$wgSpecialPages['PopulateInterwikiPageTable'] = 'PopulateInterwikiPageTable';
 68+$wgSpecialPages['InterwikiWatchlist'] = 'InterwikiWatchlist';
 69+$wgSpecialPages['InterwikiRecentChanges'] = 'InterwikiRecentChanges';
6370 $wgSpecialPageGroups['InterwikiWatchlist'] = 'changes';
6471 $wgSpecialPageGroups['InterwikiRecentChanges'] = 'changes';
 72+$wgSpecialPageGroups['PopulateInterwikiIntegrationTable'] = 'wiki';
 73+$wgSpecialPageGroups['PopulateInterwikiWatchlistTable'] = 'wiki';
 74+$wgSpecialPageGroups['PopulateInterwikiRecentChangesTable'] = 'wiki';
 75+$wgSpecialPageGroups['PopulateInterwikiPageTable'] = 'wiki';
6576 $wgSharedTables[] = 'integration_prefix';
6677 $wgSharedTables[] = 'integration_namespace';
6778 $wgSharedTables[] = 'integration_iwlinks';
6879 $wgSharedTables[] = 'integration_watchlist';
6980 $wgSharedTables[] = 'integration_recentchanges';
 81+$wgSharedTables[] = 'integration_page';
7082 $wgInterwikiIntegrationBrokenLinkStyle = "color: red";
7183 $wgAvailableRights[] = 'integration';
7284 $wgGroupPermissions['bureaucrat']['integration'] = true;
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/SpecialInterwikiWatchlist.php
@@ -0,0 +1,480 @@
 2+<?php
 3+/**
 4+ * A watchlist with changes from multiple wikis
 5+ */
 6+class InterwikiWatchlist extends SpecialPage {
 7+ /**
 8+ * Constructor
 9+ *
 10+ * @param $par Parameter passed to the page
 11+ */
 12+ function __construct() {
 13+ parent::__construct( 'InterwikiWatchlist' );
 14+ wfLoadExtensionMessages( 'InterwikiIntegration' );
 15+ }
 16+
 17+ /**
 18+ * Display the interwiki watchlist
 19+ */
 20+ function execute( $par ) {
 21+ global $wgUser, $wgOut, $wgLang, $wgRequest;
 22+ global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
 23+
 24+ // Add feed links
 25+ $wlToken = $wgUser->getOption( 'watchlisttoken' );
 26+ if (!$wlToken) {
 27+ $wlToken = sha1( mt_rand() . microtime( true ) );
 28+ $wgUser->setOption( 'watchlisttoken', $wlToken );
 29+ $wgUser->saveSettings();
 30+ }
 31+
 32+ global $wgServer, $wgScriptPath, $wgFeedClasses;
 33+ $apiParams = array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
 34+ 'wlowner' => $wgUser->getName(), 'wltoken' => $wlToken );
 35+ $feedTemplate = wfScript('api').'?';
 36+
 37+ foreach( $wgFeedClasses as $format => $class ) {
 38+ $theseParams = $apiParams + array( 'feedformat' => $format );
 39+ $url = $feedTemplate . wfArrayToCGI( $theseParams );
 40+ $wgOut->addFeedLink( $format, $url );
 41+ }
 42+
 43+ $skin = $wgUser->getSkin();
 44+ $specialTitle = SpecialPage::getTitleFor( 'InterwikiWatchlist' );
 45+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
 46+
 47+ # Anons don't get a watchlist
 48+ if( $wgUser->isAnon() ) {
 49+ $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
 50+ $llink = $skin->linkKnown(
 51+ SpecialPage::getTitleFor( 'Userlogin' ),
 52+ wfMsgHtml( 'loginreqlink' ),
 53+ array(),
 54+ array( 'returnto' => $specialTitle->getPrefixedText() )
 55+ );
 56+ $wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
 57+ return;
 58+ }
 59+
 60+ $wgOut->setPageTitle( wfMsg( 'interwikiwatchlist' ) );
 61+
 62+ $sub = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
 63+ $sub .= '<br />' . WatchlistEditor::buildTools( $wgUser->getSkin() );
 64+ $wgOut->setSubtitle( $sub );
 65+
 66+ if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
 67+ $editor = new WatchlistEditor();
 68+ $editor->execute( $wgUser, $wgOut, $wgRequest, $mode );
 69+ return;
 70+ }
 71+
 72+ $uid = $wgUser->getId();
 73+ if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) &&
 74+ $wgRequest->wasPosted() )
 75+ {
 76+ $wgUser->clearAllNotifications( $uid );
 77+ $wgOut->redirect( $specialTitle->getFullUrl() );
 78+ return;
 79+ }
 80+
 81+ $defaults = array(
 82+ /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
 83+ /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
 84+ /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
 85+ /* bool */ 'hideAnons' => (int)$wgUser->getBoolOption( 'watchlisthideanons' ),
 86+ /* bool */ 'hideLiu' => (int)$wgUser->getBoolOption( 'watchlisthideliu' ),
 87+ /* bool */ 'hidePatrolled' => (int)$wgUser->getBoolOption( 'watchlisthidepatrolled' ),
 88+ /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
 89+ /* ? */ 'namespace' => 'all',
 90+ /* ? */ 'invert' => false,
 91+ );
 92+
 93+ extract($defaults);
 94+
 95+ # Extract variables from the request, falling back to user preferences or
 96+ # other default values if these don't exist
 97+ $prefs['days'] = floatval( $wgUser->getOption( 'watchlistdays' ) );
 98+ $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
 99+ $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
 100+ $prefs['hideanons'] = $wgUser->getBoolOption( 'watchlisthideanon' );
 101+ $prefs['hideliu'] = $wgUser->getBoolOption( 'watchlisthideliu' );
 102+ $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
 103+ $prefs['hidepatrolled' ] = $wgUser->getBoolOption( 'watchlisthidepatrolled' );
 104+
 105+ # Get query variables
 106+ $days = $wgRequest->getVal( 'days' , $prefs['days'] );
 107+ $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
 108+ $hideBots = $wgRequest->getBool( 'hideBots' , $prefs['hidebots'] );
 109+ $hideAnons = $wgRequest->getBool( 'hideAnons', $prefs['hideanons'] );
 110+ $hideLiu = $wgRequest->getBool( 'hideLiu' , $prefs['hideliu'] );
 111+ $hideOwn = $wgRequest->getBool( 'hideOwn' , $prefs['hideown'] );
 112+ $hidePatrolled = $wgRequest->getBool( 'hidePatrolled' , $prefs['hidepatrolled'] );
 113+
 114+ # Get namespace value, if supplied, and prepare a WHERE fragment
 115+ $nameSpace = $wgRequest->getIntOrNull( 'namespace' );
 116+ $invert = $wgRequest->getIntOrNull( 'invert' );
 117+ if( !is_null( $nameSpace ) ) {
 118+ $nameSpace = intval( $nameSpace );
 119+ if( $invert && $nameSpace !== 'all' )
 120+ $nameSpaceClause = "integration_rc_namespace != $nameSpace";
 121+ else
 122+ $nameSpaceClause = "integration_rc_namespace = $nameSpace";
 123+ } else {
 124+ $nameSpace = '';
 125+ $nameSpaceClause = '';
 126+ }
 127+
 128+ $dbr = wfGetDB( DB_SLAVE, 'integration_watchlist' );
 129+ $recentchanges = $dbr->tableName( 'integration_recentchanges' );
 130+
 131+ $watchlistCount = $dbr->selectField( 'integration_watchlist', 'COUNT(*)',
 132+ array( 'integration_wl_user' => $uid ), __METHOD__ );
 133+ // Adjust for page X, talk:page X, which are both stored separately,
 134+ // but treated together
 135+ $nitems = floor($watchlistCount / 2);
 136+
 137+ if( is_null($days) || !is_numeric($days) ) {
 138+ $big = 1000; /* The magical big */
 139+ if($nitems > $big) {
 140+ # Set default cutoff shorter
 141+ $days = $defaults['days'] = (12.0 / 24.0); # 12 hours...
 142+ } else {
 143+ $days = $defaults['days']; # default cutoff for shortlisters
 144+ }
 145+ } else {
 146+ $days = floatval($days);
 147+ }
 148+
 149+ // Dump everything here
 150+ $nondefaults = array();
 151+
 152+ wfAppendToArrayIfNotDefault( 'days' , $days , $defaults, $nondefaults);
 153+ wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
 154+ wfAppendToArrayIfNotDefault( 'hideBots' , (int)$hideBots , $defaults, $nondefaults);
 155+ wfAppendToArrayIfNotDefault( 'hideAnons', (int)$hideAnons, $defaults, $nondefaults );
 156+ wfAppendToArrayIfNotDefault( 'hideLiu' , (int)$hideLiu , $defaults, $nondefaults );
 157+ wfAppendToArrayIfNotDefault( 'hideOwn' , (int)$hideOwn , $defaults, $nondefaults);
 158+ wfAppendToArrayIfNotDefault( 'namespace', $nameSpace , $defaults, $nondefaults);
 159+ wfAppendToArrayIfNotDefault( 'hidePatrolled', (int)$hidePatrolled, $defaults, $nondefaults );
 160+
 161+ if( $nitems == 0 ) {
 162+ $wgOut->addWikiMsg( 'nowatchlist' );
 163+ return;
 164+ }
 165+
 166+ # Possible where conditions
 167+ $conds = array();
 168+
 169+ if( $days <= 0 ) {
 170+ $andcutoff = '';
 171+ } else {
 172+ $conds[] = "integration_rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
 173+ }
 174+
 175+ # If the watchlist is relatively short, it's simplest to zip
 176+ # down its entirety and then sort the results.
 177+
 178+ # If it's relatively long, it may be worth our while to zip
 179+ # through the time-sorted page list checking for watched items.
 180+
 181+ # Up estimate of watched items by 15% to compensate for talk pages...
 182+
 183+ # Toggles
 184+ if( $hideOwn ) {
 185+ $conds[] = "integration_rc_user != $uid";
 186+ }
 187+ if( $hideBots ) {
 188+ $conds[] = 'integration_rc_bot = 0';
 189+ }
 190+ if( $hideMinor ) {
 191+ $conds[] = 'integration_rc_minor = 0';
 192+ }
 193+ if( $hideLiu ) {
 194+ $conds[] = 'integration_rc_user = 0';
 195+ }
 196+ if( $hideAnons ) {
 197+ $conds[] = 'integration_rc_user != 0';
 198+ }
 199+ if ( $wgUser->useRCPatrol() && $hidePatrolled ) {
 200+ $conds[] = 'integration_rc_patrolled != 1';
 201+ }
 202+ if( $nameSpaceClause ) {
 203+ $conds[] = $nameSpaceClause;
 204+ }
 205+
 206+ # Toggle watchlist content (all recent edits or just the latest)
 207+ if( $wgUser->getOption( 'extendwatchlist' )) {
 208+ $limitWatchlist = intval( $wgUser->getOption( 'wllimit' ) );
 209+ $usePage = false;
 210+ } else {
 211+ # Top log Ids for a page are not stored
 212+ $conds[] = 'integration_rc_this_oldid=integration_page_latest OR integration_rc_type=' . RC_LOG;
 213+ $limitWatchlist = 0;
 214+ $usePage = true;
 215+ }
 216+
 217+ # Show a message about slave lag, if applicable
 218+ if( ( $lag = $dbr->getLag() ) > 0 )
 219+ $wgOut->showLagWarning( $lag );
 220+
 221+ # Create output form
 222+ $form = Xml::fieldset( wfMsg( 'watchlist-options' ), false, array( 'id' => 'mw-watchlist-options' ) );
 223+
 224+ # Show watchlist header
 225+ $form .= wfMsgExt( 'watchlist-details', array( 'parseinline' ), $wgLang->formatNum( $nitems ) );
 226+
 227+ if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
 228+ $form .= wfMsgExt( 'wlheader-enotif', 'parse' ) . "\n";
 229+ }
 230+ if( $wgShowUpdatedMarker ) {
 231+ $form .= Xml::openElement( 'form', array( 'method' => 'post',
 232+ 'action' => $specialTitle->getLocalUrl(),
 233+ 'id' => 'mw-watchlist-resetbutton' ) ) .
 234+ wfMsgExt( 'wlheader-showupdated', array( 'parseinline' ) ) . ' ' .
 235+ Xml::submitButton( wfMsg( 'enotif_reset' ), array( 'name' => 'dummy' ) ) .
 236+ Xml::hidden( 'reset', 'all' ) .
 237+ Xml::closeElement( 'form' );
 238+ }
 239+ $form .= '<hr />';
 240+
 241+ $tables = array( 'integration_recentchanges', 'integration_watchlist' );
 242+ $fields = array( "{$recentchanges}.*" );
 243+ $join_conds = array(
 244+ 'integration_watchlist' => array('INNER JOIN',"integration_wl_user='{$uid}' AND integration_wl_namespace=integration_rc_namespace AND integration_wl_title=integration_rc_title AND integration_wl_db=integration_rc_db"),
 245+ );
 246+ $options = array( 'ORDER BY' => 'integration_rc_timestamp DESC' );
 247+ if( $wgShowUpdatedMarker ) {
 248+ $fields[] = 'integration_wl_notificationtimestamp';
 249+ }
 250+ if( $limitWatchlist ) {
 251+ $options['LIMIT'] = $limitWatchlist;
 252+ }
 253+
 254+ $rollbacker = $wgUser->isAllowed('rollback');
 255+ if ( $usePage || $rollbacker ) {
 256+ $tables[] = 'integration_page';
 257+ $join_conds['integration_page'] = array('LEFT JOIN','integration_rc_cur_id=integration_page_id','integration_rc_db=integration_page_db');
 258+ if ($rollbacker)
 259+ $fields[] = 'integration_page_latest';
 260+ }
 261+
 262+ InterwikiIntegrationFunctions::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $options, '' );
 263+ wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
 264+
 265+ $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
 266+ $numRows = $dbr->numRows( $res );
 267+
 268+ /* Start bottom header */
 269+
 270+ $wlInfo = '';
 271+ if( $days >= 1 ) {
 272+ $wlInfo = wfMsgExt( 'rcnote', 'parseinline',
 273+ $wgLang->formatNum( $numRows ),
 274+ $wgLang->formatNum( $days ),
 275+ $wgLang->timeAndDate( wfTimestampNow(), true ),
 276+ $wgLang->date( wfTimestampNow(), true ),
 277+ $wgLang->time( wfTimestampNow(), true )
 278+ ) . '<br />';
 279+ } elseif( $days > 0 ) {
 280+ $wlInfo = wfMsgExt( 'wlnote', 'parseinline',
 281+ $wgLang->formatNum( $numRows ),
 282+ $wgLang->formatNum( round($days*24) )
 283+ ) . '<br />';
 284+ }
 285+
 286+ $cutofflinks = "\n" . InterwikiWatchlist::wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n";
 287+
 288+ $thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
 289+
 290+ # Spit out some control panel links
 291+ $links[] = InterwikiWatchlist::wlShowHideLink( $nondefaults, 'rcshowhideminor', 'hideMinor', $hideMinor );
 292+ $links[] = InterwikiWatchlist::wlShowHideLink( $nondefaults, 'rcshowhidebots', 'hideBots', $hideBots );
 293+ $links[] = InterwikiWatchlist::wlShowHideLink( $nondefaults, 'rcshowhideanons', 'hideAnons', $hideAnons );
 294+ $links[] = InterwikiWatchlist::wlShowHideLink( $nondefaults, 'rcshowhideliu', 'hideLiu', $hideLiu );
 295+ $links[] = InterwikiWatchlist::wlShowHideLink( $nondefaults, 'rcshowhidemine', 'hideOwn', $hideOwn );
 296+
 297+ if( $wgUser->useRCPatrol() ) {
 298+ $links[] = InterwikiWatchlist::wlShowHideLink( $nondefaults, 'rcshowhidepatr', 'hidePatrolled', $hidePatrolled );
 299+ }
 300+
 301+ # Namespace filter and put the whole form together.
 302+ $form .= $wlInfo;
 303+ $form .= $cutofflinks;
 304+ $form .= $wgLang->pipeList( $links );
 305+ $form .= Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl(), 'id' => 'mw-watchlist-form-namespaceselector' ) );
 306+ $form .= '<hr /><p>';
 307+ $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&#160;';
 308+ $form .= Xml::namespaceSelector( $nameSpace, '' ) . '&#160;';
 309+ $form .= Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $invert ) . '&#160;';
 310+ $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
 311+ $form .= Xml::hidden( 'days', $days );
 312+ if( $hideMinor )
 313+ $form .= Xml::hidden( 'hideMinor', 1 );
 314+ if( $hideBots )
 315+ $form .= Xml::hidden( 'hideBots', 1 );
 316+ if( $hideAnons )
 317+ $form .= Xml::hidden( 'hideAnons', 1 );
 318+ if( $hideLiu )
 319+ $form .= Xml::hidden( 'hideLiu', 1 );
 320+ if( $hideOwn )
 321+ $form .= Xml::hidden( 'hideOwn', 1 );
 322+ $form .= Xml::closeElement( 'form' );
 323+ $form .= Xml::closeElement( 'fieldset' );
 324+ $wgOut->addHTML( $form );
 325+
 326+ $wgOut->addHTML( InterwikiIntegrationChangesList::flagLegend() );
 327+
 328+ # If there's nothing to show, stop here
 329+ if( $numRows == 0 ) {
 330+ $wgOut->addWikiMsg( 'watchnochange' );
 331+ return;
 332+ }
 333+
 334+ /* End bottom header */
 335+
 336+ /* Do link batch query */
 337+ $linkBatch = new LinkBatch;
 338+ while ( $row = $dbr->fetchObject( $res ) ) {
 339+ $userNameUnderscored = str_replace( ' ', '_', $row->integration_rc_user_text );
 340+ if ( $row->integration_rc_user != 0 ) {
 341+ $linkBatch->add( NS_USER, $userNameUnderscored );
 342+ }
 343+ $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
 344+
 345+ $linkBatch->add( $row->integration_rc_namespace, $row->integration_rc_title );
 346+ }
 347+ $linkBatch->execute();
 348+ $dbr->dataSeek( $res, 0 );
 349+
 350+ $list = InterwikiIntegrationChangesList::newFromUser( $wgUser );
 351+ $list->setWatchlistDivs();
 352+
 353+ $s = $list->beginRecentInterwikiIntegrationChangesList();
 354+ $counter = 1;
 355+ while ( $obj = $dbr->fetchObject( $res ) ) {
 356+ # Make RC entry
 357+ $rc = InterwikiIntegrationRecentChange::newFromRow( $obj );
 358+ $rc->counter = $counter++;
 359+
 360+ if ( $wgShowUpdatedMarker ) {
 361+ $updated = $obj->integration_wl_notificationtimestamp;
 362+ } else {
 363+ $updated = false;
 364+ }
 365+
 366+ if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
 367+ $rc->numberofWatchingusers = $dbr->selectField( 'integration_watchlist',
 368+ 'COUNT(*)',
 369+ array(
 370+ 'integration_wl_namespace' => $obj->integration_rc_namespace,
 371+ 'integration_wl_title' => $obj->integration_rc_title,
 372+ ),
 373+ __METHOD__ );
 374+ } else {
 375+ $rc->numberofWatchingusers = 0;
 376+ }
 377+
 378+ $s .= $list->recentChangesLine( $rc, $updated, $counter );
 379+ }
 380+ $s .= $list->endRecentInterwikiIntegrationChangesList();
 381+
 382+ $dbr->freeResult( $res );
 383+ $wgOut->addHTML( $s );
 384+ }
 385+
 386+ function wlShowHideLink( $options, $message, $name, $value ) {
 387+ global $wgUser;
 388+
 389+ $showLinktext = wfMsgHtml( 'show' );
 390+ $hideLinktext = wfMsgHtml( 'hide' );
 391+ $title = SpecialPage::getTitleFor( 'Watchlist' );
 392+ $skin = $wgUser->getSkin();
 393+
 394+ $label = $value ? $showLinktext : $hideLinktext;
 395+ $options[$name] = 1 - (int) $value;
 396+
 397+ return wfMsgHtml( $message, $skin->linkKnown( $title, $label, array(), $options ) );
 398+ }
 399+
 400+
 401+ function wlHoursLink( $h, $page, $options = array() ) {
 402+ global $wgUser, $wgLang, $wgContLang;
 403+
 404+ $sk = $wgUser->getSkin();
 405+ $title = Title::newFromText( $wgContLang->specialPage( $page ) );
 406+ $options['days'] = ($h / 24.0);
 407+
 408+ $s = $sk->linkKnown(
 409+ $title,
 410+ $wgLang->formatNum( $h ),
 411+ array(),
 412+ $options
 413+ );
 414+
 415+ return $s;
 416+ }
 417+
 418+ function wlDaysLink( $d, $page, $options = array() ) {
 419+ global $wgUser, $wgLang, $wgContLang;
 420+
 421+ $sk = $wgUser->getSkin();
 422+ $title = Title::newFromText( $wgContLang->specialPage( $page ) );
 423+ $options['days'] = $d;
 424+ $message = ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) );
 425+
 426+ $s = $sk->linkKnown(
 427+ $title,
 428+ $message,
 429+ array(),
 430+ $options
 431+ );
 432+
 433+ return $s;
 434+ }
 435+
 436+ /**
 437+ * Returns html
 438+ */
 439+ function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
 440+ global $wgLang;
 441+
 442+ $hours = array( 1, 2, 6, 12 );
 443+ $days = array( 1, 3, 7 );
 444+ $i = 0;
 445+ foreach( $hours as $h ) {
 446+ $hours[$i++] = InterwikiWatchlist::wlHoursLink( $h, $page, $options );
 447+ }
 448+ $i = 0;
 449+ foreach( $days as $d ) {
 450+ $days[$i++] = InterwikiWatchlist::wlDaysLink( $d, $page, $options );
 451+ }
 452+ return wfMsgExt('wlshowlast',
 453+ array('parseinline', 'replaceafter'),
 454+ $wgLang->pipeList( $hours ),
 455+ $wgLang->pipeList( $days ),
 456+ InterwikiWatchlist::wlDaysLink( 0, $page, $options ) );
 457+ }
 458+
 459+ /**
 460+ * Count the number of items on a user's watchlist
 461+ *
 462+ * @param $talk Include talk pages
 463+ * @return integer
 464+ */
 465+ function wlCountItems( &$user, $talk = true ) {
 466+ $dbr = wfGetDB( DB_SLAVE, 'integration_watchlist' );
 467+
 468+ # Fetch the raw count
 469+ $res = $dbr->select( 'integration_watchlist', 'COUNT(*) AS count',
 470+ array( 'integration_wl_user' => $user->mId ), 'wlCountItems' );
 471+ $row = $dbr->fetchObject( $res );
 472+ $count = $row->count;
 473+ $dbr->freeResult( $res );
 474+
 475+ # Halve to remove talk pages if needed
 476+ if( !$talk )
 477+ $count = floor( $count / 2 );
 478+
 479+ return( $count );
 480+ }
 481+}
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/SpecialInterwikiIntegration.php
@@ -96,7 +96,7 @@
9797 );
9898 $dbw->insert ( 'integration_namespace', $newNamespaceRow);
9999 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
100 - $wgOut->addWikiMsg( 'interwikiintegration-setuptext', $wgSitename );
 100+ $wgOut->addWikiMsg( 'integration-setuptext', $wgSitename );
101101 return;
102102 }
103103 }
@@ -175,4 +175,42 @@
176176 $wgOut->addWikiMsg( 'interwikirecentchanges-setuptext' );
177177 return;
178178 }
 179+}
 180+
 181+/**
 182+ * A special page that populates the interwiki recent changes table.
 183+ */
 184+class PopulateInterwikiPageTable extends SpecialPage {
 185+ function __construct() {
 186+ parent::__construct( 'PopulateInterwikiPageTable', 'integration' );
 187+ wfLoadExtensionMessages( 'InterwikiIntegration' );
 188+ }
 189+
 190+ function execute( $par ) {
 191+ global $wgInterwikiIntegrationPrefix, $wgOut;
 192+ $dbr = wfGetDB( DB_SLAVE );
 193+ $dbw = wfGetDB( DB_MASTER );
 194+ $dbw->delete ( 'integration_page', '*' );
 195+ $dbList = array_unique ( $wgInterwikiIntegrationPrefix );
 196+ foreach ( $dbList as $thisDb ) {
 197+ $thisDbr = wfGetDB( DB_SLAVE, array(), $thisDb );
 198+ $pageRes = $thisDbr->select(
 199+ 'page',
 200+ '*'
 201+ );
 202+ if( $pageRes->numRows() > 0 ) {
 203+ while( $row = $pageRes->fetchObject() ) {
 204+ foreach ( $row as $key => $value ) {
 205+ $newKey = "integration_" . $key;
 206+ $iPage[$newKey] = $value;
 207+ }
 208+ $iPage['integration_page_db'] = $thisDb;
 209+ $dbw->insert( 'integration_page', $iPage );
 210+ }
 211+ }
 212+ }
 213+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
 214+ $wgOut->addWikiMsg( 'interwikipage-setuptext' );
 215+ return;
 216+ }
179217 }
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/InterwikiIntegration.hooks.php
@@ -26,6 +26,10 @@
2727 'integration_recentchanges',
2828 dirname( __FILE__ ) . '/interwikiintegration-recentchanges.sql'
2929 );
 30+ $wgExtNewTables[] = array(
 31+ 'integration_page',
 32+ dirname( __FILE__ ) . '/interwikiintegration-page.sql'
 33+ );
3034 return true;
3135 }
3236
@@ -123,6 +127,19 @@
124128 */
125129 public static function InterwikiIntegrationArticleInsertComplete ( &$article, &$user, $text, $summary, $minoredit,
126130 &$watchthis, $sectionanchor, &$flags, $revision ) {
 131+ global $wgDBname;
 132+ $mDb = wfGetDB( DB_MASTER );
 133+ $result = $mDb->selectrow(
 134+ 'page',
 135+ '*',
 136+ array ('page_id' => $article->getID() )
 137+ );
 138+ foreach ( $result as $key => $value ) {
 139+ $newKey = "integration_" . $key;
 140+ $iPage[$newKey] = $value;
 141+ }
 142+ $iPage['integration_page_db'] = $wgDBname;
 143+ $mDb->insert( 'integration_page', $iPage );
127144 InterwikiIntegrationHooks::PurgeReferringPages ( $article->getTitle() );
128145 return true;
129146 }
@@ -131,12 +148,21 @@
132149 * When a page is deleted, purge caches of pages that link to it interwiki
133150 */
134151 public static function InterwikiIntegrationArticleDeleteComplete( &$article, &$user, $reason, $id ) {
 152+ global $wgDBname;
135153 InterwikiIntegrationHooks::PurgeReferringPages ( $article->getTitle() );
136154 $mDb = wfGetDB( DB_MASTER );
137155 $mDb->delete(
138156 'integration_iwlinks',
139157 array ('integration_iwl_from' => $id )
140158 );
 159+ $mDb->delete(
 160+ 'integration_page',
 161+ array (
 162+ 'integration_page_title' => str_replace ( ' ', '_', $article->getTitle()->getDBkey() ),
 163+ 'integration_page_namespace' => $article->getTitle()->getNamespace(),
 164+ 'integration_page_db' => $wgDBname
 165+ )
 166+ );
141167 return true;
142168 }
143169
@@ -145,10 +171,51 @@
146172 */
147173 public static function InterwikiIntegrationArticleUndelete ( $title, $create ) {
148174 if ( $create ) {
 175+ global $wgDBname;
 176+ $mDb = wfGetDB( DB_MASTER );
 177+ $result = $mDb->selectrow(
 178+ 'page',
 179+ '*',
 180+ array ('page_id' => $title->getArticleID() )
 181+ );
 182+ foreach ( $result as $key => $value ) {
 183+ $newKey = "integration_" . $key;
 184+ $iPage[$newKey] = $value;
 185+ }
 186+ $iPage['integration_page_db'] = $wgDBname;
 187+ $mDb->insert( 'integration_page', $iPage );
149188 InterwikiIntegrationHooks::PurgeReferringPages ( $title );
150189 }
151190 return true;
152191 }
 192+
 193+ public static function InterwikiIntegrationArticleSaveComplete( &$article, &$user, $text, $summary,
 194+ $minoredit, &$watchthis, $sectionanchor, &$flags, $revision, &$status, $baseRevId, &$redirect ) {
 195+ global $wgDBname;
 196+ if ( !is_null ( $revision ) ) {
 197+ $mDb = wfGetDB( DB_MASTER );
 198+ $result = $mDb->selectrow(
 199+ 'page',
 200+ '*',
 201+ array ('page_id' => $article->getID() )
 202+ );
 203+ if ( $result ) {
 204+ foreach ( $result as $key => $value ) {
 205+ $newKey = "integration_" . $key;
 206+ $iPage[$newKey] = $value;
 207+ }
 208+ $mDb->update(
 209+ 'integration_page',
 210+ $iPage,
 211+ array (
 212+ 'integration_page_id' => $article->getID(),
 213+ 'integration_page_db' => $wgDBname
 214+ )
 215+ );
 216+ }
 217+ }
 218+ return true;
 219+ }
153220
154221 /**
155222 * When a page is moved, purge caches of pages that link to the new page interwiki
Index: trunk/extensions/InterwikiIntegration/interwikiintegration-page.sql
@@ -0,0 +1,56 @@
 2+CREATE TABLE /*_*/integration_page (
 3+ -- Primary key.
 4+ integration_global_page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
 5+
 6+ -- Database name.
 7+ integration_page_db varchar(255) binary NOT NULL,
 8+
 9+ -- Unique identifier number. The integration_page_id will be preserved across
 10+ -- edits and rename operations, but not deletions and recreations.
 11+ integration_page_id int unsigned NOT NULL,
 12+
 13+ -- A page name is broken into a namespace and a title.
 14+ -- The namespace keys are UI-language-independent constants,
 15+ -- defined in includes/Defines.php
 16+ integration_page_namespace int NOT NULL,
 17+
 18+ -- The rest of the title, as text.
 19+ -- Spaces are transformed into underscores in title storage.
 20+ integration_page_title varchar(255) binary NOT NULL,
 21+
 22+ -- Comma-separated set of permission keys indicating who
 23+ -- can move or edit the page.
 24+ integration_page_restrictions tinyblob NOT NULL,
 25+
 26+ -- Number of times this page has been viewed.
 27+ integration_page_counter bigint unsigned NOT NULL default 0,
 28+
 29+ -- 1 indicates the article is a redirect.
 30+ integration_page_is_redirect tinyint unsigned NOT NULL default 0,
 31+
 32+ -- 1 indicates this is a new entry, with only one edit.
 33+ -- Not all pages with one edit are new pages.
 34+ integration_page_is_new tinyint unsigned NOT NULL default 0,
 35+
 36+ -- Random value between 0 and 1, used for Special:Randompage
 37+ integration_page_random real unsigned NOT NULL,
 38+
 39+ -- This timestamp is updated whenever the page changes in
 40+ -- a way requiring it to be re-rendered, invalidating caches.
 41+ -- Aside from editing this includes permission changes,
 42+ -- creation or deletion of linked pages, and alteration
 43+ -- of contained templates.
 44+ integration_page_touched binary(14) NOT NULL default '',
 45+
 46+ -- Handy key to revision.rev_id of the current revision.
 47+ -- This may be 0 during page creation, but that shouldn't
 48+ -- happen outside of a transaction... hopefully.
 49+ integration_page_latest int unsigned NOT NULL,
 50+
 51+ -- Uncompressed length in bytes of the page's current source text.
 52+ integration_page_len int unsigned NOT NULL
 53+) /*$wgDBTableOptions*/;
 54+
 55+CREATE UNIQUE INDEX /*i*/integration_name_title ON /*_*/integration_page (integration_page_namespace,integration_page_title,integration_page_db);
 56+CREATE INDEX /*i*/integration_page_random ON /*_*/integration_page (integration_page_random);
 57+CREATE INDEX /*i*/integration_page_len ON /*_*/integration_page (integration_page_len);
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/interwikiintegration-prefix.sql
@@ -3,9 +3,9 @@
44 -- Track prefixes of local wikis, for foreign wikis' benefit; probably soon to be deprecated
55 CREATE TABLE integration_prefix (
66 -- Wiki prefix
7 - integration_prefix varchar(256) binary NOT NULL PRIMARY KEY,
 7+ integration_prefix varchar(255) binary NOT NULL PRIMARY KEY,
88 -- Wiki database name
9 - integration_dbname varchar(256) binary NOT NULL,
 9+ integration_dbname varchar(255) binary NOT NULL,
1010 -- Does this wiki use pure wiki deletion?
1111 integration_pwd tinyint unsigned NOT NULL default 0
1212 )
Index: trunk/extensions/InterwikiIntegration/InterwikiIntegrationChangesList.php
@@ -0,0 +1,1189 @@
 2+<?php
 3+
 4+/**
 5+ * @todo document
 6+ */
 7+class InterwikiIntegrationRCCacheEntry extends InterwikiIntegrationRecentChange {
 8+ var $secureName, $link;
 9+ var $curlink , $difflink, $lastlink, $usertalklink, $versionlink;
 10+ var $userlink, $timestamp, $watched;
 11+
 12+ static function newFromParent( $rc ) {
 13+ $rc2 = new InterwikiIntegrationRCCacheEntry;
 14+ $rc2->mAttribs = $rc->mAttribs;
 15+ $rc2->mExtra = $rc->mExtra;
 16+ return $rc2;
 17+ }
 18+}
 19+
 20+/**
 21+ * Class to show various lists of changes:
 22+ * - what links here
 23+ * - related changes
 24+ * - recent changes
 25+ */
 26+class InterwikiIntegrationChangesList {
 27+ # Called by history lists and recent changes
 28+ public $skin;
 29+ protected $watchlist = false;
 30+
 31+ /**
 32+ * Changeslist contructor
 33+ * @param $skin Skin
 34+ */
 35+ public function __construct( $skin ) {
 36+ $this->skin = $skin;
 37+ $this->preCacheMessages();
 38+ }
 39+
 40+ /**
 41+ * Fetch an appropriate changes list class for the specified user
 42+ * Some users might want to use an enhanced list format, for instance
 43+ *
 44+ * @param $user User to fetch the list class for
 45+ * @return InterwikiIntegrationChangesList derivative
 46+ */
 47+ public static function newFromUser( &$user ) {
 48+ global $wgRequest;
 49+
 50+ $sk = $user->getSkin();
 51+ $list = null;
 52+ if( wfRunHooks( 'FetchInterwikiIntegrationChangesList', array( &$user, &$sk, &$list ) ) ) {
 53+ $new = $wgRequest->getBool( 'enhanced', $user->getOption( 'usenewrc' ) );
 54+ return $new ? new EnhancedInterwikiIntegrationChangesList( $sk ) : new OldInterwikiIntegrationChangesList( $sk );
 55+ } else {
 56+ return $list;
 57+ }
 58+ }
 59+
 60+ /**
 61+ * Sets the list to use a <li class="watchlist-(namespace)-(page)"> tag
 62+ * @param $value Boolean
 63+ */
 64+ public function setWatchlistDivs( $value = true ) {
 65+ $this->watchlist = $value;
 66+ }
 67+
 68+ /**
 69+ * As we use the same small set of messages in various methods and that
 70+ * they are called often, we call them once and save them in $this->message
 71+ */
 72+ private function preCacheMessages() {
 73+ if( !isset( $this->message ) ) {
 74+ foreach ( explode( ' ', 'cur diff hist last blocklink history ' .
 75+ 'semicolon-separator pipe-separator' ) as $msg ) {
 76+ $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
 77+ }
 78+ }
 79+ }
 80+
 81+
 82+ /**
 83+ * Returns the appropriate flags for new page, minor change and patrolling
 84+ * @param $new Boolean
 85+ * @param $minor Boolean
 86+ * @param $patrolled Boolean
 87+ * @param $nothing String to use for empty space
 88+ * @param $bot Boolean
 89+ * @return String
 90+ */
 91+ protected function recentChangesFlags( $new, $minor, $patrolled, $nothing = '&#160;', $bot = false ) {
 92+ $f = $new ? self::flag( 'newpage' ) : $nothing;
 93+ $f .= $minor ? self::flag( 'minor' ) : $nothing;
 94+ $f .= $bot ? self::flag( 'bot' ) : $nothing;
 95+ $f .= $patrolled ? self::flag( 'unpatrolled' ) : $nothing;
 96+ return $f;
 97+ }
 98+
 99+ /**
 100+ * Provide the <abbr> element appropriate to a given abbreviated flag,
 101+ * namely the flag indicating a new page, a minor edit, a bot edit, or an
 102+ * unpatrolled edit. By default in English it will contain "N", "m", "b",
 103+ * "!" respectively, plus it will have an appropriate title and class.
 104+ *
 105+ * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot'
 106+ * @return String: Raw HTML
 107+ */
 108+ public static function flag( $key ) {
 109+ static $messages = null;
 110+ if ( is_null( $messages ) ) {
 111+ foreach ( explode( ' ', 'minoreditletter boteditletter newpageletter ' .
 112+ 'unpatrolledletter recentchanges-label-minor recentchanges-label-bot ' .
 113+ 'recentchanges-label-newpage recentchanges-label-unpatrolled' ) as $msg ) {
 114+ $messages[$msg] = wfMsgExt( $msg, 'escapenoentities' );
 115+ }
 116+ }
 117+ # Inconsistent naming, bleh
 118+ if ( $key == 'newpage' || $key == 'unpatrolled' ) {
 119+ $key2 = $key;
 120+ } else {
 121+ $key2 = $key . 'edit';
 122+ }
 123+ return "<abbr class=\"$key\" title=\""
 124+ . $messages["recentchanges-label-$key"] . "\">"
 125+ . $messages["${key2}letter"]
 126+ . '</abbr>';
 127+ }
 128+
 129+ /**
 130+ * Some explanatory wrapper text for the given flag, to be used in a legend
 131+ * explaining what the flags mean. For instance, "N - new page". See
 132+ * also flag().
 133+ *
 134+ * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot'
 135+ * @return String: Raw HTML
 136+ */
 137+ private static function flagLine( $key ) {
 138+ return wfMsgExt( "recentchanges-legend-$key", array( 'escapenoentities',
 139+ 'replaceafter' ), self::flag( $key ) );
 140+ }
 141+
 142+ /**
 143+ * A handy legend to tell users what the little "m", "b", and so on mean.
 144+ *
 145+ * @return String: Raw HTML
 146+ */
 147+ public static function flagLegend() {
 148+ global $wgGroupPermissions, $wgLang;
 149+
 150+ $flags = array( self::flagLine( 'newpage' ),
 151+ self::flagLine( 'minor' ) );
 152+
 153+ # Don't show info on bot edits unless there's a bot group of some kind
 154+ foreach ( $wgGroupPermissions as $rights ) {
 155+ if ( isset( $rights['bot'] ) && $rights['bot'] ) {
 156+ $flags[] = self::flagLine( 'bot' );
 157+ break;
 158+ }
 159+ }
 160+
 161+ if ( self::usePatrol() ) {
 162+ $flags[] = self::flagLine( 'unpatrolled' );
 163+ }
 164+
 165+ return '<div class="mw-rc-label-legend">' .
 166+ wfMsgExt( 'recentchanges-label-legend', 'parseinline',
 167+ $wgLang->commaList( $flags ) ) . '</div>';
 168+ }
 169+
 170+ /**
 171+ * Returns text for the start of the tabular part of RC
 172+ * @return String
 173+ */
 174+ public function beginRecentInterwikiIntegrationChangesList() {
 175+ $this->integration_rc_cache = array();
 176+ $this->rcMoveIndex = 0;
 177+ $this->rcCacheIndex = 0;
 178+ $this->lastdate = '';
 179+ $this->rclistOpen = false;
 180+ return '';
 181+ }
 182+
 183+ /**
 184+ * Show formatted char difference
 185+ * @param $old Integer: bytes
 186+ * @param $new Integer: bytes
 187+ * @returns String
 188+ */
 189+ public static function showCharacterDifference( $old, $new ) {
 190+ global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode;
 191+ $szdiff = $new - $old;
 192+
 193+ $code = $wgLang->getCode();
 194+ static $fastCharDiff = array();
 195+ if ( !isset($fastCharDiff[$code]) ) {
 196+ $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1';
 197+ }
 198+
 199+ $formatedSize = $wgLang->formatNum($szdiff);
 200+
 201+ if ( !$fastCharDiff[$code] ) {
 202+ $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $formatedSize );
 203+ }
 204+
 205+ if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) {
 206+ $tag = 'strong';
 207+ } else {
 208+ $tag = 'span';
 209+ }
 210+ if( $szdiff === 0 ) {
 211+ return "<$tag class='mw-plusminus-null'>($formatedSize)</$tag>";
 212+ } elseif( $szdiff > 0 ) {
 213+ return "<$tag class='mw-plusminus-pos'>(+$formatedSize)</$tag>";
 214+ } else {
 215+ return "<$tag class='mw-plusminus-neg'>($formatedSize)</$tag>";
 216+ }
 217+ }
 218+
 219+ /**
 220+ * Returns text for the end of RC
 221+ * @return String
 222+ */
 223+ public function endRecentInterwikiIntegrationChangesList() {
 224+ if( $this->rclistOpen ) {
 225+ return "</ul>\n";
 226+ } else {
 227+ return '';
 228+ }
 229+ }
 230+
 231+ public function insertMove( &$s, $rc ) {
 232+ # Diff
 233+ $s .= '(' . $this->message['diff'] . ') (';
 234+ # Hist
 235+ $s .= $this->skin->link(
 236+ $rc->getMovedToTitle(),
 237+ $this->message['hist'],
 238+ array(),
 239+ array( 'action' => 'history' ),
 240+ array( 'known', 'noclasses' )
 241+ ) . ') . . ';
 242+ # "[[x]] moved to [[y]]"
 243+ $msg = ( $rc->mAttribs['integration_rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir';
 244+ $s .= wfMsg(
 245+ $msg,
 246+ $this->skin->link(
 247+ $rc->getTitle(),
 248+ null,
 249+ array(),
 250+ array( 'redirect' => 'no' ),
 251+ array( 'known', 'noclasses' )
 252+ ),
 253+ $this->skin->link(
 254+ $rc->getMovedToTitle(),
 255+ null,
 256+ array(),
 257+ array(),
 258+ array( 'known', 'noclasses' )
 259+ )
 260+ );
 261+ }
 262+
 263+ public function insertDateHeader( &$s, $integration_rc_timestamp ) {
 264+ global $wgLang;
 265+ # Make date header if necessary
 266+ $date = $wgLang->date( $integration_rc_timestamp, true, true );
 267+ if( $date != $this->lastdate ) {
 268+ if( $this->lastdate != '' ) {
 269+ $s .= "</ul>\n";
 270+ }
 271+ $s .= Xml::element( 'h4', null, $date ) . "\n<ul class=\"special\">";
 272+ $this->lastdate = $date;
 273+ $this->rclistOpen = true;
 274+ }
 275+ }
 276+
 277+ public function insertLog( &$s, $title, $logtype ) {
 278+ $logname = LogPage::logName( $logtype );
 279+ $s .= '(' . $this->skin->link(
 280+ $title,
 281+ $logname,
 282+ array(),
 283+ array(),
 284+ array( 'known', 'noclasses' )
 285+ ) . ')';
 286+ }
 287+
 288+ public function insertDiffHist( &$s, &$rc, $unpatrolled ) {
 289+ # Diff link
 290+ if( $rc->mAttribs['integration_rc_type'] == RC_NEW || $rc->mAttribs['integration_rc_type'] == RC_LOG ) {
 291+ $diffLink = $this->message['diff'];
 292+ } else if( !self::userCan($rc,Revision::DELETED_TEXT) ) {
 293+ $diffLink = $this->message['diff'];
 294+ } else {
 295+ $query = array(
 296+ 'curid' => $rc->mAttribs['integration_rc_cur_id'],
 297+ 'diff' => $rc->mAttribs['integration_rc_this_oldid'],
 298+ 'oldid' => $rc->mAttribs['integration_rc_last_oldid']
 299+ );
 300+
 301+ if( $unpatrolled ) {
 302+ $query['rcid'] = $rc->mAttribs['integration_rc_id'];
 303+ };
 304+
 305+ $diffLink = $this->skin->link(
 306+ $rc->getTitle(),
 307+ $this->message['diff'],
 308+ array( 'tabindex' => $rc->counter ),
 309+ $query,
 310+ array( 'known', 'noclasses' )
 311+ );
 312+ }
 313+ $s .= '(' . $diffLink . $this->message['pipe-separator'];
 314+ # History link
 315+ $s .= $this->skin->link(
 316+ $rc->getTitle(),
 317+ $this->message['hist'],
 318+ array(),
 319+ array(
 320+ 'curid' => $rc->mAttribs['integration_rc_cur_id'],
 321+ 'action' => 'history'
 322+ ),
 323+ array( 'known', 'noclasses' )
 324+ );
 325+ $s .= ') . . ';
 326+ }
 327+
 328+ public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) {
 329+ global $wgContLang;
 330+ # If it's a new article, there is no diff link, but if it hasn't been
 331+ # patrolled yet, we need to give users a way to do so
 332+ $params = array();
 333+
 334+ if ( $unpatrolled && $rc->mAttribs['integration_rc_type'] == RC_NEW ) {
 335+ $params['rcid'] = $rc->mAttribs['integration_rc_id'];
 336+ }
 337+
 338+ if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
 339+ $articlelink = $this->skin->link(
 340+ $rc->getTitle(),
 341+ null,
 342+ array(),
 343+ $params,
 344+ array( 'known', 'noclasses' )
 345+ );
 346+ $articlelink = '<span class="history-deleted">' . $articlelink . '</span>';
 347+ } else {
 348+ $articlelink = ' '. $this->skin->link(
 349+ $rc->getTitle(),
 350+ null,
 351+ array(),
 352+ $params,
 353+ array( 'known', 'noclasses' )
 354+ );
 355+ }
 356+ # Bolden pages watched by this user
 357+ if( $watched ) {
 358+ $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
 359+ }
 360+ # RTL/LTR marker
 361+ $articlelink .= $wgContLang->getDirMark();
 362+
 363+ wfRunHooks( 'InterwikiIntegrationChangesListInsertArticleLink',
 364+ array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) );
 365+
 366+ $s .= " $articlelink";
 367+ }
 368+
 369+ public function insertTimestamp( &$s, $rc ) {
 370+ global $wgLang;
 371+ $s .= $this->message['semicolon-separator'] .
 372+ $wgLang->time( $rc->mAttribs['integration_rc_timestamp'], true, true ) . ' . . ';
 373+ }
 374+
 375+ /** Insert links to user page, user talk page and eventually a blocking link */
 376+ public function insertUserRelatedLinks( &$s, &$rc ) {
 377+ if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) {
 378+ $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
 379+ } else {
 380+ $s .= $this->skin->userLink( $rc->mAttribs['integration_rc_user'], $rc->mAttribs['integration_rc_user_text'] );
 381+ $s .= $this->skin->userToolLinks( $rc->mAttribs['integration_rc_user'], $rc->mAttribs['integration_rc_user_text'] );
 382+ }
 383+ }
 384+
 385+ /** insert a formatted action */
 386+ public function insertAction( &$s, &$rc ) {
 387+ if( $rc->mAttribs['integration_rc_type'] == RC_LOG ) {
 388+ if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) {
 389+ $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>';
 390+ } else {
 391+ $s .= ' '.LogPage::actionText( $rc->mAttribs['integration_rc_log_type'], $rc->mAttribs['integration_rc_log_action'],
 392+ $rc->getTitle(), $this->skin, LogPage::extractParams( $rc->mAttribs['integration_rc_params'] ), true, true );
 393+ }
 394+ }
 395+ }
 396+
 397+ /** insert a formatted comment */
 398+ public function insertComment( &$s, &$rc ) {
 399+ if( $rc->mAttribs['integration_rc_type'] != RC_MOVE && $rc->mAttribs['integration_rc_type'] != RC_MOVE_OVER_REDIRECT ) {
 400+ if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) {
 401+ $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>';
 402+ } else {
 403+ $s .= $this->skin->commentBlock( $rc->mAttribs['integration_rc_comment'], $rc->getTitle() );
 404+ }
 405+ }
 406+ }
 407+
 408+ /**
 409+ * Check whether to enable recent changes patrol features
 410+ * @return Boolean
 411+ */
 412+ public static function usePatrol() {
 413+ global $wgUser;
 414+ return $wgUser->useRCPatrol();
 415+ }
 416+
 417+ /**
 418+ * Returns the string which indicates the number of watching users
 419+ */
 420+ protected function numberofWatchingusers( $count ) {
 421+ global $wgLang;
 422+ static $cache = array();
 423+ if( $count > 0 ) {
 424+ if( !isset( $cache[$count] ) ) {
 425+ $cache[$count] = wfMsgExt( 'number_of_watching_users_RCview',
 426+ array('parsemag', 'escape' ), $wgLang->formatNum( $count ) );
 427+ }
 428+ return $cache[$count];
 429+ } else {
 430+ return '';
 431+ }
 432+ }
 433+
 434+ /**
 435+ * Determine if said field of a revision is hidden
 436+ * @param $rc InterwikiIntegrationRCCacheEntry
 437+ * @param $field Integer: one of DELETED_* bitfield constants
 438+ * @return Boolean
 439+ */
 440+ public static function isDeleted( $rc, $field ) {
 441+ return ( $rc->mAttribs['integration_rc_deleted'] & $field ) == $field;
 442+ }
 443+
 444+ /**
 445+ * Determine if the current user is allowed to view a particular
 446+ * field of this revision, if it's marked as deleted.
 447+ * @param $rc InterwikiIntegrationRCCacheEntry
 448+ * @param $field Integer
 449+ * @return Boolean
 450+ */
 451+ public static function userCan( $rc, $field ) {
 452+ if( $rc->mAttribs['integration_rc_type'] == RC_LOG ) {
 453+ return LogEventsList::userCanBitfield( $rc->mAttribs['integration_rc_deleted'], $field );
 454+ } else {
 455+ return Revision::userCanBitfield( $rc->mAttribs['integration_rc_deleted'], $field );
 456+ }
 457+ }
 458+
 459+ protected function maybeWatchedLink( $link, $watched = false ) {
 460+ if( $watched ) {
 461+ return '<strong class="mw-watched">' . $link . '</strong>';
 462+ } else {
 463+ return '<span class="mw-rc-unwatched">' . $link . '</span>';
 464+ }
 465+ }
 466+
 467+ /** Inserts a rollback link */
 468+ public function insertRollback( &$s, &$rc ) {
 469+ global $wgUser;
 470+ if( !$rc->mAttribs['integration_rc_new'] && $rc->mAttribs['integration_rc_this_oldid'] && $rc->mAttribs['integration_rc_cur_id'] ) {
 471+ $page = $rc->getTitle();
 472+ /** Check for rollback and edit permissions, disallow special pages, and only
 473+ * show a link on the top-most revision */
 474+ if ($wgUser->isAllowed('rollback') && $rc->mAttribs['integration_page_latest'] == $rc->mAttribs['integration_rc_this_oldid'] )
 475+ {
 476+ $rev = new Revision( array(
 477+ 'id' => $rc->mAttribs['integration_rc_this_oldid'],
 478+ 'user' => $rc->mAttribs['integration_rc_user'],
 479+ 'user_text' => $rc->mAttribs['integration_rc_user_text'],
 480+ 'deleted' => $rc->mAttribs['integration_rc_deleted']
 481+ ) );
 482+ $rev->setTitle( $page );
 483+ $s .= ' '.$this->skin->generateRollback( $rev );
 484+ }
 485+ }
 486+ }
 487+
 488+ public function insertTags( &$s, &$rc, &$classes ) {
 489+ if ( empty($rc->mAttribs['ts_tags']) )
 490+ return;
 491+
 492+ list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' );
 493+ $classes = array_merge( $classes, $newClasses );
 494+ $s .= ' ' . $tagSummary;
 495+ }
 496+
 497+ public function insertExtra( &$s, &$rc, &$classes ) {
 498+ ## Empty, used for subclassers to add anything special.
 499+ }
 500+}
 501+
 502+
 503+/**
 504+ * Generate a list of changes using the good old system (no javascript)
 505+ */
 506+class OldInterwikiIntegrationChangesList extends InterwikiIntegrationChangesList {
 507+ /**
 508+ * Format a line using the old system (aka without any javascript).
 509+ */
 510+ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
 511+ global $wgLang, $wgRCShowChangedSize, $wgUser;
 512+ wfProfileIn( __METHOD__ );
 513+ # Should patrol-related stuff be shown?
 514+ $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['integration_rc_patrolled'];
 515+
 516+ $dateheader = ''; // $s now contains only <li>...</li>, for hooks' convenience.
 517+ $this->insertDateHeader( $dateheader, $rc->mAttribs['integration_rc_timestamp'] );
 518+
 519+ $s = '';
 520+ $classes = array();
 521+ // use mw-line-even/mw-line-odd class only if linenumber is given (feature from bug 14468)
 522+ if( $linenumber ) {
 523+ if( $linenumber & 1 ) {
 524+ $classes[] = 'mw-line-odd';
 525+ }
 526+ else {
 527+ $classes[] = 'mw-line-even';
 528+ }
 529+ }
 530+
 531+ // Moved pages
 532+ if( $rc->mAttribs['integration_rc_type'] == RC_MOVE || $rc->mAttribs['integration_rc_type'] == RC_MOVE_OVER_REDIRECT ) {
 533+ $this->insertMove( $s, $rc );
 534+ // Log entries
 535+ } elseif( $rc->mAttribs['integration_rc_log_type'] ) {
 536+ $logtitle = Title::newFromText( 'Log/'.$rc->mAttribs['integration_rc_log_type'], NS_SPECIAL );
 537+ $this->insertLog( $s, $logtitle, $rc->mAttribs['integration_rc_log_type'] );
 538+ // Log entries (old format) or log targets, and special pages
 539+ } elseif( $rc->mAttribs['integration_rc_namespace'] == NS_SPECIAL ) {
 540+ list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $rc->mAttribs['integration_rc_title'] );
 541+ if( $name == 'Log' ) {
 542+ $this->insertLog( $s, $rc->getTitle(), $subpage );
 543+ }
 544+ // Regular entries
 545+ } else {
 546+ $this->insertDiffHist( $s, $rc, $unpatrolled );
 547+ # M, N, b and ! (minor, new, bot and unpatrolled)
 548+ $s .= $this->recentChangesFlags( $rc->mAttribs['integration_rc_new'], $rc->mAttribs['integration_rc_minor'],
 549+ $unpatrolled, '', $rc->mAttribs['integration_rc_bot'] );
 550+ $this->insertArticleLink( $s, $rc, $unpatrolled, $watched );
 551+ }
 552+ # Edit/log timestamp
 553+ $this->insertTimestamp( $s, $rc );
 554+ # Bytes added or removed
 555+ if( $wgRCShowChangedSize ) {
 556+ $cd = $rc->getCharacterDifference();
 557+ if( $cd != '' ) {
 558+ $s .= "$cd . . ";
 559+ }
 560+ }
 561+ # User tool links
 562+ $this->insertUserRelatedLinks( $s, $rc );
 563+ # Log action text (if any)
 564+ $this->insertAction( $s, $rc );
 565+ # Edit or log comment
 566+ $this->insertComment( $s, $rc );
 567+ # Tags
 568+ $this->insertTags( $s, $rc, $classes );
 569+ # Rollback
 570+ $this->insertRollback( $s, $rc );
 571+ # For subclasses
 572+ $this->insertExtra( $s, $rc, $classes );
 573+
 574+ # How many users watch this page
 575+ if( $rc->numberofWatchingusers > 0 ) {
 576+ $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview',
 577+ array( 'parsemag', 'escape' ), $wgLang->formatNum( $rc->numberofWatchingusers ) );
 578+ }
 579+
 580+ if( $this->watchlist ) {
 581+ $classes[] = Sanitizer::escapeClass( 'watchlist-'.$rc->mAttribs['integration_rc_namespace'].'-'.$rc->mAttribs['integration_rc_title'] );
 582+ }
 583+
 584+ wfRunHooks( 'OldInterwikiIntegrationChangesListRecentChangesLine', array(&$this, &$s, $rc) );
 585+
 586+ wfProfileOut( __METHOD__ );
 587+ return "$dateheader<li class=\"".implode( ' ', $classes )."\">".$s."</li>\n";
 588+ }
 589+}
 590+
 591+
 592+/**
 593+ * Generate a list of changes using an Enhanced system (uses javascript).
 594+ */
 595+class EnhancedInterwikiIntegrationChangesList extends InterwikiIntegrationChangesList {
 596+ /**
 597+ * Add the JavaScript file for enhanced changeslist
 598+ * @return String
 599+ */
 600+ public function beginRecentChangesList() {
 601+ global $wgStylePath, $wgStyleVersion;
 602+ $this->integration_rc_cache = array();
 603+ $this->rcMoveIndex = 0;
 604+ $this->rcCacheIndex = 0;
 605+ $this->lastdate = '';
 606+ $this->rclistOpen = false;
 607+ $script = Html::linkedScript( $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" );
 608+ return $script;
 609+ }
 610+ /**
 611+ * Format a line for enhanced recentchange (aka with javascript and block of lines).
 612+ */
 613+ public function recentChangesLine( &$baseRC, $watched = false ) {
 614+ global $wgLang, $wgUser;
 615+
 616+ wfProfileIn( __METHOD__ );
 617+
 618+ # Create a specialised object
 619+ $rc = InterwikiIntegrationRCCacheEntry::newFromParent( $baseRC );
 620+
 621+ # Extract fields from DB into the function scope (integration_rc_xxxx variables)
 622+ // FIXME: Would be good to replace this extract() call with something
 623+ // that explicitly initializes variables.
 624+ extract( $rc->mAttribs );
 625+ $curIdEq = array( 'curid' => $integration_rc_cur_id );
 626+
 627+ # If it's a new day, add the headline and flush the cache
 628+ $date = $wgLang->date( $integration_rc_timestamp, true );
 629+ $ret = '';
 630+ if( $date != $this->lastdate ) {
 631+ # Process current cache
 632+ $ret = $this->recentChangesBlock();
 633+ $this->integration_rc_cache = array();
 634+ $ret .= Xml::element( 'h4', null, $date );
 635+ $this->lastdate = $date;
 636+ }
 637+
 638+ # Should patrol-related stuff be shown?
 639+ if( $wgUser->useRCPatrol() ) {
 640+ $rc->unpatrolled = !$integration_rc_patrolled;
 641+ } else {
 642+ $rc->unpatrolled = false;
 643+ }
 644+
 645+ $showdifflinks = true;
 646+ # Make article link
 647+ // Page moves
 648+ if( $integration_rc_type == RC_MOVE || $integration_rc_type == RC_MOVE_OVER_REDIRECT ) {
 649+ $msg = ( $integration_rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir";
 650+ $clink = wfMsg( $msg, $this->skin->linkKnown( $rc->getTitle(), null,
 651+ array(), array( 'redirect' => 'no' ) ),
 652+ $this->skin->linkKnown( $rc->getMovedToTitle() ) );
 653+ // New unpatrolled pages
 654+ } else if( $rc->unpatrolled && $integration_rc_type == RC_NEW ) {
 655+ $clink = $this->skin->linkKnown( $rc->getTitle(), null, array(),
 656+ array( 'rcid' => $integration_rc_id ) );
 657+ // Log entries
 658+ } else if( $integration_rc_type == RC_LOG ) {
 659+ if( $integration_rc_log_type ) {
 660+ $logtitle = SpecialPage::getTitleFor( 'Log', $integration_rc_log_type );
 661+ $clink = '(' . $this->skin->linkKnown( $logtitle,
 662+ LogPage::logName($integration_rc_log_type) ) . ')';
 663+ } else {
 664+ $clink = $this->skin->link( $rc->getTitle() );
 665+ }
 666+ $watched = false;
 667+ // Log entries (old format) and special pages
 668+ } elseif( $integration_rc_namespace == NS_SPECIAL ) {
 669+ list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $integration_rc_title );
 670+ if ( $specialName == 'Log' ) {
 671+ # Log updates, etc
 672+ $logname = LogPage::logName( $logtype );
 673+ $clink = '(' . $this->skin->linkKnown( $rc->getTitle(), $logname ) . ')';
 674+ } else {
 675+ wfDebug( "Unexpected special page in recentchanges\n" );
 676+ $clink = '';
 677+ }
 678+ // Edits
 679+ } else {
 680+ $clink = $this->skin->linkKnown( $rc->getTitle() );
 681+ }
 682+
 683+ # Don't show unusable diff links
 684+ if ( !InterwikiIntegrationChangesList::userCan($rc,Revision::DELETED_TEXT) ) {
 685+ $showdifflinks = false;
 686+ }
 687+
 688+ $time = $wgLang->time( $integration_rc_timestamp, true, true );
 689+ $rc->watched = $watched;
 690+ $rc->link = $clink;
 691+ $rc->timestamp = $time;
 692+ $rc->numberofWatchingusers = $baseRC->numberofWatchingusers;
 693+
 694+ # Make "cur" and "diff" links. Do not use link(), it is too slow if
 695+ # called too many times (50% of CPU time on RecentChanges!).
 696+ if( $rc->unpatrolled ) {
 697+ $rcIdQuery = array( 'rcid' => $integration_rc_id );
 698+ } else {
 699+ $rcIdQuery = array();
 700+ }
 701+ $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $integration_rc_this_oldid );
 702+ $querydiff = $curIdEq + array( 'diff' => $integration_rc_this_oldid, 'oldid' =>
 703+ $integration_rc_last_oldid ) + $rcIdQuery;
 704+
 705+ if( !$showdifflinks ) {
 706+ $curLink = $this->message['cur'];
 707+ $diffLink = $this->message['diff'];
 708+ } else if( in_array( $integration_rc_type, array(RC_NEW,RC_LOG,RC_MOVE,RC_MOVE_OVER_REDIRECT) ) ) {
 709+ if ( $integration_rc_type != RC_NEW ) {
 710+ $curLink = $this->message['cur'];
 711+ } else {
 712+ $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) );
 713+ $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>";
 714+ }
 715+ $diffLink = $this->message['diff'];
 716+ } else {
 717+ $diffUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querydiff ) );
 718+ $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) );
 719+ $diffLink = "<a href=\"$diffUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['diff']}</a>";
 720+ $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>";
 721+ }
 722+
 723+ # Make "last" link
 724+ if( !$showdifflinks || !$integration_rc_last_oldid ) {
 725+ $lastLink = $this->message['last'];
 726+ } else if( $integration_rc_type == RC_LOG || $integration_rc_type == RC_MOVE || $integration_rc_type == RC_MOVE_OVER_REDIRECT ) {
 727+ $lastLink = $this->message['last'];
 728+ } else {
 729+ $lastLink = $this->skin->linkKnown( $rc->getTitle(), $this->message['last'],
 730+ array(), $curIdEq + array('diff' => $integration_rc_this_oldid, 'oldid' => $integration_rc_last_oldid) + $rcIdQuery );
 731+ }
 732+
 733+ # Make user links
 734+ if( $this->isDeleted($rc,Revision::DELETED_USER) ) {
 735+ $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
 736+ } else {
 737+ $rc->userlink = $this->skin->userLink( $integration_rc_user, $integration_rc_user_text );
 738+ $rc->usertalklink = $this->skin->userToolLinks( $integration_rc_user, $integration_rc_user_text );
 739+ }
 740+
 741+ $rc->lastlink = $lastLink;
 742+ $rc->curlink = $curLink;
 743+ $rc->difflink = $diffLink;
 744+
 745+ # Put accumulated information into the cache, for later display
 746+ # Page moves go on their own line
 747+ $title = $rc->getTitle();
 748+ $secureName = $title->getPrefixedDBkey();
 749+ if( $integration_rc_type == RC_MOVE || $integration_rc_type == RC_MOVE_OVER_REDIRECT ) {
 750+ # Use an @ character to prevent collision with page names
 751+ $this->integration_rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc);
 752+ } else {
 753+ # Logs are grouped by type
 754+ if( $integration_rc_type == RC_LOG ){
 755+ $secureName = SpecialPage::getTitleFor( 'Log', $integration_rc_log_type )->getPrefixedDBkey();
 756+ }
 757+ if( !isset( $this->integration_rc_cache[$secureName] ) ) {
 758+ $this->integration_rc_cache[$secureName] = array();
 759+ }
 760+
 761+ array_push( $this->integration_rc_cache[$secureName], $rc );
 762+ }
 763+
 764+ wfProfileOut( __METHOD__ );
 765+
 766+ return $ret;
 767+ }
 768+
 769+ /**
 770+ * Enhanced RC group
 771+ */
 772+ protected function recentChangesBlockGroup( $block ) {
 773+ global $wgLang, $wgContLang, $wgRCShowChangedSize;
 774+
 775+ wfProfileIn( __METHOD__ );
 776+
 777+ $r = '<table class="mw-enhanced-rc"><tr>';
 778+
 779+ # Collate list of users
 780+ $userlinks = array();
 781+ # Other properties
 782+ $unpatrolled = false;
 783+ $isnew = false;
 784+ $curId = $currentRevision = 0;
 785+ # Some catalyst variables...
 786+ $namehidden = true;
 787+ $allLogs = true;
 788+ foreach( $block as $rcObj ) {
 789+ $oldid = $rcObj->mAttribs['integration_rc_last_oldid'];
 790+ if( $rcObj->mAttribs['integration_rc_new'] ) {
 791+ $isnew = true;
 792+ }
 793+ // If all log actions to this page were hidden, then don't
 794+ // give the name of the affected page for this block!
 795+ if( !$this->isDeleted( $rcObj, LogPage::DELETED_ACTION ) ) {
 796+ $namehidden = false;
 797+ }
 798+ $u = $rcObj->userlink;
 799+ if( !isset( $userlinks[$u] ) ) {
 800+ $userlinks[$u] = 0;
 801+ }
 802+ if( $rcObj->unpatrolled ) {
 803+ $unpatrolled = true;
 804+ }
 805+ if( $rcObj->mAttribs['integration_rc_type'] != RC_LOG ) {
 806+ $allLogs = false;
 807+ }
 808+ # Get the latest entry with a integration_page_id and oldid
 809+ # since logs may not have these.
 810+ if( !$curId && $rcObj->mAttribs['integration_rc_cur_id'] ) {
 811+ $curId = $rcObj->mAttribs['integration_rc_cur_id'];
 812+ }
 813+ if( !$currentRevision && $rcObj->mAttribs['integration_rc_this_oldid'] ) {
 814+ $currentRevision = $rcObj->mAttribs['integration_rc_this_oldid'];
 815+ }
 816+
 817+ $bot = $rcObj->mAttribs['integration_rc_bot'];
 818+ $userlinks[$u]++;
 819+ }
 820+
 821+ # Sort the list and convert to text
 822+ krsort( $userlinks );
 823+ asort( $userlinks );
 824+ $users = array();
 825+ foreach( $userlinks as $userlink => $count) {
 826+ $text = $userlink;
 827+ $text .= $wgContLang->getDirMark();
 828+ if( $count > 1 ) {
 829+ $text .= ' (' . $wgLang->formatNum( $count ) . '×)';
 830+ }
 831+ array_push( $users, $text );
 832+ }
 833+
 834+ $users = ' <span class="changedby">[' .
 835+ implode( $this->message['semicolon-separator'], $users ) . ']</span>';
 836+
 837+ # ID for JS visibility toggle
 838+ $jsid = $this->rcCacheIndex;
 839+ # onclick handler to toggle hidden/expanded
 840+ $toggleLink = "onclick='toggleVisibility($jsid); return false'";
 841+ # Title for <a> tags
 842+ $expandTitle = htmlspecialchars( wfMsg( 'rc-enhanced-expand' ) );
 843+ $closeTitle = htmlspecialchars( wfMsg( 'rc-enhanced-hide' ) );
 844+
 845+ $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>";
 846+ $tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>";
 847+ $r .= '<td class="mw-enhanced-rc">'.$tl.'&#160;';
 848+
 849+ # Main line
 850+ $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&#160;', $bot );
 851+
 852+ # Timestamp
 853+ $r .= '&#160;'.$block[0]->timestamp.'&#160;</td><td style="padding:0px;">';
 854+
 855+ # Article link
 856+ if( $namehidden ) {
 857+ $r .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>';
 858+ } else if( $allLogs ) {
 859+ $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
 860+ } else {
 861+ $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched );
 862+ }
 863+
 864+ $r .= $wgContLang->getDirMark();
 865+
 866+ $queryParams['curid'] = $curId;
 867+ # Changes message
 868+ $n = count($block);
 869+ static $nchanges = array();
 870+ if ( !isset( $nchanges[$n] ) ) {
 871+ $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) );
 872+ }
 873+ # Total change link
 874+ $r .= ' ';
 875+ if( !$allLogs ) {
 876+ $r .= '(';
 877+ if( !InterwikiIntegrationChangesList::userCan( $rcObj, Revision::DELETED_TEXT ) ) {
 878+ $r .= $nchanges[$n];
 879+ } else if( $isnew ) {
 880+ $r .= $nchanges[$n];
 881+ } else {
 882+ $params = $queryParams;
 883+ $params['diff'] = $currentRevision;
 884+ $params['oldid'] = $oldid;
 885+
 886+ $r .= $this->skin->link(
 887+ $block[0]->getTitle(),
 888+ $nchanges[$n],
 889+ array(),
 890+ $params,
 891+ array( 'known', 'noclasses' )
 892+ );
 893+ }
 894+ }
 895+
 896+ # History
 897+ if( $allLogs ) {
 898+ // don't show history link for logs
 899+ } else if( $namehidden || !$block[0]->getTitle()->exists() ) {
 900+ $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')';
 901+ } else {
 902+ $params = $queryParams;
 903+ $params['action'] = 'history';
 904+
 905+ $r .= $this->message['pipe-separator'] .
 906+ $this->skin->link(
 907+ $block[0]->getTitle(),
 908+ $this->message['hist'],
 909+ array(),
 910+ $params,
 911+ array( 'known', 'noclasses' )
 912+ ) . ')';
 913+ }
 914+ $r .= ' . . ';
 915+
 916+ # Character difference (does not apply if only log items)
 917+ if( $wgRCShowChangedSize && !$allLogs ) {
 918+ $last = 0;
 919+ $first = count($block) - 1;
 920+ # Some events (like logs) have an "empty" size, so we need to skip those...
 921+ while( $last < $first && $block[$last]->mAttribs['integration_rc_new_len'] === null ) {
 922+ $last++;
 923+ }
 924+ while( $first > $last && $block[$first]->mAttribs['integration_rc_old_len'] === null ) {
 925+ $first--;
 926+ }
 927+ # Get net change
 928+ $chardiff = $rcObj->getCharacterDifference( $block[$first]->mAttribs['integration_rc_old_len'],
 929+ $block[$last]->mAttribs['integration_rc_new_len'] );
 930+
 931+ if( $chardiff == '' ) {
 932+ $r .= ' ';
 933+ } else {
 934+ $r .= ' ' . $chardiff. ' . . ';
 935+ }
 936+ }
 937+
 938+ $r .= $users;
 939+ $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers);
 940+
 941+ $r .= "</td></tr></table>\n";
 942+
 943+ # Sub-entries
 944+ $r .= '<div id="mw-rc-subentries-'.$jsid.'" class="mw-changeslist-hidden">';
 945+ $r .= '<table class="mw-enhanced-rc">';
 946+ foreach( $block as $rcObj ) {
 947+ # Extract fields from DB into the function scope (integration_rc_xxxx variables)
 948+ // FIXME: Would be good to replace this extract() call with something
 949+ // that explicitly initializes variables.
 950+ # Classes to apply -- TODO implement
 951+ $classes = array();
 952+ extract( $rcObj->mAttribs );
 953+
 954+ #$r .= '<tr><td valign="top">'.$this->spacerArrow();
 955+ $r .= '<tr><td style="vertical-align:top;font-family:monospace; padding:0px;">';
 956+ $r .= $this->spacerIndent() . $this->spacerIndent();
 957+ $r .= $this->recentChangesFlags( $integration_rc_new, $integration_rc_minor, $rcObj->unpatrolled, '&#160;', $integration_rc_bot );
 958+ $r .= '&#160;</td><td style="vertical-align:top; padding:0px;"><span style="font-family:monospace">';
 959+
 960+ $params = $queryParams;
 961+
 962+ if( $integration_rc_this_oldid != 0 ) {
 963+ $params['oldid'] = $integration_rc_this_oldid;
 964+ }
 965+
 966+ # Log timestamp
 967+ if( $integration_rc_type == RC_LOG ) {
 968+ $link = $rcObj->timestamp;
 969+ # Revision link
 970+ } else if( !InterwikiIntegrationChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
 971+ $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> ';
 972+ } else {
 973+ if ( $rcObj->unpatrolled && $integration_rc_type == RC_NEW) {
 974+ $params['rcid'] = $rcObj->mAttribs['integration_rc_id'];
 975+ }
 976+
 977+ $link = $this->skin->link(
 978+ $rcObj->getTitle(),
 979+ $rcObj->timestamp,
 980+ array(),
 981+ $params,
 982+ array( 'known', 'noclasses' )
 983+ );
 984+ if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
 985+ $link = '<span class="history-deleted">'.$link.'</span> ';
 986+ }
 987+ $r .= $link . '</span>';
 988+
 989+ if ( !$integration_rc_type == RC_LOG || $integration_rc_type == RC_NEW ) {
 990+ $r .= ' (';
 991+ $r .= $rcObj->curlink;
 992+ $r .= $this->message['pipe-separator'];
 993+ $r .= $rcObj->lastlink;
 994+ $r .= ')';
 995+ }
 996+ $r .= ' . . ';
 997+
 998+ # Character diff
 999+ if( $wgRCShowChangedSize ) {
 1000+ $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ;
 1001+ }
 1002+ # User links
 1003+ $r .= $rcObj->userlink;
 1004+ $r .= $rcObj->usertalklink;
 1005+ // log action
 1006+ $this->insertAction( $r, $rcObj );
 1007+ // log comment
 1008+ $this->insertComment( $r, $rcObj );
 1009+ # Rollback
 1010+ $this->insertRollback( $r, $rcObj );
 1011+ # Tags
 1012+ $this->insertTags( $r, $rcObj, $classes );
 1013+
 1014+ $r .= "</td></tr>\n";
 1015+ }
 1016+ $r .= "</table></div>\n";
 1017+
 1018+ $this->rcCacheIndex++;
 1019+
 1020+ wfProfileOut( __METHOD__ );
 1021+
 1022+ return $r;
 1023+ }
 1024+
 1025+ /**
 1026+ * Generate HTML for an arrow or placeholder graphic
 1027+ * @param $dir String: one of '', 'd', 'l', 'r'
 1028+ * @param $alt String: text
 1029+ * @param $title String: text
 1030+ * @return String: HTML <img> tag
 1031+ */
 1032+ protected function arrow( $dir, $alt='', $title='' ) {
 1033+ global $wgStylePath;
 1034+ $encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' );
 1035+ $encAlt = htmlspecialchars( $alt );
 1036+ $encTitle = htmlspecialchars( $title );
 1037+ return "<img src=\"$encUrl\" width=\"12\" height=\"12\" alt=\"$encAlt\" title=\"$encTitle\" />";
 1038+ }
 1039+
 1040+ /**
 1041+ * Generate HTML for a right- or left-facing arrow,
 1042+ * depending on language direction.
 1043+ * @return String: HTML <img> tag
 1044+ */
 1045+ protected function sideArrow() {
 1046+ global $wgContLang;
 1047+ $dir = $wgContLang->isRTL() ? 'l' : 'r';
 1048+ return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) );
 1049+ }
 1050+
 1051+ /**
 1052+ * Generate HTML for a down-facing arrow
 1053+ * depending on language direction.
 1054+ * @return String: HTML <img> tag
 1055+ */
 1056+ protected function downArrow() {
 1057+ return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) );
 1058+ }
 1059+
 1060+ /**
 1061+ * Generate HTML for a spacer image
 1062+ * @return String: HTML <img> tag
 1063+ */
 1064+ protected function spacerArrow() {
 1065+ return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space
 1066+ }
 1067+
 1068+ /**
 1069+ * Add a set of spaces
 1070+ * @return String: HTML <td> tag
 1071+ */
 1072+ protected function spacerIndent() {
 1073+ return '&#160;&#160;&#160;&#160;&#160;';
 1074+ }
 1075+
 1076+ /**
 1077+ * Enhanced RC ungrouped line.
 1078+ * @return String: a HTML formated line (generated using $r)
 1079+ */
 1080+ protected function recentChangesBlockLine( $rcObj ) {
 1081+ global $wgRCShowChangedSize;
 1082+
 1083+ wfProfileIn( __METHOD__ );
 1084+
 1085+ # Extract fields from DB into the function scope (integration_rc_xxxx variables)
 1086+ // FIXME: Would be good to replace this extract() call with something
 1087+ // that explicitly initializes variables.
 1088+ $classes = array(); // TODO implement
 1089+ extract( $rcObj->mAttribs );
 1090+ $query['curid'] = $integration_rc_cur_id;
 1091+
 1092+ $r = '<table class="mw-enhanced-rc"><tr>';
 1093+ $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow() . '&#160;';
 1094+ # Flag and Timestamp
 1095+ if( $integration_rc_type == RC_MOVE || $integration_rc_type == RC_MOVE_OVER_REDIRECT ) {
 1096+ $r .= '&#160;&#160;&#160;&#160;'; // 4 flags -> 4 spaces
 1097+ } else {
 1098+ $r .= $this->recentChangesFlags( $integration_rc_type == RC_NEW, $integration_rc_minor, $rcObj->unpatrolled, '&#160;', $integration_rc_bot );
 1099+ }
 1100+ $r .= '&#160;'.$rcObj->timestamp.'&#160;</td><td style="padding:0px;">';
 1101+ # Article or log link
 1102+ if( $integration_rc_log_type ) {
 1103+ $logtitle = Title::newFromText( "Log/$integration_rc_log_type", NS_SPECIAL );
 1104+ $logname = LogPage::logName( $integration_rc_log_type );
 1105+ $r .= '(' . $this->skin->link(
 1106+ $logtitle,
 1107+ $logname,
 1108+ array(),
 1109+ array(),
 1110+ array( 'known', 'noclasses' )
 1111+ ) . ')';
 1112+ } else {
 1113+ $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched );
 1114+ }
 1115+ # Diff and hist links
 1116+ if ( $integration_rc_type != RC_LOG ) {
 1117+ $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator'];
 1118+ $query['action'] = 'history';
 1119+ $r .= $this->skin->link(
 1120+ $rcObj->getTitle(),
 1121+ $this->message['hist'],
 1122+ array(),
 1123+ $query,
 1124+ array( 'known', 'noclasses' )
 1125+ ) . ')';
 1126+ }
 1127+ $r .= ' . . ';
 1128+ # Character diff
 1129+ if( $wgRCShowChangedSize && ($cd = $rcObj->getCharacterDifference()) ) {
 1130+ $r .= "$cd . . ";
 1131+ }
 1132+ # User/talk
 1133+ $r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
 1134+ # Log action (if any)
 1135+ if( $integration_rc_log_type ) {
 1136+ if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) {
 1137+ $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
 1138+ } else {
 1139+ $r .= ' ' . LogPage::actionText( $integration_rc_log_type, $integration_rc_log_action, $rcObj->getTitle(),
 1140+ $this->skin, LogPage::extractParams($integration_rc_params), true, true );
 1141+ }
 1142+ }
 1143+ $this->insertComment( $r, $rcObj );
 1144+ $this->insertRollback( $r, $rcObj );
 1145+ # Tags
 1146+ $this->insertTags( $r, $rcObj, $classes );
 1147+ # Show how many people are watching this if enabled
 1148+ $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
 1149+
 1150+ $r .= "</td></tr></table>\n";
 1151+
 1152+ wfProfileOut( __METHOD__ );
 1153+
 1154+ return $r;
 1155+ }
 1156+
 1157+ /**
 1158+ * If enhanced RC is in use, this function takes the previously cached
 1159+ * RC lines, arranges them, and outputs the HTML
 1160+ */
 1161+ protected function recentChangesBlock() {
 1162+ if( count ( $this->integration_rc_cache ) == 0 ) {
 1163+ return '';
 1164+ }
 1165+
 1166+ wfProfileIn( __METHOD__ );
 1167+
 1168+ $blockOut = '';
 1169+ foreach( $this->integration_rc_cache as $block ) {
 1170+ if( count( $block ) < 2 ) {
 1171+ $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) );
 1172+ } else {
 1173+ $blockOut .= $this->recentChangesBlockGroup( $block );
 1174+ }
 1175+ }
 1176+
 1177+ wfProfileOut( __METHOD__ );
 1178+
 1179+ return '<div>'.$blockOut.'</div>';
 1180+ }
 1181+
 1182+ /**
 1183+ * Returns text for the end of RC
 1184+ * If enhanced RC is in use, returns pretty much all the text
 1185+ */
 1186+ public function endRecentInterwikiIntegrationChangesList() {
 1187+ return $this->recentChangesBlock() . parent::endRecentInterwikiIntegrationChangesList();
 1188+ }
 1189+
 1190+}
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/interwikiintegration-iwlinks.sql
@@ -3,7 +3,7 @@
44 -- Track all interwiki links on all wikis in this farm
55 CREATE TABLE integration_iwlinks (
66 -- integration_dbname of the referring wiki
7 - integration_iwl_from_db varchar(256) binary NOT NULL,
 7+ integration_iwl_from_db varchar(255) binary NOT NULL,
88
99 -- page_id of the referring page
1010 integration_iwl_from int unsigned NOT NULL default 0,
Index: trunk/extensions/InterwikiIntegration/InterwikiIntegrationRecentChange.php
@@ -0,0 +1,709 @@
 2+<?php
 3+
 4+/**
 5+ * Utility class for creating new RC entries
 6+ * mAttribs:
 7+ * integration_rc_id id of the row in the integration_recentchanges table
 8+ * integration_rc_db id/name of the database
 9+ * integration_rc_timestamp time the entry was made
 10+ * integration_rc_cur_time timestamp on the cur row
 11+ * integration_rc_namespace namespace #
 12+ * integration_rc_title non-prefixed db key
 13+ * integration_rc_type is new entry, used to determine whether updating is necessary
 14+ * integration_rc_minor is minor
 15+ * integration_rc_cur_id integration_page_id of associated page entry
 16+ * integration_rc_user user id who made the entry
 17+ * integration_rc_user_text user name who made the entry
 18+ * integration_rc_comment edit summary
 19+ * integration_rc_this_oldid rev_id associated with this entry (or zero)
 20+ * integration_rc_last_oldid rev_id associated with the entry before this one (or zero)
 21+ * integration_rc_bot is bot, hidden
 22+ * integration_rc_ip IP address of the user in dotted quad notation
 23+ * integration_rc_new obsolete, use integration_rc_type==RC_NEW
 24+ * integration_rc_patrolled boolean whether or not someone has marked this edit as patrolled
 25+ * integration_rc_old_len integer byte length of the text before the edit
 26+ * integration_rc_new_len the same after the edit
 27+ * integration_rc_deleted partial deletion
 28+ * integration_rc_logid the log_id value for this log entry (or zero)
 29+ * integration_rc_log_type the log type (or null)
 30+ * integration_rc_log_action the log action (or null)
 31+ * integration_rc_params log params
 32+ *
 33+ * mExtra:
 34+ * prefixedDBkey prefixed db key, used by external app via msg queue
 35+ * lastTimestamp timestamp of previous entry, used in WHERE clause during update
 36+ * lang the interwiki prefix, automatically set in save()
 37+ * oldSize text size before the change
 38+ * newSize text size after the change
 39+ *
 40+ * temporary: not stored in the database
 41+ * notificationtimestamp
 42+ * numberofWatchingusers
 43+ *
 44+ * @todo document functions and variables
 45+ */
 46+class InterwikiIntegrationRecentChange {
 47+ var $mAttribs = array(), $mExtra = array();
 48+ var $mTitle = false, $mMovedToTitle = false;
 49+ var $numberofWatchingusers = 0 ; # Dummy to prevent error message in SpecialRecentchangeslinked
 50+
 51+ # Factory methods
 52+
 53+ public static function newFromRow( $row ) {
 54+ $rc = new InterwikiIntegrationRecentChange;
 55+ $rc->loadFromRow( $row );
 56+ return $rc;
 57+ }
 58+
 59+ public static function newFromCurRow( $row ) {
 60+ $rc = new InterwikiIntegrationRecentChange;
 61+ $rc->loadFromCurRow( $row );
 62+ $rc->notificationtimestamp = false;
 63+ $rc->numberofWatchingusers = false;
 64+ return $rc;
 65+ }
 66+
 67+ /**
 68+ * Obtain the recent change with a given integration_rc_id value
 69+ *
 70+ * @param $rcid integration_rc_id value to retrieve
 71+ * @return InterwikiIntegrationRecentChange
 72+ */
 73+ public static function newFromId( $rcid ) {
 74+ $dbr = wfGetDB( DB_SLAVE );
 75+ $res = $dbr->select( 'integration_recentchanges', '*', array( 'integration_rc_id' => $rcid ), __METHOD__ );
 76+ if( $res && $dbr->numRows( $res ) > 0 ) {
 77+ $row = $dbr->fetchObject( $res );
 78+ $dbr->freeResult( $res );
 79+ return self::newFromRow( $row );
 80+ } else {
 81+ return null;
 82+ }
 83+ }
 84+
 85+ /**
 86+ * Find the first recent change matching some specific conditions
 87+ *
 88+ * @param $conds Array of conditions
 89+ * @param $fname Mixed: override the method name in profiling/logs
 90+ * @return InterwikiIntegrationRecentChange
 91+ */
 92+ public static function newFromConds( $conds, $fname = false ) {
 93+ if( $fname === false )
 94+ $fname = __METHOD__;
 95+ $dbr = wfGetDB( DB_SLAVE );
 96+ $res = $dbr->select(
 97+ 'integration_recentchanges',
 98+ '*',
 99+ $conds,
 100+ $fname
 101+ );
 102+ if( $res instanceof ResultWrapper && $res->numRows() > 0 ) {
 103+ $row = $res->fetchObject();
 104+ $res->free();
 105+ return self::newFromRow( $row );
 106+ }
 107+ return null;
 108+ }
 109+
 110+ # Accessors
 111+
 112+ public function setAttribs( $attribs ) {
 113+ $this->mAttribs = $attribs;
 114+ }
 115+
 116+ public function setExtra( $extra ) {
 117+ $this->mExtra = $extra;
 118+ }
 119+
 120+ public function &getTitle() {
 121+ if( $this->mTitle === false ) {
 122+ $this->mTitle = IntegrationInterwikiTitle::makeTitle( $this->mAttribs['integration_rc_namespace'], $this->mAttribs['integration_rc_title'], '', $this->mAttribs['integration_rc_db'] );
 123+ # Make sure the correct page ID is process cached
 124+ $this->mTitle->resetArticleID( $this->mAttribs['integration_rc_cur_id'] );
 125+ }
 126+ return $this->mTitle;
 127+ }
 128+
 129+ public function getMovedToTitle() {
 130+ if( $this->mMovedToTitle === false ) {
 131+ $this->mMovedToTitle = Title::makeTitle( $this->mAttribs['integration_rc_moved_to_ns'],
 132+ $this->mAttribs['integration_rc_moved_to_title'] );
 133+ }
 134+ return $this->mMovedToTitle;
 135+ }
 136+
 137+ # Writes the data in this object to the database
 138+ public function save() {
 139+ global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPOmitBots;
 140+ $fname = 'RecentChange::save';
 141+
 142+ $dbw = wfGetDB( DB_MASTER );
 143+ if( !is_array($this->mExtra) ) {
 144+ $this->mExtra = array();
 145+ }
 146+ $this->mExtra['lang'] = $wgLocalInterwiki;
 147+
 148+ if( !$wgPutIPinRC ) {
 149+ $this->mAttribs['integration_rc_ip'] = '';
 150+ }
 151+
 152+ # If our database is strict about IP addresses, use NULL instead of an empty string
 153+ if( $dbw->strictIPs() and $this->mAttribs['integration_rc_ip'] == '' ) {
 154+ unset( $this->mAttribs['integration_rc_ip'] );
 155+ }
 156+
 157+ # Fixup database timestamps
 158+ $this->mAttribs['integration_rc_timestamp'] = $dbw->timestamp($this->mAttribs['integration_rc_timestamp']);
 159+ $this->mAttribs['integration_rc_cur_time'] = $dbw->timestamp($this->mAttribs['integration_rc_cur_time']);
 160+ $this->mAttribs['integration_rc_id'] = $dbw->nextSequenceValue( 'integration_recentchanges_integration_rc_id_seq' );
 161+
 162+ ## If we are using foreign keys, an entry of 0 for the integration_page_id will fail, so use NULL
 163+ if( $dbw->cascadingDeletes() and $this->mAttribs['integration_rc_cur_id']==0 ) {
 164+ unset( $this->mAttribs['integration_rc_cur_id'] );
 165+ }
 166+
 167+ # Insert new row
 168+ $dbw->insert( 'integration_recentchanges', $this->mAttribs, $fname );
 169+
 170+ # Set the ID
 171+ $this->mAttribs['integration_rc_id'] = $dbw->insertId();
 172+
 173+ # Notify extensions
 174+ wfRunHooks( 'RecentChange_save', array( &$this ) );
 175+
 176+ # Notify external application via UDP
 177+ if( $wgRC2UDPAddress && ( !$this->mAttribs['integration_rc_bot'] || !$wgRC2UDPOmitBots ) ) {
 178+ self::sendToUDP( $this->getIRCLine() );
 179+ }
 180+
 181+ # E-mail notifications
 182+ global $wgUseEnotif, $wgShowUpdatedMarker, $wgUser;
 183+ if( $wgUseEnotif || $wgShowUpdatedMarker ) {
 184+ // Users
 185+ if( $this->mAttribs['integration_rc_user'] ) {
 186+ $editor = ($wgUser->getId() == $this->mAttribs['integration_rc_user']) ?
 187+ $wgUser : User::newFromID( $this->mAttribs['integration_rc_user'] );
 188+ // Anons
 189+ } else {
 190+ $editor = ($wgUser->getName() == $this->mAttribs['integration_rc_user_text']) ?
 191+ $wgUser : User::newFromName( $this->mAttribs['integration_rc_user_text'], false );
 192+ }
 193+ # FIXME: this would be better as an extension hook
 194+ $enotif = new EmailNotification();
 195+ $title = Title::makeTitle( $this->mAttribs['integration_rc_namespace'], $this->mAttribs['integration_rc_title'] );
 196+ $enotif->notifyOnPageChange( $editor, $title,
 197+ $this->mAttribs['integration_rc_timestamp'],
 198+ $this->mAttribs['integration_rc_comment'],
 199+ $this->mAttribs['integration_rc_minor'],
 200+ $this->mAttribs['integration_rc_last_oldid'] );
 201+ }
 202+ }
 203+
 204+ public function notifyRC2UDP() {
 205+ global $wgRC2UDPAddress, $wgRC2UDPOmitBots;
 206+ # Notify external application via UDP
 207+ if( $wgRC2UDPAddress && ( !$this->mAttribs['integration_rc_bot'] || !$wgRC2UDPOmitBots ) ) {
 208+ self::sendToUDP( $this->getIRCLine() );
 209+ }
 210+ }
 211+
 212+ /**
 213+ * Send some text to UDP
 214+ * @param $line String: text to send
 215+ * @param $prefix String
 216+ * @param $address String: address
 217+ * @return Boolean: success
 218+ */
 219+ public static function sendToUDP( $line, $address = '', $prefix = '' ) {
 220+ global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort;
 221+ # Assume default for standard RC case
 222+ $address = $address ? $address : $wgRC2UDPAddress;
 223+ $prefix = $prefix ? $prefix : $wgRC2UDPPrefix;
 224+ # Notify external application via UDP
 225+ if( $address ) {
 226+ $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
 227+ if( $conn ) {
 228+ $line = $prefix . $line;
 229+ wfDebug( __METHOD__ . ": sending UDP line: $line\n" );
 230+ socket_sendto( $conn, $line, strlen($line), 0, $address, $wgRC2UDPPort );
 231+ socket_close( $conn );
 232+ return true;
 233+ } else {
 234+ wfDebug( __METHOD__ . ": failed to create UDP socket\n" );
 235+ }
 236+ }
 237+ return false;
 238+ }
 239+
 240+ /**
 241+ * Remove newlines, carriage returns and decode html entites
 242+ * @param $text String
 243+ * @return String
 244+ */
 245+ public static function cleanupForIRC( $text ) {
 246+ return Sanitizer::decodeCharReferences( str_replace( array( "\n", "\r" ), array( "", "" ), $text ) );
 247+ }
 248+
 249+ /**
 250+ * Mark a given change as patrolled
 251+ *
 252+ * @param $change Mixed: InterwikiIntegrationRecentChange or corresponding integration_rc_id
 253+ * @param $auto Boolean: for automatic patrol
 254+ * @return See doMarkPatrolled(), or null if $change is not an existing integration_rc_id
 255+ */
 256+ public static function markPatrolled( $change, $auto = false ) {
 257+ $change = $change instanceof InterwikiIntegrationRecentChange
 258+ ? $change
 259+ : InterwikiIntegrationRecentChange::newFromId($change);
 260+ if( !$change instanceof InterwikiIntegrationRecentChange ) {
 261+ return null;
 262+ }
 263+ return $change->doMarkPatrolled( $auto );
 264+ }
 265+
 266+ /**
 267+ * Mark this InterwikiIntegrationRecentChange as patrolled
 268+ *
 269+ * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and 'markedaspatrollederror-noautopatrol' as errors
 270+ * @param $auto Boolean: for automatic patrol
 271+ * @return array of permissions errors, see Title::getUserPermissionsErrors()
 272+ */
 273+ public function doMarkPatrolled( $auto = false ) {
 274+ global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol;
 275+ $errors = array();
 276+ // If recentchanges patrol is disabled, only new pages
 277+ // can be patrolled
 278+ if( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute('integration_rc_type') != RC_NEW ) ) {
 279+ $errors[] = array('rcpatroldisabled');
 280+ }
 281+ // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
 282+ $right = $auto ? 'autopatrol' : 'patrol';
 283+ $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $wgUser ) );
 284+ if( !wfRunHooks('MarkPatrolled', array($this->getAttribute('integration_rc_id'), &$wgUser, false)) ) {
 285+ $errors[] = array('hookaborted');
 286+ }
 287+ // Users without the 'autopatrol' right can't patrol their
 288+ // own revisions
 289+ if( $wgUser->getName() == $this->getAttribute('integration_rc_user_text') && !$wgUser->isAllowed('autopatrol') ) {
 290+ $errors[] = array('markedaspatrollederror-noautopatrol');
 291+ }
 292+ if( $errors ) {
 293+ return $errors;
 294+ }
 295+ // If the change was patrolled already, do nothing
 296+ if( $this->getAttribute('integration_rc_patrolled') ) {
 297+ return array();
 298+ }
 299+ // Actually set the 'patrolled' flag in RC
 300+ $this->reallyMarkPatrolled();
 301+ // Log this patrol event
 302+ PatrolLog::record( $this, $auto );
 303+ wfRunHooks( 'MarkPatrolledComplete', array($this->getAttribute('integration_rc_id'), &$wgUser, false) );
 304+ return array();
 305+ }
 306+
 307+ /**
 308+ * Mark this InterwikiIntegrationRecentChange patrolled, without error checking
 309+ * @return Integer: number of affected rows
 310+ */
 311+ public function reallyMarkPatrolled() {
 312+ $dbw = wfGetDB( DB_MASTER );
 313+ $dbw->update(
 314+ 'integration_recentchanges',
 315+ array(
 316+ 'integration_rc_patrolled' => 1
 317+ ),
 318+ array(
 319+ 'integration_rc_id' => $this->getAttribute('integration_rc_id')
 320+ ),
 321+ __METHOD__
 322+ );
 323+ return $dbw->affectedRows();
 324+ }
 325+
 326+ # Makes an entry in the database corresponding to an edit
 327+ public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId,
 328+ $lastTimestamp, $bot, $ip='', $oldSize=0, $newSize=0, $newId=0, $patrol=0 )
 329+ {
 330+ if( !$ip ) {
 331+ $ip = wfGetIP();
 332+ if( !$ip ) $ip = '';
 333+ }
 334+
 335+ $rc = new InterwikiIntegrationRecentChange;
 336+ $rc->mAttribs = array(
 337+ 'integration_rc_timestamp' => $timestamp,
 338+ 'integration_rc_cur_time' => $timestamp,
 339+ 'integration_rc_namespace' => $title->getNamespace(),
 340+ 'integration_rc_title' => $title->getDBkey(),
 341+ 'integration_rc_type' => RC_EDIT,
 342+ 'integration_rc_minor' => $minor ? 1 : 0,
 343+ 'integration_rc_cur_id' => $title->getArticleID(),
 344+ 'integration_rc_user' => $user->getId(),
 345+ 'integration_rc_user_text' => $user->getName(),
 346+ 'integration_rc_comment' => $comment,
 347+ 'integration_rc_this_oldid' => $newId,
 348+ 'integration_rc_last_oldid' => $oldId,
 349+ 'integration_rc_bot' => $bot ? 1 : 0,
 350+ 'integration_rc_moved_to_ns' => 0,
 351+ 'integration_rc_moved_to_title' => '',
 352+ 'integration_rc_ip' => $ip,
 353+ 'integration_rc_patrolled' => intval($patrol),
 354+ 'integration_rc_new' => 0, # obsolete
 355+ 'integration_rc_old_len' => $oldSize,
 356+ 'integration_rc_new_len' => $newSize,
 357+ 'integration_rc_deleted' => 0,
 358+ 'integration_rc_logid' => 0,
 359+ 'integration_rc_log_type' => null,
 360+ 'integration_rc_log_action' => '',
 361+ 'integration_rc_params' => ''
 362+ );
 363+
 364+ $rc->mExtra = array(
 365+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
 366+ 'lastTimestamp' => $lastTimestamp,
 367+ 'oldSize' => $oldSize,
 368+ 'newSize' => $newSize,
 369+ );
 370+ $rc->save();
 371+ return $rc;
 372+ }
 373+
 374+ /**
 375+ * Makes an entry in the database corresponding to page creation
 376+ * Note: the title object must be loaded with the new id using resetArticleID()
 377+ * @todo Document parameters and return
 378+ */
 379+ public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
 380+ $ip='', $size=0, $newId=0, $patrol=0 )
 381+ {
 382+ if( !$ip ) {
 383+ $ip = wfGetIP();
 384+ if( !$ip ) $ip = '';
 385+ }
 386+
 387+ $rc = new InterwikiIntegrationRecentChange;
 388+ $rc->mAttribs = array(
 389+ 'integration_rc_timestamp' => $timestamp,
 390+ 'integration_rc_cur_time' => $timestamp,
 391+ 'integration_rc_namespace' => $title->getNamespace(),
 392+ 'integration_rc_title' => $title->getDBkey(),
 393+ 'integration_rc_type' => RC_NEW,
 394+ 'integration_rc_minor' => $minor ? 1 : 0,
 395+ 'integration_rc_cur_id' => $title->getArticleID(),
 396+ 'integration_rc_user' => $user->getId(),
 397+ 'integration_rc_user_text' => $user->getName(),
 398+ 'integration_rc_comment' => $comment,
 399+ 'integration_rc_this_oldid' => $newId,
 400+ 'integration_rc_last_oldid' => 0,
 401+ 'integration_rc_bot' => $bot ? 1 : 0,
 402+ 'integration_rc_moved_to_ns' => 0,
 403+ 'integration_rc_moved_to_title' => '',
 404+ 'integration_rc_ip' => $ip,
 405+ 'integration_rc_patrolled' => intval($patrol),
 406+ 'integration_rc_new' => 1, # obsolete
 407+ 'integration_rc_old_len' => 0,
 408+ 'integration_rc_new_len' => $size,
 409+ 'integration_rc_deleted' => 0,
 410+ 'integration_rc_logid' => 0,
 411+ 'integration_rc_log_type' => null,
 412+ 'integration_rc_log_action' => '',
 413+ 'integration_rc_params' => ''
 414+ );
 415+
 416+ $rc->mExtra = array(
 417+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
 418+ 'lastTimestamp' => 0,
 419+ 'oldSize' => 0,
 420+ 'newSize' => $size
 421+ );
 422+ $rc->save();
 423+ return $rc;
 424+ }
 425+
 426+ # Makes an entry in the database corresponding to a rename
 427+ public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
 428+ {
 429+ global $wgRequest;
 430+ if( !$ip ) {
 431+ $ip = wfGetIP();
 432+ if( !$ip ) $ip = '';
 433+ }
 434+
 435+ $rc = new InterwikiIntegrationRecentChange;
 436+ $rc->mAttribs = array(
 437+ 'integration_rc_timestamp' => $timestamp,
 438+ 'integration_rc_cur_time' => $timestamp,
 439+ 'integration_rc_namespace' => $oldTitle->getNamespace(),
 440+ 'integration_rc_title' => $oldTitle->getDBkey(),
 441+ 'integration_rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE,
 442+ 'integration_rc_minor' => 0,
 443+ 'integration_rc_cur_id' => $oldTitle->getArticleID(),
 444+ 'integration_rc_user' => $user->getId(),
 445+ 'integration_rc_user_text' => $user->getName(),
 446+ 'integration_rc_comment' => $comment,
 447+ 'integration_rc_this_oldid' => 0,
 448+ 'integration_rc_last_oldid' => 0,
 449+ 'integration_rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
 450+ 'integration_rc_moved_to_ns' => $newTitle->getNamespace(),
 451+ 'integration_rc_moved_to_title' => $newTitle->getDBkey(),
 452+ 'integration_rc_ip' => $ip,
 453+ 'integration_rc_new' => 0, # obsolete
 454+ 'integration_rc_patrolled' => 1,
 455+ 'integration_rc_old_len' => null,
 456+ 'integration_rc_new_len' => null,
 457+ 'integration_rc_deleted' => 0,
 458+ 'integration_rc_logid' => 0, # notifyMove not used anymore
 459+ 'integration_rc_log_type' => null,
 460+ 'integration_rc_log_action' => '',
 461+ 'integration_rc_params' => ''
 462+ );
 463+
 464+ $rc->mExtra = array(
 465+ 'prefixedDBkey' => $oldTitle->getPrefixedDBkey(),
 466+ 'lastTimestamp' => 0,
 467+ 'prefixedMoveTo' => $newTitle->getPrefixedDBkey()
 468+ );
 469+ $rc->save();
 470+ }
 471+
 472+ public static function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
 473+ InterwikiIntegrationRecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, false );
 474+ }
 475+
 476+ public static function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) {
 477+ InterwikiIntegrationRecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true );
 478+ }
 479+
 480+ public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', $type,
 481+ $action, $target, $logComment, $params, $newId=0 )
 482+ {
 483+ global $wgLogRestrictions;
 484+ # Don't add private logs to RC!
 485+ if( isset($wgLogRestrictions[$type]) && $wgLogRestrictions[$type] != '*' ) {
 486+ return false;
 487+ }
 488+ $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
 489+ $target, $logComment, $params, $newId );
 490+ $rc->save();
 491+ return true;
 492+ }
 493+
 494+ public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip='',
 495+ $type, $action, $target, $logComment, $params, $newId=0 )
 496+ {
 497+ global $wgRequest;
 498+ if( !$ip ) {
 499+ $ip = wfGetIP();
 500+ if( !$ip ) $ip = '';
 501+ }
 502+
 503+ $rc = new InterwikiIntegrationRecentChange;
 504+ $rc->mAttribs = array(
 505+ 'integration_rc_timestamp' => $timestamp,
 506+ 'integration_rc_cur_time' => $timestamp,
 507+ 'integration_rc_namespace' => $target->getNamespace(),
 508+ 'integration_rc_title' => $target->getDBkey(),
 509+ 'integration_rc_type' => RC_LOG,
 510+ 'integration_rc_minor' => 0,
 511+ 'integration_rc_cur_id' => $target->getArticleID(),
 512+ 'integration_rc_user' => $user->getId(),
 513+ 'integration_rc_user_text' => $user->getName(),
 514+ 'integration_rc_comment' => $logComment,
 515+ 'integration_rc_this_oldid' => 0,
 516+ 'integration_rc_last_oldid' => 0,
 517+ 'integration_rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
 518+ 'integration_rc_moved_to_ns' => 0,
 519+ 'integration_rc_moved_to_title' => '',
 520+ 'integration_rc_ip' => $ip,
 521+ 'integration_rc_patrolled' => 1,
 522+ 'integration_rc_new' => 0, # obsolete
 523+ 'integration_rc_old_len' => null,
 524+ 'integration_rc_new_len' => null,
 525+ 'integration_rc_deleted' => 0,
 526+ 'integration_rc_logid' => $newId,
 527+ 'integration_rc_log_type' => $type,
 528+ 'integration_rc_log_action' => $action,
 529+ 'integration_rc_params' => $params
 530+ );
 531+ $rc->mExtra = array(
 532+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
 533+ 'lastTimestamp' => 0,
 534+ 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
 535+ );
 536+ return $rc;
 537+ }
 538+
 539+ # Initialises the members of this object from a mysql row object
 540+ public function loadFromRow( $row ) {
 541+ $this->mAttribs = get_object_vars( $row );
 542+ $this->mAttribs['integration_rc_timestamp'] = wfTimestamp(TS_MW, $this->mAttribs['integration_rc_timestamp']);
 543+ $this->mAttribs['integration_rc_deleted'] = $row->integration_rc_deleted; // MUST be set
 544+ }
 545+
 546+ # Makes a pseudo-RC entry from a cur row
 547+ public function loadFromCurRow( $row ) {
 548+ $this->mAttribs = array(
 549+ 'integration_rc_timestamp' => wfTimestamp(TS_MW, $row->rev_timestamp),
 550+ 'integration_rc_cur_time' => $row->rev_timestamp,
 551+ 'integration_rc_user' => $row->rev_user,
 552+ 'integration_rc_user_text' => $row->rev_user_text,
 553+ 'integration_rc_namespace' => $row->integration_page_namespace,
 554+ 'integration_rc_title' => $row->integration_page_title,
 555+ 'integration_rc_comment' => $row->rev_comment,
 556+ 'integration_rc_minor' => $row->rev_minor_edit ? 1 : 0,
 557+ 'integration_rc_type' => $row->integration_page_is_new ? RC_NEW : RC_EDIT,
 558+ 'integration_rc_cur_id' => $row->integration_page_id,
 559+ 'integration_rc_this_oldid' => $row->rev_id,
 560+ 'integration_rc_last_oldid' => isset($row->integration_rc_last_oldid) ? $row->integration_rc_last_oldid : 0,
 561+ 'integration_rc_bot' => 0,
 562+ 'integration_rc_moved_to_ns' => 0,
 563+ 'integration_rc_moved_to_title' => '',
 564+ 'integration_rc_ip' => '',
 565+ 'integration_rc_id' => $row->integration_rc_id,
 566+ 'integration_rc_db' => $row->integration_rc_db,
 567+ 'integration_rc_patrolled' => $row->integration_rc_patrolled,
 568+ 'integration_rc_new' => $row->integration_page_is_new, # obsolete
 569+ 'integration_rc_old_len' => $row->integration_rc_old_len,
 570+ 'integration_rc_new_len' => $row->integration_rc_new_len,
 571+ 'integration_rc_params' => isset($row->integration_rc_params) ? $row->integration_rc_params : '',
 572+ 'integration_rc_log_type' => isset($row->integration_rc_log_type) ? $row->integration_rc_log_type : null,
 573+ 'integration_rc_log_action' => isset($row->integration_rc_log_action) ? $row->integration_rc_log_action : null,
 574+ 'integration_rc_log_id' => isset($row->integration_rc_log_id) ? $row->integration_rc_log_id: 0,
 575+ 'integration_rc_deleted' => $row->integration_rc_deleted // MUST be set
 576+ );
 577+ }
 578+
 579+ /**
 580+ * Get an attribute value
 581+ *
 582+ * @param $name Attribute name
 583+ * @return mixed
 584+ */
 585+ public function getAttribute( $name ) {
 586+ return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
 587+ }
 588+
 589+ public function getAttributes() {
 590+ return $this->mAttribs;
 591+ }
 592+
 593+ /**
 594+ * Gets the end part of the diff URL associated with this object
 595+ * Blank if no diff link should be displayed
 596+ */
 597+ public function diffLinkTrail( $forceCur ) {
 598+ if( $this->mAttribs['integration_rc_type'] == RC_EDIT ) {
 599+ $trail = "curid=" . (int)($this->mAttribs['integration_rc_cur_id']) .
 600+ "&oldid=" . (int)($this->mAttribs['integration_rc_last_oldid']);
 601+ if( $forceCur ) {
 602+ $trail .= '&diff=0' ;
 603+ } else {
 604+ $trail .= '&diff=' . (int)($this->mAttribs['integration_rc_this_oldid']);
 605+ }
 606+ } else {
 607+ $trail = '';
 608+ }
 609+ return $trail;
 610+ }
 611+
 612+ public function getIRCLine() {
 613+ global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki;
 614+
 615+ // FIXME: Would be good to replace these 2 extract() calls with something more explicit
 616+ // e.g. list ($integration_rc_type, $integration_rc_id) = array_values ($this->mAttribs); [or something like that]
 617+ extract($this->mAttribs);
 618+ extract($this->mExtra);
 619+
 620+ if( $integration_rc_type == RC_LOG ) {
 621+ $titleObj = Title::newFromText( "Log/$integration_rc_log_type", NS_SPECIAL );
 622+ } else {
 623+ $titleObj =& $this->getTitle();
 624+ }
 625+ $title = $titleObj->getPrefixedText();
 626+ $title = self::cleanupForIRC( $title );
 627+
 628+ if( $integration_rc_type == RC_LOG ) {
 629+ $url = '';
 630+ } else {
 631+ if( $integration_rc_type == RC_NEW ) {
 632+ $url = "oldid=$integration_rc_this_oldid";
 633+ } else {
 634+ $url = "diff=$integration_rc_this_oldid&oldid=$integration_rc_last_oldid";
 635+ }
 636+ if( $wgUseRCPatrol || ($integration_rc_type == RC_NEW && $wgUseNPPatrol) ) {
 637+ $url .= "&rcid=$integration_rc_id";
 638+ }
 639+ // XXX: *HACK* this should use getFullURL(), hacked for SSL madness --brion 2005-12-26
 640+ // XXX: *HACK^2* the preg_replace() undoes much of what getInternalURL() does, but we
 641+ // XXX: need to call it so that URL paths on the Wikimedia secure server can be fixed
 642+ // XXX: by a custom GetInternalURL hook --vyznev 2008-12-10
 643+ $url = preg_replace( '/title=[^&]*&/', '', $titleObj->getInternalURL( $url ) );
 644+ }
 645+
 646+ if( isset( $oldSize ) && isset( $newSize ) ) {
 647+ $szdiff = $newSize - $oldSize;
 648+ if($szdiff < -500) {
 649+ $szdiff = "\002$szdiff\002";
 650+ } elseif($szdiff >= 0) {
 651+ $szdiff = '+' . $szdiff ;
 652+ }
 653+ $szdiff = '(' . $szdiff . ')' ;
 654+ } else {
 655+ $szdiff = '';
 656+ }
 657+
 658+ $user = self::cleanupForIRC( $integration_rc_user_text );
 659+
 660+ if( $integration_rc_type == RC_LOG ) {
 661+ $targetText = $this->getTitle()->getPrefixedText();
 662+ $comment = self::cleanupForIRC( str_replace("[[$targetText]]","[[\00302$targetText\00310]]",$actionComment) );
 663+ $flag = $integration_rc_log_action;
 664+ } else {
 665+ $comment = self::cleanupForIRC( $integration_rc_comment );
 666+ $flag = '';
 667+ if( !$integration_rc_patrolled && ($wgUseRCPatrol || $integration_rc_new && $wgUseNPPatrol) ) {
 668+ $flag .= '!';
 669+ }
 670+ $flag .= ($integration_rc_new ? "N" : "") . ($integration_rc_minor ? "M" : "") . ($integration_rc_bot ? "B" : "");
 671+ }
 672+
 673+ if ( $wgRC2UDPInterwikiPrefix === true ) {
 674+ $prefix = $wgLocalInterwiki;
 675+ } elseif ( $wgRC2UDPInterwikiPrefix ) {
 676+ $prefix = $wgRC2UDPInterwikiPrefix;
 677+ } else {
 678+ $prefix = false;
 679+ }
 680+ if ( $prefix !== false ) {
 681+ $titleString = "\00314[[\00303$prefix:\00307$title\00314]]";
 682+ } else {
 683+ $titleString = "\00314[[\00307$title\00314]]";
 684+ }
 685+
 686+ # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
 687+ # no colour (\003) switches back to the term default
 688+ $fullString = "$titleString\0034 $flag\00310 " .
 689+ "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
 690+
 691+ return $fullString;
 692+ }
 693+
 694+ /**
 695+ * Returns the change size (HTML).
 696+ * The lengths can be given optionally.
 697+ */
 698+ public function getCharacterDifference( $old = 0, $new = 0 ) {
 699+ if( $old === 0 ) {
 700+ $old = $this->mAttribs['integration_rc_old_len'];
 701+ }
 702+ if( $new === 0 ) {
 703+ $new = $this->mAttribs['integration_rc_new_len'];
 704+ }
 705+ if( $old === null || $new === null ) {
 706+ return '';
 707+ }
 708+ return InterwikiIntegrationChangesList::showCharacterDifference( $old, $new );
 709+ }
 710+}
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/interwikiintegration-watchlist.sql
@@ -4,7 +4,7 @@
55 -- Key to user.user_id
66 integration_wl_user int unsigned NOT NULL,
77 -- Database name of the wiki
8 - integration_wl_db varchar(256) binary NOT NULL,
 8+ integration_wl_db varchar(255) binary NOT NULL,
99 -- Key to page_namespace
1010 integration_wl_namespace int NOT NULL default 0,
1111 -- Key to page_title
@@ -14,7 +14,7 @@
1515 integration_wl_notificationtimestamp varbinary(14)
1616 );
1717
18 -CREATE UNIQUE INDEX integration_wl_user ON integration_watchlist (integration_wl_user, integration_wl_namespace, integration_wl_title);
19 -CREATE INDEX integration_namespace_title ON integration_watchlist (integration_wl_namespace, integration_wl_title);
 18+CREATE UNIQUE INDEX integration_wl_user ON integration_watchlist (integration_wl_user, integration_wl_namespace, integration_wl_title, integration_wl_db );
 19+CREATE INDEX integration_namespace_title ON integration_watchlist (integration_wl_namespace, integration_wl_title );
2020
2121 COMMIT;
Index: trunk/extensions/InterwikiIntegration/InterwikiIntegration.body.php
@@ -0,0 +1,98 @@
 2+<?php
 3+class InterwikiIntegrationFunctions {
 4+ public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
 5+ &$join_conds, &$options, $filter_tag = false ) {
 6+ global $wgRequest, $wgUseTagFilter;
 7+
 8+ if( $filter_tag === false ) {
 9+ $filter_tag = $wgRequest->getVal( 'tagfilter' );
 10+ }
 11+
 12+ // Figure out which conditions can be done.
 13+ $join_field = '';
 14+ if ( in_array( 'integration_recentchanges', $tables ) ) {
 15+ $join_cond = 'rc_id';
 16+ $join_cond2 = 'integration_rc_id';
 17+ } elseif( in_array( 'integration_watchlist', $tables ) ) {
 18+ $join_cond = 'wl_id';
 19+ $join_cond2 = 'integration_wl_id';
 20+ } elseif( in_array( 'logging', $tables ) ) {
 21+ $join_cond = 'log_id';
 22+ } elseif ( in_array( 'revision', $tables ) ) {
 23+ $join_cond = 'rev_id';
 24+ } else {
 25+ throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
 26+ }
 27+
 28+ // JOIN on tag_summary
 29+ $tables[] = 'tag_summary';
 30+ if ( !isset ( $join_cond )) {
 31+ $join_cond2 = $join_cond;
 32+ }
 33+ $join_conds['tag_summary'] = array( 'LEFT JOIN', "ts_$join_cond=$join_cond2" );
 34+ $fields[] = 'ts_tags';
 35+
 36+ if( $wgUseTagFilter && $filter_tag ) {
 37+ // Somebody wants to filter on a tag.
 38+ // Add an INNER JOIN on change_tag
 39+
 40+ // FORCE INDEX -- change_tags will almost ALWAYS be the correct query plan.
 41+ global $wgOldChangeTagsIndex;
 42+ $index = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
 43+ $options['USE INDEX'] = array( 'change_tag' => $index );
 44+ unset( $options['FORCE INDEX'] );
 45+ $tables[] = 'change_tag';
 46+ $join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" );
 47+ $conds['ct_tag'] = $filter_tag;
 48+ }
 49+ }
 50+}
 51+
 52+class IntegrationInterwikiTitle extends Title {
 53+ /**
 54+ * Create a new Title from a namespace index and a DB key.
 55+ * It's assumed that $ns and $title are *valid*, for instance when
 56+ * they came directly from the database or a special page name.
 57+ * For convenience, spaces are converted to underscores so that
 58+ * eg user_text fields can be used directly.
 59+ *
 60+ * @param $ns \type{\int} the namespace of the article
 61+ * @param $title \type{\string} the unprefixed database key form
 62+ * @param $fragment \type{\string} The link fragment (after the "#")
 63+ * @return \type{Title} the new object
 64+ */
 65+ public static function &makeTitle( $ns, $title, $fragment = '', $database = '', $interwiki = '' ) {
 66+ global $wgInterwikiIntegrationPrefix, $wgDBname;
 67+ if ( $database != '' && $database != $wgDBname ) {
 68+ $prefixes = array_keys ( $wgInterwikiIntegrationPrefix, $database );
 69+ if ( $prefixes ) {
 70+ $interwiki = $prefixes[0];
 71+ }
 72+ if ( $ns ) {
 73+ $dbr = wfGetDB( DB_SLAVE );
 74+ $namespaceResult = $dbr->selectRow (
 75+ 'integration_namespace',
 76+ 'integration_namespace_title',
 77+ array (
 78+ 'integration_dbname' => $database,
 79+ 'integration_namespace_index' => $ns
 80+ )
 81+ );
 82+ if ( $namespaceResult ) {
 83+ $nsTitle = $namespaceResult->integration_namespace_title;
 84+ $title = $nsTitle . ':' . $title;
 85+ }
 86+ $ns = 0;
 87+ }
 88+ }
 89+ $t = new Title();
 90+ $t->mInterwiki = ucfirst ( $interwiki );
 91+ $t->mFragment = $fragment;
 92+ $t->mNamespace = $ns = intval( $ns );
 93+ $t->mDbkeyform = str_replace( ' ', '_', $title );
 94+ $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
 95+ $t->mUrlform = wfUrlencode( $t->mDbkeyform );
 96+ $t->mTextform = str_replace( '_', ' ', $title );
 97+ return $t;
 98+ }
 99+}
\ No newline at end of file
Index: trunk/extensions/InterwikiIntegration/InterwikiIntegration.i18n.php
@@ -6,25 +6,29 @@
77
88 $messages = array();
99
10 -/** English
 10+/* English
1111 * @author Tisane
1212 */
1313 $messages['en'] = array(
14 - 'interwikiintegration' => 'Interwiki integration',
15 - 'interwikiintegration-desc' => 'Comprehensive interwiki integration',
16 - 'populateinterwikiintegrationtable' => 'Populate interwiki integration table',
17 - 'interwikiintegration-setuptext' => "The tables for $1 have been configured.
18 -Be sure to configure the tables on your other wikis as well.",
19 - 'right-integration' => 'Populate the interwiki integration table',
 14+ 'integration' => 'InterwikiIntegration',
 15+ 'integration-desc' => 'Comprehensive interwiki integration',
 16+ 'populateinterwikiintegrationtable' => 'Populate interwiki integration table',
 17+ 'populateinterwikiwatchlisttable' => 'Populate interwiki watchlist table',
 18+ 'populateinterwikirecentchangestable' => 'Populate interwiki recent changes table',
 19+ 'populateinterwikipagetable' => 'Populate interwiki page table',
 20+ 'integration-setuptext' => '$1\'s tables have been configured. Be sure '
 21+ .'to configure the tables on your other wikis as well.',
 22+ 'interwikiwatchlist-setuptext' => 'Interwiki watchlist tables have been '
 23+ .'populated.',
 24+ 'interwikirecentchanges-setuptext' => 'Interwiki recentchanges tables have been '
 25+ .'populated.',
 26+ 'interwikipage-setuptext' => 'Interwiki page tables have been '
 27+ .'populated.',
 28+ 'interwikiwatchlist' => 'Interwiki watchlist',
 29+ 'interwikirecentchanges' => 'Interwiki recent changes'
2030 );
2131
22 -/** Message documentation (Message documentation)
23 - * @author Siebrand
24 - */
25 -$messages['qqq'] = array(
26 - 'interwikiintegration-setuptext' => 'Parameters:
27 -* $1 is the site name',
28 -);
 32+$aliases = array();
2933
3034 /** Belarusian (Taraškievica orthography) (Беларуская (тарашкевіца))
3135 * @author EugeneZelenko
Index: trunk/extensions/InterwikiIntegration/interwikiintegration-recentchanges.sql
@@ -12,7 +12,7 @@
1313 integration_rc_id int NOT NULL,
1414
1515 -- Database name of the wiki
16 - integration_rc_db varchar(256) binary NOT NULL,
 16+ integration_rc_db varchar(255) binary NOT NULL,
1717
1818 integration_rc_timestamp varbinary(14) NOT NULL default '',
1919 integration_rc_cur_time varbinary(14) NOT NULL default '',

Follow-up revisions

RevisionCommit summaryAuthorDate
r68442Revert i18n regression from r68422tisane05:00, 23 June 2010

Comments

#Comment by Raymond (talk | contribs)   19:03, 22 June 2010

It seems you have overwritten changes from r67632 (i18n changes). Please re-add them for proper i18n. I have disabled your extension in Translatewiki for the moment.

#Comment by Bryan (talk | contribs)   20:18, 22 June 2010

I would really advise separating backend and frontend logic instead of dumping everything in one big execute() method.

#Comment by Tisane (talk | contribs)   05:00, 23 June 2010

@Raymond: Reverted.

#Comment by Tisane (talk | contribs)   05:01, 23 June 2010

@Raymond: (in r68442)

#Comment by Tisane (talk | contribs)   05:11, 23 June 2010

Correction, r68443.

#Comment by Raymond (talk | contribs)   05:41, 23 June 2010

Sorry, that is not enough. My r67632 touched a lot more files to fix i18n issues, i.e. the definition of the alias file, the -desc message and changed of message keys.

Furthermore your revert r68443 brokes all UTF-8 characters totally.

#Comment by Tisane (talk | contribs)   03:19, 24 June 2010

I am going to revert all the files to r68371. For future reference, is there a way to revert everything back to a previous version without downloading all the old files one by one? svn merge -r68443:68371 didn't seem to work.

#Comment by 😂 (talk | contribs)   15:36, 14 December 2010

i18n issues has been resolved, marking deferred.

Status & tagging log