xref: /plugin/struct/action/inline.php (revision e6a2e26b57dd1411a582eba0bdba877d874c7fa3)
1<?php
2
3/**
4 * DokuWiki Plugin struct (Action Component)
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  Andreas Gohr, Michael Große <dokuwiki@cosmocode.de>
8 */
9
10use dokuwiki\plugin\struct\meta\AccessTable;
11use dokuwiki\plugin\struct\meta\AccessTablePage;
12use dokuwiki\plugin\struct\meta\AccessTableGlobal;
13use dokuwiki\plugin\struct\meta\Assignments;
14use dokuwiki\plugin\struct\meta\Column;
15use dokuwiki\plugin\struct\meta\Schema;
16use dokuwiki\plugin\struct\meta\StructException;
17use dokuwiki\plugin\struct\meta\ValueValidator;
18
19/**
20 * Class action_plugin_struct_inline
21 *
22 * Handle inline editing
23 */
24class action_plugin_struct_inline extends DokuWiki_Action_Plugin
25{
26
27    /** @var  AccessTablePage */
28    protected $schemadata = null;
29
30    /** @var  Column */
31    protected $column = null;
32
33    /** @var String */
34    protected $pid = '';
35
36    /** @var int */
37    protected $rid = 0;
38
39    /**
40     * Registers a callback function for a given event
41     *
42     * @param Doku_Event_Handler $controller DokuWiki's event controller object
43     * @return void
44     */
45    public function register(Doku_Event_Handler $controller)
46    {
47        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax');
48    }
49
50    /**
51     * @param Doku_Event $event
52     * @param $param
53     */
54    public function handleAjax(Doku_Event $event, $param)
55    {
56        $len = strlen('plugin_struct_inline_');
57        if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return;
58        $event->preventDefault();
59        $event->stopPropagation();
60
61        if (substr($event->data, $len) == 'editor') {
62            $this->inlineEditor();
63        }
64
65        if (substr($event->data, $len) == 'save') {
66            try {
67                $this->inlineSave();
68            } catch (StructException $e) {
69                http_status(500);
70                header('Content-Type: text/plain; charset=utf-8');
71                echo $e->getMessage();
72            }
73        }
74
75        if (substr($event->data, $len) == 'cancel') {
76            $this->inlineCancel();
77        }
78    }
79
80    /**
81     * Creates the inline editor
82     */
83    protected function inlineEditor()
84    {
85        // silently fail when editing not possible
86        if (!$this->initFromInput()) return;
87        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) return;
88        if (!$this->schemadata->getSchema()->isEditable()) return;
89        if (checklock($this->pid)) return;
90
91        // lock page
92        lock($this->pid);
93
94        // output the editor
95        $value = $this->schemadata->getDataColumn($this->column);
96        $id = uniqid('struct__', false);
97        echo '<div class="field">';
98        echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">';
99        echo '</label>';
100        echo '<span class="input">';
101        echo $value->getValueEditor('entry', $id);
102        echo '</span>';
103        $hint = $this->column->getType()->getTranslatedHint();
104        if ($hint) {
105            echo '<p class="hint">';
106            echo hsc($hint);
107            echo '</p>';
108        }
109        echo '</div>';
110
111        // csrf protection
112        formSecurityToken();
113    }
114
115    /**
116     * Save the data posted by the inline editor
117     */
118    protected function inlineSave()
119    {
120        global $INPUT;
121
122        // check preconditions
123        if (!$this->initFromInput()) {
124            throw new StructException('inline save error: init');
125        }
126        self::checkCSRF();
127        if (!$this->schemadata->getRid()) {
128            $this->checkPage();
129            $assignments = Assignments::getInstance();
130            $tables = $assignments->getPageAssignments($this->pid, true);
131            if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) {
132                throw new StructException('inline save error: schema not assigned to page');
133            }
134        }
135        if (!$this->schemadata->getSchema()->isEditable()) {
136            throw new StructException('inline save error: no permission for schema');
137        }
138
139        // validate
140        $value = $INPUT->param('entry');
141        $validator = new ValueValidator();
142        if (!$validator->validateValue($this->column, $value)) {
143            throw new StructException(join("\n", $validator->getErrors()));
144        }
145
146        // current data
147        $tosave = $this->schemadata->getDataArray();
148        $tosave[$this->column->getLabel()] = $value;
149
150        // save
151        if ($this->schemadata->getRid()) {
152            $revision = 0;
153        } else {
154            $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit');
155            p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table
156        }
157        $this->schemadata->setTimestamp($revision);
158        try {
159            if (!$this->schemadata->saveData($tosave)) {
160                throw new StructException('saving failed');
161            }
162            if (!$this->schemadata->getRid()) {
163                // make sure this schema is assigned
164                /** @noinspection PhpUndefinedVariableInspection */
165                $assignments->assignPageSchema(
166                    $this->pid,
167                    $this->schemadata->getSchema()->getTable()
168                );
169            }
170        } catch (\Exception $e) {
171            // PHP <7 needs a catch block
172            throw $e;
173        } finally {
174            // unlock (unlocking a non-existing file is okay,
175            // so we don't check if it's a lookup here
176            unlock($this->pid);
177        }
178
179        // reinit then render
180        $this->initFromInput($this->schemadata->getTimestamp());
181        $value = $this->schemadata->getDataColumn($this->column);
182        $R = new Doku_Renderer_xhtml();
183        $value->render($R, 'xhtml'); // FIXME use configured default renderer
184        $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()]);
185        echo $data;
186    }
187
188    /**
189     * Unlock a page (on cancel action)
190     */
191    protected function inlineCancel()
192    {
193        global $INPUT;
194        $pid = $INPUT->str('pid');
195        unlock($pid);
196    }
197
198    /**
199     * Initialize internal state based on input variables
200     *
201     * @param int $updatedRev timestamp of currently created revision, might be newer than input variable
202     * @return bool if initialization was successful
203     */
204    protected function initFromInput($updatedRev = 0)
205    {
206        global $INPUT;
207
208        $this->schemadata = null;
209        $this->column = null;
210
211        $pid = $INPUT->str('pid');
212        $rid = $INPUT->int('rid');
213        $rev = $updatedRev ?: $INPUT->int('rev');
214
215        list($table, $field) = explode('.', $INPUT->str('field'));
216        if (blank($pid) && blank($rid)) return false;
217        if (blank($table)) return false;
218        if (blank($field)) return false;
219
220        $this->pid = $pid;
221        try {
222            if (AccessTable::isTypePage($pid, $rev)) {
223                $this->schemadata = AccessTable::getPageAccess($table, $pid);
224            } elseif (AccessTable::isTypeSerial($pid, $rev)) {
225                $this->schemadata = AccessTable::getSerialAccess($table, $pid, $rid);
226            } else {
227                $this->schemadata = AccessTable::getGlobalAccess($table, $rid);
228            }
229        } catch (StructException $ignore) {
230            return false;
231        }
232
233        $this->column = $this->schemadata->getSchema()->findColumn($field);
234        if (!$this->column || !$this->column->isVisibleInEditor()) {
235            $this->schemadata = null;
236            $this->column = null;
237            return false;
238        }
239
240        return true;
241    }
242
243    /**
244     * Checks if a page can be edited
245     *
246     * @throws StructException when check fails
247     */
248    protected function checkPage()
249    {
250        if (!page_exists($this->pid)) {
251            throw new StructException('inline save error: no such page');
252        }
253        if (auth_quickaclcheck($this->pid) < AUTH_EDIT) {
254            throw new StructException('inline save error: acl');
255        }
256        if (checklock($this->pid)) {
257            throw new StructException('inline save error: lock');
258        }
259    }
260
261    /**
262     * Our own implementation of checkSecurityToken because we don't want the msg() call
263     *
264     * @throws StructException when check fails
265     */
266    public static function checkCSRF()
267    {
268        global $INPUT;
269        if (
270            $INPUT->server->str('REMOTE_USER') &&
271            getSecurityToken() != $INPUT->str('sectok')
272        ) {
273            throw new StructException('CSRF check failed');
274        }
275    }
276}
277