3 * Copyright 1999,2000,2001,2002,2004,2005,2006,2007 $ThePhpWikiProgrammingTeam
4 * Copyright 2008-2010 Marc-Etienne Vargenau, Alcatel-Lucent
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.
23 require_once 'lib/mimelib.php';
24 require_once 'lib/Template.php';
27 * ignore fatal errors during dump
28 * @param PhpError $error
31 function _dump_error_handler($error)
33 if ($error->isFatal()) {
34 $error->errno = E_USER_WARNING;
37 return true; // Ignore error
41 * @param WikiRequest $request
42 * @param string $title
46 function StartLoadDump(&$request, $title, $html = null)
48 // FIXME: This is a hack. This really is the worst overall hack in phpwiki.
50 $html->pushContent('%BODY%');
51 $tmpl = Template('html', array('TITLE' => $title,
53 'CONTENT' => $html ? $html : '%BODY%'));
54 echo preg_replace('/%BODY%.*/s', '', $tmpl->getExpansion($html));
55 $request->chunkOutput();
57 // set marker for sendPageChangeNotification()
58 $request->_deferredPageChangeNotification = array();
62 * @param WikiRequest $request
64 function EndLoadDump(&$request)
68 $action = $request->getArg('action');
69 if ($action == 'browse') // loading virgin
70 $pagelink = WikiLink(HOME_PAGE);
72 $pagelink = WikiLink(new WikiPageName(__("PhpWikiAdministration")));
74 // do deferred sendPageChangeNotification()
75 if (!empty($request->_deferredPageChangeNotification)) {
76 $pages = $all_emails = $all_users = array();
77 foreach ($request->_deferredPageChangeNotification as $p) {
78 list($pagename, $emails, $userids) = $p;
80 $all_emails = array_unique(array_merge($all_emails, $emails));
81 $all_users = array_unique(array_merge($all_users, $userids));
83 $editedby = sprintf(_("Edited by: %s"), $request->_user->getId());
84 $content = _("Loaded the following pages:") . "\n" . join("\n", $pages);
85 if (!mail(join(',', $all_emails), "[" . WIKI_NAME . "] " . _("LoadDump"),
86 _("LoadDump") . "\n" .
90 trigger_error(sprintf(_("PageChange Notification Error: Couldn't send %s to %s"),
91 join("\n", $pages), join(',', $all_users)), E_USER_WARNING);
97 unset($request->_deferredPageChangeNotification);
99 PrintXML(HTML::p(HTML::strong(_("Complete."))),
100 HTML::p(fmt("Return to %s", $pagelink)));
101 // Ugly hack to get valid XHTML code
102 if (is_a($WikiTheme, 'WikiTheme_fusionforge')) {
107 } elseif (is_a($WikiTheme, 'WikiTheme_Sidebar')
108 or is_a($WikiTheme, 'WikiTheme_MonoBook')
114 } elseif (is_a($WikiTheme, 'WikiTheme_wikilens')) {
119 } elseif (is_a($WikiTheme, 'WikiTheme_blog')) {
122 } elseif (is_a($WikiTheme, 'WikiTheme_Crao')
123 or is_a($WikiTheme, 'WikiTheme_Hawaiian')
124 or is_a($WikiTheme, 'WikiTheme_MacOSX')
125 or is_a($WikiTheme, 'WikiTheme_shamino_com')
126 or is_a($WikiTheme, 'WikiTheme_smaller')
130 echo "</body></html>\n";
133 ////////////////////////////////////////////////////////////////
135 // Functions for dumping.
137 ////////////////////////////////////////////////////////////////
141 * http://www.nacs.uci.edu/indiv/ehood/MIME/2045/rfc2045.html
142 * http://www.faqs.org/rfcs/rfc2045.html
143 * (RFC 1521 has been superceeded by RFC 2045 & others).
145 * Also see http://www.faqs.org/rfcs/rfc2822.html
147 * @param WikiDB_Page $page
148 * @param int $nversions
151 function MailifyPage($page, $nversions = 1)
153 $current = $page->getCurrentRevision(false);
156 if (defined('STRICT_MAILABLE_PAGEDUMPS') and STRICT_MAILABLE_PAGEDUMPS) {
157 $from = defined('SERVER_ADMIN') ? SERVER_ADMIN : 'foo@bar';
158 //This is for unix mailbox format: (not RFC (2)822)
159 // $head .= "From $from " . CTime(time()) . "\r\n";
160 $head .= "Subject: " . rawurlencode($page->getName()) . "\r\n";
161 $head .= "From: $from (PhpWiki)\r\n";
162 // RFC 2822 requires only a Date: and originator (From:)
163 // field, however the obsolete standard RFC 822 also
164 // requires a destination field.
165 $head .= "To: $from (PhpWiki)\r\n";
167 $head .= "Date: " . Rfc2822DateTime($current->get('mtime')) . "\r\n";
168 $head .= sprintf("Mime-Version: 1.0 (Produced by PhpWiki %s)\r\n",
171 $iter = $page->getAllRevisions();
173 while ($revision = $iter->next()) {
174 $parts[] = MimeifyPageRevision($page, $revision);
175 if ($nversions > 0 && count($parts) >= $nversions)
178 if (count($parts) > 1)
179 return $head . MimeMultipart($parts);
181 return $head . $parts[0];
185 * Compute filename to used for storing contents of a wiki page.
187 * Basically we do a rawurlencode() which encodes everything except
188 * ASCII alphanumerics and '.', '-', and '_'.
190 * But we also want to encode leading dots to avoid filenames like
191 * '.', and '..'. (Also, there's no point in generating "hidden" file
192 * names, like '.foo'.)
194 * We have to apply a different "/" logic for dumpserial, htmldump and zipdump.
195 * dirs are allowed for zipdump and htmldump, not for dumpserial
198 * @param string $pagename Pagename.
199 * @param string $action.
200 * @return string Filename for page.
202 function FilenameForPage($pagename, $action = '')
204 $enc = rawurlencode($pagename);
207 $action = $request->getArg('action');
209 if ($action != 'dumpserial') { // zip, ziphtml, dumphtml
210 // For every %2F we will need to mkdir -p dirname($pagename)
211 $enc = preg_replace('/%2F/', '/', $enc);
213 $enc = preg_replace('/^\./', '%2E', $enc);
214 $enc = preg_replace('/%20/', ' ', $enc);
215 $enc = preg_replace('/\.$/', '%2E', $enc);
220 * The main() function which generates a zip archive of a PhpWiki.
222 * If $include_archive is false, only the current version of each page
223 * is included in the zip file; otherwise all archived versions are
226 * @param WikiRequest $request
228 function MakeWikiZip(&$request)
230 global $ErrorManager;
231 if ($request->getArg('include') == 'all') {
232 $zipname = WIKI_NAME . _("FullDump") . date('Ymd-Hi') . '.zip';
233 $include_archive = true;
235 $zipname = WIKI_NAME . _("LatestSnapshot") . date('Ymd-Hi') . '.zip';
236 $include_archive = false;
238 $include_empty = false;
239 if ($request->getArg('include') == 'empty') {
240 $include_empty = true;
243 // We may need much memory for the dump
244 ini_set("memory_limit", -1);
245 $zip = new ZipArchive();
246 $tmpfilename = "/tmp/" . $zipname;
247 if (file_exists($tmpfilename)) {
248 unlink ($tmpfilename);
250 if ($zip->open($tmpfilename, ZipArchive::CREATE) !== true) {
251 trigger_error(_("Cannot create ZIP archive"), E_USER_ERROR);
254 $zip->setArchiveComment(sprintf(_("Created by PhpWiki %s"), PHPWIKI_VERSION));
256 /* ignore fatals in plugins */
257 $ErrorManager->pushErrorHandler(new WikiFunctionCb('_dump_error_handler'));
259 $dbi =& $request->_dbi;
260 $thispage = $request->getArg('pagename'); // for "Return to ..."
261 if ($exclude = $request->getArg('exclude')) { // exclude which pagenames
262 $excludeList = explodePageList($exclude);
264 $excludeList = array();
266 if ($pages = $request->getArg('pages')) { // which pagenames
267 if ($pages == '[]') // current page
269 $page_iter = new WikiDB_Array_PageIterator(explodePageList($pages));
271 $page_iter = $dbi->getAllPages(false, false, false, $excludeList);
273 $request_args = $request->args;
274 $timeout = (!$request->getArg('start_debug')) ? 30 : 240;
276 while ($page = $page_iter->next()) {
277 $request->args = $request_args; // some plugins might change them (esp. on POST)
278 longer_timeout($timeout); // Reset watchdog
280 $current = $page->getCurrentRevision();
281 if ($current->getVersion() == 0)
284 $pagename = $page->getName();
285 $wpn = new WikiPageName($pagename);
286 if (!$wpn->isValid())
288 if (in_array($page->getName(), $excludeList)) {
292 $attrib = array('mtime' => $current->get('mtime'),
294 if ($page->get('locked'))
295 $attrib['write_protected'] = 1;
297 if ($include_archive)
298 $content = MailifyPage($page, 0);
300 $content = MailifyPage($page);
302 $zip->addFromString(FilenameForPage($pagename), $content);
306 $ErrorManager->popErrorHandler();
308 header('Content-Transfer-Encoding: binary');
309 header('Content-Disposition: attachment; filename="'.$zipname.'"');
310 header('Content-Length: '.filesize($tmpfilename));
312 readfile($tmpfilename);
317 * @param WikiRequest $request
319 function DumpToDir(&$request)
321 $directory = $request->getArg('directory');
322 if (empty($directory))
323 $directory = DEFAULT_DUMP_DIR; // See lib/plugin/WikiForm.php:87
324 if (empty($directory))
325 $request->finish(_("You must specify a directory to dump to"));
327 // see if we can access the directory the user wants us to use
328 if (!file_exists($directory)) {
329 if (!mkdir($directory, 0755))
330 $request->finish(fmt("Cannot create directory ā%sā", $directory));
332 $html = HTML::p(fmt("Created directory ā%sā for the page dump...",
335 $html = HTML::p(fmt("Using directory ā%sā", $directory));
338 StartLoadDump($request, _("Dumping Pages"), $html);
340 $dbi =& $request->_dbi;
341 $thispage = $request->getArg('pagename'); // for "Return to ..."
342 if ($exclude = $request->getArg('exclude')) { // exclude which pagenames
343 $excludeList = explodePageList($exclude);
345 $excludeList = array();
347 $include_empty = false;
348 if ($request->getArg('include') == 'empty') {
349 $include_empty = true;
351 if ($pages = $request->getArg('pages')) { // which pagenames
352 if ($pages == '[]') // current page
354 $page_iter = new WikiDB_Array_PageIterator(explodePageList($pages));
356 $page_iter = $dbi->getAllPages($include_empty, false, false, $excludeList);
359 $request_args = $request->args;
360 $timeout = (!$request->getArg('start_debug')) ? 30 : 240;
362 while ($page = $page_iter->next()) {
363 $request->args = $request_args; // some plugins might change them (esp. on POST)
364 longer_timeout($timeout); // Reset watchdog
366 $pagename = $page->getName();
367 PrintXML(HTML::br(), $pagename, ' ... ');
370 if (in_array($pagename, $excludeList)) {
371 PrintXML(_("Skipped."));
375 $filename = FilenameForPage($pagename);
377 if ($page->getName() != $filename) {
378 $msg->pushContent(HTML::small(fmt("saved as %s", $filename)),
382 if ($request->getArg('include') == 'all')
383 $data = MailifyPage($page, 0);
385 $data = MailifyPage($page);
387 if (!($fd = fopen($directory . "/" . $filename, "wb"))) {
388 $msg->pushContent(HTML::strong(fmt("couldn't open file ā%sā for writing",
389 "$directory/$filename")));
390 $request->finish($msg);
393 $num = fwrite($fd, $data, strlen($data));
394 $msg->pushContent(HTML::small(fmt("%s bytes written", $num)));
397 assert($num == strlen($data));
401 EndLoadDump($request);
404 function _copyMsg($page, $smallmsg)
406 if ($page) $msg = HTML(HTML::br(), HTML($page), HTML::small($smallmsg));
407 else $msg = HTML::small($smallmsg);
412 function mkdir_p($pathname, $permission = 0777)
414 $arr = explode("/", $pathname);
416 return mkdir($pathname, $permission);
418 $s = array_shift($arr);
420 foreach ($arr as $p) {
423 $ok = mkdir($curr, $permission);
425 if (!$ok) return FALSE;
431 * Dump all pages as XHTML to a directory, as pagename.html.
432 * Copies all used css files to the directory, all used images to a
433 * "images" subdirectory, and all used buttons to a "images/buttons" subdirectory.
434 * The webserver must have write permissions to these directories.
435 * chown httpd HTML_DUMP_DIR; chmod u+rwx HTML_DUMP_DIR
438 * @param WikiRequest $request
441 function DumpHtmlToDir(&$request)
444 $directory = $request->getArg('directory'); // Path to dump to. Default: HTML_DUMP_DIR
445 if (empty($directory))
446 $directory = HTML_DUMP_DIR; // See lib/plugin/WikiForm.php:87
447 if (empty($directory))
448 $request->finish(_("You must specify a directory to dump to"));
450 // See if we can access the directory the user wants us to use
451 if (!file_exists($directory)) {
452 if (!mkdir($directory, 0755))
453 $request->finish(fmt("Cannot create directory ā%sā", $directory));
455 $html = HTML::p(fmt("Created directory ā%sā for the page dump...",
458 $html = HTML::p(fmt("Using directory ā%sā", $directory));
460 StartLoadDump($request, _("Dumping Pages"), $html);
461 $thispage = $request->getArg('pagename'); // for "Return to ..."
463 // Comma-separated of glob-style pagenames to exclude
464 $dbi =& $request->_dbi;
465 if ($exclude = $request->getArg('exclude')) { // exclude which pagenames
466 $excludeList = explodePageList($exclude);
468 $excludeList = array('DebugAuthInfo', 'DebugGroupInfo', 'AuthInfo');
471 // Comma-separated of glob-style pagenames to dump.
472 // Also array of pagenames allowed.
473 if ($pages = $request->getArg('pages')) { // which pagenames
474 if ($pages == '[]') // current page
476 $page_iter = new WikiDB_Array_generic_iter(explodePageList($pages));
477 // not at admin page: dump only the current page
478 } elseif ($thispage != _("PhpWikiAdministration")) {
479 $page_iter = new WikiDB_Array_generic_iter(array($thispage));
481 $page_iter = $dbi->getAllPages(false, false, false, $excludeList);
484 $WikiTheme->DUMP_MODE = 'HTML';
485 _DumpHtmlToDir($directory, $page_iter, $request->getArg('exclude'));
486 $WikiTheme->DUMP_MODE = false;
488 $request->setArg('pagename', $thispage); // Template::_basepage fix
489 EndLoadDump($request);
493 * Known problem: any plugins or other code which echo()s text will
494 * lead to a corrupted html zip file which may produce the following
495 * errors upon unzipping:
497 * warning [wikihtml.zip]: 2401 extra bytes at beginning or within zipfile
498 * file #58: bad zipfile offset (local header sig): 177561
499 * (attempting to re-compensate)
501 * However, the actual wiki page data should be unaffected.
503 * @param WikiRequest $request
506 function MakeWikiZipHtml(&$request)
509 if ($request->getArg('zipname')) {
510 $zipname = basename($request->getArg('zipname'));
511 if (!preg_match("/\.zip$/i", $zipname))
513 $request->setArg('zipname', false);
515 $zipname = "wikihtml.zip";
518 // We may need much memory for the dump
519 ini_set("memory_limit", -1);
520 $zip = new ZipArchive();
521 $tmpfilename = "/tmp/" . $zipname;
522 if (file_exists($tmpfilename)) {
523 unlink ($tmpfilename);
525 if ($zip->open($tmpfilename, ZipArchive::CREATE) !== true) {
526 trigger_error(_("Cannot create ZIP archive"), E_USER_ERROR);
529 $zip->setArchiveComment(sprintf(_("Created by PhpWiki %s"), PHPWIKI_VERSION));
531 $dbi =& $request->_dbi;
532 $thispage = $request->getArg('pagename'); // for "Return to ..."
533 if ($pages = $request->getArg('pages')) { // which pagenames
534 if ($pages == '[]') // current page
536 $page_iter = new WikiDB_Array_generic_iter(explodePageList($pages));
538 $page_iter = $dbi->getAllPages(false, false, false, $request->getArg('exclude'));
541 $WikiTheme->DUMP_MODE = 'ZIPHTML';
542 _DumpHtmlToDir($zip, $page_iter, $request->getArg('exclude'), $zipname, $tmpfilename);
543 $WikiTheme->DUMP_MODE = false;
547 * Internal html dumper. Used for dumphtml, ziphtml and pdf
549 function _DumpHtmlToDir($target, $page_iter, $exclude = false, $zipname='', $tmpfilename='')
551 global $WikiTheme, $request, $ErrorManager;
555 if ($WikiTheme->DUMP_MODE == 'HTML') {
556 $directory = $target;
558 } elseif ($WikiTheme->DUMP_MODE == 'PDFHTML') {
559 $directory = $target;
560 } elseif (is_object($target)) { // $WikiTheme->DUMP_MODE == 'ZIPHTML'
564 $request->_TemplatesProcessed = array();
565 if ($exclude) { // exclude which pagenames
566 $excludeList = explodePageList($exclude);
568 $excludeList = array('DebugAuthInfo', 'DebugGroupInfo', 'AuthInfo');
570 $WikiTheme->VALID_LINKS = array();
571 if ($request->getArg('format')) { // pagelist
572 $page_iter_sav = $page_iter;
573 foreach ($page_iter_sav->asArray() as $handle) {
574 $WikiTheme->VALID_LINKS[] = is_string($handle) ? $handle : $handle->getName();
576 $page_iter_sav->reset();
579 if (defined('HTML_DUMP_SUFFIX')) {
580 $WikiTheme->HTML_DUMP_SUFFIX = HTML_DUMP_SUFFIX;
582 if (isset($WikiTheme->_MoreAttr['body'])) {
583 $_bodyAttr = $WikiTheme->_MoreAttr['body'];
584 unset($WikiTheme->_MoreAttr['body']);
587 $ErrorManager->pushErrorHandler(new WikiFunctionCb('_dump_error_handler'));
589 // check if the dumped file will be accessible from outside
590 $doc_root = $request->get("DOCUMENT_ROOT");
591 if ($WikiTheme->DUMP_MODE == 'HTML') {
592 $ldir = NormalizeLocalFileName($directory);
593 $wikiroot = NormalizeLocalFileName('');
594 if (string_starts_with($ldir, $doc_root)) {
595 $link_prefix = substr($directory, strlen($doc_root)) . "/";
596 } elseif (string_starts_with($ldir, $wikiroot)) {
597 $link_prefix = NormalizeWebFileName(substr($directory, strlen($wikiroot))) . "/";
601 $prefix = '/'; // . substr($doc_root,0,2); // add drive where apache is installed
603 $link_prefix = "file://" . $prefix . $directory . "/";
609 $request_args = $request->args;
610 $timeout = (!$request->getArg('start_debug')) ? 60 : 240;
613 $directory = str_replace("\\", "/", $directory); // no Win95 support.
614 if (!is_dir("$directory/images"))
615 mkdir("$directory/images");
619 $already_images = array();
621 while ($page = $page_iter->next()) {
622 if (is_string($page)) {
624 $page = $request->_dbi->getPage($pagename);
626 $pagename = $page->getName();
628 if (empty($firstpage)) $firstpage = $pagename;
629 if (array_key_exists($pagename, $already))
631 $already[$pagename] = 1;
632 $current = $page->getCurrentRevision();
633 //if ($current->getVersion() == 0)
636 $request->args = $request_args; // some plugins might change them (esp. on POST)
637 longer_timeout($timeout); // Reset watchdog
640 $attrib = array('mtime' => $current->get('mtime'),
642 if ($page->get('locked'))
643 $attrib['write_protected'] = 1;
644 } elseif (!$silent) {
645 PrintXML(HTML::br(), $pagename, ' ... ');
648 if (in_array($pagename, $excludeList)) {
650 PrintXML(_("Skipped."));
656 if ($WikiTheme->DUMP_MODE == 'PDFHTML')
657 $request->setArg('action', 'pdf'); // to omit cache headers
658 $request->setArg('pagename', $pagename); // Template::_basepage fix
659 $filename = FilenameForPage($pagename) . $WikiTheme->HTML_DUMP_SUFFIX;
660 $args = array('revision' => $current,
661 'CONTENT' => $current->getTransformedContent(),
662 'relative_base' => $relative_base);
663 // For every %2F will need to mkdir -p dirname($pagename)
664 if (preg_match("/(%2F|\/)/", $filename)) {
665 // mkdir -p and set relative base for subdir pages
666 $filename = preg_replace("/%2F/", "/", $filename);
667 $count = substr_count($filename, "/");
668 $dirname = dirname($filename);
670 mkdir_p($directory . "/" . $dirname);
671 // Fails with "XX / YY", "XX" is created, "XX / YY" cannot be written
672 // if (isWindows()) // interesting Windows bug: cannot mkdir "bla "
673 // Since dumps needs to be copied, we have to disallow this for all platforms.
674 $filename = preg_replace("/ \//", "/", $filename);
675 $relative_base = "../";
677 $relative_base .= "../";
680 $args['relative_base'] = $relative_base;
684 $DUMP_MODE = $WikiTheme->DUMP_MODE;
685 $data = GeneratePageasXML(new Template('browse', $request, $args),
686 $pagename, $current, $args);
687 $WikiTheme->DUMP_MODE = $DUMP_MODE;
689 if (preg_match_all("/<img .*?src=\"(\/.+?)\"/", $data, $m)) {
690 // fix to local relative path for uploaded images, so that pdf will work
691 foreach ($m[1] as $img_file) {
692 $base = basename($img_file);
693 $data = str_replace('src="' . $img_file . '"', 'src="images/' . $base . '"', $data);
694 if (array_key_exists($img_file, $already_images))
696 $already_images[$img_file] = 1;
697 // resolve src from webdata to file
698 $src = $doc_root . $img_file;
699 if (file_exists($src) and $base) {
701 $target = "$directory/images/$base";
702 if (copy($src, $target)) {
704 _copyMsg($img_file, fmt("... copied to %s", $target));
707 _copyMsg($img_file, fmt("... not copied to %s", $target));
710 $target = "images/$base";
718 $outfile = $directory . "/" . $filename;
719 if (!($fd = fopen($outfile, "wb"))) {
720 $msg->pushContent(HTML::strong(fmt("couldn't open file ā%sā for writing",
722 $request->finish($msg);
724 $len = strlen($data);
725 $num = fwrite($fd, $data, $len);
726 if ($pagename != $filename) {
727 $link = LinkURL($link_prefix . $filename, $filename);
728 $msg->pushContent(HTML::small(_("saved as "), $link, " ... "));
730 $msg->pushContent(HTML::small(fmt("%s bytes written", $num), "\n"));
734 $request->chunkOutput();
736 assert($num == $len);
738 $outfiles[] = $outfile;
740 $zip->addFromString($filename, $data);
744 $request->_dbi->_cache->invalidate_cache($pagename);
745 unset ($request->_dbi->_cache->_pagedata_cache);
746 unset ($request->_dbi->_cache->_versiondata_cache);
747 unset ($request->_dbi->_cache->_glv_cache);
749 unset ($request->_dbi->_cache->_backend->_page_data);
752 unset($current->_transformedContent);
754 if (!empty($template)) {
755 unset($template->_request);
762 if (!empty($WikiTheme->dumped_images) and is_array($WikiTheme->dumped_images)) {
763 // @mkdir("$directory/images");
764 foreach ($WikiTheme->dumped_images as $img_file) {
765 if (array_key_exists($img_file, $already_images))
767 $already_images[$img_file] = 1;
769 and ($from = $WikiTheme->_findFile($img_file, true))
773 $target = "$directory/images/" . basename($from);
775 copy($WikiTheme->_path . $from, $target);
777 if (copy($WikiTheme->_path . $from, $target)) {
778 _copyMsg($from, fmt("... copied to %s", $target));
780 _copyMsg($from, fmt("... not copied to %s", $target));
784 $target = "images/" . basename($from);
785 $zip->addSrcFile($target, $WikiTheme->_path . $from);
787 } elseif (!$silent) {
788 _copyMsg($from, _("... not found"));
793 if (!empty($WikiTheme->dumped_buttons)
794 and is_array($WikiTheme->dumped_buttons)
797 if ($directory && !is_dir("$directory/images/buttons"))
798 mkdir("$directory/images/buttons");
799 foreach ($WikiTheme->dumped_buttons as $text => $img_file) {
800 if (array_key_exists($img_file, $already_images))
802 $already_images[$img_file] = 1;
804 and ($from = $WikiTheme->_findFile($img_file, true))
808 $target = "$directory/images/buttons/" . basename($from);
810 copy($WikiTheme->_path . $from, $target);
812 if (copy($WikiTheme->_path . $from, $target)) {
813 _copyMsg($from, fmt("... copied to %s", $target));
815 _copyMsg($from, fmt("... not copied to %s", $target));
819 $target = "images/buttons/" . basename($from);
820 $zip->addSrcFile($target, $WikiTheme->_path . $from);
822 } elseif (!$silent) {
823 _copyMsg($from, _("... not found"));
827 if (!empty($WikiTheme->dumped_css) and is_array($WikiTheme->dumped_css)) {
828 foreach ($WikiTheme->dumped_css as $css_file) {
829 if (array_key_exists($css_file, $already_images))
831 $already_images[$css_file] = 1;
833 and ($from = $WikiTheme->_findFile(basename($css_file), true))
837 $target = "$directory/" . basename($css_file);
839 copy($WikiTheme->_path . $from, $target);
841 if (copy($WikiTheme->_path . $from, $target)) {
842 _copyMsg($from, fmt("... copied to %s", $target));
844 _copyMsg($from, fmt("... not copied to %s", $target));
848 //$attrib = array('is_ascii' => 0);
849 $target = basename($css_file);
850 $zip->addSrcFile($target, $WikiTheme->_path . $from);
852 } elseif (!$silent) {
853 _copyMsg($from, _("... not found"));
861 $ErrorManager->popErrorHandler();
863 header('Content-Transfer-Encoding: binary');
864 header('Content-Disposition: attachment; filename="'.$zipname.'"');
865 header('Content-Length: '.filesize($tmpfilename));
867 readfile($tmpfilename);
871 if ($WikiTheme->DUMP_MODE == 'PDFHTML') {
872 if (USE_EXTERNAL_HTML2PDF and $outfiles) {
873 $cmd = EXTERNAL_HTML2PDF_PAGELIST . ' "' . join('" "', $outfiles) . '"';
874 $filename = FilenameForPage($firstpage);
876 $tmpfile = $directory . "/createpdf.bat";
877 $fp = fopen($tmpfile, "wb");
878 fwrite($fp, $cmd . " > $filename.pdf");
881 if (!headers_sent()) {
882 Header('Content-Type: application/pdf');
885 $tmpdir = getUploadFilePath();
886 passthru($cmd . " > $tmpdir/$filename.pdf");
887 $errormsg = "<br />\nGenerated <a href=\"" . getUploadDataPath() . "$filename.pdf\">Upload:$filename.pdf</a>\n";
891 foreach ($outfiles as $f) unlink($f);
894 if (!empty($errormsg)) {
895 $request->discardOutput();
896 $GLOBALS['ErrorManager']->_postponed_errors = array();
900 $ErrorManager->popErrorHandler();
902 $WikiTheme->HTML_DUMP_SUFFIX = '';
903 $WikiTheme->DUMP_MODE = false;
904 $WikiTheme->_MoreAttr['body'] = isset($_bodyAttr) ? $_bodyAttr : '';
907 ////////////////////////////////////////////////////////////////
909 // Functions for restoring.
911 ////////////////////////////////////////////////////////////////
913 function SavePage(&$request, &$pageinfo, $source)
915 static $overwite_all = false;
916 $pagedata = $pageinfo['pagedata']; // Page level meta-data.
917 $versiondata = $pageinfo['versiondata']; // Revision level meta-data.
919 if (empty($pageinfo['pagename'])) {
920 PrintXML(HTML::p(HTML::strong(_("Empty pagename!"))));
924 if (empty($versiondata['author_id']))
925 $versiondata['author_id'] = $versiondata['author'];
927 // remove invalid backend specific chars. utf8 issues mostly
928 $pagename_check = new WikiPagename($pageinfo['pagename']);
929 if (!$pagename_check->isValid()) {
930 PrintXML(HTML::p(HTML::strong(sprintf(_("ā%sā: Bad page name"), $pageinfo['pagename']))));
933 $pagename = $pagename_check->getName();
934 $content = $pageinfo['content'];
936 if ($pagename == __("InterWikiMap"))
937 $content = _tryinsertInterWikiMap($content);
939 $dbi =& $request->_dbi;
940 $page = $dbi->getPage($pagename);
942 // Try to merge if updated pgsrc contents are different. This
943 // whole thing is hackish
944 $needs_merge = false;
948 if ($request->getArg('merge')) {
950 } elseif ($request->getArg('overwrite')) {
954 $current = $page->getCurrentRevision();
956 $edit = $request->getArg('edit');
958 if (isset($edit['keep_old'])) {
960 } elseif (isset($edit['overwrite'])) {
962 } elseif ($current and (!$current->hasDefaultContents())
963 && ($current->getPackedContent() != $content)
965 include_once 'lib/editpage.php';
966 $request->setArg('pagename', $pagename);
967 $v = $current->getVersion();
968 $request->setArg('revision', $current->getVersion());
969 $p = new LoadFileConflictPageEditor($request);
970 $p->_content = $content;
971 $p->_currentVersion = $v - 1;
972 $p->editPage($saveFailed = true);
973 return; //early return
977 foreach ($pagedata as $key => $value) {
979 $page->set($key, $value);
982 $mesg = HTML::span();
984 $mesg->pushContent(' ', fmt("from ā%sā", $source));
987 //FIXME: This should not happen! (empty vdata, corrupt cache or db)
988 $current = $page->getCurrentRevision();
990 if ($current->getVersion() == 0) {
991 $versiondata['author'] = ADMIN_USER;
992 $versiondata['author_id'] = ADMIN_USER;
993 $mesg->pushContent(' - ', _("New page"));
995 if ((!$current->hasDefaultContents())
996 && ($current->getPackedContent() != $content)
999 $mesg->pushContent(' ',
1000 fmt("has edit conflicts - overwriting anyway"));
1002 if (substr_count($source, 'pgsrc')) {
1003 $versiondata['author'] = ADMIN_USER;
1004 // but leave authorid as userid who loaded the file
1007 if (isset($edit['keep_old'])) {
1008 $mesg->pushContent(' ', fmt("keep old"));
1010 $mesg->pushContent(' ', fmt("has edit conflicts - skipped"));
1011 $needs_merge = true; // hackish, to display the buttons
1015 } elseif ($current->getPackedContent() == $content) {
1016 // The page content is the same, we don't need a new revision.
1017 $mesg->pushContent(' ',
1018 fmt("content is identical to current version %d - no new revision created",
1019 $current->getVersion()));
1025 // in case of failures print the culprit:
1026 PrintXML(HTML::span(WikiLink($pagename)));
1028 $new = $page->save($content, WIKIDB_FORCE_CREATE, $versiondata);
1030 $mesg->pushContent(' ', fmt("- saved to database as version %d",
1031 $new->getVersion()));
1032 $mesg->pushContent(HTML::br());
1036 // hackish, $source contains needed path+filename
1037 $f = str_replace(sprintf(_("MIME file %s"), ''), '', $f);
1038 $f = str_replace(sprintf(_("Serialized file %s"), ''), '', $f);
1039 $f = str_replace(sprintf(_("plain file %s"), ''), '', $f);
1040 //check if uploaded file? they pass just the content, but the file is gone
1042 $meb = Button(array('action' => 'loadfile',
1046 _("PhpWikiAdministration"),
1048 $owb = Button(array('action' => 'loadfile',
1049 'overwrite' => true,
1051 _("Restore Anyway"),
1052 _("PhpWikiAdministration"),
1054 $mesg->pushContent(' ', $meb, " ", $owb);
1055 if (!$overwite_all) {
1056 $args = $request->getArgs();
1057 $args['overwrite'] = 1;
1058 $owb = Button($args,
1060 _("PhpWikiAdministration"),
1062 $mesg->pushContent(HTML::span(array('class' => 'hint'), $owb));
1063 $overwite_all = true;
1066 $mesg->pushContent(HTML::em(_(" Sorry, cannot merge.")));
1071 PrintXML(HTML::em(WikiLink($pagename)), $mesg);
1077 // action=revert (by diff)
1078 function RevertPage(&$request)
1080 $mesg = HTML::div();
1081 $pagename = $request->getArg('pagename');
1082 $version = $request->getArg('version');
1083 $dbi =& $request->_dbi;
1084 $page = $dbi->getPage($pagename);
1086 $request->redirect(WikiURL($page,
1087 array('warningmsg' => _('Revert: missing required version argument'))));
1090 $current = $page->getCurrentRevision();
1091 $currversion = $current->getVersion();
1092 if ($currversion == 0) {
1093 $request->redirect(WikiURL($page,
1094 array('errormsg' => _('No revert: no page content'))));
1097 if ($currversion == $version) {
1098 $request->redirect(WikiURL($page,
1099 array('warningmsg' => _('No revert: same version page'))));
1102 if ($request->getArg('cancel')) {
1103 $request->redirect(WikiURL($page,
1104 array('warningmsg' => _('Revert cancelled'))));
1107 if (!$request->getArg('verify')) {
1108 $mesg->pushContent(HTML::p(fmt("Are you sure to revert %s to version $version?", WikiLink($pagename))),
1109 HTML::form(array('action' => $request->getPostURL(),
1110 'method' => 'post'),
1111 HiddenInputs($request->getArgs(), false, array('verify')),
1112 HiddenInputs(array('verify' => 1)),
1113 Button('submit:verify', _("Yes"), 'button'),
1114 HTML::raw(' '),
1115 Button('submit:cancel', _("Cancel"), 'button'))
1117 $rev = $page->getRevision($version);
1118 $html = HTML(HTML::fieldset($mesg), HTML::hr(), $rev->getTransformedContent());
1119 $template = Template('browse',
1120 array('CONTENT' => $html));
1121 GeneratePage($template, $pagename, $rev);
1122 $request->checkValidators();
1126 $rev = $page->getRevision($version);
1127 $content = $rev->getPackedContent();
1128 $versiondata = $rev->_data;
1129 $versiondata['summary'] = sprintf(_("Revert to version %d"), $version);
1130 $versiondata['mtime'] = time();
1131 $versiondata['author'] = $request->getUser()->getId();
1132 $new = $page->save($content, $currversion + 1, $versiondata);
1135 $mesg = HTML::span();
1136 $pagelink = WikiLink($pagename);
1137 $mesg->pushContent(fmt("Revert: %s", $pagelink),
1138 fmt("- version %d saved to database as version %d",
1139 $version, $new->getVersion()));
1140 // Force browse of current page version.
1141 $request->setArg('version', false);
1142 $template = Template('savepage', array());
1143 $template->replace('CONTENT', $new->getTransformedContent());
1145 GeneratePage($template, $mesg, $new);
1149 function _tryinsertInterWikiMap($content)
1152 if (strpos($content, '<'.'verbatim'.'>')) { // Avoid warning about unknown HTML tag
1153 //$error_html = " The newly loaded pgsrc already contains a verbatim block.";
1156 if (!$goback && !defined('INTERWIKI_MAP_FILE')) {
1157 $error_html = sprintf(" " . _("%s: not defined"), "INTERWIKI_MAP_FILE");
1160 $mapfile = FindFile(INTERWIKI_MAP_FILE, 1);
1161 if (!$goback && !file_exists($mapfile)) {
1162 $error_html = sprintf(" " . _("File ā%sā not found."), INTERWIKI_MAP_FILE);
1166 if (!empty($error_html))
1167 trigger_error(_("Default InterWiki map file not loaded.")
1168 . $error_html, E_USER_NOTICE);
1172 echo sprintf(_("Loading InterWikiMap from external file %s."), $mapfile), "<br />";
1174 $fd = fopen($mapfile, "rb");
1175 $data = fread($fd, filesize($mapfile));
1177 $content = $content . "\n<verbatim>\n$data</verbatim>\n";
1181 function ParseSerializedPage($text, $default_pagename, $user)
1183 if (!preg_match('/^a:\d+:{[si]:\d+/', $text))
1186 $pagehash = unserialize($text);
1188 // Split up pagehash into four parts:
1191 // page-level meta-data
1192 // revision-level meta-data
1194 if (!defined('FLAG_PAGE_LOCKED'))
1195 define('FLAG_PAGE_LOCKED', 1);
1196 if (!defined('FLAG_PAGE_EXTERNAL'))
1197 define('FLAG_PAGE_EXTERNAL', 1);
1198 $pageinfo = array('pagedata' => array(),
1199 'versiondata' => array());
1201 $pagedata = &$pageinfo['pagedata'];
1202 $versiondata = &$pageinfo['versiondata'];
1204 // Fill in defaults.
1205 if (empty($pagehash['pagename']))
1206 $pagehash['pagename'] = $default_pagename;
1207 if (empty($pagehash['author'])) {
1208 $pagehash['author'] = $user->getId();
1211 foreach ($pagehash as $key => $value) {
1216 $pageinfo[$key] = $value;
1219 $pageinfo[$key] = join("\n", $value);
1222 if (($value & FLAG_PAGE_LOCKED) != 0)
1223 $pagedata['locked'] = 'yes';
1224 if (($value & FLAG_PAGE_EXTERNAL) != 0)
1225 $pagedata['external'] = 'yes';
1229 $pagedata[$key] = $value;
1233 $pagedata['perm'] = ParseMimeifiedPerm($value);
1235 case 'lastmodified':
1236 $versiondata['mtime'] = $value;
1241 $versiondata[$key] = $value;
1248 function SortByPageVersion($a, $b)
1250 return $a['version'] - $b['version'];
1254 * Security alert! We should not allow to import config.ini into our wiki (or from a sister wiki?)
1255 * because the sql passwords are in plaintext there. And the webserver must be able to read it.
1256 * Detected by Santtu Jarvi.
1258 function LoadFile(&$request, $filename, $text = false)
1260 if (preg_match("/config$/", dirname($filename)) // our or other config
1261 and preg_match("/config.*\.ini/", basename($filename))
1262 ) // backups and other versions also
1264 trigger_error(sprintf("Refused to load %s", $filename), E_USER_WARNING);
1267 if (!is_string($text)) {
1269 $text = implode("", file($filename));
1272 // FIXME: basename("filewithnoslashes") seems to return garbage sometimes.
1273 $basename = basename("/dummy/" . $filename);
1275 $default_pagename = rawurldecode($basename);
1277 if (($parts = ParseMimeifiedPages($text))) {
1278 if (count($parts) > 1) {
1279 $overwrite = $request->getArg('overwrite');
1281 usort($parts, 'SortByPageVersion');
1282 foreach ($parts as $pageinfo) {
1284 if (count($parts) > 1) {
1285 $request->setArg('overwrite', 1);
1287 SavePage($request, $pageinfo, sprintf(_("MIME file %s"), $filename));
1289 if (count($parts) > 1)
1291 $request->setArg('overwrite', $overwrite);
1293 unset($request->_args['overwrite']);
1294 } elseif (($pageinfo = ParseSerializedPage($text, $default_pagename,
1295 $request->getUser()))
1297 SavePage($request, $pageinfo, sprintf(_("Serialized file %s"), $filename));
1300 $user = $request->getUser();
1302 // Assume plain text file.
1303 $pageinfo = array('pagename' => $default_pagename,
1304 'pagedata' => array(),
1306 => array('author' => $user->getId()),
1307 'content' => preg_replace('/[ \t\r]*\n/', "\n",
1310 SavePage($request, $pageinfo, sprintf(_("plain file %s"), $filename));
1314 function LoadZip(&$request, $zipfile, $files = array(), $exclude = array())
1316 $zip = new ZipArchive();
1317 $res = $zip->open($zipfile);
1318 if ($res !== true) {
1319 trigger_error(_("Cannot open ZIP archive for reading"), E_USER_ERROR);
1322 $timeout = (!$request->getArg('start_debug')) ? 20 : 120;
1324 for ($i = 0; $i < $zip->numFiles; $i++) {
1325 $fn = $zip->getNameIndex($i);
1326 $data = $zip->getFromIndex($i);
1328 // FIXME: basename("filewithnoslashes") seems to return
1329 // garbage sometimes.
1330 $fn = basename("/dummy/" . $fn);
1331 if (($files && !in_array($fn, $files))
1332 || ($exclude && in_array($fn, $exclude))
1334 PrintXML(HTML::p(WikiLink($fn)),
1335 HTML::p(_("Skipping")));
1339 LoadFile($request, $fn, $data);
1344 function LoadDir(&$request, $dirname, $files = array(), $exclude = array())
1346 $fileset = new LimitedFileSet($dirname, $files, $exclude);
1348 if (!$files and ($skiplist = $fileset->getSkippedFiles())) {
1349 PrintXML(HTML::p(HTML::strong(_("Skipping"))));
1351 foreach ($skiplist as $file)
1352 $list->pushContent(HTML::li(WikiLink($file)));
1353 PrintXML(HTML::p($list));
1356 // Defer HomePage loading until the end. If anything goes wrong
1357 // the pages can still be loaded again.
1358 $files = $fileset->getFiles();
1359 if (in_array(HOME_PAGE, $files)) {
1360 $files = array_diff($files, array(HOME_PAGE));
1361 $files[] = HOME_PAGE;
1363 $timeout = (!$request->getArg('start_debug')) ? 20 : 120;
1364 foreach ($files as $file) {
1365 longer_timeout($timeout); // longer timeout per page
1366 if (substr($file, -1, 1) != '~') // refuse to load backup files
1367 LoadFile($request, "$dirname/$file");
1371 class LimitedFileSet extends FileSet
1373 function __construct($dirname, $_include, $exclude)
1375 $this->_includefiles = $_include;
1376 $this->_exclude = $exclude;
1377 $this->_skiplist = array();
1378 parent::__construct($dirname);
1381 protected function _filenameSelector($fn)
1383 $incl = &$this->_includefiles;
1384 $excl = &$this->_exclude;
1386 if (($incl && !in_array($fn, $incl))
1387 || ($excl && in_array($fn, $excl))
1389 $this->_skiplist[] = $fn;
1396 function getSkippedFiles()
1398 return $this->_skiplist;
1402 define('ZIP_CENTHEAD_MAGIC', "PK\001\002");
1403 define('ZIP_LOCHEAD_MAGIC', "PK\003\004");
1405 function IsZipFile($filename_or_fd)
1407 // See if it looks like zip file
1408 if (is_string($filename_or_fd)) {
1409 $fd = fopen($filename_or_fd, "rb");
1410 $magic = fread($fd, 4);
1413 $fpos = ftell($filename_or_fd);
1414 $magic = fread($filename_or_fd, 4);
1415 fseek($filename_or_fd, $fpos);
1418 return $magic == ZIP_LOCHEAD_MAGIC || $magic == ZIP_CENTHEAD_MAGIC;
1422 * @param WikiRequest $request
1423 * @param string $file_or_dir
1424 * @param array $files
1425 * @param array $exclude
1427 function LoadAny(&$request, $file_or_dir, $files = array(), $exclude = array())
1429 // Try urlencoded filename for accented characters.
1430 if (!file_exists($file_or_dir)) {
1431 // Make sure there are slashes first to avoid confusing phps
1432 // with broken dirname or basename functions.
1433 // FIXME: windows uses \ and :
1434 if (is_integer(strpos($file_or_dir, "/"))) {
1435 $newfile = FindFile($file_or_dir, true);
1436 // Panic. urlencoded by the browser (e.g. San%20Diego => San Diego)
1438 $file_or_dir = dirname($file_or_dir) . "/"
1439 . rawurlencode(basename($file_or_dir));
1441 // This is probably just a file.
1442 $file_or_dir = rawurlencode($file_or_dir);
1446 $type = filetype($file_or_dir);
1447 if ($type == 'link') {
1448 // For symbolic links, use stat() to determine
1449 // the type of the underlying file.
1450 list(, , $mode) = stat($file_or_dir);
1451 $type = ($mode >> 12) & 017;
1454 elseif ($type == 004)
1459 $request->finish(fmt("Empty or not existing source. Unable to load: %s", $file_or_dir));
1460 } elseif ($type == 'dir') {
1461 LoadDir($request, $file_or_dir, $files, $exclude);
1462 } elseif ($type != 'file' && !preg_match('/^(http|ftp):/', $file_or_dir)) {
1463 $request->finish(fmt("Bad file type: %s", $type));
1464 } elseif (IsZipFile($file_or_dir)) {
1465 LoadZip($request, $file_or_dir, $files, $exclude);
1466 } else /* if (!$files || in_array(basename($file_or_dir), $files)) */ {
1467 LoadFile($request, $file_or_dir);
1472 * @param WikiRequest $request
1474 function LoadFileOrDir(&$request)
1476 $source = $request->getArg('source');
1477 $finder = new FileFinder;
1478 $source = $finder->slashifyPath($source);
1479 StartLoadDump($request, sprintf(_("Loading ā%sā"), $source));
1480 LoadAny($request, $source);
1481 EndLoadDump($request);
1485 * HomePage was not found so first-time install is supposed to run.
1486 * - import all pgsrc pages.
1487 * - Todo: installer interface to edit config/config.ini settings
1489 * @param WikiRequest $request
1491 function SetupWiki(&$request)
1493 global $GenericPages;
1495 //FIXME: This is a hack (err, "interim solution")
1496 // This is a bogo-bogo-login: Login without
1497 // saving login information in session state.
1498 // This avoids logging in the unsuspecting
1499 // visitor as ADMIN_USER
1501 // This really needs to be cleaned up...
1502 // (I'm working on it.)
1503 $request->_user = new _BogoUser(ADMIN_USER);
1505 StartLoadDump($request, _("Loading up virgin wiki"));
1507 $pgsrc = FindLocalizedFile(WIKI_PGSRC);
1508 $default_pgsrc = FindFile(DEFAULT_WIKI_PGSRC);
1509 $theme_pgsrc = FindFile("themes/".THEME."/".WIKI_PGSRC, true);
1511 $request->setArg('overwrite', true);
1512 // Load theme pgsrc, if it exists
1514 LoadAny($request, $theme_pgsrc);
1516 if ($default_pgsrc != $pgsrc) {
1517 LoadAny($request, $default_pgsrc, $GenericPages);
1519 $request->setArg('overwrite', false);
1520 LoadAny($request, $pgsrc);
1522 $dbi =& $request->_dbi;
1524 // Ensure that all mandatory pages are loaded
1525 $finder = new FileFinder;
1527 $mandatory = array('SandBox',
1528 'Template/Category',
1532 'CategoryActionPage',
1533 'PhpWikiAdministration');
1535 if ((defined('FUSIONFORGE') && FUSIONFORGE)) {
1536 $mandatory[] = 'Template/UserPage';
1538 $mandatory[] = 'Help/TextFormattingRules';
1541 $mandatory = array_merge($mandatory, $GLOBALS['AllActionPages']);
1542 $mandatory[] = constant('HOME_PAGE');
1544 foreach ($mandatory as $f) {
1545 $page = gettext($f);
1546 $epage = urlencode($page);
1547 if (!$dbi->isWikiPage($page)) {
1548 // translated version provided?
1549 if ($lf = FindLocalizedFile($pgsrc . $finder->_pathsep . $epage, 1)) {
1550 LoadAny($request, $lf);
1551 } else { // load english version of required action page
1552 LoadAny($request, FindFile(DEFAULT_WIKI_PGSRC . $finder->_pathsep . urlencode($f)));
1556 if (!$dbi->isWikiPage($page)) {
1557 trigger_error(sprintf("Mandatory file %s couldn't be loaded!", $page),
1562 $pagename = __("InterWikiMap");
1563 $map = $dbi->getPage($pagename);
1564 $map->set('locked', true);
1565 PrintXML(HTML::p(HTML::em(WikiLink($pagename)),
1566 HTML::strong(" "._("locked"))));
1567 EndLoadDump($request);
1570 function LoadPostFile(&$request)
1572 $upload = $request->getUploadedFile('file');
1575 $request->finish(_("No uploaded file to upload?")); // FIXME: more concise message
1577 // Dump http headers.
1578 StartLoadDump($request, sprintf(_("Uploading %s"), $upload->getName()));
1580 $fd = $upload->open();
1582 LoadZip($request, $upload->getTmpName(), array(), array(_("RecentChanges")));
1584 LoadFile($request, $upload->getName(), $upload->getContents());
1586 EndLoadDump($request);
1592 // c-basic-offset: 4
1593 // c-hanging-comment-ender-p: nil
1594 // indent-tabs-mode: nil