2 require_once 'lib/Template.php';
3 require_once 'lib/WikiUser.php';
11 * @var WikiDB_PageRevision $current
18 public $_currentVersion;
20 * @var UserPreferences $_prefs
24 private $_wikicontent;
27 * @param WikiRequest $request
29 function __construct(&$request)
31 $this->request = &$request;
33 $this->user = $request->getUser();
34 $this->page = $request->getPage();
36 $this->current = $this->page->getCurrentRevision(false);
38 // HACKish short circuit to browse on action=create
39 if ($request->getArg('action') == 'create') {
40 if (!$this->current->hasDefaultContents())
41 $request->redirect(WikiURL($this->page->getName())); // noreturn
44 $this->meta = array('author' => $this->user->getId(),
45 'author_id' => $this->user->getAuthenticatedId(),
48 $this->tokens = array();
50 if (defined('ENABLE_WYSIWYG') and ENABLE_WYSIWYG) {
51 $backend = WYSIWYG_BACKEND;
52 // TODO: error message
53 require_once("lib/WysiwygEdit/$backend.php");
54 $class = "WysiwygEdit_$backend";
55 $this->WysiwygEdit = new $class();
57 if (defined('ENABLE_CAPTCHA' and ENABLE_CAPTCHA)) {
58 require_once 'lib/Captcha.php';
59 $this->Captcha = new Captcha($this->meta);
62 $version = $request->getArg('version');
63 if ($version !== false) {
64 $this->selected = $this->page->getRevision($version);
65 $this->version = $version;
67 $this->version = $this->current->getVersion();
68 $this->selected = $this->page->getRevision($this->version);
71 if ($this->_restoreState()) {
72 $this->_initialEdit = false;
74 $this->_initializeState();
75 $this->_initialEdit = true;
77 // The edit request has specified some initial content from a template
78 if (($template = $request->getArg('template'))
79 and $request->_dbi->isWikiPage($template)
81 $page = $request->_dbi->getPage($template);
82 $current = $page->getCurrentRevision();
83 $this->_content = $current->getPackedContent();
84 } elseif ($initial_content = $request->getArg('initial_content')) {
85 $this->_content = $initial_content;
86 $this->_redirect_to = $request->getArg('save_and_redirect_to');
90 header("Content-Type: text/html; charset=UTF-8");
93 public function editPage()
96 $tokens = &$this->tokens;
97 $tokens['PAGE_LOCKED_MESSAGE'] = '';
98 $tokens['LOCK_CHANGED_MSG'] = '';
99 $tokens['CONCURRENT_UPDATE_MESSAGE'] = '';
100 $r =& $this->request;
102 if (isset($r->args['pref']['editWidth'])
103 and ($r->getPref('editWidth') != $r->args['pref']['editWidth'])
105 $r->_prefs->set('editWidth', $r->args['pref']['editWidth']);
107 if (isset($r->args['pref']['editHeight'])
108 and ($r->getPref('editHeight') != $r->args['pref']['editHeight'])
110 $r->_prefs->set('editHeight', $r->args['pref']['editHeight']);
113 if ($this->isModerated())
114 $tokens['PAGE_LOCKED_MESSAGE'] = $this->getModeratedMessage();
116 if (!$this->canEdit()) {
117 if ($this->isInitialEdit())
118 return $this->viewSource();
119 $tokens['PAGE_LOCKED_MESSAGE'] = $this->getLockedMessage();
120 } elseif ($r->getArg('save_and_redirect_to') != "") {
121 if (defined('ENABLE_CAPTCHA') and ENABLE_CAPTCHA && $this->Captcha->Failed()) {
122 $this->tokens['PAGE_LOCKED_MESSAGE'] =
123 HTML::p(HTML::h1($this->Captcha->failed_msg));
124 } elseif ($this->savePage()) {
127 $request->setArg('action', false);
128 $r->redirect(WikiURL($r->getArg('save_and_redirect_to')));
129 return true; // Page saved.
132 } elseif ($this->editaction == 'save') {
133 if (defined('ENABLE_CAPTCHA') and ENABLE_CAPTCHA && $this->Captcha->Failed()) {
134 $this->tokens['PAGE_LOCKED_MESSAGE'] =
135 HTML::p(HTML::h1($this->Captcha->failed_msg));
136 } elseif ($this->savePage()) {
137 return true; // Page saved.
141 } // coming from loadfile conflicts
142 elseif ($this->editaction == 'keep_old') {
143 // keep old page and do nothing
144 $this->_redirectToBrowsePage();
146 } elseif ($this->editaction == 'overwrite') {
147 // take the new content without diff
148 $source = $this->request->getArg('loadfile');
149 require_once 'lib/loadsave.php';
150 $this->request->setArg('loadfile', 1);
151 $this->request->setArg('overwrite', 1);
152 $this->request->setArg('merge', 0);
153 LoadFileOrDir($this->request);
154 $this->_redirectToBrowsePage();
156 } elseif ($this->editaction == 'upload') {
158 $plugin = new WikiPluginLoader("UpLoad");
160 // add link to content
164 if ($saveFailed and $this->isConcurrentUpdate()) {
165 // Get the text of the original page, and the two conflicting edits
166 // The diff3 class takes arrays as input. So retrieve content as
167 // an array, or convert it as necesary.
168 $orig = $this->page->getRevision($this->_currentVersion);
169 // FIXME: what if _currentVersion has be deleted?
170 $orig_content = $orig->getContent();
171 $this_content = explode("\n", $this->_content);
172 $other_content = $this->current->getContent();
173 require_once 'lib/diff3.php';
174 $diff = new diff3($orig_content, $this_content, $other_content);
175 $output = $diff->merged_output(_("Your version"), _("Other version"));
176 // Set the content of the textarea to the merged diff
177 // output, and update the version
178 $this->_content = implode("\n", $output);
179 $this->_currentVersion = $this->current->getVersion();
180 $this->version = $this->_currentVersion;
181 $unresolved = $diff->ConflictingBlocks;
182 $tokens['CONCURRENT_UPDATE_MESSAGE']
183 = $this->getConflictMessage($unresolved);
184 } elseif ($saveFailed && !$this->_isSpam) {
185 $tokens['CONCURRENT_UPDATE_MESSAGE'] =
186 HTML(HTML::h2(_("Some internal editing error")),
187 HTML::p(_("Your are probably trying to edit/create an invalid version of this page.")),
188 HTML::p(HTML::em(_("&version=-1 might help."))));
191 if ($this->editaction == 'edit_convert')
192 $tokens['PREVIEW_CONTENT'] = $this->getConvertedPreview();
193 if ($this->editaction == 'preview')
194 $tokens['PREVIEW_CONTENT'] = $this->getPreview(); // FIXME: convert to _MESSAGE?
195 if ($this->editaction == 'diff')
196 $tokens['PREVIEW_CONTENT'] = $this->getDiff();
198 // FIXME: NOT_CURRENT_MESSAGE?
199 $tokens = array_merge($tokens, $this->getFormElements());
201 return $this->output('editpage', _("Edit: %s"));
204 public function output($template, $title_fs)
207 $selected = &$this->selected;
208 $current = &$this->current;
210 if ($selected && $selected->getVersion() != $current->getVersion()) {
212 $pagelink = WikiLink($selected);
215 $pagelink = WikiLink($this->page);
218 $title = new FormattedText ($title_fs, $pagelink);
219 // not for dumphtml or viewsource
220 if (defined('ENABLE_WYSIWYG') and ENABLE_WYSIWYG and $template == 'editpage') {
221 $WikiTheme->addMoreHeaders($this->WysiwygEdit->Head());
222 //$tokens['PAGE_SOURCE'] = $this->WysiwygEdit->ConvertBefore($this->_content);
224 $template = Template($template, $this->tokens);
225 /* Tell google (and others) not to take notice of edit links */
226 if (defined('GOOGLE_LINKS_NOFOLLOW') and GOOGLE_LINKS_NOFOLLOW)
227 $args = array('ROBOTS_META' => "noindex,nofollow");
228 GeneratePage($template, $title, $rev);
232 public function viewSource()
234 assert($this->isInitialEdit());
235 assert($this->selected);
237 $this->tokens['PAGE_SOURCE'] = $this->_content;
238 $this->tokens['HIDDEN_INPUTS'] = HiddenInputs($this->request->getArgs());
239 return $this->output('viewsource', _("View Source: %s"));
242 private function updateLock()
245 if (!ENABLE_PAGE_PUBLIC && !ENABLE_EXTERNAL_PAGES) {
246 if ((bool)$this->page->get('locked') == (bool)$this->locked)
247 return false; // Not changed.
250 if (!$this->user->isAdmin()) {
251 // FIXME: some sort of message
252 return false; // not allowed.
254 if ((bool)$this->page->get('locked') != (bool)$this->locked) {
255 $this->page->set('locked', (bool)$this->locked);
256 $this->tokens['LOCK_CHANGED_MSG']
258 ? _("Page now locked.")
259 : _("Page now unlocked.") . " ");
262 if (defined('ENABLE_PAGE_PUBLIC') and ENABLE_PAGE_PUBLIC and (bool)$this->page->get('public') != (bool)$this->public) {
263 $this->page->set('public', (bool)$this->public);
264 $this->tokens['LOCK_CHANGED_MSG']
266 ? _("Page now public.")
267 : _("Page now not-public."));
271 if (defined('ENABLE_EXTERNAL_PAGES') and ENABLE_EXTERNAL_PAGES) {
272 if ((bool)$this->page->get('external') != (bool)$this->external) {
273 $this->page->set('external', (bool)$this->external);
274 $this->tokens['LOCK_CHANGED_MSG']
276 ? _("Page now external.")
277 : _("Page now not-external.")) . " ";
281 return $changed; // lock changed.
284 public function savePage()
286 $request = &$this->request;
288 if ($this->isUnchanged()) {
289 // Allow admin lock/unlock even if
290 // no text changes were made.
291 if ($this->updateLock()) {
292 $dbi = $request->getDbh();
295 // Save failed. No changes made.
296 $this->_redirectToBrowsePage();
300 if (!$this->user->isAdmin() and $this->isSpam()) {
301 $this->_isSpam = true;
305 $page = &$this->page;
307 // Include any meta-data from original page version which
308 // has not been explicitly updated.
309 $meta = $this->selected->getMetaData();
310 $meta = array_merge($meta, $this->meta);
313 $this->_content = $this->getContent();
314 $newrevision = $page->save($this->_content,
317 : $this->_currentVersion + 1,
320 if (!is_a($newrevision, 'WikiDB_PageRevision')) {
321 // Save failed. (Concurrent updates).
325 // New contents successfully saved...
328 // Clean out archived versions of this page.
329 require_once 'lib/ArchiveCleaner.php';
330 $cleaner = new ArchiveCleaner($GLOBALS['ExpireParams']);
331 $cleaner->cleanPageRevisions($page);
333 /* generate notification emails done in WikiDB::save to catch
334 all direct calls (admin plugins) */
336 // look at the errorstack
337 $errors = $GLOBALS['ErrorManager']->_postponed_errors;
338 $warnings = $GLOBALS['ErrorManager']->getPostponedErrorsAsHTML();
339 $GLOBALS['ErrorManager']->_postponed_errors = $errors;
341 $dbi = $request->getDbh();
345 if (empty($warnings->_content) && !$WikiTheme->getImageURL('signature')) {
346 // Do redirect to browse page if no signature has
347 // been defined. In this case, the user will most
348 // likely not see the rest of the HTML we generate
350 $request->setArg('action', false);
351 $this->_redirectToBrowsePage();
355 // Force browse of current page version.
356 $request->setArg('version', false);
357 // testme: does preview and more need action=edit?
358 $request->setArg('action', false);
360 $template = Template('savepage', $this->tokens);
361 $template->replace('CONTENT', $newrevision->getTransformedContent());
362 if (!empty($warnings->_content)) {
363 $template->replace('WARNINGS', $warnings);
364 unset($GLOBALS['ErrorManager']->_postponed_errors);
367 $pagelink = WikiLink($page);
369 GeneratePage($template, fmt("Saved: %s", $pagelink), $newrevision);
373 protected function isConcurrentUpdate()
375 assert($this->current->getVersion() >= $this->_currentVersion);
376 return $this->current->getVersion() != $this->_currentVersion;
379 protected function canEdit()
381 return !$this->page->get('locked') || $this->user->isAdmin();
384 protected function isInitialEdit()
386 return $this->_initialEdit;
389 private function isUnchanged()
391 $current = &$this->current;
392 return $this->_content == $current->getPackedContent();
396 * Handle AntiSpam here. How? http://wikiblacklist.blogspot.com/
397 * Need to check dynamically some blacklist wikipage settings
398 * (plugin WikiAccessRestrictions) and some static blacklist.
400 * More than NUM_SPAM_LINKS (default: 20) new external links.
401 * Disabled if NUM_SPAM_LINKS is 0
402 * ENABLE_SPAMASSASSIN: content patterns by babycart (only php >= 4.3 for now)
403 * ENABLE_SPAMBLOCKLIST: content domain blacklist
405 private function isSpam()
407 $current = &$this->current;
408 $request = &$this->request;
410 $oldtext = $current->getPackedContent();
411 $newtext =& $this->_content;
412 $numlinks = $this->numLinks($newtext);
413 $newlinks = $numlinks - $this->numLinks($oldtext);
414 // FIXME: in longer texts the NUM_SPAM_LINKS number should be increased.
415 // better use a certain text : link ratio.
417 // 1. Not more than NUM_SPAM_LINKS (default: 20) new external links
418 if ((NUM_SPAM_LINKS > 0) and ($newlinks >= NUM_SPAM_LINKS)) {
419 // Allow strictly authenticated users?
420 // TODO: mail the admin?
421 $this->tokens['PAGE_LOCKED_MESSAGE'] =
422 HTML($this->getSpamMessage(),
423 HTML::p(HTML::strong(_("Too many external links."))));
426 // 2. external babycart (SpamAssassin) check
427 // This will probably prevent from discussing sex or viagra related topics. So beware.
428 if (ENABLE_SPAMASSASSIN) {
429 require_once 'lib/spam_babycart.php';
430 if ($babycart = check_babycart($newtext, $request->get("REMOTE_ADDR"))) {
431 // TODO: mail the admin
432 if (is_array($babycart))
433 $this->tokens['PAGE_LOCKED_MESSAGE'] =
434 HTML($this->getSpamMessage(),
435 HTML::p(HTML::em(_("SpamAssassin reports: "),
436 join("\n", $babycart))));
440 // 3. extract (new) links and check surbl for blocked domains
441 if (defined('ENABLE_SPAMBLOCKLIST') and ENABLE_SPAMBLOCKLIST and ($newlinks > 5)) {
442 require_once 'lib/SpamBlocklist.php';
443 require_once 'lib/InlineParser.php';
444 $parsed = TransformLinks($newtext);
445 $oldparsed = TransformLinks($oldtext);
447 foreach ($oldparsed->_content as $link) {
448 if (is_a($link, 'Cached_ExternalLink') and !is_a($link, 'Cached_InterwikiLink')) {
449 $uri = $link->_getURL($this->page->getName());
454 foreach ($parsed->_content as $link) {
455 if (is_a($link, 'Cached_ExternalLink') and !is_a($link, 'Cached_InterwikiLink')) {
456 $uri = $link->_getURL($this->page->getName());
457 // only check new links, so admins may add blocked links.
458 if (!array_key_exists($uri, $oldlinks) and ($res = IsBlackListed($uri))) {
459 // TODO: mail the admin
460 $this->tokens['PAGE_LOCKED_MESSAGE'] =
461 HTML($this->getSpamMessage(),
462 HTML::p(HTML::strong(_("External links contain blocked domains:")),
463 HTML::ul(HTML::li(sprintf(_("%s is listed at %s with %s"),
464 $uri . " [" . $res[2] . "]", $res[0], $res[1])))));
477 /** Number of external links in the wikitext
479 private function numLinks(&$text)
481 return substr_count($text, "http://") + substr_count($text, "https://");
484 /** Header of the Anti Spam message
486 private function getSpamMessage()
489 HTML(HTML::h2(_("Spam Prevention")),
490 HTML::p(_("This page edit seems to contain spam and was therefore not saved."),
492 _("Sorry for the inconvenience.")),
496 protected function getPreview()
498 require_once 'lib/PageType.php';
499 $this->_content = $this->getContent();
500 return new TransformedText($this->page, $this->_content, $this->meta);
503 protected function getConvertedPreview()
505 require_once 'lib/PageType.php';
506 $this->_content = $this->getContent();
507 return new TransformedText($this->page, $this->_content, $this->meta);
510 private function getDiff()
512 require_once 'lib/diff.php';
515 $diff = new Diff($this->current->getContent(), explode("\n", $this->getContent()));
516 if ($diff->isEmpty()) {
517 $html->pushContent(HTML::hr(),
518 HTML::p(array('class' => 'warning_msg'),
519 _("Versions are identical")));
521 // New CSS formatted unified diffs
522 $fmt = new HtmlUnifiedDiffFormatter;
523 $html->pushContent($fmt->format($diff));
528 // possibly convert HTMLAREA content back to Wiki markup
529 private function getContent()
531 if (ENABLE_WYSIWYG) {
532 // don't store everything as html
533 if (!WYSIWYG_DEFAULT_PAGETYPE_HTML) {
534 // Wikiwyg shortcut to avoid the InlineTransformer:
535 if (WYSIWYG_BACKEND == "Wikiwyg") return $this->_content;
536 $xml_output = $this->WysiwygEdit->ConvertAfter($this->_content);
537 $this->_content = join("", $xml_output->_content);
539 $this->meta['pagetype'] = 'html';
541 return $this->_content;
543 return $this->_content;
547 protected function getLockedMessage()
550 HTML(HTML::h2(_("Page Locked")),
551 HTML::p(_("This page has been locked by the administrator so your changes can not be saved.")),
552 HTML::p(_("(Copy your changes to the clipboard. You can try editing a different page or save your text in a text editor.)")),
553 HTML::p(_("Sorry for the inconvenience.")));
556 private function isModerated()
558 return $this->page->get('moderation');
561 private function getModeratedMessage()
564 HTML(HTML::h2(WikiLink(_("ModeratedPage"))),
565 HTML::p(fmt("You can edit away, but your changes will have to be approved by the defined moderators at the definition in %s", WikiLink(_("ModeratedPage")))),
566 HTML::p(fmt("The approval has a grace period of 5 days. If you have your e-mail defined in your %s, you will get a notification of approval or rejection.",
567 WikiLink(_("UserPreferences")))));
570 protected function getConflictMessage($unresolved = false)
573 xgettext only knows about c/c++ line-continuation strings
574 it does not know about php's dot operator.
575 We want to translate this entire paragraph as one string, of course.
578 //$re_edit_link = Button('edit', _("Edit the new version"), $this->page);
581 $message = HTML::p(fmt("Some of the changes could not automatically be combined. Please look for sections beginning with ā%sā, and ending with ā%sā. You will need to edit those sections by hand before you click Save.",
582 "<<<<<<< " . _("Your version"),
583 ">>>>>>> " . _("Other version")));
585 $message = HTML::p(_("Please check it through before saving."));
587 /*$steps = HTML::ol(HTML::li(_("Copy your changes to the clipboard or to another temporary place (e.g. text editor).")),
588 HTML::li(fmt("%s of the page. You should now see the most current version of the page. Your changes are no longer there.",
590 HTML::li(_("Make changes to the file again. Paste your additions from the clipboard (or text editor).")),
591 HTML::li(_("Save your updated changes.")));
594 HTML(HTML::h2(_("Conflicting Edits!")),
595 HTML::p(_("In the time since you started editing this page, another user has saved a new version of it.")),
596 HTML::p(_("Your changes can not be saved as they are, since doing so would overwrite the other author's changes. So, your changes and those of the other author have been combined. The result is shown below.")),
600 private function getTextArea()
604 $request = &$this->request;
606 $readonly = !$this->canEdit(); // || $this->isConcurrentUpdate();
608 // WYSIWYG will need two pagetypes: raw wikitest and converted html
609 if (ENABLE_WYSIWYG) {
610 $this->_wikicontent = $this->_content;
611 $this->_content = $this->WysiwygEdit->ConvertBefore($this->_content);
612 // $this->getPreview();
613 //$this->_htmlcontent = $this->_content->asXML();
616 $textarea = HTML::textarea(array('class' => 'wikiedit',
617 'name' => 'edit[content]',
618 'id' => 'edit-content',
619 'rows' => $request->getPref('editHeight'),
620 'cols' => $request->getPref('editWidth'),
621 'readonly' => (bool)$readonly),
624 if (defined('JS_SEARCHREPLACE') and JS_SEARCHREPLACE) {
625 $this->tokens['JS_SEARCHREPLACE'] = 1;
626 $undo_btn = $WikiTheme->getImageURL("ed_undo.png");
627 $undo_d_btn = $WikiTheme->getImageURL("ed_undo_d.png");
628 // JS_SEARCHREPLACE from walterzorn.de
630 uri_undo_btn = '" . $undo_btn . "'
631 msg_undo_alt = '" . _("Undo") . "'
632 uri_undo_d_btn = '" . $undo_d_btn . "'
633 msg_undo_d_alt = '" . _("Undo disabled") . "'
634 msg_do_undo = '" . _("Operation undone") . "'
635 msg_replfound = '" . _("Substring ā\\1ā found \\2 times. Replace with ā\\3ā?") . "'
636 msg_replnot = '" . _("String ā%sā not found.") . "'
637 msg_repl_title = '" . _("Search & Replace") . "'
638 msg_repl_search = '" . _("Search for") . "'
639 msg_repl_replace_with = '" . _("Replace with") . "'
640 msg_repl_ok = '" . _("OK") . "'
641 msg_repl_close = '" . _("Close") . "'
643 if (empty($WikiTheme->_headers_printed)) {
644 $WikiTheme->addMoreHeaders($js);
645 $WikiTheme->addMoreAttr('body', "SearchReplace", " onload='define_f()'");
646 } else { // from an actionpage: WikiBlog, AddComment, WikiForum
650 $WikiTheme->addMoreAttr('body', "editfocus", "document.getElementById('edit-content]').editarea.focus()");
653 if (defined('ENABLE_WYSIWYG') and ENABLE_WYSIWYG) {
654 return $this->WysiwygEdit->Textarea($textarea, $this->_wikicontent,
655 $textarea->getAttr('name'));
656 } elseif (defined('ENABLE_EDIT_TOOLBAR') and ENABLE_EDIT_TOOLBAR) {
657 $init = JavaScript("var data_path = '" . javascript_quote_string(DATA_PATH) . "';\n");
658 $js = JavaScript('', array('src' => $WikiTheme->_findData("toolbar.js")));
659 if (empty($WikiTheme->_headers_printed)) {
660 $WikiTheme->addMoreHeaders($init);
661 $WikiTheme->addMoreHeaders($js);
662 } else { // from an actionpage: WikiBlog, AddComment, WikiForum
665 printXML(JavaScript('define_f()'));
667 $toolbar = HTML::div(array('class' => 'edit-toolbar', 'id' => 'toolbar'));
668 $toolbar->pushContent(HTML::input(array('src' => $WikiTheme->getImageURL("ed_save.png"),
669 'name' => 'edit[save]',
670 'class' => 'toolbar',
672 'title' => _('Save'),
673 'type' => 'image')));
674 $toolbar->pushContent(HTML::input(array('src' => $WikiTheme->getImageURL("ed_preview.png"),
675 'name' => 'edit[preview]',
676 'class' => 'toolbar',
677 'alt' => _('Preview'),
678 'title' => _('Preview'),
679 'type' => 'image')));
680 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_format_bold.png"),
681 'class' => 'toolbar',
682 'alt' => _('Bold text'),
683 'title' => _('Bold text'),
684 'onclick' => "insertTags('**','**','"._('Bold text')."'); return true;")));
685 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_format_italic.png"),
686 'class' => 'toolbar',
687 'alt' => _('Italic text'),
688 'title' => _('Italic text'),
689 'onclick' => "insertTags('//','//','"._('Italic text')."'); return true;")));
690 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_format_strike.png"),
691 'class' => 'toolbar',
692 'alt' => _('Strike-through text'),
693 'title' => _('Strike-through text'),
694 'onclick' => "insertTags('<s>','</s>','"._('Strike-through text')."'); return true;")));
695 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_format_color.png"),
696 'class' => 'toolbar',
697 'alt' => _('Color text'),
698 'title' => _('Color text'),
699 'onclick' => "insertTags('%color=green%','%%','"._('Color text')."'); return true;")));
700 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_pagelink.png"),
701 'class' => 'toolbar',
702 'alt' => _('Link to page'),
703 'title' => _('Link to page'),
704 'onclick' => "insertTags('[[',']]','"._('PageName|optional label')."'); return true;")));
705 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_link.png"),
706 'class' => 'toolbar',
707 'alt' => _('External link (remember http:// prefix)'),
708 'title' => _('External link (remember http:// prefix)'),
709 'onclick' => "insertTags('[[',']]','"._('http://www.example.com|optional label')."'); return true;")));
710 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_headline.png"),
711 'class' => 'toolbar',
712 'alt' => _('Level 1 headline'),
713 'title' => _('Level 1 headline'),
714 'onclick' => 'insertTags("\n== "," ==\n","'._("Headline text").'"); return true;')));
715 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_nowiki.png"),
716 'class' => 'toolbar',
717 'alt' => _('Ignore wiki formatting'),
718 'title' => _('Ignore wiki formatting'),
719 'onclick' => 'insertTags("<verbatim>\n","\n</verbatim>","'._("Insert non-formatted text here").'"); return true;')));
721 $username = $request->_user->UserName();
722 $signature = " āā[[" . $username . "]] " . CTime() . '\n';
723 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_sig.png"),
724 'class' => 'toolbar',
725 'alt' => _('Your signature'),
726 'title' => _('Your signature'),
727 'onclick' => "insertTags('".$signature."','',''); return true;")));
728 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_hr.png"),
729 'class' => 'toolbar',
730 'alt' => _('Horizontal line'),
731 'title' => _('Horizontal line'),
732 'onclick' => 'insertTags("\n----\n","",""); return true;')));
733 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_table.png"),
734 'class' => 'toolbar',
735 'alt' => _('Sample table'),
736 'title' => _('Sample table'),
737 'onclick' => 'insertTags("\n{| class=\"bordered\"\n|+ This is the table caption\n|-\n! Header A !! Header B !! Header C\n|-\n| Cell A1 || Cell B1 || Cell C1\n|-\n| Cell A2 || Cell B2 || Cell C2\n|-\n| Cell A3 || Cell B3 || Cell C3\n|}\n","",""); return true;')));
738 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_enumlist.png"),
739 'class' => 'toolbar',
740 'alt' => _('Enumeration'),
741 'title' => _('Enumeration'),
742 'onclick' => 'insertTags("\n# Item 1\n# Item 2\n# Item 3\n","",""); return true;')));
743 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_list.png"),
744 'class' => 'toolbar',
746 'title' => _('List'),
747 'onclick' => 'insertTags("\n* Item 1\n* Item 2\n* Item 3\n","",""); return true;')));
748 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_toc.png"),
749 'class' => 'toolbar',
750 'alt' => _('Table of Contents'),
751 'title' => _('Table of Contents'),
752 'onclick' => 'insertTags("<<CreateToc with_toclink||=1>>\n","",""); return true;')));
753 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_redirect.png"),
754 'class' => 'toolbar',
755 'alt' => _('Redirect'),
756 'title' => _('Redirect'),
757 'onclick' => "insertTags('<<RedirectTo page=\"','\">>','"._('Page Name')."'); return true;")));
758 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_templateplugin.png"),
759 'class' => 'toolbar',
760 'alt' => _('Insert Dynamic Template'),
761 'title' => _('Insert Dynamic Template'),
762 'onclick' => "insertTags('{{','}}','"._('Template Name')."'); return true;")));
763 if (defined('TOOLBAR_TEMPLATE_PULLDOWN') and TOOLBAR_TEMPLATE_PULLDOWN) {
764 $toolbar->pushContent($this->templatePulldown(TOOLBAR_TEMPLATE_PULLDOWN));
766 $toolbar->pushContent($this->categoriesPulldown());
767 $toolbar->pushContent($this->pluginPulldown());
768 if (defined('TOOLBAR_PAGELINK_PULLDOWN') and TOOLBAR_PAGELINK_PULLDOWN) {
769 $toolbar->pushContent($this->pagesPulldown(TOOLBAR_PAGELINK_PULLDOWN));
771 if (defined('TOOLBAR_IMAGE_PULLDOWN') and TOOLBAR_IMAGE_PULLDOWN) {
772 $toolbar->pushContent($this->imagePulldown());
774 if (defined('JS_SEARCHREPLACE') and JS_SEARCHREPLACE) {
775 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_replace.png"),
776 'class' => 'toolbar',
777 'alt' => _('Search & Replace'),
778 'title' => _('Search & Replace'),
779 'onclick' => "replace();")));
780 $toolbar->pushContent(HTML::img(array('src' => $WikiTheme->getImageURL("ed_undo_d.png"),
781 'class' => 'toolbar',
783 'alt' => _('Undo Search & Replace'),
784 'title' => _('Undo Search & Replace'),
785 'onclick' => "do_undo();")));
787 return HTML($toolbar, $textarea);
793 private function categoriesPulldown()
796 * @var WikiRequest $request
801 require_once 'lib/TextSearchQuery.php';
802 $dbi =& $request->_dbi;
803 // KEYWORDS formerly known as $KeywordLinkRegexp
804 $pages = $dbi->titleSearch(new TextSearchQuery(KEYWORDS, true));
806 $categories = array();
807 while ($p = $pages->next()) {
808 $page = $p->getName();
809 $categories[] = "['$page', '%0A----%0A%5B%5B" . $page . "%5D%5D']";
811 if (!$categories) return '';
812 // Ensure this to be inserted at the very end. Hence we added the id to the function.
813 $more_buttons = HTML::img(array('class' => "toolbar",
814 'id' => 'tb-categories',
815 'src' => $WikiTheme->getImageURL("ed_category.png"),
816 'title' => _("Insert Categories"),
817 'alt' => _("Insert Categories"), // to detect this at js
818 'onclick' => "showPulldown('" .
819 _("Insert Categories")
820 . "',[" . join(",", $categories) . "],'"
821 . _("Insert") . "','"
822 . _("Close") . "','tb-categories')"));
823 return $more_buttons;
828 private function pluginPulldown()
831 global $AllAllowedPlugins;
833 $plugin_dir = 'lib/plugin';
834 if (defined('PHPWIKI_DIR'))
835 $plugin_dir = PHPWIKI_DIR . "/$plugin_dir";
836 $pd = new fileSet($plugin_dir, '*.php');
837 $plugins = $pd->getFiles();
840 if (!empty($plugins)) {
842 require_once 'lib/WikiPlugin.php';
843 $w = new WikiPluginLoader();
844 foreach ($plugins as $plugin) {
845 $pluginName = str_replace(".php", "", $plugin);
846 if (in_array($pluginName, $AllAllowedPlugins)) {
847 $p = $w->getPlugin($pluginName, false); // second arg?
848 // trap php files which aren't WikiPlugin~s
849 if (strtolower(substr(get_parent_class($p), 0, 10)) == 'wikiplugin') {
851 $desc = $p->getArgumentsDescription();
852 $src = array("\n", '"', "'", '|', '[', ']', '\\');
853 $replace = array('%0A', '%22', '%27', '%7C', '%5B', '%5D', '%5C');
854 $desc = str_replace("<br />", ' ', $desc->asXML());
856 $plugin_args = ' ' . str_replace($src, $replace, $desc);
857 $toinsert = "%0A<<" . $pluginName . $plugin_args . ">>"; // args?
858 $plugin_js .= ",['$pluginName','$toinsert']";
862 $plugin_js = substr($plugin_js, 1);
863 $more_buttons = HTML::img(array('class' => "toolbar",
864 'id' => 'tb-plugins',
865 'src' => $WikiTheme->getImageURL("ed_plugins.png"),
866 'title' => _("Insert Plugin"),
867 'alt' => _("Insert Plugin"),
868 'onclick' => "showPulldown('" .
870 . "',[" . $plugin_js . "],'"
871 . _("Insert") . "','"
872 . _("Close") . "','tb-plugins')"));
873 return $more_buttons;
878 private function pagesPulldown($query)
881 * @var WikiRequest $request
885 require_once 'lib/TextSearchQuery.php';
886 $dbi =& $request->_dbi;
887 $page_iter = $dbi->titleSearch(new TextSearchQuery($query, false, 'auto'));
888 if ($page_iter->count() > 0) {
891 while ($p = $page_iter->next()) {
892 $page = $p->getName();
893 $pages[] = "['$page', '%5B%5B" . $page . "%5D%5D']";
895 return HTML::img(array('class' => "toolbar",
897 'src' => $WikiTheme->getImageURL("ed_pages.png"),
898 'title' => _("Insert PageLink"),
899 'alt' => _("Insert PageLink"),
900 'onclick' => "showPulldown('" .
902 . "',[" . join(",", $pages) . "],'"
903 . _("Insert") . "','"
904 . _("Close") . "','tb-pages')"));
909 private function templatePulldown($query)
912 require_once 'lib/TextSearchQuery.php';
913 $dbi =& $request->_dbi;
914 $page_iter = $dbi->titleSearch(new TextSearchQuery($query, false, 'auto'));
915 if ($page_iter->count()) {
918 while ($p = $page_iter->next()) {
919 $rev = $p->getCurrentRevision();
920 $toinsert = str_replace(array("\n", '"'), array('__nl__', '__quot__'), $rev->_get_content());
921 $pages_js .= ",['" . $p->getName() . "','__nl__$toinsert']";
923 $pages_js = substr($pages_js, 1);
924 if (!empty($pages_js))
926 (array('class' => "toolbar",
927 'id' => 'tb-templates',
928 'src' => $WikiTheme->getImageURL("ed_template.png"),
929 'title' => _("Insert Static Template"),
930 'alt' => _("Insert Static Template"),
931 'onclick' => "showPulldown('" .
932 _("Insert Static Template")
933 . "',[" . $pages_js . "],'"
934 . _("Insert") . "','"
935 . _("Close") . "','tb-templates')"));
940 private function imagePulldown()
942 global $WikiTheme, $request;
944 $image_dir = getUploadFilePath();
945 $pd = new imageOrVideoSet($image_dir, '*');
946 $images = $pd->getFiles();
948 if (defined('UPLOAD_USERDIR') and UPLOAD_USERDIR) {
949 $image_dir .= "/" . $request->_user->_userid;
950 $pd = new imageOrVideoSet($image_dir, '*');
951 $images = array_merge($images, $pd->getFiles());
955 if (!empty($images)) {
957 foreach ($images as $image) {
958 $image_js .= ",['$image','{{" . $image . "}}']";
960 $image_js = substr($image_js, 1);
961 $more_buttons = HTML::img(array('class' => "toolbar",
963 'src' => $WikiTheme->getImageURL("ed_image.png"),
964 'title' => _("Insert Image or Video"),
965 'alt' => _("Insert Image or Video"),
966 'onclick' => "showPulldown('" .
967 _("Insert Image or Video")
968 . "',[" . $image_js . "],'"
969 . _("Insert") . "','"
970 . _("Close") . "','tb-images')"));
971 return $more_buttons;
976 protected function getFormElements()
979 $request = &$this->request;
980 $page = &$this->page;
982 $h = array('action' => 'edit',
983 'pagename' => $page->getName(),
984 'version' => $this->version,
985 'edit[pagetype]' => $this->meta['pagetype'],
986 'edit[current_version]' => $this->_currentVersion);
988 $el['HIDDEN_INPUTS'] = HiddenInputs($h);
989 $el['EDIT_TEXTAREA'] = $this->getTextArea();
990 if (ENABLE_CAPTCHA) {
991 $el = array_merge($el, $this->Captcha->getFormElements());
994 = HTML::input(array('type' => 'text',
995 'class' => 'wikitext',
996 'id' => 'edit-summary',
997 'name' => 'edit[summary]',
1000 'value' => $this->meta['summary']));
1001 $el['MINOR_EDIT_CB']
1002 = HTML::input(array('type' => 'checkbox',
1003 'name' => 'edit[minor_edit]',
1004 'id' => 'edit-minor_edit',
1005 'checked' => (bool)$this->meta['is_minor_edit']));
1007 = HTML::input(array('type' => 'checkbox',
1008 'name' => 'edit[locked]',
1009 'id' => 'edit-locked',
1010 'disabled' => (bool)!$this->user->isAdmin(),
1011 'checked' => (bool)$this->locked));
1012 if (ENABLE_PAGE_PUBLIC) {
1014 = HTML::input(array('type' => 'checkbox',
1015 'name' => 'edit[public]',
1016 'id' => 'edit-public',
1017 'disabled' => (bool)!$this->user->isAdmin(),
1018 'checked' => (bool)$this->page->get('public')));
1020 if (ENABLE_EXTERNAL_PAGES) {
1022 = HTML::input(array('type' => 'checkbox',
1023 'name' => 'edit[external]',
1024 'id' => 'edit-external',
1025 'disabled' => (bool)!$this->user->isAdmin(),
1026 'checked' => (bool)$this->page->get('external')));
1028 if (ENABLE_WYSIWYG) {
1029 if (($this->version == 0) and ($request->getArg('mode') != 'wysiwyg')) {
1030 $el['WYSIWYG_B'] = Button(array("action" => "edit", "mode" => "wysiwyg"), "Wysiwyg Editor");
1033 $el['PREVIEW_B'] = Button('submit:edit[preview]', _("Preview"),
1035 array('title' => _('Preview the current content')));
1036 $el['SAVE_B'] = Button('submit:edit[save]',
1037 _("Save"), 'wikiaction',
1038 array('title' => _('Save the current content as wikipage')));
1039 $el['CHANGES_B'] = Button('submit:edit[diff]',
1040 _("Changes"), 'wikiaction',
1041 array('title' => _('Preview the current changes as diff')));
1042 $el['UPLOAD_B'] = Button('submit:edit[upload]',
1043 _("Upload"), 'wikiaction',
1044 array('title' => _('Select a local file and press Upload to attach into this page')));
1045 $el['SPELLCHECK_B'] = Button('submit:edit[SpellCheck]',
1046 _("Spell Check"), 'wikiaction',
1047 array('title' => _('Check the spelling')));
1048 $el['IS_CURRENT'] = $this->version == $this->current->getVersion();
1049 $el['SEP'] = $WikiTheme->getButtonSeparator();
1050 $el['AUTHOR_MESSAGE'] = fmt("Author will be logged as %s.",
1051 HTML::em($this->user->getId()));
1056 private function _redirectToBrowsePage()
1058 $this->request->redirect(WikiURL($this->page, array(), 'absolute_url'));
1061 private function _restoreState()
1063 $request = &$this->request;
1065 $posted = $request->getArg('edit');
1066 $request->setArg('edit', false);
1069 || !$request->isPost()
1070 || !in_array($request->getArg('action'), array('edit', 'loadfile'))
1074 if (!isset($posted['content']) || !is_string($posted['content']))
1076 $this->_content = preg_replace('/[ \t\r]+\n/', "\n",
1077 rtrim($posted['content']));
1078 $this->_content = $this->getContent();
1080 $this->_currentVersion = (int)$posted['current_version'];
1082 if ($this->_currentVersion < 0)
1084 if ($this->_currentVersion > $this->current->getVersion())
1085 return false; // FIXME: some kind of warning?
1087 $meta['summary'] = trim(substr($posted['summary'], 0, 256));
1088 $meta['is_minor_edit'] = !empty($posted['minor_edit']);
1089 $meta['pagetype'] = !empty($posted['pagetype']) ? $posted['pagetype'] : false;
1091 $meta['captcha_input'] = !empty($posted['captcha_input']) ?
1092 $posted['captcha_input'] : '';
1094 $this->meta = array_merge($this->meta, $meta);
1095 $this->locked = !empty($posted['locked']);
1096 if (ENABLE_PAGE_PUBLIC)
1097 $this->public = !empty($posted['public']);
1098 if (ENABLE_EXTERNAL_PAGES)
1099 $this->external = !empty($posted['external']);
1101 foreach (array('preview', 'save', 'edit_convert',
1102 'keep_old', 'overwrite', 'diff', 'upload') as $o) {
1103 if (!empty($posted[$o]))
1104 $this->editaction = $o;
1106 if (empty($this->editaction))
1107 $this->editaction = 'edit';
1112 private function _initializeState()
1114 $request = &$this->request;
1115 $current = &$this->current;
1116 $selected = &$this->selected;
1117 $user = &$this->user;
1120 NoSuchRevision($request, $this->page, $this->version); // noreturn
1122 $this->_currentVersion = $current->getVersion();
1123 $this->_content = $selected->getPackedContent();
1125 $this->locked = $this->page->get('locked');
1127 // If author same as previous author, default minor_edit to on.
1128 $age = $this->meta['mtime'] - $current->get('mtime');
1129 $this->meta['is_minor_edit'] = ($age < MINOR_EDIT_TIMEOUT
1130 && $current->get('author') == $user->getId()
1133 $this->meta['pagetype'] = $selected->get('pagetype');
1134 if ($this->meta['pagetype'] == 'wikiblog')
1135 $this->meta['summary'] = $selected->get('summary'); // keep blog title
1137 $this->meta['summary'] = '';
1138 $this->editaction = 'edit';
1142 class LoadFileConflictPageEditor
1145 public function editPage($saveFailed = true)
1147 $tokens = &$this->tokens;
1149 if (!$this->canEdit()) {
1150 if ($this->isInitialEdit()) {
1151 return $this->viewSource();
1153 $tokens['PAGE_LOCKED_MESSAGE'] = $this->getLockedMessage();
1154 } elseif ($this->editaction == 'save') {
1155 if ($this->savePage()) {
1156 return true; // Page saved.
1161 if ($saveFailed || $this->isConcurrentUpdate()) {
1162 // Get the text of the original page, and the two conflicting edits
1163 // The diff class takes arrays as input. So retrieve content as
1164 // an array, or convert it as necesary.
1165 $orig = $this->page->getRevision($this->_currentVersion);
1166 $this_content = explode("\n", $this->_content);
1167 $other_content = $this->current->getContent();
1168 require_once 'lib/diff.php';
1169 $diff2 = new Diff($other_content, $this_content);
1170 $context_lines = max(4, count($other_content) + 1,
1171 count($this_content) + 1);
1172 $fmt = new BlockDiffFormatter($context_lines);
1174 $this->_content = $fmt->format($diff2);
1175 // FIXME: integrate this into class BlockDiffFormatter
1176 $this->_content = str_replace(">>>>>>>\n<<<<<<<\n", "=======\n",
1178 $this->_content = str_replace("<<<<<<<\n>>>>>>>\n", "=======\n",
1181 $this->_currentVersion = $this->current->getVersion();
1182 $this->version = $this->_currentVersion;
1183 $tokens['CONCURRENT_UPDATE_MESSAGE'] = $this->getConflictMessage();
1186 if ($this->editaction == 'edit_convert')
1187 $tokens['PREVIEW_CONTENT'] = $this->getConvertedPreview();
1188 if ($this->editaction == 'preview')
1189 $tokens['PREVIEW_CONTENT'] = $this->getPreview(); // FIXME: convert to _MESSAGE?
1191 // FIXME: NOT_CURRENT_MESSAGE?
1192 $tokens = array_merge($tokens, $this->getFormElements());
1193 // we need all GET params for loadfile overwrite
1194 if ($this->request->getArg('action') == 'loadfile') {
1196 $this->tokens['HIDDEN_INPUTS'] =
1198 (array('source' => $this->request->getArg('source'),
1200 $this->tokens['HIDDEN_INPUTS']);
1201 // add two conflict resolution buttons before preview and save.
1202 $tokens['PREVIEW_B'] = HTML(
1203 Button('submit:edit[keep_old]',
1204 _("Keep old"), 'wikiaction'),
1206 Button('submit:edit[overwrite]',
1207 _("Overwrite with new"), 'wikiaction'),
1209 $tokens['PREVIEW_B']);
1211 return $this->output('editpage', _("Merge and Edit: %s"));
1214 public function output($template, $title_fs)
1216 $selected = &$this->selected;
1217 $current = &$this->current;
1219 if ($selected && $selected->getVersion() != $current->getVersion()) {
1220 $pagelink = WikiLink($selected);
1222 $pagelink = WikiLink($this->page);
1225 $title = new FormattedText ($title_fs, $pagelink);
1226 $this->tokens['HEADER'] = $title;
1227 //hack! there's no TITLE in editpage, but in the previous top template
1228 if (empty($this->tokens['PAGE_LOCKED_MESSAGE']))
1229 $this->tokens['PAGE_LOCKED_MESSAGE'] = HTML::h3($title);
1231 $this->tokens['PAGE_LOCKED_MESSAGE'] = HTML(HTML::h3($title),
1232 $this->tokens['PAGE_LOCKED_MESSAGE']);
1233 $template = Template($template, $this->tokens);
1235 //GeneratePage($template, $title, $rev);
1236 PrintXML($template);
1240 protected function getConflictMessage($unresolved = false)
1242 $message = HTML(HTML::p(fmt("Some of the changes could not automatically be combined. Please look for sections beginning with ā%sā, and ending with ā%sā. You will need to edit those sections by hand before you click Save.",
1245 HTML::p(_("Please check it through before saving."))));