xref: /plugin/struct/action/inline.php (revision d6d97f6064c3b0f90310be8341edc9585520ee54) !
14731b875SAndreas Gohr<?php
2*d6d97f60SAnna 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
104731b875SAndreas Gohr// must be run within Dokuwiki
114ec54c67SAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTable;
1294c9aa4cSAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTableData;
134eed39ffSMichael Grosseuse dokuwiki\plugin\struct\meta\Assignments;
1493ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\Column;
154731b875SAndreas Gohruse dokuwiki\plugin\struct\meta\StructException;
1693ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\ValueValidator;
174731b875SAndreas Gohr
184731b875SAndreas Gohrif (!defined('DOKU_INC')) die();
194731b875SAndreas Gohr
204731b875SAndreas Gohr/**
214731b875SAndreas Gohr * Class action_plugin_struct_inline
224731b875SAndreas Gohr *
234731b875SAndreas Gohr * Handle inline editing
244731b875SAndreas Gohr */
25*d6d97f60SAnna Dabrowskaclass action_plugin_struct_inline extends DokuWiki_Action_Plugin
26*d6d97f60SAnna Dabrowska{
274731b875SAndreas Gohr
2894c9aa4cSAndreas Gohr    /** @var  AccessTableData */
294731b875SAndreas Gohr    protected $schemadata = null;
304731b875SAndreas Gohr
314731b875SAndreas Gohr    /** @var  Column */
324731b875SAndreas Gohr    protected $column = null;
334731b875SAndreas Gohr
344731b875SAndreas Gohr    /** @var String */
354731b875SAndreas Gohr    protected $pid = '';
364731b875SAndreas Gohr
376fd73b4bSAnna Dabrowska    /** @var int */
386fd73b4bSAnna Dabrowska    protected $rid = 0;
396fd73b4bSAnna Dabrowska
404731b875SAndreas Gohr    /**
414731b875SAndreas Gohr     * Registers a callback function for a given event
424731b875SAndreas Gohr     *
434731b875SAndreas Gohr     * @param Doku_Event_Handler $controller DokuWiki's event controller object
444731b875SAndreas Gohr     * @return void
454731b875SAndreas Gohr     */
46*d6d97f60SAnna Dabrowska    public function register(Doku_Event_Handler $controller)
47*d6d97f60SAnna Dabrowska    {
484731b875SAndreas Gohr        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
494731b875SAndreas Gohr    }
504731b875SAndreas Gohr
514731b875SAndreas Gohr    /**
524731b875SAndreas Gohr     * @param Doku_Event $event
534731b875SAndreas Gohr     * @param $param
544731b875SAndreas Gohr     */
55*d6d97f60SAnna Dabrowska    public function handle_ajax(Doku_Event $event, $param)
56*d6d97f60SAnna Dabrowska    {
574731b875SAndreas Gohr        $len = strlen('plugin_struct_inline_');
584731b875SAndreas Gohr        if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return;
594731b875SAndreas Gohr        $event->preventDefault();
604731b875SAndreas Gohr        $event->stopPropagation();
614731b875SAndreas Gohr
624731b875SAndreas Gohr        if (substr($event->data, $len) == 'editor') {
634731b875SAndreas Gohr            $this->inline_editor();
644731b875SAndreas Gohr        }
654731b875SAndreas Gohr
664731b875SAndreas Gohr        if (substr($event->data, $len) == 'save') {
674731b875SAndreas Gohr            try {
684731b875SAndreas Gohr                $this->inline_save();
694731b875SAndreas Gohr            } catch (StructException $e) {
704731b875SAndreas Gohr                http_status(500);
714731b875SAndreas Gohr                header('Content-Type: text/plain; charset=utf-8');
724731b875SAndreas Gohr                echo $e->getMessage();
734731b875SAndreas Gohr            }
744731b875SAndreas Gohr        }
75cdd09a96SAndreas Gohr
76cdd09a96SAndreas Gohr        if (substr($event->data, $len) == 'cancel') {
77cdd09a96SAndreas Gohr            $this->inline_cancel();
78cdd09a96SAndreas Gohr        }
794731b875SAndreas Gohr    }
804731b875SAndreas Gohr
81cdd09a96SAndreas Gohr    /**
82cdd09a96SAndreas Gohr     * Creates the inline editor
83cdd09a96SAndreas Gohr     */
84*d6d97f60SAnna Dabrowska    protected function inline_editor()
85*d6d97f60SAnna Dabrowska    {
86cdd09a96SAndreas Gohr        // silently fail when editing not possible
874731b875SAndreas Gohr        if (!$this->initFromInput()) return;
88cdd09a96SAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) return;
896ebbbb8eSAndreas Gohr        if (!$this->schemadata->getSchema()->isEditable()) 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     */
119*d6d97f60SAnna Dabrowska    protected function inline_save()
120*d6d97f60SAnna 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)) {
1444731b875SAndreas Gohr            throw new StructException(join("\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
18569f7ec8fSAnna Dabrowska        $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()]);
18669f7ec8fSAnna Dabrowska        echo $data;
1874731b875SAndreas Gohr    }
1884731b875SAndreas Gohr
1894731b875SAndreas Gohr    /**
190cdd09a96SAndreas Gohr     * Unlock a page (on cancel action)
191cdd09a96SAndreas Gohr     */
192*d6d97f60SAnna Dabrowska    protected function inline_cancel()
193*d6d97f60SAnna 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     */
205*d6d97f60SAnna Dabrowska    protected function initFromInput($updatedRev = 0)
206*d6d97f60SAnna 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
2164731b875SAndreas Gohr        list($table, $field) = explode('.', $INPUT->str('field'));
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 {
2236fd73b4bSAnna Dabrowska            $this->schemadata = AccessTable::byTableName($table, $pid, $rev, $rid);
2244ec54c67SAndreas Gohr        } catch (StructException $ignore) {
2254731b875SAndreas Gohr            return false;
2264731b875SAndreas Gohr        }
2274731b875SAndreas Gohr
2284ec54c67SAndreas Gohr        $this->column = $this->schemadata->getSchema()->findColumn($field);
2294731b875SAndreas Gohr        if (!$this->column || !$this->column->isVisibleInEditor()) {
2304731b875SAndreas Gohr            $this->schemadata = null;
2314731b875SAndreas Gohr            $this->column = null;
2324731b875SAndreas Gohr            return false;
2334731b875SAndreas Gohr        }
2344731b875SAndreas Gohr
2354731b875SAndreas Gohr        return true;
2364731b875SAndreas Gohr    }
2374731b875SAndreas Gohr
23813eddb0fSAndreas Gohr    /**
23913eddb0fSAndreas Gohr     * Checks if a page can be edited
24013eddb0fSAndreas Gohr     *
24113eddb0fSAndreas Gohr     * @throws StructException when check fails
24213eddb0fSAndreas Gohr     */
243*d6d97f60SAnna Dabrowska    protected function checkPage()
244*d6d97f60SAnna Dabrowska    {
24513eddb0fSAndreas Gohr        if (!page_exists($this->pid)) {
24613eddb0fSAndreas Gohr            throw new StructException('inline save error: no such page');
24713eddb0fSAndreas Gohr        }
24813eddb0fSAndreas Gohr        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) {
24913eddb0fSAndreas Gohr            throw new StructException('inline save error: acl');
25013eddb0fSAndreas Gohr        }
25113eddb0fSAndreas Gohr        if (checklock($this->pid)) {
25213eddb0fSAndreas Gohr            throw new StructException('inline save error: lock');
25313eddb0fSAndreas Gohr        }
25413eddb0fSAndreas Gohr    }
25513eddb0fSAndreas Gohr
25613eddb0fSAndreas Gohr    /**
25713eddb0fSAndreas Gohr     * Our own implementation of checkSecurityToken because we don't want the msg() call
25813eddb0fSAndreas Gohr     *
25913eddb0fSAndreas Gohr     * @throws StructException when check fails
26013eddb0fSAndreas Gohr     */
261*d6d97f60SAnna Dabrowska    public static function checkCSRF()
262*d6d97f60SAnna Dabrowska    {
26313eddb0fSAndreas Gohr        global $INPUT;
26413eddb0fSAndreas Gohr        if (
26513eddb0fSAndreas Gohr            $INPUT->server->str('REMOTE_USER') &&
26613eddb0fSAndreas Gohr            getSecurityToken() != $INPUT->str('sectok')
26713eddb0fSAndreas Gohr        ) {
26813eddb0fSAndreas Gohr            throw new StructException('CSRF check failed');
26913eddb0fSAndreas Gohr        }
27013eddb0fSAndreas Gohr    }
2714731b875SAndreas Gohr}
272