4 * Copyright 2006 $ThePhpWikiProgrammingTeam
6 * This file is part of PhpWiki.
8 * PhpWiki is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * PhpWiki is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 * required argument: url = <rpc interface to main wiki>
25 * optional arguments: noimport, noexport, noupload
27 * 1. check RPC2 interface or admin url (lang?) of external wiki
28 * get external pagelist, only later than our last mergepoint
29 * 2. Download all externally changed sources:
30 * If local page is older than the mergepoint, import it.
31 * If local page does not exist (deleted?), and there is no revision, import it.
32 * Else we deleted it. Skip the import, but don't delete the external. Should be added to conflict.
33 * If local page is newer than the mergepoint, then add it to the conflict pages.
34 * 3. check our to_delete, to_add, to_merge
35 * 4. get our pagelist of pages only later than our last mergepoint
36 * 5. check external to_delete, to_add, to_merge
37 * 6. store log (where, how?)
39 require_once 'lib/loadsave.php';
40 include_once 'lib/plugin/WikiAdminUtils.php';
42 class WikiPlugin_SyncWiki
43 extends WikiPlugin_WikiAdminUtils
50 function getDescription()
52 return _("Synchronize pages with external PhpWiki.");
55 function getDefaultArguments()
57 return array('url' => '',
61 'label' => $this->getName(),
68 function run($dbi, $argstr, &$request, $basepage)
70 $args = $this->getArgs($argstr, $request);
71 $args['action'] = 'syncwiki';
73 if (empty($args['url'])) {
74 return $this->error(fmt("A required argument ā%sā is missing.", "url"));
76 if ($request->getArg('action') != 'browse') {
77 return $this->disabled(_("Plugin not run: not in browse mode"));
79 $posted = $request->getArg('wikiadminutils');
80 if ($request->isPost()
81 and $posted['action'] == $action
82 and $posted['url'] == $url
85 return $this->_do_syncwiki($request, $posted);
87 return $this->_makeButton($request, $args, $label);
90 private function _do_syncwiki(&$request, $args)
94 if (!function_exists('wiki_xmlrpc_post')) {
95 include_once 'lib/XmlRpcClient.php';
97 $dbh = $request->getDbh();
98 $merge_point = $dbh->get('mergepoint');
99 if (empty($merge_point)) {
100 $page = $dbh->getPage("ReleaseNotes"); // this is usually the latest official page
101 $last = $page->getCurrentRevision(false);
102 $merge_point = $last->get("mtime"); // for testing: 1160396075
103 $dbh->set('mergepoint', $merge_point);
105 //TODO: remote auth, set session cookie
106 $pagelist = wiki_xmlrpc_post('wiki.getRecentChanges',
107 iso8601_encode($merge_point, 1),
108 $args['url'], $args);
110 //$html->pushContent(HTML::div(HTML::em("check RPC2 interface...")));
111 if (gettype($pagelist) === "array") {
112 //$request->_deferredPageChangeNotification = array();
113 $request->discardOutput();
114 StartLoadDump($request, _("Syncing this PhpWiki"));
115 PrintXML(HTML::strong(fmt("Download all externally changed sources.")));
117 PrintXML(fmt("Retrieving from external url %s wiki.getRecentChanges(%s)...",
118 $args['url'], iso8601_encode($merge_point, 1)));
120 $ouriter = $dbh->mostRecent(array('since' => $merge_point));
123 foreach ($pagelist as $ext) {
124 $reaction = _("<unknown>");
125 // compare existance and dates with local page
126 $extdate = iso8601_decode($ext['lastModified']->scalar, 1);
127 // TODO: urldecode ???
128 $name = utf8_decode($ext['name']);
129 $our = $dbh->getPage($name);
131 $ourrev = $our->getCurrentRevision(false);
133 if (!$our->exists()) {
134 // we might have deleted or moved it on purpose?
135 // check date of latest revision if there's one, and > mergepoint
136 if (($ourrev->getVersion() > 1) and ($ourrev->get('mtime') > $merge_point)) {
137 // our was deleted after sync, and changed after last sync.
138 $this->_addConflict('delete', $args, $our, $extdate);
139 $reaction = (_(" skipped") . " (" . "locally deleted or moved" . ")");
141 $reaction = $this->_import($args, $our, $extdate);
144 $ourdate = $ourrev->get('mtime');
145 if ($extdate > $ourdate and $ourdate < $merge_point) {
147 $reaction = $this->_import($args, $our, $extdate);
148 } elseif ($extdate > $ourdate and $ourdate >= $merge_point) {
150 // our is older then external but newer than last sync
151 $reaction = $this->_addConflict('import', $args, $our, $extdate);
152 } elseif ($extdate < $ourdate and $extdate < $merge_point) {
154 $reaction = $this->_export($args, $our);
155 } elseif ($extdate < $ourdate and $extdate >= $merge_point) {
157 // our is newer and external is also newer
158 $reaction = $this->_addConflict('export', $args, $our, $extdate);
161 $reaction = _("same date");
164 /*$ol->pushContent(HTML::li(HTML::strong($name)," ",
165 $extdate,"<=>",$ourdate," ",
166 HTML::strong($reaction))); */
167 PrintXML(HTML::strong($name), " ",
168 $extdate, " $rel ", $ourdate, " ",
169 HTML::strong($reaction),
171 $request->chunkOutput();
173 //$html->pushContent($ol);
175 $html->pushContent("xmlrpc error: wiki.getRecentChanges returned "
176 . "(" . gettype($pagelist) . ") " . $pagelist);
177 trigger_error("xmlrpc error: wiki.getRecentChanges returned "
178 . "(" . gettype($pagelist) . ") " . $pagelist, E_USER_WARNING);
179 EndLoadDump($request);
180 return $this->error($html);
183 if (empty($args['noexport'])) {
184 PrintXML(HTML::strong(fmt("Now upload all locally newer pages.")));
186 PrintXML(fmt("Checking all local pages newer than %s...",
187 iso8601_encode($merge_point, 1)));
189 while ($our = $ouriter->next()) {
190 $name = $our->getName();
191 if ($done[$name]) continue;
192 $reaction = _(" skipped");
193 $ext = wiki_xmlrpc_post('wiki.getPageInfo', $name, $args['url']);
194 if (is_array($ext)) {
195 $extdate = iso8601_decode($ext['lastModified']->scalar, 1);
196 $ourdate = $our->get('mtime');
197 if ($extdate < $ourdate and $extdate < $merge_point) {
198 $reaction = $this->_export($args, $our);
199 } elseif ($extdate < $ourdate and $extdate >= $merge_point) {
200 // our newer and external newer
201 $reaction = $this->_addConflict($args, $our, $extdate);
204 $reaction = 'xmlrpc error';
206 PrintXML(HTML::strong($name), " ",
207 $extdate, " < ", $ourdate, " ",
208 HTML::strong($reaction),
210 $request->chunkOutput();
213 PrintXML(HTML::strong(fmt("Now upload all locally newer uploads.")));
215 PrintXML(fmt("Checking all local uploads newer than %s...",
216 iso8601_encode($merge_point, 1)));
218 $this->_fileList = array();
219 $prefix = getUploadFilePath();
220 $this->_dir($prefix);
221 $len = strlen($prefix);
222 foreach ($this->_fileList as $path) {
224 $file = substr($path, $len);
225 $ourdate = filemtime($path);
226 $oursize = filesize($path);
227 $reaction = _(" skipped");
228 $ext = wiki_xmlrpc_post('wiki.getUploadedFileInfo', $file, $args['url']);
229 if (is_array($ext)) {
230 $extdate = iso8601_decode($ext['lastModified']->scalar, 1);
231 $extsize = $ext['size'];
232 if (empty($extsize) or $extdate < $ourdate) {
233 $timeout = $oursize * 0.0002; // assume 50kb/sec upload speed
234 $reaction = $this->_upload($args, $path, $timeout);
237 $reaction = 'xmlrpc error wiki.getUploadedFileInfo not supported';
239 PrintXML(HTML::strong($name), " ",
240 "$extdate ($extsize) < $ourdate ($oursize)",
241 HTML::strong($reaction),
243 $request->chunkOutput();
247 $dbh->set('mergepoint', time());
248 EndLoadDump($request);
252 /* path must have ending slash */
253 private function _dir($path)
255 $dh = @opendir($path);
256 while ($filename = readdir($dh)) {
257 if ($filename[0] == '.')
259 $ft = filetype($path . $filename);
261 array_push($this->_fileList, $path . $filename);
262 else if ($ft == 'dir')
263 $this->_dir($path . $filename . "/");
268 private function _addConflict($what, $args, $our, $extdate = null)
270 $pagename = $our->getName();
271 $meb = Button(array('action' => $args['action'],
277 $owb = Button(array('action' => $args['action'],
280 sprintf(_("%s force"), strtoupper(substr($what, 0, 1)) . substr($what, 1)),
283 $this->_conflicts[] = $pagename;
284 return HTML(fmt(_("Postponed %s for %s."), $what, $pagename), " ", $meb, " ", $owb);
287 // TODO: store log or checkpoint for restauration?
288 private function _import($args, $our, $extdate = null)
290 $reaction = 'import ';
291 if ($args['noimport']) return ($reaction . _("skipped"));
292 //$userid = $request->_user->_userid;
293 $name = $our->getName();
294 $pagedata = wiki_xmlrpc_post('wiki.getPage', $name, $args['url']);
295 if (is_object($pagedata)) {
296 $pagedata = $pagedata->scalar;
297 $ourrev = $our->getCurrentRevision(true);
298 $content = $ourrev->getPackedContent();
299 if ($pagedata == $content)
300 return $reaction . _("skipped") . ' ' . _("same content");
301 if (is_null($extdate))
303 $our->save(utf8_decode($pagedata), -1, array('author' => $userid,
304 'mtime' => $extdate));
305 $reaction .= _("OK");
307 $reaction .= (_("FAILED") . ' (' . gettype($pagedata) . ')');
311 // TODO: store log or checkpoint for restauration?
312 private function _export($args, $our)
315 $reaction = 'export ';
316 if ($args['noexport']) return ($reaction . _("skipped"));
317 $userid = $request->_user->_userid;
318 $name = $our->getName();
319 $ourrev = $our->getCurrentRevision(true);
320 $content = $ourrev->getPackedContent();
321 $extdata = wiki_xmlrpc_post('wiki.getPage', $name, $args['url']);
322 if (is_object($extdata)) {
323 $extdata = $extdata->scalar;
324 if ($extdata == $content)
325 return $reaction . _("skipped") . ' ' . _("same content");
327 $mypass = $request->getPref('passwd'); // this usually fails
328 $success = wiki_xmlrpc_post('wiki.putPage',
329 array($name, $content, $userid, $mypass), $args['url']);
330 if (is_array($success)) {
331 if ($success['code'] == 200)
332 $reaction .= (_("OK") . ' ' . $success['code'] . " " . $success['message']);
334 $reaction .= (_("FAILED") . ' ' . $success['code'] . " " . $success['message']);
336 $reaction .= (_("FAILED"));
340 // TODO: store log or checkpoint for restauration?
341 private function _upload($args, $path, $timeout)
343 $reaction = 'upload ';
344 if ($args['noupload']) return ($reaction . _("skipped"));
346 //$userid = $request->_user->_userid;
348 $url = str_replace("/RPC2.php", "/index.php", $url);
349 $server = parse_url($url);
350 $http = new HttpClient($server['host'], $server['port']);
351 $http->timeout = $timeout + 5;
352 $success = $http->postfile($server['url'], $path);
354 if ($http->getStatus() == 200)
355 $reaction .= _("OK");
357 $reaction .= (_("FAILED") . ' ' . $http->getStatus());
359 $reaction .= (_("FAILED") . ' ' . $http->getStatus() . " " . $http->errormsg);
368 // c-hanging-comment-ender-p: nil
369 // indent-tabs-mode: nil