xref: /plugin/struct/action/inline.php (revision 748e747f37aa44250ee32847b5fc3ff1e47f0835)
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
104ec54c67SAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTable;
1194c9aa4cSAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTableData;
124eed39ffSMichael Grosseuse dokuwiki\plugin\struct\meta\Assignments;
1393ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\Column;
144731b875SAndreas Gohruse dokuwiki\plugin\struct\meta\StructException;
1593ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\ValueValidator;
164731b875SAndreas Gohr
174731b875SAndreas Gohr/**
184731b875SAndreas Gohr * Class action_plugin_struct_inline
194731b875SAndreas Gohr *
204731b875SAndreas Gohr * Handle inline editing
214731b875SAndreas Gohr */
22d6d97f60SAnna Dabrowskaclass action_plugin_struct_inline extends DokuWiki_Action_Plugin
23d6d97f60SAnna Dabrowska{
244731b875SAndreas Gohr
2594c9aa4cSAndreas Gohr    /** @var  AccessTableData */
264731b875SAndreas Gohr    protected $schemadata = null;
274731b875SAndreas Gohr
284731b875SAndreas Gohr    /** @var  Column */
294731b875SAndreas Gohr    protected $column = null;
304731b875SAndreas Gohr
314731b875SAndreas Gohr    /** @var String */
324731b875SAndreas Gohr    protected $pid = '';
334731b875SAndreas Gohr
346fd73b4bSAnna Dabrowska    /** @var int */
356fd73b4bSAnna Dabrowska    protected $rid = 0;
366fd73b4bSAnna Dabrowska
374731b875SAndreas Gohr    /**
384731b875SAndreas Gohr     * Registers a callback function for a given event
394731b875SAndreas Gohr     *
404731b875SAndreas Gohr     * @param Doku_Event_Handler $controller DokuWiki's event controller object
414731b875SAndreas Gohr     * @return void
424731b875SAndreas Gohr     */
43d6d97f60SAnna Dabrowska    public function register(Doku_Event_Handler $controller)
44d6d97f60SAnna Dabrowska    {
45*748e747fSAnna Dabrowska        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax');
464731b875SAndreas Gohr    }
474731b875SAndreas Gohr
484731b875SAndreas Gohr    /**
494731b875SAndreas Gohr     * @param Doku_Event $event
504731b875SAndreas Gohr     * @param $param
514731b875SAndreas Gohr     */
52*748e747fSAnna Dabrowska    public function handleAjax(Doku_Event $event, $param)
53d6d97f60SAnna Dabrowska    {
544731b875SAndreas Gohr        $len = strlen('plugin_struct_inline_');
554731b875SAndreas Gohr        if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return;
564731b875SAndreas Gohr        $event->preventDefault();
574731b875SAndreas Gohr        $event->stopPropagation();
584731b875SAndreas Gohr
594731b875SAndreas Gohr        if (substr($event->data, $len) == 'editor') {
60*748e747fSAnna Dabrowska            $this->inlineEditor();
614731b875SAndreas Gohr        }
624731b875SAndreas Gohr
634731b875SAndreas Gohr        if (substr($event->data, $len) == 'save') {
644731b875SAndreas Gohr            try {
65*748e747fSAnna Dabrowska                $this->inlineSave();
664731b875SAndreas Gohr            } catch (StructException $e) {
674731b875SAndreas Gohr                http_status(500);
684731b875SAndreas Gohr                header('Content-Type: text/plain; charset=utf-8');
694731b875SAndreas Gohr                echo $e->getMessage();
704731b875SAndreas Gohr            }
714731b875SAndreas Gohr        }
72cdd09a96SAndreas Gohr
73cdd09a96SAndreas Gohr        if (substr($event->data, $len) == 'cancel') {
74*748e747fSAnna Dabrowska            $this->inlineCancel();
75cdd09a96SAndreas Gohr        }
764731b875SAndreas Gohr    }
774731b875SAndreas Gohr
78cdd09a96SAndreas Gohr    /**
79cdd09a96SAndreas Gohr     * Creates the inline editor
80cdd09a96SAndreas Gohr     */
81*748e747fSAnna Dabrowska    protected function inlineEditor()
82d6d97f60SAnna Dabrowska    {
83cdd09a96SAndreas Gohr        // silently fail when editing not possible
844731b875SAndreas Gohr        if (!$this->initFromInput()) return;
85cdd09a96SAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) return;
866ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) return;
87cdd09a96SAndreas Gohr        if (checklock($this->pid)) return;
884731b875SAndreas Gohr
89cdd09a96SAndreas Gohr        // lock page
90cdd09a96SAndreas Gohr        lock($this->pid);
914731b875SAndreas Gohr
92cdd09a96SAndreas Gohr        // output the editor
934731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
94ee983135SMichael Große        $id = uniqid('struct__', false);
957c4f397eSRandolf Rotta        echo '<div class="field">';
96ee983135SMichael Große        echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">';
9763e019d0SAndreas Gohr        echo '</label>';
987c4f397eSRandolf Rotta        echo '<span class="input">';
99ee983135SMichael Große        echo $value->getValueEditor('entry', $id);
1007c4f397eSRandolf Rotta        echo '</span>';
1014731b875SAndreas Gohr        $hint = $this->column->getType()->getTranslatedHint();
1024731b875SAndreas Gohr        if ($hint) {
103712bc832SMichael Große            echo '<p class="hint">';
1044731b875SAndreas Gohr            echo hsc($hint);
105712bc832SMichael Große            echo '</p>';
1064731b875SAndreas Gohr        }
1077c4f397eSRandolf Rotta        echo '</div>';
108cdd09a96SAndreas Gohr
109cdd09a96SAndreas Gohr        // csrf protection
110cdd09a96SAndreas Gohr        formSecurityToken();
1114731b875SAndreas Gohr    }
1124731b875SAndreas Gohr
113cdd09a96SAndreas Gohr    /**
114cdd09a96SAndreas Gohr     * Save the data posted by the inline editor
115cdd09a96SAndreas Gohr     */
116*748e747fSAnna Dabrowska    protected function inlineSave()
117d6d97f60SAnna Dabrowska    {
1184731b875SAndreas Gohr        global $INPUT;
1194731b875SAndreas Gohr
12013eddb0fSAndreas Gohr        // check preconditions
1214d2da382SAndreas Gohr        if (!$this->initFromInput()) {
1224d2da382SAndreas Gohr            throw new StructException('inline save error: init');
1234d2da382SAndreas Gohr        }
12413eddb0fSAndreas Gohr        self::checkCSRF();
1250ceefd5cSAnna Dabrowska        if (!$this->schemadata->getRid()) {
12613eddb0fSAndreas Gohr            $this->checkPage();
1274eed39ffSMichael Grosse            $assignments = Assignments::getInstance();
1284eed39ffSMichael Grosse            $tables = $assignments->getPageAssignments($this->pid, true);
1294eed39ffSMichael Grosse            if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) {
1304eed39ffSMichael Grosse                throw new StructException('inline save error: schema not assigned to page');
1314eed39ffSMichael Grosse            }
1324731b875SAndreas Gohr        }
1336ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) {
1346ebbbb8eSAndreas Gohr            throw new StructException('inline save error: no permission for schema');
1356ebbbb8eSAndreas Gohr        }
1364731b875SAndreas Gohr
1374731b875SAndreas Gohr        // validate
1384731b875SAndreas Gohr        $value = $INPUT->param('entry');
13993ca6f4fSAndreas Gohr        $validator = new ValueValidator();
1404731b875SAndreas Gohr        if (!$validator->validateValue($this->column, $value)) {
1414731b875SAndreas Gohr            throw new StructException(join("\n", $validator->getErrors()));
1424731b875SAndreas Gohr        }
1434731b875SAndreas Gohr
1444731b875SAndreas Gohr        // current data
1454731b875SAndreas Gohr        $tosave = $this->schemadata->getDataArray();
1464731b875SAndreas Gohr        $tosave[$this->column->getLabel()] = $value;
1474731b875SAndreas Gohr
1484731b875SAndreas Gohr        // save
149efe74305SAnna Dabrowska        if ($this->schemadata->getRid()) {
15013eddb0fSAndreas Gohr            $revision = 0;
15113eddb0fSAndreas Gohr        } else {
15213eddb0fSAndreas Gohr            $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit');
153858c5caaSMichael Grosse            p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table
15413eddb0fSAndreas Gohr        }
15513eddb0fSAndreas Gohr        $this->schemadata->setTimestamp($revision);
15613eddb0fSAndreas Gohr        try {
15713eddb0fSAndreas Gohr            if (!$this->schemadata->saveData($tosave)) {
15813eddb0fSAndreas Gohr                throw new StructException('saving failed');
15913eddb0fSAndreas Gohr            }
1600ceefd5cSAnna Dabrowska            if (!$this->schemadata->getRid()) {
1614eed39ffSMichael Grosse                // make sure this schema is assigned
162b8cff1dfSAndreas Gohr                /** @noinspection PhpUndefinedVariableInspection */
1634eed39ffSMichael Grosse                $assignments->assignPageSchema(
1644eed39ffSMichael Grosse                    $this->pid,
1654eed39ffSMichael Grosse                    $this->schemadata->getSchema()->getTable()
1664eed39ffSMichael Grosse                );
1674eed39ffSMichael Grosse            }
168b8cff1dfSAndreas Gohr        } catch (\Exception $e) {
169b8cff1dfSAndreas Gohr            // PHP <7 needs a catch block
170b8cff1dfSAndreas Gohr            throw $e;
17113eddb0fSAndreas Gohr        } finally {
17213eddb0fSAndreas Gohr            // unlock (unlocking a non-existing file is okay,
17313eddb0fSAndreas Gohr            // so we don't check if it's a lookup here
174cdd09a96SAndreas Gohr            unlock($this->pid);
17513eddb0fSAndreas Gohr        }
1764731b875SAndreas Gohr
1774731b875SAndreas Gohr        // reinit then render
17869f7ec8fSAnna Dabrowska        $this->initFromInput($this->schemadata->getTimestamp());
1794731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
1804731b875SAndreas Gohr        $R = new Doku_Renderer_xhtml();
1814731b875SAndreas Gohr        $value->render($R, 'xhtml'); // FIXME use configured default renderer
18269f7ec8fSAnna Dabrowska        $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()]);
18369f7ec8fSAnna Dabrowska        echo $data;
1844731b875SAndreas Gohr    }
1854731b875SAndreas Gohr
1864731b875SAndreas Gohr    /**
187cdd09a96SAndreas Gohr     * Unlock a page (on cancel action)
188cdd09a96SAndreas Gohr     */
189*748e747fSAnna Dabrowska    protected function inlineCancel()
190d6d97f60SAnna Dabrowska    {
191cdd09a96SAndreas Gohr        global $INPUT;
192cdd09a96SAndreas Gohr        $pid = $INPUT->str('pid');
193cdd09a96SAndreas Gohr        unlock($pid);
194cdd09a96SAndreas Gohr    }
195cdd09a96SAndreas Gohr
196cdd09a96SAndreas Gohr    /**
1974731b875SAndreas Gohr     * Initialize internal state based on input variables
1984731b875SAndreas Gohr     *
19969f7ec8fSAnna Dabrowska     * @param int $updatedRev timestamp of currently created revision, might be newer than input variable
20069f7ec8fSAnna Dabrowska     * @return bool if initialization was successful
2014731b875SAndreas Gohr     */
202d6d97f60SAnna Dabrowska    protected function initFromInput($updatedRev = 0)
203d6d97f60SAnna Dabrowska    {
2044731b875SAndreas Gohr        global $INPUT;
2054731b875SAndreas Gohr
2064731b875SAndreas Gohr        $this->schemadata = null;
2074731b875SAndreas Gohr        $this->column = null;
2084731b875SAndreas Gohr
2094731b875SAndreas Gohr        $pid = $INPUT->str('pid');
2106fd73b4bSAnna Dabrowska        $rid = $INPUT->int('rid');
21169f7ec8fSAnna Dabrowska        $rev = $updatedRev ?: $INPUT->int('rev');
21269f7ec8fSAnna Dabrowska
2134731b875SAndreas Gohr        list($table, $field) = explode('.', $INPUT->str('field'));
2146fd73b4bSAnna Dabrowska        if (blank($pid) && blank($rid)) return false;
2154731b875SAndreas Gohr        if (blank($table)) return false;
2164731b875SAndreas Gohr        if (blank($field)) return false;
2174731b875SAndreas Gohr
2184731b875SAndreas Gohr        $this->pid = $pid;
2194ec54c67SAndreas Gohr        try {
2206fd73b4bSAnna Dabrowska            $this->schemadata = AccessTable::byTableName($table, $pid, $rev, $rid);
2214ec54c67SAndreas Gohr        } catch (StructException $ignore) {
2224731b875SAndreas Gohr            return false;
2234731b875SAndreas Gohr        }
2244731b875SAndreas Gohr
2254ec54c67SAndreas Gohr        $this->column = $this->schemadata->getSchema()->findColumn($field);
2264731b875SAndreas Gohr        if (!$this->column || !$this->column->isVisibleInEditor()) {
2274731b875SAndreas Gohr            $this->schemadata = null;
2284731b875SAndreas Gohr            $this->column = null;
2294731b875SAndreas Gohr            return false;
2304731b875SAndreas Gohr        }
2314731b875SAndreas Gohr
2324731b875SAndreas Gohr        return true;
2334731b875SAndreas Gohr    }
2344731b875SAndreas Gohr
23513eddb0fSAndreas Gohr    /**
23613eddb0fSAndreas Gohr     * Checks if a page can be edited
23713eddb0fSAndreas Gohr     *
23813eddb0fSAndreas Gohr     * @throws StructException when check fails
23913eddb0fSAndreas Gohr     */
240d6d97f60SAnna Dabrowska    protected function checkPage()
241d6d97f60SAnna Dabrowska    {
24213eddb0fSAndreas Gohr        if (!page_exists($this->pid)) {
24313eddb0fSAndreas Gohr            throw new StructException('inline save error: no such page');
24413eddb0fSAndreas Gohr        }
24513eddb0fSAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) {
24613eddb0fSAndreas Gohr            throw new StructException('inline save error: acl');
24713eddb0fSAndreas Gohr        }
24813eddb0fSAndreas Gohr        if (checklock($this->pid)) {
24913eddb0fSAndreas Gohr            throw new StructException('inline save error: lock');
25013eddb0fSAndreas Gohr        }
25113eddb0fSAndreas Gohr    }
25213eddb0fSAndreas Gohr
25313eddb0fSAndreas Gohr    /**
25413eddb0fSAndreas Gohr     * Our own implementation of checkSecurityToken because we don't want the msg() call
25513eddb0fSAndreas Gohr     *
25613eddb0fSAndreas Gohr     * @throws StructException when check fails
25713eddb0fSAndreas Gohr     */
258d6d97f60SAnna Dabrowska    public static function checkCSRF()
259d6d97f60SAnna Dabrowska    {
26013eddb0fSAndreas Gohr        global $INPUT;
26113eddb0fSAndreas Gohr        if (
26213eddb0fSAndreas Gohr            $INPUT->server->str('REMOTE_USER') &&
26313eddb0fSAndreas Gohr            getSecurityToken() != $INPUT->str('sectok')
26413eddb0fSAndreas Gohr        ) {
26513eddb0fSAndreas Gohr            throw new StructException('CSRF check failed');
26613eddb0fSAndreas Gohr        }
26713eddb0fSAndreas Gohr    }
2684731b875SAndreas Gohr}
269