xref: /plugin/struct/action/inline.php (revision 4cd5cc28e2bf004aed676e9b1f46cc188a4b2240)
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;
12*4cd5cc28SAnna Dabrowskause dokuwiki\plugin\struct\meta\AccessTableLookup;
134eed39ffSMichael Grosseuse dokuwiki\plugin\struct\meta\Assignments;
1493ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\Column;
15*4cd5cc28SAnna Dabrowskause dokuwiki\plugin\struct\meta\Schema;
164731b875SAndreas Gohruse dokuwiki\plugin\struct\meta\StructException;
1793ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\ValueValidator;
184731b875SAndreas Gohr
194731b875SAndreas Gohr/**
204731b875SAndreas Gohr * Class action_plugin_struct_inline
214731b875SAndreas Gohr *
224731b875SAndreas Gohr * Handle inline editing
234731b875SAndreas Gohr */
24d6d97f60SAnna Dabrowskaclass action_plugin_struct_inline extends DokuWiki_Action_Plugin
25d6d97f60SAnna Dabrowska{
264731b875SAndreas Gohr
2794c9aa4cSAndreas Gohr    /** @var  AccessTableData */
284731b875SAndreas Gohr    protected $schemadata = null;
294731b875SAndreas Gohr
304731b875SAndreas Gohr    /** @var  Column */
314731b875SAndreas Gohr    protected $column = null;
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     *
424731b875SAndreas Gohr     * @param Doku_Event_Handler $controller DokuWiki's event controller object
434731b875SAndreas Gohr     * @return void
444731b875SAndreas Gohr     */
45d6d97f60SAnna Dabrowska    public function register(Doku_Event_Handler $controller)
46d6d97f60SAnna Dabrowska    {
47748e747fSAnna Dabrowska        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax');
484731b875SAndreas Gohr    }
494731b875SAndreas Gohr
504731b875SAndreas Gohr    /**
514731b875SAndreas Gohr     * @param Doku_Event $event
524731b875SAndreas Gohr     * @param $param
534731b875SAndreas Gohr     */
54748e747fSAnna Dabrowska    public function handleAjax(Doku_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;
87cdd09a96SAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) return;
886ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) return;
89cdd09a96SAndreas Gohr        if (checklock($this->pid)) return;
904731b875SAndreas Gohr
91cdd09a96SAndreas Gohr        // lock page
92cdd09a96SAndreas Gohr        lock($this->pid);
934731b875SAndreas Gohr
94cdd09a96SAndreas Gohr        // output the editor
954731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
96ee983135SMichael Große        $id = uniqid('struct__', false);
977c4f397eSRandolf Rotta        echo '<div class="field">';
98ee983135SMichael Große        echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">';
9963e019d0SAndreas Gohr        echo '</label>';
1007c4f397eSRandolf Rotta        echo '<span class="input">';
101ee983135SMichael Große        echo $value->getValueEditor('entry', $id);
1027c4f397eSRandolf Rotta        echo '</span>';
1034731b875SAndreas Gohr        $hint = $this->column->getType()->getTranslatedHint();
1044731b875SAndreas Gohr        if ($hint) {
105712bc832SMichael Große            echo '<p class="hint">';
1064731b875SAndreas Gohr            echo hsc($hint);
107712bc832SMichael Große            echo '</p>';
1084731b875SAndreas Gohr        }
1097c4f397eSRandolf Rotta        echo '</div>';
110cdd09a96SAndreas Gohr
111cdd09a96SAndreas Gohr        // csrf protection
112cdd09a96SAndreas Gohr        formSecurityToken();
1134731b875SAndreas Gohr    }
1144731b875SAndreas Gohr
115cdd09a96SAndreas Gohr    /**
116cdd09a96SAndreas Gohr     * Save the data posted by the inline editor
117cdd09a96SAndreas Gohr     */
118748e747fSAnna Dabrowska    protected function inlineSave()
119d6d97f60SAnna Dabrowska    {
1204731b875SAndreas Gohr        global $INPUT;
1214731b875SAndreas Gohr
12213eddb0fSAndreas Gohr        // check preconditions
1234d2da382SAndreas Gohr        if (!$this->initFromInput()) {
1244d2da382SAndreas Gohr            throw new StructException('inline save error: init');
1254d2da382SAndreas Gohr        }
12613eddb0fSAndreas Gohr        self::checkCSRF();
1270ceefd5cSAnna Dabrowska        if (!$this->schemadata->getRid()) {
12813eddb0fSAndreas Gohr            $this->checkPage();
1294eed39ffSMichael Grosse            $assignments = Assignments::getInstance();
1304eed39ffSMichael Grosse            $tables = $assignments->getPageAssignments($this->pid, true);
1314eed39ffSMichael Grosse            if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) {
1324eed39ffSMichael Grosse                throw new StructException('inline save error: schema not assigned to page');
1334eed39ffSMichael Grosse            }
1344731b875SAndreas Gohr        }
1356ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) {
1366ebbbb8eSAndreas Gohr            throw new StructException('inline save error: no permission for schema');
1376ebbbb8eSAndreas Gohr        }
1384731b875SAndreas Gohr
1394731b875SAndreas Gohr        // validate
1404731b875SAndreas Gohr        $value = $INPUT->param('entry');
14193ca6f4fSAndreas Gohr        $validator = new ValueValidator();
1424731b875SAndreas Gohr        if (!$validator->validateValue($this->column, $value)) {
1434731b875SAndreas Gohr            throw new StructException(join("\n", $validator->getErrors()));
1444731b875SAndreas Gohr        }
1454731b875SAndreas Gohr
1464731b875SAndreas Gohr        // current data
1474731b875SAndreas Gohr        $tosave = $this->schemadata->getDataArray();
1484731b875SAndreas Gohr        $tosave[$this->column->getLabel()] = $value;
1494731b875SAndreas Gohr
1504731b875SAndreas Gohr        // save
151efe74305SAnna Dabrowska        if ($this->schemadata->getRid()) {
15213eddb0fSAndreas Gohr            $revision = 0;
15313eddb0fSAndreas Gohr        } else {
15413eddb0fSAndreas Gohr            $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit');
155858c5caaSMichael Grosse            p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table
15613eddb0fSAndreas Gohr        }
15713eddb0fSAndreas Gohr        $this->schemadata->setTimestamp($revision);
15813eddb0fSAndreas Gohr        try {
15913eddb0fSAndreas Gohr            if (!$this->schemadata->saveData($tosave)) {
16013eddb0fSAndreas Gohr                throw new StructException('saving failed');
16113eddb0fSAndreas Gohr            }
1620ceefd5cSAnna Dabrowska            if (!$this->schemadata->getRid()) {
1634eed39ffSMichael Grosse                // make sure this schema is assigned
164b8cff1dfSAndreas Gohr                /** @noinspection PhpUndefinedVariableInspection */
1654eed39ffSMichael Grosse                $assignments->assignPageSchema(
1664eed39ffSMichael Grosse                    $this->pid,
1674eed39ffSMichael Grosse                    $this->schemadata->getSchema()->getTable()
1684eed39ffSMichael Grosse                );
1694eed39ffSMichael Grosse            }
170b8cff1dfSAndreas Gohr        } catch (\Exception $e) {
171b8cff1dfSAndreas Gohr            // PHP <7 needs a catch block
172b8cff1dfSAndreas Gohr            throw $e;
17313eddb0fSAndreas Gohr        } finally {
17413eddb0fSAndreas Gohr            // unlock (unlocking a non-existing file is okay,
17513eddb0fSAndreas Gohr            // so we don't check if it's a lookup here
176cdd09a96SAndreas Gohr            unlock($this->pid);
17713eddb0fSAndreas Gohr        }
1784731b875SAndreas Gohr
1794731b875SAndreas Gohr        // reinit then render
18069f7ec8fSAnna Dabrowska        $this->initFromInput($this->schemadata->getTimestamp());
1814731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
1824731b875SAndreas Gohr        $R = new Doku_Renderer_xhtml();
1834731b875SAndreas Gohr        $value->render($R, 'xhtml'); // FIXME use configured default renderer
18469f7ec8fSAnna Dabrowska        $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()]);
18569f7ec8fSAnna Dabrowska        echo $data;
1864731b875SAndreas Gohr    }
1874731b875SAndreas Gohr
1884731b875SAndreas Gohr    /**
189cdd09a96SAndreas Gohr     * Unlock a page (on cancel action)
190cdd09a96SAndreas Gohr     */
191748e747fSAnna Dabrowska    protected function inlineCancel()
192d6d97f60SAnna Dabrowska    {
193cdd09a96SAndreas Gohr        global $INPUT;
194cdd09a96SAndreas Gohr        $pid = $INPUT->str('pid');
195cdd09a96SAndreas Gohr        unlock($pid);
196cdd09a96SAndreas Gohr    }
197cdd09a96SAndreas Gohr
198cdd09a96SAndreas Gohr    /**
1994731b875SAndreas Gohr     * Initialize internal state based on input variables
2004731b875SAndreas Gohr     *
20169f7ec8fSAnna Dabrowska     * @param int $updatedRev timestamp of currently created revision, might be newer than input variable
20269f7ec8fSAnna Dabrowska     * @return bool if initialization was successful
2034731b875SAndreas Gohr     */
204d6d97f60SAnna Dabrowska    protected function initFromInput($updatedRev = 0)
205d6d97f60SAnna Dabrowska    {
2064731b875SAndreas Gohr        global $INPUT;
2074731b875SAndreas Gohr
2084731b875SAndreas Gohr        $this->schemadata = null;
2094731b875SAndreas Gohr        $this->column = null;
2104731b875SAndreas Gohr
2114731b875SAndreas Gohr        $pid = $INPUT->str('pid');
2126fd73b4bSAnna Dabrowska        $rid = $INPUT->int('rid');
21369f7ec8fSAnna Dabrowska        $rev = $updatedRev ?: $INPUT->int('rev');
21469f7ec8fSAnna Dabrowska
2154731b875SAndreas Gohr        list($table, $field) = explode('.', $INPUT->str('field'));
2166fd73b4bSAnna Dabrowska        if (blank($pid) && blank($rid)) return false;
2174731b875SAndreas Gohr        if (blank($table)) return false;
2184731b875SAndreas Gohr        if (blank($field)) return false;
2194731b875SAndreas Gohr
2204731b875SAndreas Gohr        $this->pid = $pid;
2214ec54c67SAndreas Gohr        try {
222*4cd5cc28SAnna Dabrowska            if (AccessTable::isTypePage($pid, $rev)) {
223*4cd5cc28SAnna Dabrowska                $this->schemadata = AccessTable::getPageAccess($table, $pid);
224*4cd5cc28SAnna Dabrowska            } elseif (AccessTable::isTypeSerial($pid, $rev)) {
225*4cd5cc28SAnna Dabrowska                $this->schemadata = AccessTable::getSerialAccess($table, $pid, $rid);
226*4cd5cc28SAnna Dabrowska            } else {
227*4cd5cc28SAnna Dabrowska                $this->schemadata = AccessTable::getLookupAccess($table, $rid);
228*4cd5cc28SAnna Dabrowska            }
2294ec54c67SAndreas Gohr        } catch (StructException $ignore) {
2304731b875SAndreas Gohr            return false;
2314731b875SAndreas Gohr        }
2324731b875SAndreas Gohr
2334ec54c67SAndreas Gohr        $this->column = $this->schemadata->getSchema()->findColumn($field);
2344731b875SAndreas Gohr        if (!$this->column || !$this->column->isVisibleInEditor()) {
2354731b875SAndreas Gohr            $this->schemadata = null;
2364731b875SAndreas Gohr            $this->column = null;
2374731b875SAndreas Gohr            return false;
2384731b875SAndreas Gohr        }
2394731b875SAndreas Gohr
2404731b875SAndreas Gohr        return true;
2414731b875SAndreas Gohr    }
2424731b875SAndreas Gohr
24313eddb0fSAndreas Gohr    /**
24413eddb0fSAndreas Gohr     * Checks if a page can be edited
24513eddb0fSAndreas Gohr     *
24613eddb0fSAndreas Gohr     * @throws StructException when check fails
24713eddb0fSAndreas Gohr     */
248d6d97f60SAnna Dabrowska    protected function checkPage()
249d6d97f60SAnna Dabrowska    {
25013eddb0fSAndreas Gohr        if (!page_exists($this->pid)) {
25113eddb0fSAndreas Gohr            throw new StructException('inline save error: no such page');
25213eddb0fSAndreas Gohr        }
25313eddb0fSAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) {
25413eddb0fSAndreas Gohr            throw new StructException('inline save error: acl');
25513eddb0fSAndreas Gohr        }
25613eddb0fSAndreas Gohr        if (checklock($this->pid)) {
25713eddb0fSAndreas Gohr            throw new StructException('inline save error: lock');
25813eddb0fSAndreas Gohr        }
25913eddb0fSAndreas Gohr    }
26013eddb0fSAndreas Gohr
26113eddb0fSAndreas Gohr    /**
26213eddb0fSAndreas Gohr     * Our own implementation of checkSecurityToken because we don't want the msg() call
26313eddb0fSAndreas Gohr     *
26413eddb0fSAndreas Gohr     * @throws StructException when check fails
26513eddb0fSAndreas Gohr     */
266d6d97f60SAnna Dabrowska    public static function checkCSRF()
267d6d97f60SAnna Dabrowska    {
26813eddb0fSAndreas Gohr        global $INPUT;
26913eddb0fSAndreas Gohr        if (
27013eddb0fSAndreas Gohr            $INPUT->server->str('REMOTE_USER') &&
27113eddb0fSAndreas Gohr            getSecurityToken() != $INPUT->str('sectok')
27213eddb0fSAndreas Gohr        ) {
27313eddb0fSAndreas Gohr            throw new StructException('CSRF check failed');
27413eddb0fSAndreas Gohr        }
27513eddb0fSAndreas Gohr    }
2764731b875SAndreas Gohr}
277