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