MediaWiki extensions manual
OOjs UI icon advanced.svg
Release status: experimental
Implementation Parser extension
Description Retrieve the exact rendering of a page at a given date
Author(s) Seb35talk
Latest version 0.1
MediaWiki 1.10.1
License GNU General Public License 2.0 or later
Download No link
Translate the BackwardsTimeTravel extension if it is available at translatewiki.net
Check usage and version matrix.


This extension is a "subset" of the (much more complete) Memento extension, which aims at providing a standard (RFC 7089) feature to retrieve an old version of a resource. However, this extension has some MediaWiki-specific functionalities it would be worth integrating into Memento (see below). Additionaly, the roadmap above is still relevant to improve the MediaWiki implementation of Memento. Possibly the MediaWiki core could also be added parts of these codes to natively show the as-exact-as-possible old versions of a page.

Features in BackwardsTimeTravel it would be worth integrating into Memento:

  • time parser functions (hook ParserGetVariableValueTs)
  • follow moves of a page (and more generally it would be worth thinking about how to follow moves+deletions+undeletions, see also roadmap above)
  • display the rendering of a page at an arbitrary past date, not necessarily only when the page is modified (templates can change, link colours can change, time parser functions change, etc.); this need an additional URL parameter (epoch for BackwardsTimeTravel, date or datetime could be better names)

What can this extension do?Edit

The extension BackwardsTimeTravel intends to display the old versions of an article exactly as it was at the time of publishing. Indeed, with the standard version of MediaWiki, displaying an old version of a page retrieve only the old wikitext of the page and use the current versions for the templates, use the current time in the variables, and so on.

In the facts, the rendering with this extension is often very very near of the old version, but it can remain on some pages details which is impossible to retrieve (see issues), like the value of the magic word {{NUMBEROFARTICLES}}.


