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