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