old refers to the time of publishing.

  • Use the old version of the templates (but the templates shouldn't have been moved or deleted, see also issues and parameters).
  • Use the old time.

Issues and roadmapEdit

  • Currently the color of the links can differ (fix me).
  • It is not always possible to retrieve the old version of a template, because with the play of moves and deletions it is possible to mix any version of a page with any other version of another page and put it on a third page... We are sure to find the correct version if there no moves from another page to this template, and if there is no deletion of the template before the old version. An alternative is to alert the user if the version is possibly unlike the old version or if it is impossible to retrieve a version of a template.
  • It is impossible to retrieve the value of some magic words depending of the statistics of the site, like numberofarticles, numberoffiles, numberofedits, numberofusers, numberofactiveusers, numberofpages, numberofadmins, numberofviews and possibly others.
  • It is impossible to retrieve the dynamic transcluded page like the special pages Newpages or Recentchanges, or CategoryTree or DynamicPageList.
  • About the linked JavaScripts and CSS, the current version of this extension doesn't retrieve the old versions of these pages; we should also retrieve the old versions of the linked pages (imported scripts and styles) local to the wiki.
  • It is impossible to retrieve any old version of JavaScript or CSS out of the wiki.
  • At a sysadmin level, any change in the LocalSetting.php can have an influence in the rendering of the pages and introduce a difference in the rendering of old pages, particularly the cancelling of a parser extension; or the updating of MediaWiki and/or extensions (see the next point).
  • At a developer level, any change about the parser can modify the rendering of old pages.

So the very very very strict equal rendering (HTML version) of an old version can be different, but the differences should be very very very often minimalist...


If the extension is active by default, the simple fact to display an old version in the history displays it 'exactly' as it was. It is possible to inhibit the extension by adding the parameter softoldid in the address bar.

If the extension is not active by default, it is possible to activate it by adding the parameter hardoldid in the address bar.

It is always possible to add the parameter epoch=yyyymmddhhmmss in the address bar to retrieve the rendering of the page as it was at the date dd/mm/yyyy hh:mm:ss, even if it is not the date of a change of the page.

Download instructionsEdit

Please cut and paste the code found below and place it in $IP/extensions/BackwardsTimeTravel/BackwardsTimeTravel.php. Note: $IP stands for the root directory of your MediaWiki installation, the same directory that holds LocalSettings.php .


To install this extension, add the following to LocalSettings.php :

# Configuration parameters
$wgBackwardsTimeDefault = true;

# Include the extension

Configuration parametersEdit

  • $wgBackwardsTimeDefault = true : use this extension by default when displaying an old version in the history (see also use). Warning: the systematic use of this extension can increase the load of the server.



@license GPL-2.0+

if ( !defined( 'MEDIAWIKI' ) ) die();

$wgExtensionCredits['other'][] = array(
    'path' => __FILE__,
    'name' => 'BackwardsTimeTravel',
    'description' => 'Retrieve the exact rendering of a page at a given date',
    'descriptionmsg' => 'backwardstimetravel-desc',
    'version' => '0.1',
    'author' => array( 'Seb35' ),
    'url' => 'https://www.mediawiki.org/wiki/Extension:BackwardsTimeTravel',
    'license-name' => 'GPL-2.0+',

$wgHooks['ArticleViewHeader'][] = 'BackwardsTime::onArticleViewHeader';
$wgHooks['BeforeParserFetchTemplateAndtitle'][] = 'BackwardsTime::onBeforeParserFetchTemplateAndtitle';
$wgHooks['ParserGetVariableValueTs'][] = 'BackwardsTime::onParserGetVariableValueTs';

// Activate by default when displaying an old version in the history
$wgBackwardsTimeDefault = true;

class BackwardsTime {
   var $mActivate;
   var $mEpoch;
    * Empty constructor
   private function __construct() {
      $this->mActivate = false;
      $this->mEpoch = '19700101000000';
      $this->mUnsureSoft = false;
      $this->mUnsureHard = false;
    * Return a singleton of this class
   public static function &singleton() {
      static $instance;
      if ( !isset( $instance ) ) {
          $instance = new BackwardsTime;
      return $instance;
    * Check if we backwards-time-travel when we check the URL
    * @param $article Title: title of the article
    * @param $outputDone boolean: is the output finished?
    * @param $pcache boolean: should the parser cache be used
   public static function onArticleViewHeader( &$article, &$outputDone, &$pcache ) {
      global $wgRequest, $wgBackwardsTimeDefault;
      if( $epoch = $wgRequest->getText( 'epoch' ) ) {
         $backwardsTime = BackwardsTime::singleton();
         $backwardsTime->mActivate = true;
         $backwardsTime->mEpoch = wfTimestamp( TS_MW, $epoch );
         wfDebug( __METHOD__ . "(1): mEpoch=" . $backwardsTime->mEpoch . "\n" );
      elseif( !$article->isCurrent() ) {
         if( $wgBackwardsTimeDefault == false ) {
            if( $wgRequest->getCheck('hardoldid') ) {
               $backwardsTime = BackwardsTime::singleton();
               $backwardsTime->mActivate = true;
               $rev = Revision::newFromId($article->getOldID());
               $backwardsTime->mEpoch = $rev->getTimestamp();
               wfDebug( __METHOD__ . "(2): mEpoch=" . $backwardsTime->mEpoch . "\n" );
         else /* $wgBackwardsTimeDefault == true */ {
            if( !$wgRequest->getCheck('softoldid') ) {
               $backwardsTime = BackwardsTime::singleton();
               $backwardsTime->mActivate = true;
               $rev = Revision::newFromId($article->getOldID());
               $backwardsTime->mEpoch = $rev->getTimestamp();
               wfDebug( __METHOD__ . "(3): mEpoch=" . $backwardsTime->mEpoch . "\n" );
      return true;
    * During the parse, give the original id of the template
    * @param $parser Parser: instance of the parser
    * @param $title Title: title of the template
    * @param $skip boolean: ignore this function
    * @param $id int: ID of the template
   public static function onBeforeParserFetchTemplateAndtitle( $parser, $title, $skip, $id ) {
      $backwardsTime = BackwardsTime::singleton();
      if( $backwardsTime->mActivate === true ) {
         $id = $backwardsTime->retrieveOldId( $title );
         wfDebug( __METHOD__ . ": id=" . $id . "\n" );
      return true;
    * Change the date when viewing an old page
    * @param $parser Parser: instance of the parser
    * @param $time int: UNIX time
   public static function onParserGetVariableValueTs( &$parser, &$time ) {
      $backwardsTime = BackwardsTime::singleton();
      if( $backwardsTime->mActivate === true ) {
         $time = wfTimestamp( TS_UNIX, $backwardsTime->mEpoch );
      return true;
    * Retrieve the id of a page according to the given epoch
    * @param $title Title: title of the page
    * @return $id int: ID of the revision
   private function retrieveOldId( $title ) {
      $dbr = wfGetDB( DB_SLAVE );
      $epoch = $this->mEpoch;
      // Check if the page has been moved
      $articleID = $this->followMoves( $dbr, $title, $epoch, $epoch );
      wfDebug( __METHOD__.": \$articleID=$articleID\n" );
      if( $articleID == 0 ) return $title->getArticleID();
      // Get the first revision older than the given epoch
      $rev = BackwardsTime::newFromConds(
               array( 'rev_timestamp <= '.$dbr->addQuotes($epoch),
                      'rev_page' => $articleID                   ),
               array( 'ORDER BY' => 'rev_timestamp DESC',
                      'LIMIT'    => 1                   )
      if( $rev instanceof Revision ) return $rev->getId();
      else {
         wfDebug( __METHOD__.": not a 'Revision' class.\n" );
         return $title->getArticleID();
    * Follow the moves done since the old date
    * @param $db Database: database object, usually a slave database
    * @param $title Title: 'new' title of the page
    * @param $originalepoch: the epoch initially asked
    * @param $epoch: the epoch of the last page followed
   private function followMoves( $db, $title, $originalepoch, $epoch ) {
      global $wgFollowDeletedMoves;
      static $followMovesNumber = 0;
      // Count the number of moves since the old version
       // MOVES //
      // Get the move logs about this title
      $res = $db->select(
                array( 'log_timestamp', 'log_params', 'log_deleted' ),
                array( 'log_type'      => 'move'                ,
                       'log_timestamp > '.$db->addQuotes($epoch),
                       'log_namespace' => $title->getNamespace(),
                       'log_title'     => $title->getDBkey()    ),
                array( 'ORDER BY' => 'log_timestamp ASC')
      $logMove = $db->resultObject( $res );
      // There is no result: we have the most recent article
      if( $logMove->numRows() == 0 ) return $title->getArticleID();
      // There is a result
      else {
         $dispersedTemplates = array();
         for( $i = 0; $i < $logMove->numRows(); $i++ ) {
            // Follow the template up to the title it has now
            $row = $logMove->fetchObject();
            // Verify we have the right to follow the log
            if( !LogEventsList::userCan( $row, LogPage::DELETED_ACTION ) && !$wgFollowDeletedMoves ) {
               $this->mUnsureHard = true;
               return 0;
            // Get the new title
            $paramArray = LogPage::extractParams( $row->log_params );
            $newtitle = Title::newFromText( $paramArray[0] );
            $newepoch = wfTimestamp( TS_MW, $row->log_timestamp );
            $dispersedTemplates[$newepoch] = $this->followMoves( $db, $newtitle, $originalepoch, $newepoch );
         krsort( $dispersedTemplates, SORT_NUMERIC );
         return reset($dispersedTemplates);
   private static function newFromConds( $conditions, $options ) {
      $dbr = wfGetDB( DB_SLAVE );
      $row = BackwardsTime::loadFromConds( $dbr, $conditions, $options );
      if( is_null( $row ) && wfGetLB()->getServerCount() > 1 ) {
         $dbw = wfGetDB( DB_MASTER );
         $row = BackwardsTime::loadFromConds( $dbw, $conditions, $options );
      return $row;
   private static function loadFromConds( $db, $conditions, $options ) {
      $res = BackwardsTime::fetchFromConds( $db, $conditions, $options );
      if( $res ) {
          $row = $res->fetchObject();
          if( $row ) {
              $ret = new Revision( $row );
              return $ret;
      $res = null;
      return $res;
   private static function fetchFromConds( $db, $conditions, $options ) {
      $fields = Revision::selectFields();
      $fields[] = 'page_namespace';
      $fields[] = 'page_title';
      $fields[] = 'page_latest';
      $res = $db->select(
         array( 'page', 'revision' ),
         $options );
      $ret = $db->resultObject( $res );
      return $ret;

See alsoEdit