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