1<?php
2/**
3 * Copyright 2003-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (GPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/gpl.
7 *
8 * @category Horde
9 * @license  http://www.horde.org/licenses/gpl GPL
10 * @author   Jan Schneider <jan@horde.org>
11 * @author   Tyler Colbert <tyler@colberts.us>
12 * @package  Wicked
13 */
14
15/**
16 * Page class for regular pages.
17 *
18 * @category Horde
19 * @license  http://www.horde.org/licenses/gpl GPL
20 * @author   Jan Schneider <jan@horde.org>
21 * @author   Tyler Colbert <tyler@colberts.us>
22 * @package  Wicked
23 */
24class Wicked_Page_StandardPage extends Wicked_Page
25{
26    /**
27     * Display modes supported by this page.
28     *
29     * @var array
30     */
31    public $supportedModes = array(
32        Wicked::MODE_DISPLAY => true,
33        Wicked::MODE_EDIT => true,
34        Wicked::MODE_REMOVE => true,
35        Wicked::MODE_HISTORY => true,
36        Wicked::MODE_DIFF => true);
37
38    /**
39     * A Horde_Locks instance for un-/locking this page.
40     *
41     * @var Horde_Lock
42     */
43    protected $_locks = null;
44
45    /**
46     * Lock information if this page is currently locked.
47     *
48     * @var array
49     */
50    protected $_lock = null;
51
52    /**
53     * Constructs a standard page class to represent a wiki page.
54     *
55     * @param string $pagename The name of the page to represent.
56     */
57    public function __construct($pagename)
58    {
59        if (is_array($pagename)) {
60            $this->_page = $pagename;
61            return;
62        }
63
64        $page = null;
65        try {
66            $page = $GLOBALS['wicked']->retrieveByName($pagename);
67        } catch (Wicked_Exception $e) {
68            // If we can't load $pagename, see if there's default data for it.
69            // Protect against directory traversion.
70            $pagepath = realpath(WICKED_BASE . '/data/'
71                                 . $GLOBALS['conf']['wicked']['format']);
72            $pagefile = realpath($pagepath . '/' . $pagename);
73            if ($pagefile &&
74                Horde_String::common($pagefile, $pagepath) == $pagepath &&
75                substr($pagename, 0, 1) != '.' &&
76                file_exists($pagefile) &&
77                ($text = file_get_contents($pagefile))) {
78                try {
79                    $GLOBALS['wicked']->newPage($pagename, $text);
80                    try {
81                        $page = $GLOBALS['wicked']->retrieveByName($pagename);
82                    } catch (Wicked_Exception $e) {
83                        $GLOBALS['notification']->push(sprintf(_("Unable to create %s"), $pagename), 'horde.error');
84                    }
85                } catch (Wicked_Exception $e) {}
86            }
87        }
88
89        if ($page) {
90            $this->_page = $page;
91        } else {
92            if ($pagename == 'Wiki/Home') {
93                $GLOBALS['notification']->push(_("Unable to create Wiki/Home. The wiki is not configured."), 'horde.error');
94            }
95            $this->_page = array();
96        }
97
98        // Make sure 'wicked' permission exists. Set reasonable defaults if
99        // necessary.
100        $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
101        $corePerms = $GLOBALS['injector']->getInstance('Horde_Core_Perms');
102        if (!$perms->exists('wicked')) {
103            $perm = $corePerms->newPermission('wicked');
104            $perm->addGuestPermission(Horde_Perms::SHOW | Horde_Perms::READ, false);
105            $perm->addDefaultPermission(Horde_Perms::SHOW | Horde_Perms::READ | Horde_Perms::EDIT | Horde_Perms::DELETE, false);
106            $perms->addPermission($perm);
107        }
108
109        // Make sure 'wicked:pages' exists. Copy from 'wicked' if it does not
110        // exist.
111        if (!$perms->exists('wicked:pages')) {
112            $perm = $corePerms->newPermission('wicked:pages');
113            $copyFrom = $perms->getPermission('wicked');
114            $perm->addGuestPermission($copyFrom->getGuestPermissions(), false);
115            $perm->addDefaultPermission($copyFrom->getDefaultPermissions(), false);
116            $perm->addCreatorPermission($copyFrom->getCreatorPermissions(), false);
117            foreach ($copyFrom->getUserPermissions() as $user => $uperm) {
118                $perm->addUserPermission($user, $uperm, false);
119            }
120            foreach ($copyFrom->getGroupPermissions() as $group => $gperm) {
121                $perm->addGroupPermission($group, $gperm, false);
122            }
123            $perms->addPermission($perm);
124        }
125
126        if ($GLOBALS['conf']['lock']['driver'] != 'none') {
127            $this->supportedModes[Wicked::MODE_LOCKING] = $this->supportedModes[Wicked::MODE_UNLOCKING] = true;
128            $this->_locks = $GLOBALS['injector']->getInstance('Horde_Lock');
129            $locks = $this->_locks->getLocks('wicked', $pagename, Horde_Lock::TYPE_EXCLUSIVE);
130            if ($locks) {
131                $this->_lock = reset($locks);
132            }
133        }
134    }
135
136    /**
137     * Returns if the page allows a mode. Access rights and user state
138     * are taken into consideration.
139     *
140     * @see $supportedModes
141     *
142     * @param integer $mode  The mode to check for.
143     *
144     * @return boolean  True if the mode is allowed.
145     */
146    public function allows($mode)
147    {
148        switch ($mode) {
149        case Wicked::MODE_EDIT:
150            if ($this->isLocked()) {
151                return Wicked::lockUser() == $this->_lock['lock_owner'];
152            }
153            break;
154
155        case Wicked::MODE_LOCKING:
156            if ($GLOBALS['browser']->isRobot()) {
157                return false;
158            }
159            if ($GLOBALS['registry']->isAdmin()) {
160                return true;
161            }
162            if (($this->getPermissions() & Horde_Perms::EDIT) == 0) {
163                return false;
164            }
165            break;
166
167        case Wicked::MODE_UNLOCKING:
168            if ($GLOBALS['registry']->isAdmin()) {
169                return true;
170            }
171            if ($this->_lock) {
172                return Wicked::lockUser() == $this->_lock['lock_owner'];
173            }
174            return false;
175        }
176        return parent::allows($mode);
177    }
178
179    /**
180     * @throws Wicked_Exception
181     */
182    public function displayContents($isBlock)
183    {
184        $view = $GLOBALS['injector']->createInstance('Horde_View');
185        $view->text = $this->getProcessor()->transform($this->getText());
186        if ($isBlock) {
187            return $view->render('display/standard');
188        }
189
190        $view->showTools = true;
191        if ($this->allows(Wicked::MODE_EDIT) &&
192            !$this->isLocked(Wicked::lockUser())) {
193            $view->edit = Horde::widget(array(
194                'url' => Wicked::url('EditPage')
195                    ->add('referrer', $this->pageName()),
196                'title' => _("_Edit"),
197                'class' => 'wicked-edit',
198            ));
199        }
200        if ($this->isLocked()) {
201            if ($this->allows(Wicked::MODE_UNLOCKING)) {
202                $view->unlock = Horde::widget(array(
203                    'url' => $this->pageUrl(null, 'unlock')->remove('version'),
204                    'title' => _("Un_lock"),
205                    'class' => 'wicked-unlock',
206                ));
207            }
208        } else {
209            if ($this->allows(Wicked::MODE_LOCKING)) {
210                $view->lock = Horde::widget(array(
211                    'url' => $this->pageUrl(null, 'lock')->remove('version'),
212                    'title' => _("_Lock"),
213                    'class' => 'wicked-lock',
214                ));
215            }
216        }
217        if ($this->allows(Wicked::MODE_REMOVE)) {
218            $params = array('referrer' => $this->pageName());
219            if ($this->isOld()) {
220                $params['version'] = $this->version();
221            }
222            $view->remove = Horde::widget(array(
223                'url' => Wicked::url('DeletePage')->add($params),
224                'title' => _("_Delete"),
225                'class' => 'wicked-delete',
226            ));
227        }
228        if ($this->allows(Wicked::MODE_REMOVE) &&
229            !$this->isLocked(Wicked::lockUser())) {
230            $view->rename = Horde::widget(array(
231                'url' => Wicked::url('MergeOrRename')
232                    ->add('referrer', $this->pageName()),
233                'title' => _("_Merge/Rename")
234            ));
235        }
236        $view->backLinks = Horde::widget(array(
237            'url' => Wicked::url('BackLinks')
238                ->add('referrer', $this->pageName()),
239            'title' => _("_Backlinks")
240        ));
241        $view->likePages = Horde::widget(array(
242            'url' => Wicked::url('LikePages')
243                ->add('referrer', $this->pageName()),
244            'title' => _("S_imilar Pages")
245        ));
246        $view->attachedFiles = Horde::widget(array(
247            'url' => Wicked::url('AttachedFiles')
248                ->add('referrer', $this->pageName()),
249            'title' => _("Attachments")
250        ));
251        if ($this->allows(Wicked::MODE_HISTORY)) {
252            $view->changes = Horde::widget(array(
253                'url' => $this->pageUrl('history.php')->remove('version'),
254                'title' => _("Hi_story")
255            ));
256        }
257        if ($GLOBALS['registry']->isAdmin()) {
258            $permsurl = Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/admin/perms/edit.php')
259                ->add(array(
260                    'category' => 'wicked:pages:' . $this->pageId(),
261                    'autocreate' => 1,
262                    'autocreate_copy' => 'wicked',
263                    'autocreate_guest' => Horde_Perms::SHOW | Horde_Perms::READ,
264                    'autocreate_default' => Horde_Perms::SHOW | Horde_Perms::READ | Horde_Perms::EDIT | Horde_Perms::DELETE
265                ));
266            $view->perms = Horde::widget(array(
267                'url' => $permsurl,
268                'target' => '_blank',
269                'title' => _("Permissio_ns")
270            ));
271        }
272        if ($histories = $GLOBALS['session']->get('wicked', 'history')) {
273            $view->history = Horde::widget(array(
274                'url' => '#',
275                'onclick' => 'document.location = document.display.history[document.display.history.selectedIndex].value;',
276                'title' => _("Ba_ck to")
277            ));
278            $view->histories = array();
279            foreach ($histories as $history) {
280                if (!strlen($history)) {
281                    continue;
282                }
283                $view->histories[(string)Wicked::url($history)] = $history;
284            }
285        }
286        $pageId = $GLOBALS['wicked']->getPageId($this->pageName());
287        $attachments = $GLOBALS['wicked']->getAttachedFiles($pageId);
288        if (count($attachments)) {
289            $view->attachments = array();
290            foreach ($attachments as $attachment) {
291                $url = $GLOBALS['registry']
292                    ->downloadUrl(
293                        $attachment['attachment_name'],
294                        array(
295                            'page' => $this->pageName(),
296                            'file' => $attachment['attachment_name'],
297                            'version' => $attachment['attachment_version']
298                        )
299                    );
300                $icon = $GLOBALS['injector']
301                    ->getInstance('Horde_Core_Factory_MimeViewer')
302                    ->getIcon(
303                        Horde_Mime_Magic::filenameToMime(
304                            $attachment['attachment_name']
305                        )
306                    );
307                $view->attachments[] = Horde::link($url)
308                    . '<img src="' . $icon . '" width="16" height="16" alt="" />&nbsp;'
309                    . htmlspecialchars($attachment['attachment_name'])
310                    . '</a>';
311            }
312        }
313        $view->downloadPlain = Wicked::url($this->pageName())
314            ->add(array('actionID' => 'export', 'format' => 'plain'))
315            ->link()
316            . _("Plain Text") . '</a>';
317        $view->downloadHtml = Wicked::url($this->pageName())
318            ->add(array('actionID' => 'export', 'format' => 'html'))
319            ->link()
320            . _("HTML") . '</a>';
321        $view->downloadLatex = Wicked::url($this->pageName())
322            ->add(array('actionID' => 'export', 'format' => 'tex'))
323            ->link()
324            . _("Latex") . '</a>';
325        $view->downloadRest = Wicked::url($this->pageName())
326            ->add(array('actionID' => 'export', 'format' => 'rst'))
327            ->link()
328            . _("reStructuredText") . '</a>';
329
330        return $view->render('display/standard');
331    }
332
333    /**
334     * Renders this page in History mode.
335     *
336     * @return string  The content.
337     * @throws Wicked_Exception
338     */
339    public function history()
340    {
341        global $injector, $page_output;
342
343        $page_output->addScriptFile('history.js');
344
345        $view = $injector->createInstance('Horde_View');
346
347        // Header.
348        $view->formInput = Horde_Util::formInput();
349        $view->name = $this->pageName();
350        $view->pageLink = $this->pageUrl()->link()
351            . htmlspecialchars($this->pageName()) . '</a>';
352        $view->refreshLink = $this->pageUrl('history.php')->link()
353            . Horde::img('reload.png', _("Reload History")) . '</a>';
354        if ($this->allows(Wicked::MODE_REMOVE)) {
355            $view->remove = Horde::img('delete.png', _("Delete Version"));
356        }
357        if ($this->allows(Wicked::MODE_EDIT) &&
358            !$this->isLocked(Wicked::lockUser())) {
359            $view->edit = Horde::img('edit.png', _("Edit Version"));
360            $view->restore = Horde::img('restore.png', _("Restore Version"));
361        }
362        $content = $view->render('history/header');
363
364        // First item is this page.
365        $view->showRestore = false;
366        $this->_setViewProperties($view, $this);
367        $content .= $view->render('history/summary');
368
369        // Now the rest of the histories.
370        $view->showRestore = true;
371        foreach ($GLOBALS['wicked']->getHistory($this->pageName()) as $page) {
372            $page = new Wicked_Page_StandardHistoryPage($page);
373            $this->_setViewProperties($view, $page);
374            $view->pversion = $page->version();
375            $content .= $view->render('history/summary');
376        }
377
378        // Footer.
379        return $content . $view->render('history/footer');
380    }
381
382    protected function _setViewProperties($view, $page)
383    {
384        $view->displayLink = $page->pageUrl()
385            ->link(array(
386                'title' => sprintf(_("Display Version %s"), $page->version())
387            ))
388            . htmlspecialchars($page->version()) . '</a>';
389
390        $text = sprintf(_("Delete Version %s"), $page->version());
391        $view->deleteLink = Wicked::url('DeletePage')
392            ->add(array(
393                'referrer' => $page->pageName(),
394                'version' => $page->version()
395            ))
396            ->link(array('title' => $text))
397            . Horde::img('delete.png', $text) . '</a>';
398
399        $text = sprintf(_("Edit Version %s"), $page->version());
400        $view->editLink = Wicked::url('EditPage')
401            ->add(array('referrer' => $page->pageName()))
402            ->link(array('title' => $text))
403            . Horde::img('edit.png', $text) . '</a>';
404
405        $text = sprintf(_("Revert to version %s"), $page->version());
406        $view->restoreLink = Wicked::url('RevertPage')
407            ->add(array(
408                'referrer' => $page->pageName(),
409                'version' => $page->version()
410            ))
411            ->link(array('title' => $text))
412            . Horde::img('restore.png', $text) . '</a>';
413
414        $view->author = $page->author();
415        $view->date = $page->formatVersionCreated();
416        $view->version = $page->version();
417        $view->changelog = $page->changeLog();
418    }
419
420    public function isLocked($owner = null)
421    {
422        if (empty($this->_lock)) {
423            return false;
424        }
425        if (is_null($owner)) {
426            return true;
427        }
428        return $owner != $this->_lock['lock_owner'];
429    }
430
431    /**
432     * @throws Wicked_Exception
433     */
434    public function lock()
435    {
436        if ($this->_locks) {
437            $id = $this->_locks->setLock(Wicked::lockUser(), 'wicked', $this->pageName(), $GLOBALS['conf']['wicked']['lock']['time'] * 60, Horde_Lock::TYPE_EXCLUSIVE);
438            if ($id) {
439                $this->_lock = $this->_locks->getLockInfo($id);
440            } else {
441                throw new Wicked_Exception(_("The page is already locked."));
442            }
443        }
444    }
445
446    public function unlock()
447    {
448        if ($this->_locks && $this->_lock) {
449            $this->_locks->clearLock($this->_lock['lock_id']);
450            unset($this->_lock);
451        }
452    }
453
454    public function getLockRequestor()
455    {
456        $requestor = $this->_lock['lock_owner'];
457        if ($requestor) {
458            $name = $GLOBALS['injector']
459                ->getInstance('Horde_Core_Factory_Identity')
460                ->create($requestor)
461                ->getValue('fullname');
462            if (!strlen($name)) {
463                $name = $requestor;
464            }
465            return $name;
466        }
467        return _("a guest");
468    }
469
470    public function getLockTime()
471    {
472        $time = ceil(($this->_lock['lock_expiry_timestamp'] - time()) / 60);
473        return sprintf(ngettext("%d minute", "%d minutes", $time), $time);
474    }
475
476    /**
477     * @throws Wicked_Exception
478     */
479    public function updateText($newtext, $changelog)
480    {
481        $version = $this->version();
482        $result = $GLOBALS['wicked']->updateText($this->pageName(), $newtext,
483                                                 $changelog);
484
485        $url = Wicked::url($this->pageName(), true, -1);
486        $new_page = $this->getPage($this->pageName());
487
488        $message = "Modified page: $url\n"
489            . 'New Revision:  ' . $new_page->version() . "\n"
490            . ($changelog ? 'Change log:  ' . $changelog . "\n" : '')
491            . "\n"
492            . $new_page->getDiff($version);
493        Wicked::mail($message,
494                     array('Subject' => '[' . $GLOBALS['registry']->get('name')
495                           . '] changed: ' . $this->pageName()));
496
497        $this->_page['page_text'] = $newtext;
498    }
499
500    public function pageID()
501    {
502        return isset($this->_page['page_id']) ? $this->_page['page_id'] : '';
503    }
504
505    public function pageName()
506    {
507        return isset($this->_page['page_name'])
508            ? $this->_page['page_name']
509            : '';
510    }
511
512    public function getText()
513    {
514        return isset($this->_page['page_text'])
515            ? $this->_page['page_text']
516            : '';
517    }
518
519    public function versionCreated()
520    {
521        return isset($this->_page['version_created'])
522            ? $this->_page['version_created']
523            : '';
524    }
525
526    public function hits()
527    {
528        return !empty($this->_page['page_hits'])
529            ? $this->_page['page_hits']
530            : 0;
531    }
532
533    public function changeLog()
534    {
535        return $this->_page['change_log'];
536    }
537
538    public function version()
539    {
540        if (isset($this->_page['page_version'])) {
541            return $this->_page['page_version'];
542        } else {
543            return '';
544        }
545    }
546
547    /**
548     * Renders this page in diff mode.
549     *
550     * @param string $version  The version to diff this page against.
551     */
552    public function diff($version)
553    {
554        $view = $GLOBALS['injector']->createInstance('Horde_View');
555        $view->link = $this->pageUrl()->link()
556            . htmlspecialchars($this->pageName())
557            . '</a>';
558        $view->version1 = $version;
559        $view->version2 = $this->version();
560        $view->diff = $this->getDiff($version, 'inline');
561        echo $view->render('diff/diff');
562    }
563
564    /**
565     * Produces a diff for this page.
566     *
567     * @param string $version   Previous version, or null if diffing with
568     *                          `before the beginning' (empty).
569     * @param string $renderer  The diff renderer.
570     */
571    public function getDiff($version, $renderer = 'unified')
572    {
573        if (is_null($version)) {
574            $old_page_text = '';
575        } else {
576            $old_page = $this->getPage($this->pageName(), $version);
577            $old_page_text = $old_page->getText();
578        }
579        $diff = new Horde_Text_Diff('auto',
580                                    array(explode("\n", $old_page_text),
581                                          explode("\n", $this->getText())));
582        $class = 'Horde_Text_Diff_Renderer_' . Horde_String::ucfirst($renderer);
583        $renderer = new $class();
584        return $renderer->render($diff);
585    }
586
587}
588