xref: /plugin/struct/action/inline.php (revision 00bff81c57638c56aa4e85d91c0f80d92dd8338f)
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() && !$this->schemadata->getPid()) {
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();
176        $value = $this->schemadata->getDataColumn($this->column);
177        $R = new Doku_Renderer_xhtml();
178        $value->render($R, 'xhtml'); // FIXME use configured default renderer
179        echo $R->doc;
180    }
181
182    /**
183     * Unlock a page (on cancel action)
184     */
185    protected function inline_cancel() {
186        global $INPUT;
187        $pid = $INPUT->str('pid');
188        unlock($pid);
189    }
190
191    /**
192     * Initialize internal state based on input variables
193     *
194     * @return bool if initialization was successfull
195     */
196    protected function initFromInput() {
197        global $INPUT;
198
199        $this->schemadata = null;
200        $this->column = null;
201
202        $pid = $INPUT->str('pid');
203        $rid = $INPUT->int('rid');
204        $rev = $INPUT->int('rev');
205        list($table, $field) = explode('.', $INPUT->str('field'));
206        if(blank($pid) && blank($rid)) return false;
207        if(blank($table)) return false;
208        if(blank($field)) return false;
209
210        $this->pid = $pid;
211        try {
212            $this->schemadata = AccessTable::byTableName($table, $pid, $rev, $rid);
213        } catch(StructException $ignore) {
214            return false;
215        }
216
217        $this->column = $this->schemadata->getSchema()->findColumn($field);
218        if(!$this->column || !$this->column->isVisibleInEditor()) {
219            $this->schemadata = null;
220            $this->column = null;
221            return false;
222        }
223
224        return true;
225    }
226
227    /**
228     * Checks if a page can be edited
229     *
230     * @throws StructException when check fails
231     */
232    protected function checkPage() {
233        if(!page_exists($this->pid)) {
234            throw new StructException('inline save error: no such page');
235        }
236        if(auth_quickaclcheck($this->pid) < AUTH_EDIT) {
237            throw new StructException('inline save error: acl');
238        }
239        if(checklock($this->pid)) {
240            throw new StructException('inline save error: lock');
241        }
242    }
243
244    /**
245     * Our own implementation of checkSecurityToken because we don't want the msg() call
246     *
247     * @throws StructException when check fails
248     */
249    public static function checkCSRF() {
250        global $INPUT;
251        if(
252            $INPUT->server->str('REMOTE_USER') &&
253            getSecurityToken() != $INPUT->str('sectok')
254        ) {
255            throw new StructException('CSRF check failed');
256        }
257    }
258
259}
260