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