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