xref: /plugin/struct/action/entry.php (revision 90bc792b956d2227e405f3597f5ce64278503d48)
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
10if(!defined('DOKU_INC')) die();
11
12use plugin\struct\meta\Assignments;
13use plugin\struct\meta\SchemaData;
14use plugin\struct\meta\ValidationException;
15use plugin\struct\types\AbstractBaseType;
16
17/**
18 * Class action_plugin_struct_entry
19 *
20 * Handles the whole struct data entry process
21 */
22class action_plugin_struct_entry extends DokuWiki_Action_Plugin {
23
24    /**
25     * @var string The form name we use to transfer schema data
26     */
27    protected static $VAR = 'struct_schema_data';
28
29    /** @var helper_plugin_sqlite */
30    protected $sqlite;
31
32    /** @var  bool has the data been validated correctly? */
33    protected $validated;
34
35    /** @var  array these schemas have changed data and need to be saved */
36    protected $tosave;
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        // add the struct editor to the edit form;
46        $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_editform');
47        // validate data on preview and save;
48        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_validation');
49        // ensure a page revision is created when struct data changes:
50        $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handle_pagesave_before');
51        // save struct data after page has been saved:
52        $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'AFTER', $this, 'handle_pagesave_after');
53    }
54
55    /**
56     * Enhance the editing form with structural data editing
57     *
58     * @param Doku_Event $event event object by reference
59     * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
60     *                           handler was registered]
61     * @return bool
62     */
63    public function handle_editform(Doku_Event $event, $param) {
64        global $ID;
65
66        $assignments = new Assignments();
67        $tables = $assignments->getPageAssignments($ID);
68
69        $html = '';
70        foreach($tables as $table) {
71            $html .= $this->createForm($table);
72        }
73
74        /** @var Doku_Form $form */
75        $form = $event->data;
76        $html = "<div class=\"struct\">$html</div>";
77        $pos = $form->findElementById('wiki__editbar'); // insert the form before the main buttons
78        $form->insertElement($pos, $html);
79
80        return true;
81    }
82
83    /**
84     * Clean up and validate the input data
85     *
86     * @param Doku_Event $event event object by reference
87     * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
88     *                           handler was registered]
89     * @return bool
90     */
91    public function handle_validation(Doku_Event $event, $param) {
92        global $ID, $INPUT;
93        $act = act_clean($event->data);
94        if(!in_array($act, array('save', 'preview'))) return false;
95
96        $assignments = new Assignments();
97        $tables = $assignments->getPageAssignments($ID);
98        $structData = $INPUT->arr(self::$VAR);
99        $timestamp = time();
100
101        $this->tosave = array();
102        $this->validated = true;
103        foreach($tables as $table) {
104            $schemaData = new SchemaData($table, $ID, $timestamp);
105            if(!$schemaData->getId()) {
106                // this schema is not available for some reason. skip it
107                continue;
108            }
109
110            $newData = $structData[$table];
111            foreach($schemaData->getColumns() as $col) {
112                // fix multi value types
113                $type = $col->getType();
114                $label = $type->getLabel();
115                $trans = $type->getTranslatedLabel();
116                if($type->isMulti() && !is_array($newData[$label])) {
117                    $newData[$label] = $type->splitValues($newData[$label]);
118                }
119                // strip empty fields from multi vals
120                if(is_array($newData[$label])) {
121                    $newData[$label] = array_filter($newData[$label], array($this, 'filter'));
122                    $newData[$label] = array_values($newData[$label]); // reset the array keys
123                }
124
125                // validate data
126                $this->validated = $this->validated && $this->validate($type, $trans, $newData[$label]);
127            }
128
129            // has the data changed? mark it for saving.
130            $olddata = $schemaData->getDataArray();
131            if($olddata != $newData) {
132                $this->tosave[] = $table;
133            }
134
135            // write back cleaned up data
136            $structData[$table] = $newData;
137        }
138        // write back cleaned up structData
139        $INPUT->post->set(self::$VAR, $structData);
140
141        // did validation go through? otherwise abort saving
142        if(!$this->validated && $act == 'save') {
143            $event->data = 'edit';
144        }
145
146        return false;
147    }
148
149    /**
150     * Check if the page has to be changed
151     *
152     * @param Doku_Event $event event object by reference
153     * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
154     *                           handler was registered]
155     * @return bool
156     */
157    public function handle_pagesave_before(Doku_Event $event, $param) {
158        if($event->data['contentChanged']) return; // will be saved for page changes
159        if(count($this->tosave)) {
160            if(trim($event->data['newContent']) === '') {
161                // this happens when a new page is tried to be created with only struct data
162                msg($this->getLang('emptypage'), -1);
163            } else {
164                $event->data['contentChanged'] = true; // save for data changes
165            }
166        }
167    }
168
169    /**
170     * Save the data
171     *
172     * When this is called, INPUT data has been validated already. On a restore action, the data is
173     * loaded from the database and not validated again.
174     *
175     * @param Doku_Event $event event object by reference
176     * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
177     *                           handler was registered]
178     * @return bool
179     */
180    public function handle_pagesave_after(Doku_Event $event, $param) {
181        global $INPUT;
182        global $ACT;
183        global $REV;
184
185        $assignments = new Assignments();
186
187        if($ACT == 'revert' && $REV) {
188            // reversion is a special case, we load the data to restore from DB:
189            $structData = array();
190            $this->tosave = $assignments->getPageAssignments($event->data['id']);
191            foreach($this->tosave as $table) {
192                $oldData = new SchemaData($table, $event->data['id'], $REV);
193                $structData[$table] = $oldData->getDataArray();
194            }
195        } else {
196            // data comes from the edit form
197            $structData = $INPUT->arr(self::$VAR);
198        }
199
200        if($event->data['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
201            // clear all data
202            $tables = $assignments->getPageAssignments($event->data['id']);
203            foreach($tables as $table) {
204                $schemaData = new SchemaData($table, $event->data['id'], time());
205                $schemaData->clearData();
206            }
207        } else {
208            // save the provided data
209            foreach($this->tosave as $table) {
210                $schemaData = new SchemaData($table, $event->data['id'], $event->data['newRevision']);
211                $schemaData->saveData($structData[$table]);
212
213                // make sure this schema is assigned
214                $assignments->assignPageSchema($event->data['id'], $table);
215            }
216        }
217    }
218
219    /**
220     * Validate the given data
221     *
222     * Catches the Validation exceptions and transforms them into proper messages.
223     *
224     * Blank values are not validated and always pass
225     *
226     * @param AbstractBaseType $type
227     * @param string $label
228     * @param array|string|int $data
229     * @return bool true if the data validates, otherwise false
230     */
231    protected function validate(AbstractBaseType $type, $label, $data) {
232        $prefix = sprintf($this->getLang('validation_prefix'), $label);
233
234        $ok = true;
235        if(is_array($data)) {
236            foreach($data as $value) {
237                if(!blank($value)) {
238                    try {
239                        $type->validate($value);
240                    } catch(ValidationException $e) {
241                        msg($prefix . $e->getMessage(), -1);
242                        $ok = false;
243                    }
244                }
245            }
246            return $ok;
247        }
248
249        if(!blank($data)) {
250            try {
251                $type->validate($data);
252            } catch(ValidationException $e) {
253                msg($prefix . hsc($e->getMessage()), -1);
254                $ok = false;
255            }
256        }
257        return $ok;
258    }
259
260    /**
261     * Create the form to edit schemadata
262     *
263     * @param string $tablename
264     * @return string The HTML for this schema's form
265     */
266    protected function createForm($tablename) {
267        global $ID;
268        global $REV;
269        global $INPUT;
270        if (auth_quickaclcheck($ID) == AUTH_READ) return '';
271        if (checklock($ID)) return '';
272        $schema = new SchemaData($tablename, $ID, $REV);
273        $schemadata = $schema->getData();
274
275        $structdata = $INPUT->arr(self::$VAR);
276        if(isset($structdata[$tablename])) {
277            $postdata = $structdata[$tablename];
278        } else {
279            $postdata = array();
280        }
281
282        // we need a short, unique identifier to use in the cookie. this should be good enough
283        $schemaid = 'SRCT'.substr(str_replace(array('+', '/'), '', base64_encode(sha1($tablename, true))), 0, 5);
284        $html = '<fieldset data-schema="' . $schemaid . '">';
285        $html .= '<legend>' . hsc($tablename) . '</legend>';
286        foreach($schemadata as $field) {
287            $label = $field->getColumn()->getLabel();
288            if(isset($postdata[$label])) {
289                // posted data trumps stored data
290                $field->setValue($postdata[$label]);
291            }
292            $trans = hsc($field->getColumn()->getTranslatedLabel());
293            $hint  = hsc($field->getColumn()->getTranslatedHint());
294            $class = $hint ? 'hashint' : '';
295
296            $name = self::$VAR . "[$tablename][$label]";
297            $input = $field->getValueEditor($name);
298            $html .= '<label>';
299            $html .= "<span class=\"label $class\" title=\"$hint\">$trans</span>";
300            $html .= "<span class=\"input\">$input</span>";
301            $html .= '</label>';
302        }
303        $html .= '</fieldset>';
304
305        return $html;
306    }
307
308    /**
309     * Simple filter to remove blank values
310     *
311     * @param string $val
312     * @return bool
313     */
314    public function filter($val) {
315        return !blank($val);
316    }
317}
318
319// vim:ts=4:sw=4:et:
320