<?php if (!defined('PmWiki')) exit();

/*	=== PageTopStore ===
 *	Copyright 2009 Eemeli Aro <eemeli@gmail.com>
 *
 *	A PageStore alternative which doesn't mangle page contents when viewed outside PmWiki
 *
 *	Developed and tested using PmWiki 2.2.0
 *
 *	To install, add the following to your configuration file:

		include_once("$FarmD/cookbook/pagetopstore.php");
		$WikiDir = new PageTopStore( 'wiki.d/{$FullName}', 'wikitop.d/{$FullName}' );

 *	For more information, please see the online documentation at
 *		http://www.pmwiki.org/wiki/Cookbook/PageTopStore
 *
 *	This program is free software; you can redistribute it and/or
 *	modify it under the terms of the GNU General Public License,
 *	Version 2, as published by the Free Software Foundation.
 *	http://www.gnu.org/copyleft/gpl.html
 */

$RecipeInfo['PageTopStore']['Version'] = '2009-11-27';

SDV( $HandleActions['filltop'], 'HandleFillTop' );
function HandleFillTop( $pagename, $auth = 'attr' ) {
	global $ScriptUrl;
	if (empty( $_GET['ps'] )) Abort( "No PageStore given!<br />usage: $ScriptUrl?action=filltop&ps=YourPageStoreVariableNameHere<br />use ps=WikiDir for default" );
	$psn = $_GET['ps'];
	if (empty( $GLOBALS[$psn]->topfmt )) Abort( "$psn at {$GLOBALS[$psn]->dirfmt} isn't a valid PageTopStore!" );
	$page = RetrieveAuthPage( $pagename, $auth, true, READPAGE_CURRENT );
	$GLOBALS[$psn]->fill();
}

class PageTopStore extends PageStore {
	var $dirfmt;
	var $topfmt;
	var $iswrite;
	var $attr;
	var $author;
	var $r0;
	var $r1;

	function PageTopStore( $d='$WorkDir/$FullName', $t='wikitop.d/{$FullName}', $w=0, $a=NULL, $u='[PageTopStore]', $r=array( '&lt;'=>'&amp;lt;', '<'=>'&lt;' ) ) {
		$this->dirfmt = $d;
		$this->topfmt = $t;
		$this->iswrite = $w;
		$this->attr = (array)$a;
		$this->author = $u;
		$this->r0 = array_keys((array)$r);		## '<' replacement prevents scripting attacks
		$this->r1 = array_values((array)$r);
		$GLOBALS['PageExistsCache'] = array();
	}

	## inherited: function pagefile( $pagename )

	function pagetopfile( $pagename ) {
		global $FarmD;
		$tfmt = $this->topfmt;
		if ($pagename > '') {
			$pagename = str_replace('/', '.', $pagename);
			## optimizations for standard locations
			if ( $tfmt == 'wikitop.d/{$FullName}' )				return "wikitop.d/$pagename";
			if ( $tfmt == 'wikitop.d/{$Group}/{$FullName}' )	return preg_replace( '/([^.]+).*/', 'wikitop.d/$1/$0', $pagename );
		}
		return FmtPageName( $tfmt, $pagename );
	}

	function update( $pagename, &$page, &$new ) {
		global $Now, $EnablePost, $IsPagePosted, $Author;
		Lock(2);
			$prev_Now = $Now;
			$prev_EnablePost = $EnablePost;
			$prev_IsPagePosted = $IsPagePosted;
			$prev_SERVER_REMOTE_ADDR = $_SERVER['REMOTE_ADDR'];
			$prev_SERVER_HTTP_USER_AGENT = @$_SERVER['HTTP_USER_AGENT'];
			$prev_Author = $Author;

			$Now = min( $Now, max( $page['time'], $new['time'] ) );
			$EnablePost = 1;
			$_SERVER['REMOTE_ADDR'] = 'local';
			$_SERVER['HTTP_USER_AGENT'] = 'PageTopStore';
			$Author = $this->author;

			UpdatePage( $pagename, $page, $new );

			$Now = $prev_Now;
			$EnablePost = $prev_EnablePost;
			$IsPagePosted = $prev_IsPagePosted;
			$_SERVER['REMOTE_ADDR'] = $prev_SERVER_REMOTE_ADDR;
			$_SERVER['HTTP_USER_AGENT'] = $prev_SERVER_HTTP_USER_AGENT;
			$Author = $prev_Author;
		Lock(0);
	}

	function fixtop( $pagename ) {
		global $Now;

		$pagefile = $this->pagefile($pagename); if (empty($pagefile)) return;
		$topfile = $this->pagetopfile($pagename); if (empty($topfile)) return;
		if ( !file_exists($pagefile) ) {
			if ( file_exists($topfile) ) {
				$new = $this->read( $pagename, READPAGE_CURRENT );
				$t = max( $new['time'], filemtime($topfile) );
				$page = array( 'ctime' => $t, 'time' => $t );
				$this->update( $pagename, $page, $new );
			}
			return;
		}
		if ( !file_exists($topfile) ) {
			$page = parent::read( $pagename, READPAGE_CURRENT );
			$this->writetop( $pagename, $page );
			return;
		}
		if ( filemtime($topfile) > filemtime($pagefile) ) {
			$page = parent::read( $pagename, 0 );
			$new = array_merge( $page, $this->read( $pagename, READPAGE_CURRENT ) );
			$new['version'] = $page['version'];
			if ( $new != $page ) {
				$new['time'] = max( $new['time'], filemtime($topfile) );
				$this->update( $pagename, $page, $new );
			}
		}
	}

	function writetop( $pagename, &$page ) {
		global $Now, $Version;
		$topfile = $this->pagetopfile($pagename);
		$dir = dirname($topfile);
		mkdirp($dir);
		if ( !file_exists("$dir/.htaccess") && ( $fp = @fopen( "$dir/.htaccess", 'w' ) ) ) {
			fwrite( $fp, "Order Deny,Allow\nDeny from all\n" );
			fclose($fp);
		}

		$st = FALSE;
		if ( $topfile && ( $fp = fopen( "$topfile,new", 'w' ) ) ) {
			$r0 = array( '%',   "\n",  '<' );
			$r1 = array( '%25', '%0a', '%3c' );
			$x = "version=$Version fmt=pagetop\n";
			$st = true && fputs( $fp, $x );
			$tz = strlen($x);

			//if (empty($page)) $page = array( 'ctime' => $Now, 'time' => $Now );
			uksort( $page, 'CmpPageAttr' );
			foreach( $page as $k => $v ) if (
				( $k > '' ) && ( $k[0] != '=' ) &&
				( $k != 'version' ) && ( $k != 'text' ) && ( $k != 'newline' )
			) {
				if (strpos( $k, ':' )) break;
				$x = str_replace( $r0, $r1, "$k=$v" ) . "\n";
				$st = $st && fputs( $fp, $x );
				$tz += strlen($x);
			}

			$text = str_replace( $this->r0, $this->r1, $page['text'] );
			$st = $st && fputs( $fp, "\n$text\n" );
			$tz += 2 + strlen($text);

			$st = fclose($fp) && $st;
			$st = $st && ( filesize("$topfile,new") > $tz * 0.95 );
			if (file_exists( $topfile )) $st = $st && unlink($topfile);
			$st = $st && rename( "$topfile,new", $topfile );
		}
		if ($st) {
			fixperms($topfile);
			touch( $topfile, $page['time'] );
		} else Abort("Cannot write page $pagename top to ($topfile)...");
	}

	function fill() {
		global $ScriptUrl;
		print( "<html><head><title>PageTopStore::fill()</title></head><body><pre>\nFilling PageTopStore at {$this->topfmt} from PageStore at {$this->dirfmt}...\n" );
		foreach( @parent::ls() as $pagename ) {
			print("  $pagename\n"); flush();
			$page = parent::read( $pagename, READPAGE_CURRENT );
			$this->writetop( $pagename, $page );
		}
		print("\nall done.\n</pre>\n<p><a href='$ScriptUrl'>Return to $ScriptUrl</a></p>\n</body></html>");
	}

	function read( $pagename, $since=0 ) {
		global $EnablePageTopStoreAutofill;

		if ( $since != READPAGE_CURRENT ) {
			if (IsEnabled( $EnablePageTopStoreAutofill, TRUE )) $this->fixtop( $pagename );
			return parent::read( $pagename, $since );
		}

		$urlencoded = FALSE;
		$topfile = $this->pagetopfile($pagename);
		if ( $topfile && ( $ft = @fopen($topfile,'r') ) ) {
			$page = $this->attr;
			while ( !feof($ft) ) {
				## headers
				$line = fgets( $ft, 4096 );
				while ( ( substr( $line, -1, 1 ) != "\n" ) && !feof($ft) ) $line .= fgets( $ft, 4096 );
				$line = rtrim($line);
				if (!$line) break;	## empty line indicates end of headers
				if ($urlencoded) $line = urldecode(str_replace( '+', '%2b', $line ));
				@list($k,$v) = explode( '=', $line, 2 );
				if (!$k) continue;
				if ( $k == 'version' ) $urlencoded = ( strpos( $v, 'urlencoded=1' ) !== FALSE );
				$page[$k] = $v;
			}
			//$page['text'] = stream_get_contents( $ft );
			$page['text'] = '';
			while (!feof( $ft )) $page['text'] .= fgets( $ft, 4096 );
			$page['text'] = str_replace( $this->r1, $this->r0, $page['text'] );
			if ( substr( $page['text'], -1 ) == "\n" ) $page['text'] = substr( $page['text'], 0, -1 );
			fclose($ft);
			return $page;
		}

		$page = parent::read( $pagename, READPAGE_CURRENT );
		if ( IsEnabled( $EnablePageTopStoreAutofill, TRUE ) && !empty($page) && $topfile && !file_exists($topfile) )
			$this->writetop( $pagename, $page );
		return $page;
	}

	function write( $pagename, $page ) {
		global $PCache;

		parent::write( $pagename, $page );

		$text = $page['text'];
		$page = $PCache[$pagename];
		if (empty( $page )) Abort("Page $pagename is blank?");
		$page['text'] = $text;

		$this->writetop( $pagename, $page );
	}

	function exists( $pagename ) {
		if (!$pagename) return false;
		$pagefile = $this->pagefile($pagename);
		$topfile = $this->pagetopfile($pagename);
		return ( ( $pagefile && file_exists($pagefile) ) || ( $topfile && file_exists($topfile) ) );
	}

	function delete($pagename) {
		global $Now;
		$pagefile = $this->pagefile($pagename);
		@rename( $pagefile, "$pagefile,del-$Now" );
		@unlink( $this->pagetopfile($pagename) );
	}

	## inherited: function ls($pats=NULL)
}