]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/editpage.php
Custom pages for Fusionforge: FindPage, FullTextSearch, TitleSearch
[SourceForge/phpwiki.git] / lib / editpage.php
1 <?php
2 require_once 'lib/Template.php';
3 require_once 'lib/WikiUser.php';
4
5 class PageEditor
6 {
7     public $request;
8     public $user;
9     public $page;
10     /**
11      * @var WikiDB_PageRevision $current
12      */
13     public $current;
14     public $editaction;
15     public $locked;
16     public $public;
17     public $external;
18     public $_currentVersion;
19
20     /**
21      * @var UserPreferences $_prefs
22      */
23     private $_prefs;
24     private $_isSpam;
25     private $_wikicontent;
26
27     /**
28      * @param WikiRequest $request
29      */
30     function __construct(&$request)
31     {
32         $this->request = &$request;
33
34         $this->user = $request->getUser();
35         $this->page = $request->getPage();
36
37         $this->current = $this->page->getCurrentRevision(false);
38
39         // HACKish short circuit to browse on action=create
40         if ($request->getArg('action') == 'create') {
41             if (!$this->current->hasDefaultContents())
42                 $request->redirect(WikiURL($this->page->getName())); // noreturn
43         }
44
45         $this->meta = array('author' => $this->user->getId(),
46             'author_id' => $this->user->getAuthenticatedId(),
47             'mtime' => time());
48
49         $this->tokens = array();
50
51         if (defined('ENABLE_WYSIWYG') and ENABLE_WYSIWYG) {
52             $backend = WYSIWYG_BACKEND;
53             // TODO: error message
54             require_once("lib/WysiwygEdit/$backend.php");
55             $class = "WysiwygEdit_$backend";
56             $this->WysiwygEdit = new $class();
57         }
58         if (defined('ENABLE_CAPTCHA' and ENABLE_CAPTCHA)) {
59             require_once 'lib/Captcha.php';
60             $this->Captcha = new Captcha($this->meta);
61         }
62
63         $version = $request->getArg('version');
64         if ($version !== false) {
65             $this->selected = $this->page->getRevision($version);
66             $this->version = $version;
67         } else {
68             $this->version = $this->current->getVersion();
69             $this->selected = $this->page->getRevision($this->version);
70         }
71
72         if ($this->_restoreState()) {
73             $this->_initialEdit = false;
74         } else {
75             $this->_initializeState();
76             $this->_initialEdit = true;
77
78             // The edit request has specified some initial content from a template
79             if (($template = $request->getArg('template'))
80                 and $request->_dbi->isWikiPage($template)
81             ) {
82                 $page = $request->_dbi->getPage($template);
83                 $current = $page->getCurrentRevision();
84                 $this->_content = $current->getPackedContent();
85             } elseif ($initial_content = $request->getArg('initial_content')) {
86                 $this->_content = $initial_content;
87                 $this->_redirect_to = $request->getArg('save_and_redirect_to');
88             }
89         }
90         if (!headers_sent())
91             header("Content-Type: text/html; charset=UTF-8");
92     }
93
94     public function editPage()
95     {
96         $saveFailed = false;
97         $tokens = &$this->tokens;
98         $tokens['PAGE_LOCKED_MESSAGE'] = '';
99         $tokens['LOCK_CHANGED_MSG'] = '';
100         $tokens['CONCURRENT_UPDATE_MESSAGE'] = '';
101         $r =& $this->request;
102
103         if (isset($r->args['pref']['editWidth'])
104             and ($r->getPref('editWidth') != $r->args['pref']['editWidth'])
105         ) {
106             $r->_prefs->set('editWidth', $r->args['pref']['editWidth']);
107         }
108         if (isset($r->args['pref']['editHeight'])
109             and ($r->getPref('editHeight') != $r->args['pref']['editHeight'])
110         ) {
111             $r->_prefs->set('editHeight', $r->args['pref']['editHeight']);
112         }
113
114         if ($this->isModerated())
115             $tokens['PAGE_LOCKED_MESSAGE'] = $this->getModeratedMessage();
116
117         if (!$this->canEdit()) {
118             if ($this->isInitialEdit())
119                 return $this->viewSource();
120             $tokens['PAGE_LOCKED_MESSAGE'] = $this->getLockedMessage();
121         } elseif ($r->getArg('save_and_redirect_to') != "") {
122             if (defined('ENABLE_CAPTCHA') and ENABLE_CAPTCHA && $this->Captcha->Failed()) {
123                 $this->tokens['PAGE_LOCKED_MESSAGE'] =
124                     HTML::p(HTML::h1($this->Captcha->failed_msg));
125             } elseif ($this->savePage()) {
126                 // noreturn
127                 global $request;
128                 $request->setArg('action', false);
129                 $r->redirect(WikiURL($r->getArg('save_and_redirect_to')));
130                 return true; // Page saved.
131             }
132             $saveFailed = true;
133         } elseif ($this->editaction == 'save') {
134             if (defined('ENABLE_CAPTCHA') and ENABLE_CAPTCHA && $this->Captcha->Failed()) {
135                 $this->tokens['PAGE_LOCKED_MESSAGE'] =
136                     HTML::p(HTML::h1($this->Captcha->failed_msg));
137             } elseif ($this->savePage()) {
138                 return true; // Page saved.
139             } else {
140                 $saveFailed = true;
141             }
142         } // coming from loadfile conflicts
143         elseif ($this->editaction == 'keep_old') {
144             // keep old page and do nothing
145             $this->_redirectToBrowsePage();
146             return true;
147         } elseif ($this->editaction == 'overwrite') {
148             // take the new content without diff
149             $source = $this->request->getArg('loadfile');
150             require_once 'lib/loadsave.php';
151             $this->request->setArg('loadfile', 1);
152             $this->request->setArg('overwrite', 1);
153             $this->request->setArg('merge', 0);
154             LoadFileOrDir($this->request);
155             $this->_redirectToBrowsePage();
156             return true;
157         } elseif ($this->editaction == 'upload') {
158             // run plugin UpLoad
159             $plugin = new WikiPluginLoader("UpLoad");
160             $plugin->run();
161             // add link to content
162             ;
163         }
164
165         if ($saveFailed and $this->isConcurrentUpdate()) {
166             // Get the text of the original page, and the two conflicting edits
167             // The diff3 class takes arrays as input.  So retrieve content as
168             // an array, or convert it as necesary.
169             $orig = $this->page->getRevision($this->_currentVersion);
170             // FIXME: what if _currentVersion has be deleted?
171             $orig_content = $orig->getContent();
172             $this_content = explode("\n", $this->_content);
173             $other_content = $this->current->getContent();
174             require_once 'lib/diff3.php';
175             $diff = new diff3($orig_content, $this_content, $other_content);
176             $output = $diff->merged_output(_("Your version"), _("Other version"));
177             // Set the content of the textarea to the merged diff
178             // output, and update the version
179             $this->_content = implode("\n", $output);
180             $this->_currentVersion = $this->current->getVersion();
181             $this->version = $this->_currentVersion;
182             $unresolved = $diff->ConflictingBlocks;
183             $tokens['CONCURRENT_UPDATE_MESSAGE']
184                 = $this->getConflictMessage($unresolved);
185         } elseif ($saveFailed && !$this->_isSpam) {
186             $tokens['CONCURRENT_UPDATE_MESSAGE'] =
187                 HTML(HTML::h2(_("Some internal editing error")),
188                     HTML::p(_("Your are probably trying to edit/create an invalid version of this page.")),
189                     HTML::p(HTML::em(_("&version=-1 might help."))));
190         }
191
192         if ($this->editaction == 'edit_convert')
193             $tokens['PREVIEW_CONTENT'] = $this->getConvertedPreview();
194         if ($this->editaction == 'preview')
195             $tokens['PREVIEW_CONTENT'] = $this->getPreview(); // FIXME: convert to _MESSAGE?
196         if ($this->editaction == 'diff')
197             $tokens['PREVIEW_CONTENT'] = $this->getDiff();
198
199         // FIXME: NOT_CURRENT_MESSAGE?
200         $tokens = array_merge($tokens, $this->getFormElements());
201
202         if (ENABLE_EDIT_TOOLBAR and !ENABLE_WYSIWYG) {
203             require_once 'lib/EditToolbar.php';
204             $toolbar = new EditToolbar();
205             $tokens = array_merge($tokens, $toolbar->getTokens());
206         } else {
207             $tokens['EDIT_TOOLBAR'] = '';
208         }
209
210         return $this->output('editpage', _("Edit: %s"));
211     }
212
213     public function output($template, $title_fs)
214     {
215         global $WikiTheme;
216         $selected = &$this->selected;
217         $current = &$this->current;
218
219         if ($selected && $selected->getVersion() != $current->getVersion()) {
220             $rev = $selected;
221             $pagelink = WikiLink($selected);
222         } else {
223             $rev = $current;
224             $pagelink = WikiLink($this->page);
225         }
226
227         $title = new FormattedText ($title_fs, $pagelink);
228         // not for dumphtml or viewsource
229         if (defined('ENABLE_WYSIWYG') and ENABLE_WYSIWYG and $template == 'editpage') {
230             $WikiTheme->addMoreHeaders($this->WysiwygEdit->Head());
231             //$tokens['PAGE_SOURCE'] = $this->WysiwygEdit->ConvertBefore($this->_content);
232         }
233         $template = Template($template, $this->tokens);
234         /* Tell google (and others) not to take notice of edit links */
235         if (defined('GOOGLE_LINKS_NOFOLLOW') and GOOGLE_LINKS_NOFOLLOW)
236             $args = array('ROBOTS_META' => "noindex,nofollow");
237         GeneratePage($template, $title, $rev);
238         return true;
239     }
240
241     public function viewSource()
242     {
243         assert($this->isInitialEdit());
244         assert($this->selected);
245
246         $this->tokens['PAGE_SOURCE'] = $this->_content;
247         $this->tokens['HIDDEN_INPUTS'] = HiddenInputs($this->request->getArgs());
248         return $this->output('viewsource', _("View Source: %s"));
249     }
250
251     private function updateLock()
252     {
253         $changed = false;
254         if (!ENABLE_PAGE_PUBLIC && !ENABLE_EXTERNAL_PAGES) {
255             if ((bool)$this->page->get('locked') == (bool)$this->locked)
256                 return false; // Not changed.
257         }
258
259         if (!$this->user->isAdmin()) {
260             // FIXME: some sort of message
261             return false; // not allowed.
262         }
263         if ((bool)$this->page->get('locked') != (bool)$this->locked) {
264             $this->page->set('locked', (bool)$this->locked);
265             $this->tokens['LOCK_CHANGED_MSG']
266                 .= ($this->locked
267                 ? _("Page now locked.")
268                 : _("Page now unlocked.") . " ");
269             $changed = true;
270         }
271         if (defined('ENABLE_PAGE_PUBLIC') and ENABLE_PAGE_PUBLIC and (bool)$this->page->get('public') != (bool)$this->public) {
272             $this->page->set('public', (bool)$this->public);
273             $this->tokens['LOCK_CHANGED_MSG']
274                 .= ($this->public
275                 ? _("Page now public.")
276                 : _("Page now not-public."));
277             $changed = true;
278         }
279
280         if (defined('ENABLE_EXTERNAL_PAGES') and ENABLE_EXTERNAL_PAGES) {
281             if ((bool)$this->page->get('external') != (bool)$this->external) {
282                 $this->page->set('external', (bool)$this->external);
283                 $this->tokens['LOCK_CHANGED_MSG']
284                     = ($this->external
285                     ? _("Page now external.")
286                     : _("Page now not-external.")) . " ";
287                 $changed = true;
288             }
289         }
290         return $changed; // lock changed.
291     }
292
293     public function savePage()
294     {
295         $request = &$this->request;
296
297         if ($this->isUnchanged()) {
298             // Allow admin lock/unlock even if
299             // no text changes were made.
300             if ($this->updateLock()) {
301                 $dbi = $request->getDbh();
302                 $dbi->touch();
303             }
304             // Save failed. No changes made.
305             $this->_redirectToBrowsePage();
306             return true;
307         }
308
309         if (!$this->user->isAdmin() and $this->isSpam()) {
310             $this->_isSpam = true;
311             return false;
312         }
313
314         $page = &$this->page;
315
316         // Include any meta-data from original page version which
317         // has not been explicitly updated.
318         $meta = $this->selected->getMetaData();
319         $meta = array_merge($meta, $this->meta);
320
321         // Save new revision
322         $this->_content = $this->getContent();
323         $newrevision = $page->save($this->_content,
324             $this->version == -1
325                 ? -1
326                 : $this->_currentVersion + 1,
327             // force new?
328             $meta);
329         if (!is_a($newrevision, 'WikiDB_PageRevision')) {
330             // Save failed.  (Concurrent updates).
331             return false;
332         }
333
334         // New contents successfully saved...
335         $this->updateLock();
336
337         // Clean out archived versions of this page.
338         require_once 'lib/ArchiveCleaner.php';
339         $cleaner = new ArchiveCleaner($GLOBALS['ExpireParams']);
340         $cleaner->cleanPageRevisions($page);
341
342         /* generate notification emails done in WikiDB::save to catch
343          all direct calls (admin plugins) */
344
345         // look at the errorstack
346         $errors = $GLOBALS['ErrorManager']->_postponed_errors;
347         $warnings = $GLOBALS['ErrorManager']->getPostponedErrorsAsHTML();
348         $GLOBALS['ErrorManager']->_postponed_errors = $errors;
349
350         $dbi = $request->getDbh();
351         $dbi->touch();
352
353         global $WikiTheme;
354         if (empty($warnings->_content) && !$WikiTheme->getImageURL('signature')) {
355             // Do redirect to browse page if no signature has
356             // been defined.  In this case, the user will most
357             // likely not see the rest of the HTML we generate
358             // (below).
359             $request->setArg('action', false);
360             $this->_redirectToBrowsePage();
361             return true;
362         }
363
364         // Force browse of current page version.
365         $request->setArg('version', false);
366         // testme: does preview and more need action=edit?
367         $request->setArg('action', false);
368
369         $template = Template('savepage', $this->tokens);
370         $template->replace('CONTENT', $newrevision->getTransformedContent());
371         if (!empty($warnings->_content)) {
372             $template->replace('WARNINGS', $warnings);
373             unset($GLOBALS['ErrorManager']->_postponed_errors);
374         }
375
376         $pagelink = WikiLink($page);
377
378         GeneratePage($template, fmt("Saved: %s", $pagelink), $newrevision);
379         return true;
380     }
381
382     protected function isConcurrentUpdate()
383     {
384         assert($this->current->getVersion() >= $this->_currentVersion);
385         return $this->current->getVersion() != $this->_currentVersion;
386     }
387
388     protected function canEdit()
389     {
390         return !$this->page->get('locked') || $this->user->isAdmin();
391     }
392
393     protected function isInitialEdit()
394     {
395         return $this->_initialEdit;
396     }
397
398     private function isUnchanged()
399     {
400         $current = &$this->current;
401         return $this->_content == $current->getPackedContent();
402     }
403
404     /**
405      * Handle AntiSpam here. How? http://wikiblacklist.blogspot.com/
406      * Need to check dynamically some blacklist wikipage settings
407      * (plugin WikiAccessRestrictions) and some static blacklist.
408      * DONE:
409      *   More than NUM_SPAM_LINKS (default: 20) new external links.
410      *        Disabled if NUM_SPAM_LINKS is 0
411      *   ENABLE_SPAMASSASSIN:  content patterns by babycart (only php >= 4.3 for now)
412      *   ENABLE_SPAMBLOCKLIST: content domain blacklist
413      */
414     private function isSpam()
415     {
416         $current = &$this->current;
417         $request = &$this->request;
418
419         $oldtext = $current->getPackedContent();
420         $newtext =& $this->_content;
421         $numlinks = $this->numLinks($newtext);
422         $newlinks = $numlinks - $this->numLinks($oldtext);
423         // FIXME: in longer texts the NUM_SPAM_LINKS number should be increased.
424         //        better use a certain text : link ratio.
425
426         // 1. Not more than NUM_SPAM_LINKS (default: 20) new external links
427         if ((NUM_SPAM_LINKS > 0) and ($newlinks >= NUM_SPAM_LINKS)) {
428             // Allow strictly authenticated users?
429             // TODO: mail the admin?
430             $this->tokens['PAGE_LOCKED_MESSAGE'] =
431                 HTML($this->getSpamMessage(),
432                     HTML::p(HTML::strong(_("Too many external links."))));
433             return true;
434         }
435         // 2. external babycart (SpamAssassin) check
436         // This will probably prevent from discussing sex or viagra related topics. So beware.
437         if (ENABLE_SPAMASSASSIN) {
438             require_once 'lib/spam_babycart.php';
439             if ($babycart = check_babycart($newtext, $request->get("REMOTE_ADDR"),
440                 $this->user->getId())
441             ) {
442                 // TODO: mail the admin
443                 if (is_array($babycart))
444                     $this->tokens['PAGE_LOCKED_MESSAGE'] =
445                         HTML($this->getSpamMessage(),
446                             HTML::p(HTML::em(_("SpamAssassin reports: "),
447                                 join("\n", $babycart))));
448                 return true;
449             }
450         }
451         // 3. extract (new) links and check surbl for blocked domains
452         if (defined('ENABLE_SPAMBLOCKLIST') and ENABLE_SPAMBLOCKLIST and ($newlinks > 5)) {
453             require_once 'lib/SpamBlocklist.php';
454             require_once 'lib/InlineParser.php';
455             $parsed = TransformLinks($newtext);
456             $oldparsed = TransformLinks($oldtext);
457             $oldlinks = array();
458             foreach ($oldparsed->_content as $link) {
459                 if (is_a($link, 'Cached_ExternalLink') and !is_a($link, 'Cached_InterwikiLink')) {
460                     $uri = $link->_getURL($this->page->getName());
461                     $oldlinks[$uri]++;
462                 }
463             }
464             unset($oldparsed);
465             foreach ($parsed->_content as $link) {
466                 if (is_a($link, 'Cached_ExternalLink') and !is_a($link, 'Cached_InterwikiLink')) {
467                     $uri = $link->_getURL($this->page->getName());
468                     // only check new links, so admins may add blocked links.
469                     if (!array_key_exists($uri, $oldlinks) and ($res = IsBlackListed($uri))) {
470                         // TODO: mail the admin
471                         $this->tokens['PAGE_LOCKED_MESSAGE'] =
472                             HTML($this->getSpamMessage(),
473                                 HTML::p(HTML::strong(_("External links contain blocked domains:")),
474                                     HTML::ul(HTML::li(sprintf(_("%s is listed at %s with %s"),
475                                         $uri . " [" . $res[2] . "]", $res[0], $res[1])))));
476                         return true;
477                     }
478                 }
479             }
480             unset($oldlinks);
481             unset($parsed);
482             unset($oldparsed);
483         }
484
485         return false;
486     }
487
488     /** Number of external links in the wikitext
489      */
490     private function numLinks(&$text)
491     {
492         return substr_count($text, "http://") + substr_count($text, "https://");
493     }
494
495     /** Header of the Anti Spam message
496      */
497     private function getSpamMessage()
498     {
499         return
500             HTML(HTML::h2(_("Spam Prevention")),
501                 HTML::p(_("This page edit seems to contain spam and was therefore not saved."),
502                     HTML::br(),
503                     _("Sorry for the inconvenience.")),
504                 HTML::p(""));
505     }
506
507     protected function getPreview()
508     {
509         require_once 'lib/PageType.php';
510         $this->_content = $this->getContent();
511         return new TransformedText($this->page, $this->_content, $this->meta);
512     }
513
514     protected function getConvertedPreview()
515     {
516         require_once 'lib/PageType.php';
517         $this->_content = $this->getContent();
518         return new TransformedText($this->page, $this->_content, $this->meta);
519     }
520
521     private function getDiff()
522     {
523         require_once 'lib/diff.php';
524         $html = HTML();
525
526         $diff = new Diff($this->current->getContent(), explode("\n", $this->getContent()));
527         if ($diff->isEmpty()) {
528             $html->pushContent(HTML::hr(),
529                                HTML::p(array('class' => 'warning_msg'),
530                                        _("Versions are identical")));
531         } else {
532             // New CSS formatted unified diffs
533             $fmt = new HtmlUnifiedDiffFormatter;
534             $html->pushContent($fmt->format($diff));
535         }
536         return $html;
537     }
538
539     // possibly convert HTMLAREA content back to Wiki markup
540     private function getContent()
541     {
542         if (ENABLE_WYSIWYG) {
543             // don't store everything as html
544             if (!WYSIWYG_DEFAULT_PAGETYPE_HTML) {
545                 // Wikiwyg shortcut to avoid the InlineTransformer:
546                 if (WYSIWYG_BACKEND == "Wikiwyg") return $this->_content;
547                 $xml_output = $this->WysiwygEdit->ConvertAfter($this->_content);
548                 $this->_content = join("", $xml_output->_content);
549             } else {
550                 $this->meta['pagetype'] = 'html';
551             }
552             return $this->_content;
553         } else {
554             return $this->_content;
555         }
556     }
557
558     protected function getLockedMessage()
559     {
560         return
561             HTML(HTML::h2(_("Page Locked")),
562                 HTML::p(_("This page has been locked by the administrator so your changes can not be saved.")),
563                 HTML::p(_("(Copy your changes to the clipboard. You can try editing a different page or save your text in a text editor.)")),
564                 HTML::p(_("Sorry for the inconvenience.")));
565     }
566
567     private function isModerated()
568     {
569         return $this->page->get('moderation');
570     }
571
572     private function getModeratedMessage()
573     {
574         return
575             HTML(HTML::h2(WikiLink(_("ModeratedPage"))),
576                 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")))),
577                 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.",
578                     WikiLink(_("UserPreferences")))));
579     }
580
581     protected function getConflictMessage($unresolved = false)
582     {
583         /*
584          xgettext only knows about c/c++ line-continuation strings
585          it does not know about php's dot operator.
586          We want to translate this entire paragraph as one string, of course.
587          */
588
589         //$re_edit_link = Button('edit', _("Edit the new version"), $this->page);
590
591         if ($unresolved)
592             $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.",
593                 "<<<<<<< " . _("Your version"),
594                 ">>>>>>> " . _("Other version")));
595         else
596             $message = HTML::p(_("Please check it through before saving."));
597
598         /*$steps = HTML::ol(HTML::li(_("Copy your changes to the clipboard or to another temporary place (e.g. text editor).")),
599           HTML::li(fmt("%s of the page. You should now see the most current version of the page. Your changes are no longer there.",
600                        $re_edit_link)),
601           HTML::li(_("Make changes to the file again. Paste your additions from the clipboard (or text editor).")),
602           HTML::li(_("Save your updated changes.")));
603         */
604         return
605             HTML(HTML::h2(_("Conflicting Edits!")),
606                 HTML::p(_("In the time since you started editing this page, another user has saved a new version of it.")),
607                 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.")),
608                 $message);
609     }
610
611     private function getTextArea()
612     {
613         $request = &$this->request;
614
615         $readonly = !$this->canEdit(); // || $this->isConcurrentUpdate();
616
617         // WYSIWYG will need two pagetypes: raw wikitest and converted html
618         if (ENABLE_WYSIWYG) {
619             $this->_wikicontent = $this->_content;
620             $this->_content = $this->WysiwygEdit->ConvertBefore($this->_content);
621             //                $this->getPreview();
622             //$this->_htmlcontent = $this->_content->asXML();
623         }
624
625         $textarea = HTML::textarea(array('class' => 'wikiedit',
626                 'name' => 'edit[content]',
627                 'id' => 'edit-content',
628                 'rows' => $request->getPref('editHeight'),
629                 'cols' => $request->getPref('editWidth'),
630                 'readonly' => (bool)$readonly),
631             $this->_content);
632         if (defined('ENABLE_WYSIWYG') and ENABLE_WYSIWYG) {
633             return $this->WysiwygEdit->Textarea($textarea, $this->_wikicontent,
634                 $textarea->getAttr('name'));
635         } else
636             return $textarea;
637     }
638
639     protected function getFormElements()
640     {
641         global $WikiTheme;
642         $request = &$this->request;
643         $page = &$this->page;
644
645         $h = array('action' => 'edit',
646             'pagename' => $page->getName(),
647             'version' => $this->version,
648             'edit[pagetype]' => $this->meta['pagetype'],
649             'edit[current_version]' => $this->_currentVersion);
650
651         $el['HIDDEN_INPUTS'] = HiddenInputs($h);
652         $el['EDIT_TEXTAREA'] = $this->getTextArea();
653         if (ENABLE_CAPTCHA) {
654             $el = array_merge($el, $this->Captcha->getFormElements());
655         }
656         $el['SUMMARY_INPUT']
657             = HTML::input(array('type' => 'text',
658             'class' => 'wikitext',
659             'id' => 'edit-summary',
660             'name' => 'edit[summary]',
661             'size' => 50,
662             'maxlength' => 256,
663             'value' => $this->meta['summary']));
664         $el['MINOR_EDIT_CB']
665             = HTML::input(array('type' => 'checkbox',
666             'name' => 'edit[minor_edit]',
667             'id' => 'edit-minor_edit',
668             'checked' => (bool)$this->meta['is_minor_edit']));
669         $el['LOCKED_CB']
670             = HTML::input(array('type' => 'checkbox',
671             'name' => 'edit[locked]',
672             'id' => 'edit-locked',
673             'disabled' => (bool)!$this->user->isAdmin(),
674             'checked' => (bool)$this->locked));
675         if (ENABLE_PAGE_PUBLIC) {
676             $el['PUBLIC_CB']
677                 = HTML::input(array('type' => 'checkbox',
678                 'name' => 'edit[public]',
679                 'id' => 'edit-public',
680                 'disabled' => (bool)!$this->user->isAdmin(),
681                 'checked' => (bool)$this->page->get('public')));
682         }
683         if (ENABLE_EXTERNAL_PAGES) {
684             $el['EXTERNAL_CB']
685                 = HTML::input(array('type' => 'checkbox',
686                 'name' => 'edit[external]',
687                 'id' => 'edit-external',
688                 'disabled' => (bool)!$this->user->isAdmin(),
689                 'checked' => (bool)$this->page->get('external')));
690         }
691         if (ENABLE_WYSIWYG) {
692             if (($this->version == 0) and ($request->getArg('mode') != 'wysiwyg')) {
693                 $el['WYSIWYG_B'] = Button(array("action" => "edit", "mode" => "wysiwyg"), "Wysiwyg Editor");
694             }
695         }
696
697         $el['PREVIEW_B'] = Button('submit:edit[preview]', _("Preview"),
698             'wikiaction',
699             array('accesskey' => 'p',
700                 'title' => _('Preview the current content [alt-p]')));
701
702         //if (!$this->isConcurrentUpdate() && $this->canEdit())
703         $el['SAVE_B'] = Button('submit:edit[save]',
704             _("Save"), 'wikiaction',
705             array('accesskey' => 's',
706                 'title' => _('Save the current content as wikipage [alt-s]')));
707         $el['CHANGES_B'] = Button('submit:edit[diff]',
708             _("Changes"), 'wikiaction',
709             array('accesskey' => 'c',
710                 'title' => _('Preview the current changes as diff [alt-c]')));
711         $el['UPLOAD_B'] = Button('submit:edit[upload]',
712             _("Upload"), 'wikiaction',
713             array('title' => _('Select a local file and press Upload to attach into this page')));
714         $el['SPELLCHECK_B'] = Button('submit:edit[SpellCheck]',
715             _("Spell Check"), 'wikiaction',
716             array('title' => _('Check the spelling')));
717         $el['IS_CURRENT'] = $this->version == $this->current->getVersion();
718
719         $el['WIDTH_PREF']
720             = HTML::input(array('type' => 'text',
721             'size' => 3,
722             'maxlength' => 4,
723             'class' => "numeric",
724             'name' => 'pref[editWidth]',
725             'id' => 'pref-editWidth',
726             'value' => $request->getPref('editWidth'),
727             'onchange' => 'this.form.submit();'));
728         $el['HEIGHT_PREF']
729             = HTML::input(array('type' => 'text',
730             'size' => 3,
731             'maxlength' => 4,
732             'class' => "numeric",
733             'name' => 'pref[editHeight]',
734             'id' => 'pref-editHeight',
735             'value' => $request->getPref('editHeight'),
736             'onchange' => 'this.form.submit();'));
737         $el['SEP'] = $WikiTheme->getButtonSeparator();
738         $el['AUTHOR_MESSAGE'] = fmt("Author will be logged as %s.",
739             HTML::em($this->user->getId()));
740
741         return $el;
742     }
743
744     private function _redirectToBrowsePage()
745     {
746         $this->request->redirect(WikiURL($this->page, array(), 'absolute_url'));
747     }
748
749     private function _restoreState()
750     {
751         $request = &$this->request;
752
753         $posted = $request->getArg('edit');
754         $request->setArg('edit', false);
755
756         if (!$posted
757             || !$request->isPost()
758             || !in_array($request->getArg('action'), array('edit', 'loadfile'))
759         )
760             return false;
761
762         if (!isset($posted['content']) || !is_string($posted['content']))
763             return false;
764         $this->_content = preg_replace('/[ \t\r]+\n/', "\n",
765             rtrim($posted['content']));
766         $this->_content = $this->getContent();
767
768         $this->_currentVersion = (int)$posted['current_version'];
769
770         if ($this->_currentVersion < 0)
771             return false;
772         if ($this->_currentVersion > $this->current->getVersion())
773             return false; // FIXME: some kind of warning?
774
775         $meta['summary'] = trim(substr($posted['summary'], 0, 256));
776         $meta['is_minor_edit'] = !empty($posted['minor_edit']);
777         $meta['pagetype'] = !empty($posted['pagetype']) ? $posted['pagetype'] : false;
778         if (ENABLE_CAPTCHA)
779             $meta['captcha_input'] = !empty($posted['captcha_input']) ?
780                 $posted['captcha_input'] : '';
781
782         $this->meta = array_merge($this->meta, $meta);
783         $this->locked = !empty($posted['locked']);
784         if (ENABLE_PAGE_PUBLIC)
785             $this->public = !empty($posted['public']);
786         if (ENABLE_EXTERNAL_PAGES)
787             $this->external = !empty($posted['external']);
788
789         foreach (array('preview', 'save', 'edit_convert',
790                      'keep_old', 'overwrite', 'diff', 'upload') as $o) {
791             if (!empty($posted[$o]))
792                 $this->editaction = $o;
793         }
794         if (empty($this->editaction))
795             $this->editaction = 'edit';
796
797         return true;
798     }
799
800     private function _initializeState()
801     {
802         $request = &$this->request;
803         $current = &$this->current;
804         $selected = &$this->selected;
805         $user = &$this->user;
806
807         if (!$selected)
808             NoSuchRevision($request, $this->page, $this->version); // noreturn
809
810         $this->_currentVersion = $current->getVersion();
811         $this->_content = $selected->getPackedContent();
812
813         $this->locked = $this->page->get('locked');
814
815         // If author same as previous author, default minor_edit to on.
816         $age = $this->meta['mtime'] - $current->get('mtime');
817         $this->meta['is_minor_edit'] = ($age < MINOR_EDIT_TIMEOUT
818             && $current->get('author') == $user->getId()
819         );
820
821         $this->meta['pagetype'] = $selected->get('pagetype');
822         if ($this->meta['pagetype'] == 'wikiblog')
823             $this->meta['summary'] = $selected->get('summary'); // keep blog title
824         else
825             $this->meta['summary'] = '';
826         $this->editaction = 'edit';
827     }
828 }
829
830 class LoadFileConflictPageEditor
831     extends PageEditor
832 {
833     public function editPage($saveFailed = true)
834     {
835         $tokens = &$this->tokens;
836
837         if (!$this->canEdit()) {
838             if ($this->isInitialEdit()) {
839                 return $this->viewSource();
840             }
841             $tokens['PAGE_LOCKED_MESSAGE'] = $this->getLockedMessage();
842         } elseif ($this->editaction == 'save') {
843             if ($this->savePage()) {
844                 return true; // Page saved.
845             }
846             $saveFailed = true;
847         }
848
849         if ($saveFailed || $this->isConcurrentUpdate()) {
850             // Get the text of the original page, and the two conflicting edits
851             // The diff class takes arrays as input.  So retrieve content as
852             // an array, or convert it as necesary.
853             $orig = $this->page->getRevision($this->_currentVersion);
854             $this_content = explode("\n", $this->_content);
855             $other_content = $this->current->getContent();
856             require_once 'lib/diff.php';
857             $diff2 = new Diff($other_content, $this_content);
858             $context_lines = max(4, count($other_content) + 1,
859                 count($this_content) + 1);
860             $fmt = new BlockDiffFormatter($context_lines);
861
862             $this->_content = $fmt->format($diff2);
863             // FIXME: integrate this into class BlockDiffFormatter
864             $this->_content = str_replace(">>>>>>>\n<<<<<<<\n", "=======\n",
865                 $this->_content);
866             $this->_content = str_replace("<<<<<<<\n>>>>>>>\n", "=======\n",
867                 $this->_content);
868
869             $this->_currentVersion = $this->current->getVersion();
870             $this->version = $this->_currentVersion;
871             $tokens['CONCURRENT_UPDATE_MESSAGE'] = $this->getConflictMessage();
872         }
873
874         if ($this->editaction == 'edit_convert')
875             $tokens['PREVIEW_CONTENT'] = $this->getConvertedPreview();
876         if ($this->editaction == 'preview')
877             $tokens['PREVIEW_CONTENT'] = $this->getPreview(); // FIXME: convert to _MESSAGE?
878
879         // FIXME: NOT_CURRENT_MESSAGE?
880         $tokens = array_merge($tokens, $this->getFormElements());
881         // we need all GET params for loadfile overwrite
882         if ($this->request->getArg('action') == 'loadfile') {
883
884             $this->tokens['HIDDEN_INPUTS'] =
885                 HTML(HiddenInputs
886                     (array('source' => $this->request->getArg('source'),
887                         'merge' => 1)),
888                     $this->tokens['HIDDEN_INPUTS']);
889             // add two conflict resolution buttons before preview and save.
890             $tokens['PREVIEW_B'] = HTML(
891                 Button('submit:edit[keep_old]',
892                     _("Keep old"), 'wikiaction'),
893                 $tokens['SEP'],
894                 Button('submit:edit[overwrite]',
895                     _("Overwrite with new"), 'wikiaction'),
896                 $tokens['SEP'],
897                 $tokens['PREVIEW_B']);
898         }
899         if (ENABLE_EDIT_TOOLBAR and !ENABLE_WYSIWYG) {
900             include_once 'lib/EditToolbar.php';
901             $toolbar = new EditToolbar();
902             $tokens = array_merge($tokens, $toolbar->getTokens());
903         }
904
905         return $this->output('editpage', _("Merge and Edit: %s"));
906     }
907
908     public function output($template, $title_fs)
909     {
910         $selected = &$this->selected;
911         $current = &$this->current;
912
913         if ($selected && $selected->getVersion() != $current->getVersion()) {
914             $pagelink = WikiLink($selected);
915         } else {
916             $pagelink = WikiLink($this->page);
917         }
918
919         $title = new FormattedText ($title_fs, $pagelink);
920         $this->tokens['HEADER'] = $title;
921         //hack! there's no TITLE in editpage, but in the previous top template
922         if (empty($this->tokens['PAGE_LOCKED_MESSAGE']))
923             $this->tokens['PAGE_LOCKED_MESSAGE'] = HTML::h3($title);
924         else
925             $this->tokens['PAGE_LOCKED_MESSAGE'] = HTML(HTML::h3($title),
926                 $this->tokens['PAGE_LOCKED_MESSAGE']);
927         $template = Template($template, $this->tokens);
928
929         //GeneratePage($template, $title, $rev);
930         PrintXML($template);
931         return true;
932     }
933
934     protected function getConflictMessage($unresolved = false)
935     {
936         $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.",
937                 "<<<<<<<",
938                 "======="),
939             HTML::p(_("Please check it through before saving."))));
940         return $message;
941     }
942 }
943
944 // Local Variables:
945 // mode: php
946 // tab-width: 8
947 // c-basic-offset: 4
948 // c-hanging-comment-ender-p: nil
949 // indent-tabs-mode: nil
950 // End: