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