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