<?php // -*-php-*-
rcs_id('$Id: SyncWiki.php,v 1.3 2007/03/18 10:34:42 rurban Exp $');
/**
 Copyright 2006 $ThePhpWikiProgrammingTeam

 This file is part of PhpWiki.

 PhpWiki is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 PhpWiki is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with PhpWiki; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/**
 * required argument:  url = <rpc interface to main wiki>
 * optional arguments: noimport, noexport, noupload
 *	
 * 1. check RPC2 interface or admin url (lang?) of external wiki
 *    get external pagelist, only later than our last mergepoint
 * 2. Download all externally changed sources:
 *    If local page is older than the mergepoint, import it.
 *    If local page does not exist (deleted?), and there is no revision, import it.
 *    Else we deleted it. Skip the import, but don't delete the external. Should be added to conflict.
 *    If local page is newer than the mergepoint, then add it to the conflict pages.
 * 3. check our to_delete, to_add, to_merge 
 * 4. get our pagelist of pages only later than our last mergepoint
 * 5. check external to_delete, to_add, to_merge
 * 6. store log (where, how?)
 */
require_once("lib/loadsave.php");
include_once("lib/plugin/WikiAdminUtils.php");

class WikiPlugin_SyncWiki
extends WikiPlugin_WikiAdminUtils
{
    function getName () {
        return _("SyncWiki");
    }

    function getDescription () {
        return _("Synchronize pages with external PhpWiki");
    }

    function getVersion() {
        return preg_replace("/[Revision: $]/", '',
                            "\$Revision: 1.3 $");
    }

    function getDefaultArguments() {
        return array('url'    => '',
		     'noimport' => 0,
		     'noexport' => 0,
		     'noupload' => 0,
		     'label'  => $this->getName(),
		     //'userid' => false,
		     'passwd' => false,
		     'sid'    => false,
		     );
    }

    function run($dbi, $argstr, &$request, $basepage) {
        $args = $this->getArgs($argstr, $request);
        $args['action'] = 'syncwiki';
	extract($args);
	if (empty($args['url']))
	    return $this->error(fmt("A required argument '%s' is missing.", "url"));
        if ($request->getArg('action') != 'browse')
            return $this->disabled("(action != 'browse')");
	$posted = $request->getArg('wikiadminutils');
	if ($request->isPost() 
	    and $posted['action'] == $action 
	    and $posted['url'] == $url) // multiple buttons
	{
	    return $this->_do_syncwiki($request, $posted);
	}
        return $this->_makeButton($request, $args, $label);
    }

    function _do_syncwiki(&$request, $args) {
	global $charset;
	longer_timeout(240);

	if (!function_exists('wiki_xmlrpc_post')) {
	    include_once("lib/XmlRpcClient.php");
	}
	$userid = $request->_user->_userid;
	$dbh = $request->getDbh();
	$merge_point = $dbh->get('mergepoint');
	if (empty($merge_point)) {
	    $page = $dbh->getPage("ReleaseNotes"); // this is usually the latest official page
	    $last = $page->getCurrentRevision(false);
	    $merge_point = $last->get("mtime");    // for testing: 1160396075
	    $dbh->set('mergepoint', $merge_point);
	}
	//TODO: remote auth, set session cookie
	$pagelist = wiki_xmlrpc_post('wiki.getRecentChanges', 
				     iso8601_encode($merge_point,1), 
				     $args['url'], $args);
	$html = HTML();
	//$html->pushContent(HTML::div(HTML::em("check RPC2 interface...")));
	if (gettype($pagelist) === "array") {
	    //$request->_deferredPageChangeNotification = array();
	    $request->discardOutput();
	    StartLoadDump($request, _("Syncing this PhpWiki"));
	    PrintXML(HTML::strong(fmt("Download all externally changed sources.")));
	    echo "<br />\n";
	    PrintXML(fmt("Retrieving from external url %s wiki.getRecentChanges(%s)...", 
	             $args['url'], iso8601_encode($merge_point,1)));
	    echo "<br />\n";
	    $ouriter = $dbh->mostRecent(array('since' => $merge_point));
	    //$ol = HTML::ol();
	    $done = array();
	    foreach ($pagelist as $ext) {
		$reaction = _("<unknown>");
		// compare existance and dates with local page
		$extdate = iso8601_decode($ext['lastModified']->scalar,1);
		// TODO: urldecode ???
		$name = utf8_decode($ext['name']);
		$our = $dbh->getPage($name);
		$done[$name] = 1;
		$ourrev  = $our->getCurrentRevision(false);
		$rel = '<=>';
		if (!$our->exists()) {
		    // we might have deleted or moved it on purpose?
		    // check date of latest revision if there's one, and > mergepoint
		    if (($ourrev->getVersion() > 1) and ($ourrev->get('mtime') > $merge_point)) {
			// our was deleted after sync, and changed after last sync.
			$this->_addConflict('delete', $args, $our, $extdate);
			$reaction = (_(" skipped")." ("."locally deleted or moved".")");
		    } else {
			$reaction = $this->_import($args, $our, $extdate);
		    }
		} else {
		    $ourdate = $ourrev->get('mtime');
		    if ($extdate > $ourdate and $ourdate < $merge_point) {
		    	$rel = '>';
			$reaction = $this->_import($args, $our, $extdate);
		    } elseif ($extdate > $ourdate and $ourdate >= $merge_point) {
		    	$rel = '>';
			// our is older then external but newer than last sync
			$reaction = $this->_addConflict('import', $args, $our, $extdate);
		    } elseif ($extdate < $ourdate and $extdate < $merge_point) {
		    	$rel = '>';
			$reaction = $this->_export($args, $our);
		    } elseif ($extdate < $ourdate and $extdate >= $merge_point) {
		    	$rel = '>';
			// our is newer and external is also newer
			$reaction = $this->_addConflict('export', $args, $our, $extdate);
		    } else {
		    	$rel = '==';
			$reaction = _("same date");
		    }
		}
		/*$ol->pushContent(HTML::li(HTML::strong($name)," ",
					  $extdate,"<=>",$ourdate," ",
					  HTML::strong($reaction))); */
		PrintXML(HTML::strong($name)," ",
			 $extdate," $rel ",$ourdate," ",
			 HTML::strong($reaction),
			 HTML::br());
		$request->chunkOutput();
	    }
	    //$html->pushContent($ol);
	} else {
	    $html->pushContent("xmlrpc error:  wiki.getRecentChanges returned "
	                  ."(".gettype($pagelist).") ".$pagelist);
	    trigger_error("xmlrpc error:  wiki.getRecentChanges returned "
	                  ."(".gettype($pagelist).") ".$pagelist, E_USER_WARNING);
	    EndLoadDump($request);
            return $this->error($html);
	}

	if (empty($args['noexport'])) {
	    PrintXML(HTML::strong(fmt("Now upload all locally newer pages.")));
	    echo "<br />\n";
	    PrintXML(fmt("Checking all local pages newer than %s...", 
	             iso8601_encode($merge_point,1)));
	    echo "<br />\n";
	    while ($our = $ouriter->next()) {
		$name = $our->getName();
		if ($done[$name]) continue;
		$reaction = _(" skipped");
		$ext = wiki_xmlrpc_post('wiki.getPageInfo', $name, $args['url']);
		if (is_array($ext)) {
		    $extdate = iso8601_decode($ext['lastModified']->scalar,1);
		    $ourdate = $our->get('mtime');
		    if ($extdate < $ourdate and $extdate < $merge_point) {
			$reaction = $this->_export($args, $our);
		    } elseif ($extdate < $ourdate and $extdate >= $merge_point) {
			// our newer and external newer
			$reaction = $this->_addConflict($args, $our, $extdate);
		    }
		} else {
		    $reaction = 'xmlrpc error';
		}
		PrintXML(HTML::strong($name)," ",
			 $extdate," < ",$ourdate," ",
			 HTML::strong($reaction),
			 HTML::br());
		$request->chunkOutput();
	    }

	    PrintXML(HTML::strong(fmt("Now upload all locally newer uploads.")));
	    echo "<br />\n";
	    PrintXML(fmt("Checking all local uploads newer than %s...", 
	             iso8601_encode($merge_point,1)));
	    echo "<br />\n";
	    $this->_fileList = array();
	    $prefix = getUploadFilePath();
	    $this->_dir($prefix);
	    $len = strlen($prefix);
	    foreach ($this->_fileList as $path) {
		// strip prefix
		$file = substr($path,$len);
		$ourdate = filemtime($path);
		$oursize = filesize($path);
		$reaction = _(" skipped");
		$ext = wiki_xmlrpc_post('wiki.getUploadedFileInfo', $file, $args['url']);
		if (is_array($ext)) {
		    $extdate = iso8601_decode($ext['lastModified']->scalar,1);
		    $extsize = $ext['size'];
		    if (empty($extsize) or $extdate < $ourdate) {
			$timeout = $oursize * 0.0002;  // assume 50kb/sec upload speed
			$reaction = $this->_upload($args, $path, $timeout);
		    }
		} else {
		    $reaction = 'xmlrpc error wiki.getUploadedFileInfo not supported';
		}
		PrintXML(HTML::strong($name)," ",
			 "$extdate ($extsize) < $ourdate ($oursize)",
			 HTML::strong($reaction),
			 HTML::br());
		$request->chunkOutput();
	    }
	}

	$dbh->set('mergepoint', time());
	EndLoadDump($request);
	return ''; //$html;
    }

    /* path must have ending slash */
    function _dir($path) {
	$dh = @opendir($path);
	while ($filename = readdir($dh)) {
	    if ($filename[0] == '.')
		continue;
	    $ft = filetype($path . $filename);
	    if ($ft == 'file')
		array_push($this->_fileList, $path . $filename);
	    else if ($ft == 'dir')
		$this->_dir($path . $filename . "/");
	}
	closedir($dh);
    }

    function _addConflict($what, $args, $our, $extdate = null) {
	$pagename = $our->getName();
	$meb = Button(array('action' => $args['action'],
			    'merge'=> true,
			    'source'=> $f),
		      _("Merge Edit"),
		      $args['pagename'],
		      'wikiadmin');
	$owb = Button(array('action' => $args['action'],
			    'overwrite'=> true,
			    'source'=> $f),
		      sprintf(_("%s force"), strtoupper(substr($what, 0, 1)).substr($what, 1)),
		      $args['pagename'],
		      'wikiunsafe');
	$this->_conflicts[] = $pagename;
	return HTML(fmt(_("Postponed %s for %s."), $what, $pagename), " ", $meb, " ", $owb);
    }

    // TODO: store log or checkpoint for restauration?
    function _import($args, $our, $extdate = null) {
	global $request;
	$reaction = 'import ';
	if ($args['noimport']) return ($reaction._("skipped"));
	//$userid = $request->_user->_userid;
	$name = $our->getName();
	$pagedata = wiki_xmlrpc_post('wiki.getPage', $name, $args['url']);
	if (is_object($pagedata)) {
	    $pagedata = $pagedata->scalar;
	    $ourrev  = $our->getCurrentRevision(true);
	    $content = $ourrev->getPackedContent();
	    if ($pagedata == $content)
	    	return $reaction . _("skipped").' '._("same content");
	    if (is_null($extdate))
	        $extdate = time();	
	    $our->save(utf8_decode($pagedata), -1, array('author' => $userid,
	    						 'mtime' => $extdate));
	    $reaction .= _("OK");
	} else 
	      $reaction .= (_("FAILED").' ('.gettype($pagedata).')');
	return $reaction;
    }

    // TODO: store log or checkpoint for restauration?
    function _export($args, $our) {
	global $request;
	$reaction = 'export ';
	if ($args['noexport']) return ($reaction._("skipped"));
	$userid  = $request->_user->_userid;
	$name    = $our->getName();
	$ourrev  = $our->getCurrentRevision(true);
	$content = $ourrev->getPackedContent();
	$extdata = wiki_xmlrpc_post('wiki.getPage', $name, $args['url']);
	if (is_object($extdata)) {
	    $extdata = $extdata->scalar;
	    if ($extdata == $content)
	    	return $reaction . _("skipped").' '._("same content");
	}
	$mypass  = $request->getPref('passwd'); // this usually fails
	$success = wiki_xmlrpc_post('wiki.putPage',
				    array($name, $content, $userid, $mypass), $args['url']);
	if (is_array($success)) {
	    if ($success['code'] == 200)
	        $reaction .= (_("OK").' '.$success['code']." ".$success['message']);
	    else
	        $reaction .= (_("FAILED").' '.$success['code']." ".$success['message']);
	} else 
	    $reaction .= (_("FAILED"));
	return $reaction;
    }

    // TODO: store log or checkpoint for restauration?
    function _upload($args, $path, $timeout) {
	global $request;
	$reaction = 'upload ';
	if ($args['noupload']) return ($reaction._("skipped"));

	//$userid  = $request->_user->_userid;
	$url = $args['url'];
	$url = str_replace("/RPC2.php","/index.php", $url);
	$server = parse_url($url);
	$http = new HttpClient($server['host'], $server['port']);
	$http->timeout = $timeout + 5;
	$success = $http->postfile($server['url'], $path);
	if ($success) {
	    if ($http->getStatus() == 200)
	        $reaction .= _("OK");
	    else
	        $reaction .= (_("FAILED").' '.$http->getStatus());
	} else
	    $reaction .= (_("FAILED").' '.$http->getStatus()." ".$http->errormsg);
	return $reaction;
    }
};

// $Log: SyncWiki.php,v $
// Revision 1.3  2007/03/18 10:34:42  rurban
// Fix for multiple buttons
//
// Revision 1.2  2007/01/20 11:24:46  rurban
// Be more strict: link to merge conflicts (not yet handled)
//
// Revision 1.1  2007/01/02 13:22:57  rurban
// add SyncWiki
//

// For emacs users
// Local Variables:
// mode: php
// tab-width: 8
// c-basic-offset: 4
// c-hanging-comment-ender-p: nil
// indent-tabs-mode: nil
// End:
?>
