xref: /plugin/struct/action/inline.php (revision efe74305e33c04e0a85fabebfa21d58c1c78c3dd) !
14731b875SAndreas Gohr<?php
24731b875SAndreas Gohr/**
34731b875SAndreas Gohr * DokuWiki Plugin struct (Action Component)
44731b875SAndreas Gohr *
54731b875SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
64731b875SAndreas Gohr * @author  Andreas Gohr, Michael Große <dokuwiki@cosmocode.de>
74731b875SAndreas Gohr */
84731b875SAndreas Gohr
94731b875SAndreas Gohr// must be run within Dokuwiki
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 Gohrif(!defined('DOKU_INC')) die();
184731b875SAndreas Gohr
194731b875SAndreas Gohr/**
204731b875SAndreas Gohr * Class action_plugin_struct_inline
214731b875SAndreas Gohr *
224731b875SAndreas Gohr * Handle inline editing
234731b875SAndreas Gohr */
244731b875SAndreas Gohrclass action_plugin_struct_inline extends DokuWiki_Action_Plugin {
254731b875SAndreas Gohr
2694c9aa4cSAndreas Gohr    /** @var  AccessTableData */
274731b875SAndreas Gohr    protected $schemadata = null;
284731b875SAndreas Gohr
294731b875SAndreas Gohr    /** @var  Column */
304731b875SAndreas Gohr    protected $column = null;
314731b875SAndreas Gohr
324731b875SAndreas Gohr    /** @var String */
334731b875SAndreas Gohr    protected $pid = '';
344731b875SAndreas Gohr
356fd73b4bSAnna Dabrowska    /** @var int */
366fd73b4bSAnna Dabrowska    protected $rid = 0;
376fd73b4bSAnna Dabrowska
384731b875SAndreas Gohr    /**
394731b875SAndreas Gohr     * Registers a callback function for a given event
404731b875SAndreas Gohr     *
414731b875SAndreas Gohr     * @param Doku_Event_Handler $controller DokuWiki's event controller object
424731b875SAndreas Gohr     * @return void
434731b875SAndreas Gohr     */
444731b875SAndreas Gohr    public function register(Doku_Event_Handler $controller) {
454731b875SAndreas Gohr        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
464731b875SAndreas Gohr    }
474731b875SAndreas Gohr
484731b875SAndreas Gohr    /**
494731b875SAndreas Gohr     * @param Doku_Event $event
504731b875SAndreas Gohr     * @param $param
514731b875SAndreas Gohr     */
524731b875SAndreas Gohr    public function handle_ajax(Doku_Event $event, $param) {
534731b875SAndreas Gohr        $len = strlen('plugin_struct_inline_');
544731b875SAndreas Gohr        if(substr($event->data, 0, $len) != 'plugin_struct_inline_') return;
554731b875SAndreas Gohr        $event->preventDefault();
564731b875SAndreas Gohr        $event->stopPropagation();
574731b875SAndreas Gohr
584731b875SAndreas Gohr        if(substr($event->data, $len) == 'editor') {
594731b875SAndreas Gohr            $this->inline_editor();
604731b875SAndreas Gohr        }
614731b875SAndreas Gohr
624731b875SAndreas Gohr        if(substr($event->data, $len) == 'save') {
634731b875SAndreas Gohr            try {
644731b875SAndreas Gohr                $this->inline_save();
654731b875SAndreas Gohr            } catch(StructException $e) {
664731b875SAndreas Gohr                http_status(500);
674731b875SAndreas Gohr                header('Content-Type: text/plain; charset=utf-8');
684731b875SAndreas Gohr                echo $e->getMessage();
694731b875SAndreas Gohr            }
704731b875SAndreas Gohr        }
71cdd09a96SAndreas Gohr
72cdd09a96SAndreas Gohr        if(substr($event->data, $len) == 'cancel') {
73cdd09a96SAndreas Gohr            $this->inline_cancel();
74cdd09a96SAndreas Gohr        }
754731b875SAndreas Gohr    }
764731b875SAndreas Gohr
77cdd09a96SAndreas Gohr    /**
78cdd09a96SAndreas Gohr     * Creates the inline editor
79cdd09a96SAndreas Gohr     */
804731b875SAndreas Gohr    protected function inline_editor() {
81cdd09a96SAndreas Gohr        // silently fail when editing not possible
824731b875SAndreas Gohr        if(!$this->initFromInput()) return;
83cdd09a96SAndreas Gohr        if(auth_quickaclcheck($this->pid) < AUTH_EDIT) return;
846ebbbb8eSAndreas Gohr        if(!$this->schemadata->getSchema()->isEditable()) return;
85cdd09a96SAndreas Gohr        if(checklock($this->pid)) return;
864731b875SAndreas Gohr
87cdd09a96SAndreas Gohr        // lock page
88cdd09a96SAndreas Gohr        lock($this->pid);
894731b875SAndreas Gohr
90cdd09a96SAndreas Gohr        // output the editor
914731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
92ee983135SMichael Große        $id = uniqid('struct__', false);
937c4f397eSRandolf Rotta        echo '<div class="field">';
94ee983135SMichael Große        echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">';
9563e019d0SAndreas Gohr        echo '</label>';
967c4f397eSRandolf Rotta        echo '<span class="input">';
97ee983135SMichael Große        echo $value->getValueEditor('entry', $id);
987c4f397eSRandolf Rotta        echo '</span>';
994731b875SAndreas Gohr        $hint = $this->column->getType()->getTranslatedHint();
1004731b875SAndreas Gohr        if($hint) {
101712bc832SMichael Große            echo '<p class="hint">';
1024731b875SAndreas Gohr            echo hsc($hint);
103712bc832SMichael Große            echo '</p>';
1044731b875SAndreas Gohr        }
1057c4f397eSRandolf Rotta        echo '</div>';
106cdd09a96SAndreas Gohr
107cdd09a96SAndreas Gohr        // csrf protection
108cdd09a96SAndreas Gohr        formSecurityToken();
1094731b875SAndreas Gohr    }
1104731b875SAndreas Gohr
111cdd09a96SAndreas Gohr    /**
112cdd09a96SAndreas Gohr     * Save the data posted by the inline editor
113cdd09a96SAndreas Gohr     */
1144731b875SAndreas Gohr    protected function inline_save() {
1154731b875SAndreas Gohr        global $INPUT;
1164731b875SAndreas Gohr
11713eddb0fSAndreas Gohr        // check preconditions
1184d2da382SAndreas Gohr        if(!$this->initFromInput()) {
1194d2da382SAndreas Gohr            throw new StructException('inline save error: init');
1204d2da382SAndreas Gohr        }
12113eddb0fSAndreas Gohr        self::checkCSRF();
1220ceefd5cSAnna Dabrowska        if(!$this->schemadata->getRid()) {
12313eddb0fSAndreas Gohr            $this->checkPage();
1244eed39ffSMichael Grosse            $assignments = Assignments::getInstance();
1254eed39ffSMichael Grosse            $tables = $assignments->getPageAssignments($this->pid, true);
1264eed39ffSMichael Grosse            if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) {
1274eed39ffSMichael Grosse                throw new StructException('inline save error: schema not assigned to page');
1284eed39ffSMichael Grosse            }
1294731b875SAndreas Gohr        }
1306ebbbb8eSAndreas Gohr        if(!$this->schemadata->getSchema()->isEditable()) {
1316ebbbb8eSAndreas Gohr            throw new StructException('inline save error: no permission for schema');
1326ebbbb8eSAndreas Gohr        }
1334731b875SAndreas Gohr
1344731b875SAndreas Gohr        // validate
1354731b875SAndreas Gohr        $value = $INPUT->param('entry');
13693ca6f4fSAndreas Gohr        $validator = new ValueValidator();
1374731b875SAndreas Gohr        if(!$validator->validateValue($this->column, $value)) {
1384731b875SAndreas Gohr            throw new StructException(join("\n", $validator->getErrors()));
1394731b875SAndreas Gohr        }
1404731b875SAndreas Gohr
1414731b875SAndreas Gohr        // current data
1424731b875SAndreas Gohr        $tosave = $this->schemadata->getDataArray();
1434731b875SAndreas Gohr        $tosave[$this->column->getLabel()] = $value;
1444731b875SAndreas Gohr
1454731b875SAndreas Gohr        // save
146*efe74305SAnna Dabrowska        if($this->schemadata->getRid()) {
14713eddb0fSAndreas Gohr            $revision = 0;
14813eddb0fSAndreas Gohr        } else {
14913eddb0fSAndreas Gohr            $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit');
150858c5caaSMichael Grosse            p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table
15113eddb0fSAndreas Gohr        }
15213eddb0fSAndreas Gohr        $this->schemadata->setTimestamp($revision);
15313eddb0fSAndreas Gohr        try {
15413eddb0fSAndreas Gohr            if(!$this->schemadata->saveData($tosave)) {
15513eddb0fSAndreas Gohr                throw new StructException('saving failed');
15613eddb0fSAndreas Gohr            }
1570ceefd5cSAnna Dabrowska            if(!$this->schemadata->getRid()) {
1584eed39ffSMichael Grosse                // make sure this schema is assigned
159b8cff1dfSAndreas Gohr                /** @noinspection PhpUndefinedVariableInspection */
1604eed39ffSMichael Grosse                $assignments->assignPageSchema(
1614eed39ffSMichael Grosse                    $this->pid,
1624eed39ffSMichael Grosse                    $this->schemadata->getSchema()->getTable()
1634eed39ffSMichael Grosse                );
1644eed39ffSMichael Grosse            }
165b8cff1dfSAndreas Gohr        } catch (\Exception $e) {
166b8cff1dfSAndreas Gohr            // PHP <7 needs a catch block
167b8cff1dfSAndreas Gohr            throw $e;
16813eddb0fSAndreas Gohr        } finally {
16913eddb0fSAndreas Gohr            // unlock (unlocking a non-existing file is okay,
17013eddb0fSAndreas Gohr            // so we don't check if it's a lookup here
171cdd09a96SAndreas Gohr            unlock($this->pid);
17213eddb0fSAndreas Gohr        }
1734731b875SAndreas Gohr
1744731b875SAndreas Gohr        // reinit then render
17569f7ec8fSAnna Dabrowska        $this->initFromInput($this->schemadata->getTimestamp());
1764731b875SAndreas Gohr        $value = $this->schemadata->getDataColumn($this->column);
1774731b875SAndreas Gohr        $R = new Doku_Renderer_xhtml();
1784731b875SAndreas Gohr        $value->render($R, 'xhtml'); // FIXME use configured default renderer
17969f7ec8fSAnna Dabrowska        $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()]);
18069f7ec8fSAnna Dabrowska        echo $data;
1814731b875SAndreas Gohr    }
1824731b875SAndreas Gohr
1834731b875SAndreas Gohr    /**
184cdd09a96SAndreas Gohr     * Unlock a page (on cancel action)
185cdd09a96SAndreas Gohr     */
186cdd09a96SAndreas Gohr    protected function inline_cancel() {
187cdd09a96SAndreas Gohr        global $INPUT;
188cdd09a96SAndreas Gohr        $pid = $INPUT->str('pid');
189cdd09a96SAndreas Gohr        unlock($pid);
190cdd09a96SAndreas Gohr    }
191cdd09a96SAndreas Gohr
192cdd09a96SAndreas Gohr    /**
1934731b875SAndreas Gohr     * Initialize internal state based on input variables
1944731b875SAndreas Gohr     *
19569f7ec8fSAnna Dabrowska     * @param int $updatedRev timestamp of currently created revision, might be newer than input variable
19669f7ec8fSAnna Dabrowska     * @return bool if initialization was successful
1974731b875SAndreas Gohr     */
19869f7ec8fSAnna Dabrowska    protected function initFromInput($updatedRev = 0) {
1994731b875SAndreas Gohr        global $INPUT;
2004731b875SAndreas Gohr
2014731b875SAndreas Gohr        $this->schemadata = null;
2024731b875SAndreas Gohr        $this->column = null;
2034731b875SAndreas Gohr
2044731b875SAndreas Gohr        $pid = $INPUT->str('pid');
2056fd73b4bSAnna Dabrowska        $rid = $INPUT->int('rid');
20669f7ec8fSAnna Dabrowska        $rev = $updatedRev ?: $INPUT->int('rev');
20769f7ec8fSAnna Dabrowska
2084731b875SAndreas Gohr        list($table, $field) = explode('.', $INPUT->str('field'));
2096fd73b4bSAnna Dabrowska        if(blank($pid) && blank($rid)) return false;
2104731b875SAndreas Gohr        if(blank($table)) return false;
2114731b875SAndreas Gohr        if(blank($field)) return false;
2124731b875SAndreas Gohr
2134731b875SAndreas Gohr        $this->pid = $pid;
2144ec54c67SAndreas Gohr        try {
2156fd73b4bSAnna Dabrowska            $this->schemadata = AccessTable::byTableName($table, $pid, $rev, $rid);
2164ec54c67SAndreas Gohr        } catch(StructException $ignore) {
2174731b875SAndreas Gohr            return false;
2184731b875SAndreas Gohr        }
2194731b875SAndreas Gohr
2204ec54c67SAndreas Gohr        $this->column = $this->schemadata->getSchema()->findColumn($field);
2214731b875SAndreas Gohr        if(!$this->column || !$this->column->isVisibleInEditor()) {
2224731b875SAndreas Gohr            $this->schemadata = null;
2234731b875SAndreas Gohr            $this->column = null;
2244731b875SAndreas Gohr            return false;
2254731b875SAndreas Gohr        }
2264731b875SAndreas Gohr
2274731b875SAndreas Gohr        return true;
2284731b875SAndreas Gohr    }
2294731b875SAndreas Gohr
23013eddb0fSAndreas Gohr    /**
23113eddb0fSAndreas Gohr     * Checks if a page can be edited
23213eddb0fSAndreas Gohr     *
23313eddb0fSAndreas Gohr     * @throws StructException when check fails
23413eddb0fSAndreas Gohr     */
23513eddb0fSAndreas Gohr    protected function checkPage() {
23613eddb0fSAndreas Gohr        if(!page_exists($this->pid)) {
23713eddb0fSAndreas Gohr            throw new StructException('inline save error: no such page');
23813eddb0fSAndreas Gohr        }
23913eddb0fSAndreas Gohr        if(auth_quickaclcheck($this->pid) < AUTH_EDIT) {
24013eddb0fSAndreas Gohr            throw new StructException('inline save error: acl');
24113eddb0fSAndreas Gohr        }
24213eddb0fSAndreas Gohr        if(checklock($this->pid)) {
24313eddb0fSAndreas Gohr            throw new StructException('inline save error: lock');
24413eddb0fSAndreas Gohr        }
24513eddb0fSAndreas Gohr    }
24613eddb0fSAndreas Gohr
24713eddb0fSAndreas Gohr    /**
24813eddb0fSAndreas Gohr     * Our own implementation of checkSecurityToken because we don't want the msg() call
24913eddb0fSAndreas Gohr     *
25013eddb0fSAndreas Gohr     * @throws StructException when check fails
25113eddb0fSAndreas Gohr     */
252c498205aSAndreas Gohr    public static function checkCSRF() {
25313eddb0fSAndreas Gohr        global $INPUT;
25413eddb0fSAndreas Gohr        if(
25513eddb0fSAndreas Gohr            $INPUT->server->str('REMOTE_USER') &&
25613eddb0fSAndreas Gohr            getSecurityToken() != $INPUT->str('sectok')
25713eddb0fSAndreas Gohr        ) {
25813eddb0fSAndreas Gohr            throw new StructException('CSRF check failed');
25913eddb0fSAndreas Gohr        }
26013eddb0fSAndreas Gohr    }
26113eddb0fSAndreas Gohr
2624731b875SAndreas Gohr}
263