xref: /plugin/struct/meta/Schema.php (revision 127d6bacbaa5831b9606d5ae91b03cfbe9108473)
1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5use dokuwiki\plugin\struct\types\AbstractBaseType;
6
7if(!defined('JSON_PRETTY_PRINT')) define('JSON_PRETTY_PRINT', 0); // PHP 5.3 compatibility
8
9/**
10 * Class Schema
11 *
12 * Represents the schema of a single data table and all its properties. It defines what can be stored in
13 * the represented data table and how those contents are formatted.
14 *
15 * It can be initialized with a timestamp to access the schema as it looked at that particular point in time.
16 *
17 * @package dokuwiki\plugin\struct\meta
18 */
19class Schema {
20
21    /** @var \helper_plugin_sqlite|null */
22    protected $sqlite;
23
24    /** @var int The ID of this schema */
25    protected $id = 0;
26
27    /** @var string the user who last edited this schema */
28    protected $user = '';
29
30    /** @var string name of the associated table */
31    protected $table = '';
32
33    /** @var bool is this a lookup schema? */
34    protected $islookup = false;
35
36    /**
37     * @var string the current checksum of this schema
38     */
39    protected $chksum = '';
40
41    /** @var Column[] all the colums */
42    protected $columns = array();
43
44    /** @var int */
45    protected $maxsort = 0;
46
47    /** @var int */
48    protected $ts = 0;
49
50    /** @var string struct version info */
51    protected $structversion = '?';
52
53    /** @var array config array with label translations */
54    protected $config = array();
55
56    /**
57     * Schema constructor
58     *
59     * @param string $table The table this schema is for
60     * @param int $ts The timestamp for when this schema was valid, 0 for current
61     * @param bool $islookup only used when creating a new schema, makes the new schema a lookup
62     */
63    public function __construct($table, $ts = 0, $islookup = false) {
64        $baseconfig = array('allowed editors' => '');
65
66        /** @var \helper_plugin_struct_db $helper */
67        $helper = plugin_load('helper', 'struct_db');
68        $info = $helper->getInfo();
69        $this->structversion = $info['date'];
70        $this->sqlite = $helper->getDB();
71        $table = self::cleanTableName($table);
72        $this->table = $table;
73        $this->ts = $ts;
74
75        // load info about the schema itself
76        if($ts) {
77            $sql = "SELECT *
78                      FROM schemas
79                     WHERE tbl = ?
80                       AND ts <= ?
81                  ORDER BY ts DESC
82                     LIMIT 1";
83            $opt = array($table, $ts);
84        } else {
85            $sql = "SELECT *
86                      FROM schemas
87                     WHERE tbl = ?
88                  ORDER BY ts DESC
89                     LIMIT 1";
90            $opt = array($table);
91        }
92        $res = $this->sqlite->query($sql, $opt);
93        $config = array();
94        if($this->sqlite->res2count($res)) {
95            $schema = $this->sqlite->res2arr($res);
96            $result = array_shift($schema);
97            $this->id = $result['id'];
98            $this->user = $result['user'];
99            $this->chksum = $result['chksum'];
100            $this->islookup = $result['islookup'];
101            $this->ts = $result['ts'];
102            $config = json_decode($result['config'], true);
103        } else {
104            $this->islookup = $islookup;
105        }
106        $this->sqlite->res_close($res);
107        $this->config = array_merge($baseconfig, $config);
108        $this->initTransConfig();
109        if(!$this->id) return;
110
111        // load existing columns
112        $sql = "SELECT SC.*, T.*
113                  FROM schema_cols SC,
114                       types T
115                 WHERE SC.sid = ?
116                   AND SC.tid = T.id
117              ORDER BY SC.sort";
118        $res = $this->sqlite->query($sql, $this->id);
119        $rows = $this->sqlite->res2arr($res);
120        $this->sqlite->res_close($res);
121
122        $typeclasses = Column::allTypes();
123        foreach($rows as $row) {
124            if($row['class'] == 'Integer') {
125                $row['class'] = 'Decimal';
126            }
127
128            $class = $typeclasses[$row['class']];
129            if(!class_exists($class)) {
130                // This usually never happens, except during development
131                msg('Unknown type "' . hsc($row['class']) . '" falling back to Text', -1);
132                $class = 'dokuwiki\\plugin\\struct\\types\\Text';
133            }
134
135            $config = json_decode($row['config'], true);
136            /** @var AbstractBaseType $type */
137            $type = new $class($config, $row['label'], $row['ismulti'], $row['tid']);
138            $column = new Column(
139                $row['sort'],
140                $type,
141                $row['colref'],
142                $row['enabled'],
143                $table
144            );
145            $type->setContext($column);
146
147            $this->columns[] = $column;
148            if($row['sort'] > $this->maxsort) $this->maxsort = $row['sort'];
149        }
150    }
151
152    /**
153     * Add the translatable keys to the configuration
154     *
155     * This checks if a configuration for the translation plugin exists and if so
156     * adds all configured languages to the config array.
157     *
158     * Adapted from @see \dokuwiki\plugin\struct\types\AbstractBaseType::initTransConfig
159     */
160    protected function initTransConfig() {
161        global $conf;
162        $lang = $conf['lang'];
163        if(isset($conf['plugin']['translation']['translations'])) {
164            $lang .= ' ' . $conf['plugin']['translation']['translations'];
165        }
166        $langs = explode(' ', $lang);
167        $langs = array_map('trim', $langs);
168        $langs = array_filter($langs);
169        $langs = array_unique($langs);
170
171        if(!isset($this->config['label'])) $this->config['label'] = array();
172        // initialize missing keys
173        foreach($langs as $lang) {
174            if(!isset($this->config['label'][$lang])) $this->config['label'][$lang] = '';
175        }
176        // strip unknown languages
177        foreach(array_keys($this->config['label']) as $key) {
178            if(!in_array($key, $langs)) unset($this->config['label'][$key]);
179        }
180    }
181
182    /**
183     * @return string identifer for debugging purposes
184     */
185    function __toString() {
186        return __CLASS__ . ' ' . $this->table . ' (' . $this->id . ') ' . ($this->islookup ? 'LOOKUP' : 'DATA');
187    }
188
189    /**
190     * Cleans any unwanted stuff from table names
191     *
192     * @param string $table
193     * @return string
194     */
195    static public function cleanTableName($table) {
196        $table = strtolower($table);
197        $table = preg_replace('/[^a-z0-9_]+/', '', $table);
198        $table = preg_replace('/^[0-9_]+/', '', $table);
199        $table = trim($table);
200        return $table;
201    }
202
203    /**
204     * Returns the translated label for this schema
205     *
206     * Uses the current language as determined by $conf['lang']. Falls back to english
207     * and then to the table name
208     *
209     * @see \dokuwiki\plugin\struct\types\AbstractBaseType::getTranslatedLabel
210     *
211     * @return string
212     */
213    public function getTranslatedLabel() {
214        global $conf;
215        $lang = $conf['lang'];
216        if(!blank($this->config['label'][$lang])) {
217            return $this->config['label'][$lang];
218        }
219        if(!blank($this->config['label']['en'])) {
220            return $this->config['label']['en'];
221        }
222        return $this->table;
223    }
224
225    /**
226     * Gets a list of all available schemas
227     *
228     * @param string $filter either 'page' or 'lookup'
229     * @return \string[]
230     */
231    static public function getAll($filter = '') {
232        /** @var \helper_plugin_struct_db $helper */
233        $helper = plugin_load('helper', 'struct_db');
234        $db = $helper->getDB(false);
235        if(!$db) return array();
236
237        if($filter == 'page') {
238            $where = 'islookup = 0';
239        } elseif($filter == 'lookup') {
240            $where = 'islookup = 1';
241        } else {
242            $where = '1 = 1';
243        }
244
245        $res = $db->query("SELECT DISTINCT tbl FROM schemas WHERE $where ORDER BY tbl");
246        $tables = $db->res2arr($res);
247        $db->res_close($res);
248
249        $result = array();
250        foreach($tables as $row) {
251            $result[] = $row['tbl'];
252        }
253        return $result;
254    }
255
256    /**
257     * Delete all data associated with this schema
258     *
259     * This is really all data ever! Be careful!
260     */
261    public function delete() {
262        if(!$this->id) throw new StructException('can not delete unsaved schema');
263
264        $this->sqlite->query('BEGIN TRANSACTION');
265
266        $sql = "DROP TABLE ?";
267        $this->sqlite->query($sql, 'data_' . $this->table);
268        $this->sqlite->query($sql, 'multi_' . $this->table);
269
270        $sql = "DELETE FROM schema_assignments WHERE tbl = ?";
271        $this->sqlite->query($sql, $this->table);
272
273        $sql = "DELETE FROM schema_assignments_patterns WHERE tbl = ?";
274        $this->sqlite->query($sql, $this->table);
275
276        $sql = "SELECT T.id
277                  FROM types T, schema_cols SC, schemas S
278                 WHERE T.id = SC.tid
279                   AND SC.sid = S.id
280                   AND S.tbl = ?";
281        $sql = "DELETE FROM types WHERE id IN ($sql)";
282        $this->sqlite->query($sql, $this->table);
283
284        $sql = "SELECT id
285                  FROM schemas
286                 WHERE tbl = ?";
287        $sql = "DELETE FROM schema_cols WHERE sid IN ($sql)";
288        $this->sqlite->query($sql, $this->table);
289
290        $sql = "DELETE FROM schemas WHERE tbl = ?";
291        $this->sqlite->query($sql, $this->table);
292
293        $this->sqlite->query('COMMIT TRANSACTION');
294        $this->sqlite->query('VACUUM');
295
296        // a deleted schema should not be used anymore, but let's make sure it's somewhat sane anyway
297        $this->id = 0;
298        $this->chksum = '';
299        $this->columns = array();
300        $this->maxsort = 0;
301        $this->ts = 0;
302    }
303
304    /**
305     * @return string
306     */
307    public function getChksum() {
308        return $this->chksum;
309    }
310
311    /**
312     * @return int
313     */
314    public function getId() {
315        return $this->id;
316    }
317
318    /**
319     * @return int returns the timestamp this Schema was created at
320     */
321    public function getTimeStamp() {
322        return $this->ts;
323    }
324
325    /**
326     * @return bool is this a lookup schema?
327     */
328    public function isLookup() {
329        return $this->islookup;
330    }
331
332    /**
333     * @return string
334     */
335    public function getUser() {
336        return $this->user;
337    }
338
339    public function getConfig() {
340        return $this->config;
341    }
342
343    /**
344     * Checks if the current user may edit data in this schema
345     *
346     * @return bool
347     */
348    public function isEditable() {
349        global $USERINFO;
350        if($this->config['allowed editors'] === '') return true;
351        if(blank($_SERVER['REMOTE_USER'])) return false;
352        if(auth_isadmin()) return true;
353        return auth_isMember($this->config['allowed editors'], $_SERVER['REMOTE_USER'], $USERINFO['grps']);
354    }
355
356    /**
357     * Returns a list of columns in this schema
358     *
359     * @param bool $withDisabled if false, disabled columns will not be returned
360     * @return Column[]
361     */
362    public function getColumns($withDisabled = true) {
363        if(!$withDisabled) {
364            return array_filter(
365                $this->columns,
366                function (Column $col) {
367                    return $col->isEnabled();
368                }
369            );
370        }
371
372        return $this->columns;
373    }
374
375    /**
376     * Find a column in the schema by its label
377     *
378     * Only enabled columns are returned!
379     *
380     * @param $name
381     * @return bool|Column
382     */
383    public function findColumn($name) {
384        foreach($this->columns as $col) {
385            if($col->isEnabled() && utf8_strtolower($col->getLabel()) == utf8_strtolower($name)) {
386                return $col;
387            }
388        }
389        return false;
390    }
391
392    /**
393     * @return string
394     */
395    public function getTable() {
396        return $this->table;
397    }
398
399    /**
400     * @return int the highest sort number used in this schema
401     */
402    public function getMaxsort() {
403        return $this->maxsort;
404    }
405
406    /**
407     * @return string the JSON representing this schema
408     */
409    public function toJSON() {
410        $data = array(
411            'structversion' => $this->structversion,
412            'schema' => $this->getTable(),
413            'id' => $this->getId(),
414            'user' => $this->getUser(),
415            'config' => $this->getConfig(),
416            'columns' => array()
417        );
418
419        foreach($this->columns as $column) {
420            $data['columns'][] = array(
421                'colref' => $column->getColref(),
422                'ismulti' => $column->isMulti(),
423                'isenabled' => $column->isEnabled(),
424                'sort' => $column->getSort(),
425                'label' => $column->getLabel(),
426                'class' => $column->getType()->getClass(),
427                'config' => $column->getType()->getConfig(),
428            );
429        }
430
431        return json_encode($data, JSON_PRETTY_PRINT);
432    }
433}
434