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