<?php
+
/**
* This is to display changes made to all articles linked in an article.
- * @file
* @ingroup SpecialPage
*/
+class SpecialRecentchangeslinked extends SpecialRecentchanges {
-require_once( 'SpecialRecentchanges.php' );
-
-/**
- * Entrypoint
- * @param string $par parent page we will look at
- */
-function wfSpecialRecentchangeslinked( $par = NULL ) {
- global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgTitle, $wgScript;
-
- $days = $wgRequest->getInt( 'days' );
- $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
- $hideminor = $wgRequest->getBool( 'hideminor' ) ? 1 : 0;
- $showlinkedto = $wgRequest->getBool( 'showlinkedto' ) ? 1 : 0;
-
- $title = Title::newFromURL( $target );
- $target = $title ? $title->getPrefixedText() : '';
-
- $wgOut->setPagetitle( wfMsg( 'recentchangeslinked' ) );
- $sk = $wgUser->getSkin();
-
- $wgOut->addHTML(
- Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', array(), wfMsg( 'recentchangeslinked' ) ) . "\n" .
- Xml::inputLabel( wfMsg( 'recentchangeslinked-page' ), 'target', 'recentchangeslinked-target', 40, $target ) .
- " <span style='white-space: nowrap'>" .
- Xml::check( 'showlinkedto', $showlinkedto, array('id' => 'showlinkedto') ) . ' ' .
- Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) .
- "</span><br/>\n" .
- Xml::hidden( 'title', $wgTitle->getPrefixedText() ). "\n" .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' ) . "\n"
- );
-
- if ( !$target ) {
- return;
- }
- $nt = Title::newFromURL( $target );
- if( !$nt ) {
- $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
- return;
+ function __construct(){
+ SpecialPage::SpecialPage( 'Recentchangeslinked' );
+ $this->includable( true );
}
- $id = $nt->getArticleId();
- $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) );
- $wgOut->setSyndicated();
- $wgOut->setFeedAppendQuery( "target=" . urlencode( $target ) );
+ public function getDefaultOptions() {
+ $opts = parent::getDefaultOptions();
+ $opts->add( 'target', '' );
+ $opts->add( 'showlinkedto', false );
+ $opts->add( 'tagfilter', '' );
+ return $opts;
+ }
- if ( !$days ) {
- $days = (int)$wgUser->getOption( 'rcdays', 7 );
+ public function parseParameters( $par, FormOptions $opts ) {
+ $opts['target'] = $par;
}
- list( $limit, /* offset */ ) = wfCheckLimits( 100, 'rclimit' );
-
- $dbr = wfGetDB( DB_SLAVE,'recentchangeslinked' );
- $cutoff = $dbr->timestamp( time() - ( $days * 86400 ) );
-
- $hideminor = ($hideminor ? 1 : 0);
- if ( $hideminor ) {
- $mlink = $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchangeslinked' ),
- wfMsg( 'show' ), 'target=' . htmlspecialchars( $nt->getPrefixedURL() ) .
- "&days={$days}&limit={$limit}&hideminor=0&showlinkedto={$showlinkedto}" );
- } else {
- $mlink = $sk->makeKnownLink( $wgContLang->specialPage( "Recentchangeslinked" ),
- wfMsg( "hide" ), "target=" . htmlspecialchars( $nt->getPrefixedURL() ) .
- "&days={$days}&limit={$limit}&hideminor=1&showlinkedto={$showlinkedto}" );
+
+ public function feedSetup() {
+ global $wgRequest;
+ $opts = parent::feedSetup();
+ # Feed is cached on limit,hideminor,target; other params would randomly not work
+ $opts['target'] = $wgRequest->getVal( 'target' );
+ return $opts;
}
- if ( $hideminor ) {
- $cmq = 'AND rc_minor=0';
- } else { $cmq = ''; }
-
- list($recentchanges, $categorylinks, $pagelinks, $watchlist) =
- $dbr->tableNamesN( 'recentchanges', 'categorylinks', 'pagelinks', "watchlist" );
-
- $uid = $wgUser->getId();
- // The fields we are selecting
- $fields = "rc_cur_id,rc_namespace,rc_title,
- rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor,rc_log_type,rc_log_action,rc_params,rc_deleted,
- rc_new, rc_id, rc_this_oldid, rc_last_oldid, rc_bot, rc_patrolled, rc_type, rc_old_len, rc_new_len";
- $fields .= $uid ? ",wl_user" : "";
-
- // Check if this should be a feed
-
- $feed = false;
- global $wgFeedLimit;
- $feedFormat = $wgRequest->getVal( 'feed' );
- if( $feedFormat ) {
+
+ public function getFeedObject( $feedFormat ){
$feed = new ChangesFeed( $feedFormat, false );
- # Sanity check
- if( $limit > $wgFeedLimit ) {
- $limit = $wgFeedLimit;
- }
+ $feedObj = $feed->getFeedObject(
+ wfMsgForContent( 'recentchangeslinked-title', $this->mTargetTitle->getPrefixedText() ),
+ wfMsgForContent( 'recentchangeslinked-feed' )
+ );
+ return array( $feed, $feedObj );
}
- // If target is a Category, use categorylinks and invert from and to
- if( $nt->getNamespace() == NS_CATEGORY ) {
- $catkey = $dbr->addQuotes( $nt->getDBkey() );
- # The table clauses
- $tables = "$categorylinks, $recentchanges";
- $tables .= $uid ? " LEFT JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "";
-
- $sql = "SELECT /* wfSpecialRecentchangeslinked */ $fields FROM $tables
- WHERE rc_timestamp > '{$cutoff}' {$cmq}
- AND cl_from=rc_cur_id
- AND cl_to=$catkey
- GROUP BY $fields ORDER BY rc_timestamp DESC LIMIT {$limit}"; // Shitty-ass GROUP BY by for postgres apparently
- } else {
- if( $showlinkedto ) {
- $ns = $dbr->addQuotes( $nt->getNamespace() );
- $dbkey = $dbr->addQuotes( $nt->getDBkey() );
- $joinConds = "AND pl_namespace={$ns} AND pl_title={$dbkey} AND pl_from=rc_cur_id";
+ public function doMainQuery( $conds, $opts ) {
+ global $wgUser, $wgOut;
+
+ $target = $opts['target'];
+ $showlinkedto = $opts['showlinkedto'];
+ $limit = $opts['limit'];
+
+ if ( $target === '' ) {
+ return false;
+ }
+ $title = Title::newFromURL( $target );
+ if( !$title || $title->getInterwiki() != '' ){
+ $wgOut->wrapWikiMsg( '<div class="errorbox">$1</div><br clear="both" />', 'allpagesbadtitle' );
+ return false;
+ }
+ $this->mTargetTitle = $title;
+
+ $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $title->getPrefixedText() ) );
+
+ /*
+ * Ordinary links are in the pagelinks table, while transclusions are
+ * in the templatelinks table, categorizations in categorylinks and
+ * image use in imagelinks. We need to somehow combine all these.
+ * Special:Whatlinkshere does this by firing multiple queries and
+ * merging the results, but the code we inherit from our parent class
+ * expects only one result set so we use UNION instead.
+ */
+
+ $dbr = wfGetDB( DB_SLAVE, 'recentchangeslinked' );
+ $id = $title->getArticleId();
+ $ns = $title->getNamespace();
+ $dbkey = $title->getDBkey();
+
+ $tables = array( 'recentchanges' );
+ $select = array( $dbr->tableName( 'recentchanges' ) . '.*' );
+ $join_conds = array();
+ $query_options = array();
+
+ // left join with watchlist table to highlight watched rows
+ if( $uid = $wgUser->getId() ) {
+ $tables[] = 'watchlist';
+ $select[] = 'wl_user';
+ $join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" );
+ }
+ if ( $wgUser->isAllowed( 'rollback' ) ) {
+ $tables[] = 'page';
+ $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id');
+ $select[] = 'page_latest';
+ }
+
+ ChangeTags::modifyDisplayQuery( $tables, $select, $conds, $join_conds,
+ $query_options, $opts['tagfilter'] );
+
+ // XXX: parent class does this, should we too?
+ // wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
+
+ if( $ns == NS_CATEGORY && !$showlinkedto ) {
+ // special handling for categories
+ // XXX: should try to make this less klugy
+ $link_tables = array( 'categorylinks' );
+ $showlinkedto = true;
} else {
- $joinConds = "AND pl_namespace=rc_namespace AND pl_title=rc_title AND pl_from=$id";
+ // for now, always join on these tables; really should be configurable as in whatlinkshere
+ $link_tables = array( 'pagelinks', 'templatelinks' );
+ // imagelinks only contains links to pages in NS_FILE
+ if( $ns == NS_FILE || !$showlinkedto ) $link_tables[] = 'imagelinks';
}
- # The table clauses
- $tables = "$pagelinks, $recentchanges";
- $tables .= $uid ? " LEFT JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "";
-
- $sql = "SELECT /* wfSpecialRecentchangeslinked */ $fields FROM $tables
- WHERE rc_timestamp > '{$cutoff}' {$cmq}
- {$joinConds}
- GROUP BY $fields ORDER BY rc_timestamp DESC LIMIT {$limit}"; // Shitty-ass GROUP BY by for postgres apparently
- }
- # Actually do the query
- $res = $dbr->query( $sql, __METHOD__ );
- $count = $dbr->numRows( $res );
- $rchanges = array();
- # Output feeds now and be done with it!
- if( $feed ) {
- if( $count ) {
- $counter = 1;
- while ( $limit ) {
- if ( 0 == $count ) { break; }
- $obj = $dbr->fetchObject( $res );
- --$count;
- $rc = RecentChange::newFromRow( $obj );
- $rc->counter = $counter++;
- --$limit;
- $rchanges[] = $obj;
+
+ if( $id == 0 && !$showlinkedto )
+ return false; // nonexistent pages can't link to any pages
+
+ // field name prefixes for all the various tables we might want to join with
+ $prefix = array( 'pagelinks' => 'pl', 'templatelinks' => 'tl', 'categorylinks' => 'cl', 'imagelinks' => 'il' );
+
+ $subsql = array(); // SELECT statements to combine with UNION
+
+ foreach( $link_tables as $link_table ) {
+ $pfx = $prefix[$link_table];
+
+ // imagelinks and categorylinks tables have no xx_namespace field, and have xx_to instead of xx_title
+ if( $link_table == 'imagelinks' ) $link_ns = NS_FILE;
+ else if( $link_table == 'categorylinks' ) $link_ns = NS_CATEGORY;
+ else $link_ns = 0;
+
+ if( $showlinkedto ) {
+ // find changes to pages linking to this page
+ if( $link_ns ) {
+ if( $ns != $link_ns ) continue; // should never happen, but check anyway
+ $subconds = array( "{$pfx}_to" => $dbkey );
+ } else {
+ $subconds = array( "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey );
+ }
+ $subjoin = "rc_cur_id = {$pfx}_from";
+ } else {
+ // find changes to pages linked from this page
+ $subconds = array( "{$pfx}_from" => $id );
+ if( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
+ $subconds["rc_namespace"] = $link_ns;
+ $subjoin = "rc_title = {$pfx}_to";
+ } else {
+ $subjoin = "rc_namespace = {$pfx}_namespace AND rc_title = {$pfx}_title";
+ }
}
+
+ $subsql[] = $dbr->selectSQLText(
+ array_merge( $tables, array( $link_table ) ),
+ $select,
+ $conds + $subconds,
+ __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) + $query_options,
+ $join_conds + array( $link_table => array( 'INNER JOIN', $subjoin ) )
+ );
}
- $wgOut->disable();
- $feedObj = $feed->getFeedObject(
- wfMsgForContent( 'recentchangeslinked-title', $nt->getPrefixedText() ),
- wfMsgForContent( 'recentchangeslinked' )
- );
- ChangesFeed::generateFeed( $rchanges, $feedObj );
- return;
+ if( count($subsql) == 0 )
+ return false; // should never happen
+ if( count($subsql) == 1 )
+ $sql = $subsql[0];
+ else {
+ // need to resort and relimit after union
+ $sql = $dbr->unionQueries($subsql, false).' ORDER BY rc_timestamp DESC';
+ $sql = $dbr->limitResult($sql, $limit, false);
+ }
+
+ $res = $dbr->query( $sql, __METHOD__ );
+
+ if( $res->numRows() == 0 )
+ $this->mResultEmpty = true;
+
+ return $res;
}
- # Otherwise, carry on with regular output...
- $wgOut->addHTML("< ".$sk->makeLinkObj($nt, "", "redirect=no" )."<br />\n");
- $note = wfMsgExt( "rcnote", array ( 'parseinline' ), $limit, $days, $wgLang->timeAndDate( wfTimestampNow(), true ) );
- $wgOut->addHTML( "<hr />\n{$note}\n<br />" );
-
- $note = rcDayLimitlinks( $days, $limit, "Recentchangeslinked",
- "target=" . $nt->getPrefixedURL() . "&hideminor={$hideminor}&showlinkedto={$showlinkedto}",
- false, $mlink );
-
- $wgOut->addHTML( $note."\n" );
-
- $list = ChangesList::newFromUser( $wgUser );
- $s = $list->beginRecentChangesList();
-
- if ( $count ) {
- $counter = 1;
- while ( $limit ) {
- if ( 0 == $count ) { break; }
- $obj = $dbr->fetchObject( $res );
- --$count;
- $rc = RecentChange::newFromRow( $obj );
- $rc->counter = $counter++;
- $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) );
- --$limit;
- }
- } else {
- $wgOut->addWikiMsg('recentchangeslinked-noresult');
+ function getExtraOptions( $opts ){
+ $opts->consumeValues( array( 'showlinkedto', 'target', 'tagfilter' ) );
+ $extraOpts = array();
+ $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
+ $extraOpts['target'] = array( wfMsgHtml( 'recentchangeslinked-page' ),
+ Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) .
+ Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' .
+ Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) );
+ $tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
+ if ($tagFilter)
+ $extraOpts['tagfilter'] = $tagFilter;
+ return $extraOpts;
+ }
+
+ function setTopText( OutputPage $out, FormOptions $opts ) {
+ global $wgUser;
+ $skin = $wgUser->getSkin();
+ if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) )
+ $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $this->mTargetTitle,
+ $this->mTargetTitle->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) );
}
- $s .= $list->endRecentChangesList();
- $dbr->freeResult( $res );
- $wgOut->addHTML( $s );
+ function setBottomText( OutputPage $out, FormOptions $opts ){
+ if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ){
+ $out->setFeedAppendQuery( "target=" . urlencode( $this->mTargetTitle->getPrefixedDBkey() ) );
+ }
+ if( isset( $this->mResultEmpty ) && $this->mResultEmpty ){
+ $out->addWikiMsg( 'recentchangeslinked-noresult' );
+ }
+ }
}