xref: /plugin/struct/action/inline.php (revision 5e29103a15bd9873f422f66a6a5239b6aec4651e)
14731b875SAndreas Gohr<?php
2d6d97f60SAnna Dabrowska
34731b875SAndreas Gohr/**
44731b875SAndreas Gohr * DokuWiki Plugin struct (Action Component)
54731b875SAndreas Gohr *
64731b875SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
74731b875SAndreas Gohr * @author  Andreas Gohr, Michael Große <dokuwiki@cosmocode.de>
84731b875SAndreas Gohr */
94731b875SAndreas Gohr
107234bfb1Ssplitbrainuse dokuwiki\Extension\ActionPlugin;
117234bfb1Ssplitbrainuse dokuwiki\Extension\EventHandler;
127234bfb1Ssplitbrainuse dokuwiki\Extension\Event;
134ec54c67SAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTable;
14308cc83fSAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTablePage;
154eed39ffSMichael Grosseuse dokuwiki\plugin\struct\meta\Assignments;
1693ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\Column;
174731b875SAndreas Gohruse dokuwiki\plugin\struct\meta\StructException;
1893ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\ValueValidator;
194731b875SAndreas Gohr
204731b875SAndreas Gohr/**
214731b875SAndreas Gohr * Class action_plugin_struct_inline
224731b875SAndreas Gohr *
234731b875SAndreas Gohr * Handle inline editing
244731b875SAndreas Gohr */
257234bfb1Ssplitbrainclass action_plugin_struct_inline extends ActionPlugin
26d6d97f60SAnna Dabrowska{
27308cc83fSAndreas Gohr    /** @var  AccessTablePage */
287234bfb1Ssplitbrain    protected $schemadata;
294731b875SAndreas Gohr
304731b875SAndreas Gohr    /** @var  Column */
317234bfb1Ssplitbrain    protected $column;
324731b875SAndreas Gohr
334731b875SAndreas Gohr    /** @var String */
344731b875SAndreas Gohr    protected $pid = '';
354731b875SAndreas Gohr
366fd73b4bSAnna Dabrowska    /** @var int */
376fd73b4bSAnna Dabrowska    protected $rid = 0;
386fd73b4bSAnna Dabrowska
394731b875SAndreas Gohr    /**
404731b875SAndreas Gohr     * Registers a callback function for a given event
414731b875SAndreas Gohr     *
42*5e29103aSannda     * @param EventHandler $controller DokuWiki's event controller object
434731b875SAndreas Gohr     * @return void
444731b875SAndreas Gohr     */
457234bfb1Ssplitbrain    public function register(EventHandler $controller)
46d6d97f60SAnna Dabrowska    {
47748e747fSAnna Dabrowska        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax');
484731b875SAndreas Gohr    }
494731b875SAndreas Gohr
504731b875SAndreas Gohr    /**
51*5e29103aSannda     * @param Event $event
524731b875SAndreas Gohr     * @param $param
534731b875SAndreas Gohr     */
547234bfb1Ssplitbrain    public function handleAjax(Event $event, $param)
55d6d97f60SAnna Dabrowska    {
564731b875SAndreas Gohr        $len = strlen('plugin_struct_inline_');
574731b875SAndreas Gohr        if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return;
584731b875SAndreas Gohr        $event->preventDefault();
594731b875SAndreas Gohr        $event->stopPropagation();
604731b875SAndreas Gohr
614731b875SAndreas Gohr        if (substr($event->data, $len) == 'editor') {
62748e747fSAnna Dabrowska            $this->inlineEditor();
634731b875SAndreas Gohr        }
644731b875SAndreas Gohr
654731b875SAndreas Gohr        if (substr($event->data, $len) == 'save') {
664731b875SAndreas Gohr            try {
67748e747fSAnna Dabrowska                $this->inlineSave();
684731b875SAndreas Gohr            } catch (StructException $e) {
694731b875SAndreas Gohr                http_status(500);
704731b875SAndreas Gohr                header('Content-Type: text/plain; charset=utf-8');
714731b875SAndreas Gohr                echo $e->getMessage();
724731b875SAndreas Gohr            }
734731b875SAndreas Gohr        }
74cdd09a96SAndreas Gohr
75cdd09a96SAndreas Gohr        if (substr($event->data, $len) == 'cancel') {
76748e747fSAnna Dabrowska            $this->inlineCancel();
77cdd09a96SAndreas Gohr        }
784731b875SAndreas Gohr    }
794731b875SAndreas Gohr
80cdd09a96SAndreas Gohr    /**
81cdd09a96SAndreas Gohr     * Creates the inline editor
82cdd09a96SAndreas Gohr     */
83748e747fSAnna Dabrowska    protected function inlineEditor()
84d6d97f60SAnna Dabrowska    {
85cdd09a96SAndreas Gohr        // silently fail when editing not possible
864731b875SAndreas Gohr        if (!$this->initFromInput()) return;
876ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) return;
884c3a60d8SAnna Dabrowska        // only check page permissions for data with pid, skip for global data
894c3a60d8SAnna Dabrowska        if ($this->pid && auth_quickaclcheck($this->pid) < AUTH_EDIT) return;
90cdd09a96SAndreas Gohr        if (checklock($this->pid)) return;
914731b875SAndreas Gohr
92cdd09a96SAndreas Gohr        // lock page
93cdd09a96SAndreas Gohr        lock($this->pid);
944731b875SAndreas Gohr
95cdd09a96SAndreas Gohr        // output the editor
964731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
97ee983135SMichael Große        $id = uniqid('struct__', false);
987c4f397eSRandolf Rotta        echo '<div class="field">';
99ee983135SMichael Große        echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">';
10063e019d0SAndreas Gohr        echo '</label>';
1017c4f397eSRandolf Rotta        echo '<span class="input">';
102ee983135SMichael Große        echo $value->getValueEditor('entry', $id);
1037c4f397eSRandolf Rotta        echo '</span>';
1044731b875SAndreas Gohr        $hint = $this->column->getType()->getTranslatedHint();
1054731b875SAndreas Gohr        if ($hint) {
106712bc832SMichael Große            echo '<p class="hint">';
1074731b875SAndreas Gohr            echo hsc($hint);
108712bc832SMichael Große            echo '</p>';
1094731b875SAndreas Gohr        }
1107c4f397eSRandolf Rotta        echo '</div>';
111cdd09a96SAndreas Gohr
112cdd09a96SAndreas Gohr        // csrf protection
113cdd09a96SAndreas Gohr        formSecurityToken();
1144731b875SAndreas Gohr    }
1154731b875SAndreas Gohr
116cdd09a96SAndreas Gohr    /**
117cdd09a96SAndreas Gohr     * Save the data posted by the inline editor
118cdd09a96SAndreas Gohr     */
119748e747fSAnna Dabrowska    protected function inlineSave()
120d6d97f60SAnna Dabrowska    {
1214731b875SAndreas Gohr        global $INPUT;
1224731b875SAndreas Gohr
12313eddb0fSAndreas Gohr        // check preconditions
1244d2da382SAndreas Gohr        if (!$this->initFromInput()) {
1254d2da382SAndreas Gohr            throw new StructException('inline save error: init');
1264d2da382SAndreas Gohr        }
12713eddb0fSAndreas Gohr        self::checkCSRF();
1280ceefd5cSAnna Dabrowska        if (!$this->schemadata->getRid()) {
12913eddb0fSAndreas Gohr            $this->checkPage();
1304eed39ffSMichael Grosse            $assignments = Assignments::getInstance();
1314eed39ffSMichael Grosse            $tables = $assignments->getPageAssignments($this->pid, true);
1324eed39ffSMichael Grosse            if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) {
1334eed39ffSMichael Grosse                throw new StructException('inline save error: schema not assigned to page');
1344eed39ffSMichael Grosse            }
1354731b875SAndreas Gohr        }
1366ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) {
1376ebbbb8eSAndreas Gohr            throw new StructException('inline save error: no permission for schema');
1386ebbbb8eSAndreas Gohr        }
1394731b875SAndreas Gohr
1404731b875SAndreas Gohr        // validate
1414731b875SAndreas Gohr        $value = $INPUT->param('entry');
14293ca6f4fSAndreas Gohr        $validator = new ValueValidator();
1434731b875SAndreas Gohr        if (!$validator->validateValue($this->column, $value)) {
1447234bfb1Ssplitbrain            throw new StructException(implode("\n", $validator->getErrors()));
1454731b875SAndreas Gohr        }
1464731b875SAndreas Gohr
1474731b875SAndreas Gohr        // current data
1484731b875SAndreas Gohr        $tosave = $this->schemadata->getDataArray();
1494731b875SAndreas Gohr        $tosave[$this->column->getLabel()] = $value;
1504731b875SAndreas Gohr
1514731b875SAndreas Gohr        // save
152efe74305SAnna Dabrowska        if ($this->schemadata->getRid()) {
15313eddb0fSAndreas Gohr            $revision = 0;
15413eddb0fSAndreas Gohr        } else {
15513eddb0fSAndreas Gohr            $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit');
156858c5caaSMichael Grosse            p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table
15713eddb0fSAndreas Gohr        }
15813eddb0fSAndreas Gohr        $this->schemadata->setTimestamp($revision);
15913eddb0fSAndreas Gohr        try {
16013eddb0fSAndreas Gohr            if (!$this->schemadata->saveData($tosave)) {
16113eddb0fSAndreas Gohr                throw new StructException('saving failed');
16213eddb0fSAndreas Gohr            }
1630ceefd5cSAnna Dabrowska            if (!$this->schemadata->getRid()) {
1644eed39ffSMichael Grosse                // make sure this schema is assigned
165b8cff1dfSAndreas Gohr                /** @noinspection PhpUndefinedVariableInspection */
1664eed39ffSMichael Grosse                $assignments->assignPageSchema(
1674eed39ffSMichael Grosse                    $this->pid,
1684eed39ffSMichael Grosse                    $this->schemadata->getSchema()->getTable()
1694eed39ffSMichael Grosse                );
1704eed39ffSMichael Grosse            }
171b8cff1dfSAndreas Gohr        } catch (\Exception $e) {
172b8cff1dfSAndreas Gohr            // PHP <7 needs a catch block
173b8cff1dfSAndreas Gohr            throw $e;
17413eddb0fSAndreas Gohr        } finally {
17513eddb0fSAndreas Gohr            // unlock (unlocking a non-existing file is okay,
17613eddb0fSAndreas Gohr            // so we don't check if it's a lookup here
177cdd09a96SAndreas Gohr            unlock($this->pid);
17813eddb0fSAndreas Gohr        }
1794731b875SAndreas Gohr
1804731b875SAndreas Gohr        // reinit then render
18169f7ec8fSAnna Dabrowska        $this->initFromInput($this->schemadata->getTimestamp());
1824731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
1834731b875SAndreas Gohr        $R = new Doku_Renderer_xhtml();
1844731b875SAndreas Gohr        $value->render($R, 'xhtml'); // FIXME use configured default renderer
185*5e29103aSannda        $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()], JSON_THROW_ON_ERROR);
18669f7ec8fSAnna Dabrowska        echo $data;
1874731b875SAndreas Gohr    }
1884731b875SAndreas Gohr
1894731b875SAndreas Gohr    /**
190cdd09a96SAndreas Gohr     * Unlock a page (on cancel action)
191cdd09a96SAndreas Gohr     */
192748e747fSAnna Dabrowska    protected function inlineCancel()
193d6d97f60SAnna Dabrowska    {
194cdd09a96SAndreas Gohr        global $INPUT;
195cdd09a96SAndreas Gohr        $pid = $INPUT->str('pid');
196cdd09a96SAndreas Gohr        unlock($pid);
197cdd09a96SAndreas Gohr    }
198cdd09a96SAndreas Gohr
199cdd09a96SAndreas Gohr    /**
2004731b875SAndreas Gohr     * Initialize internal state based on input variables
2014731b875SAndreas Gohr     *
20269f7ec8fSAnna Dabrowska     * @param int $updatedRev timestamp of currently created revision, might be newer than input variable
20369f7ec8fSAnna Dabrowska     * @return bool if initialization was successful
2044731b875SAndreas Gohr     */
205d6d97f60SAnna Dabrowska    protected function initFromInput($updatedRev = 0)
206d6d97f60SAnna Dabrowska    {
2074731b875SAndreas Gohr        global $INPUT;
2084731b875SAndreas Gohr
2094731b875SAndreas Gohr        $this->schemadata = null;
2104731b875SAndreas Gohr        $this->column = null;
2114731b875SAndreas Gohr
2124731b875SAndreas Gohr        $pid = $INPUT->str('pid');
2136fd73b4bSAnna Dabrowska        $rid = $INPUT->int('rid');
21469f7ec8fSAnna Dabrowska        $rev = $updatedRev ?: $INPUT->int('rev');
21569f7ec8fSAnna Dabrowska
2167234bfb1Ssplitbrain        [$table, $field] = explode('.', $INPUT->str('field'), 2);
2176fd73b4bSAnna Dabrowska        if (blank($pid) && blank($rid)) return false;
2184731b875SAndreas Gohr        if (blank($table)) return false;
2194731b875SAndreas Gohr        if (blank($field)) return false;
2204731b875SAndreas Gohr
2214731b875SAndreas Gohr        $this->pid = $pid;
2224ec54c67SAndreas Gohr        try {
2234cd5cc28SAnna Dabrowska            if (AccessTable::isTypePage($pid, $rev)) {
2244cd5cc28SAnna Dabrowska                $this->schemadata = AccessTable::getPageAccess($table, $pid);
2254cd5cc28SAnna Dabrowska            } elseif (AccessTable::isTypeSerial($pid, $rev)) {
2264cd5cc28SAnna Dabrowska                $this->schemadata = AccessTable::getSerialAccess($table, $pid, $rid);
2274cd5cc28SAnna Dabrowska            } else {
228308cc83fSAndreas Gohr                $this->schemadata = AccessTable::getGlobalAccess($table, $rid);
2294cd5cc28SAnna Dabrowska            }
2304ec54c67SAndreas Gohr        } catch (StructException $ignore) {
2314731b875SAndreas Gohr            return false;
2324731b875SAndreas Gohr        }
2334731b875SAndreas Gohr
2344ec54c67SAndreas Gohr        $this->column = $this->schemadata->getSchema()->findColumn($field);
2354731b875SAndreas Gohr        if (!$this->column || !$this->column->isVisibleInEditor()) {
2364731b875SAndreas Gohr            $this->schemadata = null;
2374731b875SAndreas Gohr            $this->column = null;
2384731b875SAndreas Gohr            return false;
2394731b875SAndreas Gohr        }
2404731b875SAndreas Gohr
2414731b875SAndreas Gohr        return true;
2424731b875SAndreas Gohr    }
2434731b875SAndreas Gohr
24413eddb0fSAndreas Gohr    /**
24513eddb0fSAndreas Gohr     * Checks if a page can be edited
24613eddb0fSAndreas Gohr     *
24713eddb0fSAndreas Gohr     * @throws StructException when check fails
24813eddb0fSAndreas Gohr     */
249d6d97f60SAnna Dabrowska    protected function checkPage()
250d6d97f60SAnna Dabrowska    {
25113eddb0fSAndreas Gohr        if (!page_exists($this->pid)) {
25213eddb0fSAndreas Gohr            throw new StructException('inline save error: no such page');
25313eddb0fSAndreas Gohr        }
25413eddb0fSAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) {
25513eddb0fSAndreas Gohr            throw new StructException('inline save error: acl');
25613eddb0fSAndreas Gohr        }
25713eddb0fSAndreas Gohr        if (checklock($this->pid)) {
25813eddb0fSAndreas Gohr            throw new StructException('inline save error: lock');
25913eddb0fSAndreas Gohr        }
26013eddb0fSAndreas Gohr    }
26113eddb0fSAndreas Gohr
26213eddb0fSAndreas Gohr    /**
26313eddb0fSAndreas Gohr     * Our own implementation of checkSecurityToken because we don't want the msg() call
26413eddb0fSAndreas Gohr     *
26513eddb0fSAndreas Gohr     * @throws StructException when check fails
26613eddb0fSAndreas Gohr     */
267d6d97f60SAnna Dabrowska    public static function checkCSRF()
268d6d97f60SAnna Dabrowska    {
26913eddb0fSAndreas Gohr        global $INPUT;
27013eddb0fSAndreas Gohr        if (
27113eddb0fSAndreas Gohr            $INPUT->server->str('REMOTE_USER') &&
27213eddb0fSAndreas Gohr            getSecurityToken() != $INPUT->str('sectok')
27313eddb0fSAndreas Gohr        ) {
27413eddb0fSAndreas Gohr            throw new StructException('CSRF check failed');
27513eddb0fSAndreas Gohr        }
27613eddb0fSAndreas Gohr    }
2774731b875SAndreas Gohr}
278