1<?php
2/**
3 * @author Bart Visscher <bartv@thisnet.nl>
4 * @author Morris Jobke <hey@morrisjobke.de>
5 * @author Oliver Gasser <oliver.gasser@gmail.com>
6 * @author Robin Appelman <icewind@owncloud.com>
7 * @author Robin McCorkell <robin@mccorkell.me.uk>
8 * @author Thomas Müller <thomas.mueller@tmit.eu>
9 * @author Victor Dubiniuk <dubiniuk@owncloud.com>
10 * @author Vincent Petry <pvince81@owncloud.com>
11 *
12 * @copyright Copyright (c) 2018, ownCloud GmbH
13 * @license AGPL-3.0
14 *
15 * This code is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU Affero General Public License, version 3,
17 * as published by the Free Software Foundation.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
23 *
24 * You should have received a copy of the GNU Affero General Public License, version 3,
25 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26 *
27 */
28
29namespace OC\DB;
30
31use Doctrine\DBAL\Platforms\AbstractPlatform;
32use Doctrine\DBAL\Schema\Schema;
33use OCP\IConfig;
34
35class MDB2SchemaReader {
36
37	/** @var string $DBTABLEPREFIX */
38	protected $DBTABLEPREFIX;
39
40	/** @var \Doctrine\DBAL\Platforms\AbstractPlatform $platform */
41	protected $platform;
42
43	/**
44	 * @param \OCP\IConfig $config
45	 * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
46	 */
47	public function __construct(IConfig $config, AbstractPlatform $platform) {
48		$this->platform = $platform;
49		$this->DBTABLEPREFIX = $config->getSystemValue('dbtableprefix', 'oc_');
50	}
51
52	/**
53	 * @param string $file
54	 * @param Schema $schema
55	 * @return Schema
56	 */
57	public function loadSchemaFromFile($file, Schema $schema) {
58		$loadEntities = \libxml_disable_entity_loader(false);
59		$xml = \simplexml_load_file($file);
60		\libxml_disable_entity_loader($loadEntities);
61		foreach ($xml->children() as $child) {
62			/**
63			 * @var \SimpleXMLElement $child
64			 */
65			switch ($child->getName()) {
66				case 'name':
67				case 'create':
68				case 'overwrite':
69				case 'charset':
70					break;
71				case 'table':
72					$this->loadTable($schema, $child);
73					break;
74				default:
75					throw new \DomainException('Unknown element: ' . $child->getName());
76
77			}
78		}
79		return $schema;
80	}
81
82	/**
83	 * @param \Doctrine\DBAL\Schema\Schema $schema
84	 * @param \SimpleXMLElement $xml
85	 * @throws \DomainException
86	 */
87	private function loadTable($schema, $xml) {
88		$table = null;
89		foreach ($xml->children() as $child) {
90			/**
91			 * @var \SimpleXMLElement $child
92			 */
93			switch ($child->getName()) {
94				case 'name':
95					$name = (string)$child;
96					$name = \str_replace('*dbprefix*', $this->DBTABLEPREFIX, $name);
97					$name = $this->platform->quoteIdentifier($name);
98					$table = $schema->createTable($name);
99					break;
100				case 'create':
101				case 'overwrite':
102				case 'charset':
103					break;
104				case 'declaration':
105					if ($table === null) {
106						throw new \DomainException('Table declaration before table name');
107					}
108					$this->loadDeclaration($table, $child);
109					break;
110				default:
111					throw new \DomainException('Unknown element: ' . $child->getName());
112
113			}
114		}
115	}
116
117	/**
118	 * @param \Doctrine\DBAL\Schema\Table $table
119	 * @param \SimpleXMLElement $xml
120	 * @throws \DomainException
121	 */
122	private function loadDeclaration($table, $xml) {
123		foreach ($xml->children() as $child) {
124			/**
125			 * @var \SimpleXMLElement $child
126			 */
127			switch ($child->getName()) {
128				case 'field':
129					$this->loadField($table, $child);
130					break;
131				case 'index':
132					$this->loadIndex($table, $child);
133					break;
134				default:
135					throw new \DomainException('Unknown element: ' . $child->getName());
136
137			}
138		}
139	}
140
141	/**
142	 * @param \Doctrine\DBAL\Schema\Table $table
143	 * @param \SimpleXMLElement $xml
144	 * @throws \DomainException
145	 */
146	private function loadField($table, $xml) {
147		$options = ['notnull' => false];
148		$primary = null;
149		foreach ($xml->children() as $child) {
150			/**
151			 * @var \SimpleXMLElement $child
152			 */
153			switch ($child->getName()) {
154				case 'name':
155					$name = (string)$child;
156					$name = $this->platform->quoteIdentifier($name);
157					break;
158				case 'type':
159					$type = (string)$child;
160					switch ($type) {
161						case 'text':
162							$type = 'string';
163							break;
164						case 'clob':
165							$type = 'text';
166							break;
167						case 'timestamp':
168							$type = 'datetime';
169							break;
170						case 'numeric':
171							$type = 'decimal';
172							break;
173					}
174					break;
175				case 'length':
176					$length = (string)$child;
177					$options['length'] = $length;
178					break;
179				case 'unsigned':
180					$unsigned = $this->asBool($child);
181					$options['unsigned'] = $unsigned;
182					break;
183				case 'notnull':
184					$notnull = $this->asBool($child);
185					$options['notnull'] = $notnull;
186					break;
187				case 'autoincrement':
188					$autoincrement = $this->asBool($child);
189					$options['autoincrement'] = $autoincrement;
190					break;
191				case 'default':
192					$default = (string)$child;
193					$options['default'] = $default;
194					break;
195				case 'comments':
196					$comment = (string)$child;
197					$options['comment'] = $comment;
198					break;
199				case 'primary':
200					$primary = $this->asBool($child);
201					break;
202				case 'precision':
203					$precision = (string)$child;
204					$options['precision'] = $precision;
205					break;
206				case 'scale':
207					$scale = (string)$child;
208					$options['scale'] = $scale;
209					break;
210				default:
211					throw new \DomainException('Unknown element: ' . $child->getName());
212
213			}
214		}
215		if (isset($name, $type)) {
216			if (isset($options['default']) && empty($options['default'])) {
217				if (empty($options['notnull']) || !$options['notnull']) {
218					unset($options['default']);
219					$options['notnull'] = false;
220				} else {
221					$options['default'] = '';
222				}
223				if ($type == 'integer' || $type == 'decimal') {
224					$options['default'] = 0;
225				} elseif ($type == 'boolean') {
226					$options['default'] = false;
227				}
228				if (!empty($options['autoincrement']) && $options['autoincrement']) {
229					unset($options['default']);
230				}
231			}
232			if ($type === 'integer' && isset($options['default'])) {
233				$options['default'] = (int)$options['default'];
234			}
235			if ($type === 'integer' && isset($options['length'])) {
236				$length = $options['length'];
237				if ($length < 4) {
238					$type = 'smallint';
239				} elseif ($length > 4) {
240					$type = 'bigint';
241				}
242			}
243			if ($type === 'boolean' && isset($options['default'])) {
244				$options['default'] = $this->asBool($options['default']);
245			}
246			if (!empty($options['autoincrement'])
247				&& !empty($options['notnull'])
248			) {
249				$primary = true;
250			}
251
252			$table->addColumn($name, $type, $options);
253			if ($primary) {
254				$table->setPrimaryKey([$name]);
255			}
256		}
257	}
258
259	/**
260	 * @param \Doctrine\DBAL\Schema\Table $table
261	 * @param \SimpleXMLElement $xml
262	 * @throws \DomainException
263	 */
264	private function loadIndex($table, $xml) {
265		$name = null;
266		$fields = [];
267		foreach ($xml->children() as $child) {
268			/**
269			 * @var \SimpleXMLElement $child
270			 */
271			switch ($child->getName()) {
272				case 'name':
273					$name = (string)$child;
274					break;
275				case 'primary':
276					$primary = $this->asBool($child);
277					break;
278				case 'unique':
279					$unique = $this->asBool($child);
280					break;
281				case 'field':
282					foreach ($child->children() as $field) {
283						/**
284						 * @var \SimpleXMLElement $field
285						 */
286						switch ($field->getName()) {
287							case 'name':
288								$field_name = (string)$field;
289								$field_name = $this->platform->quoteIdentifier($field_name);
290								$fields[] = $field_name;
291								break;
292							case 'sorting':
293								break;
294							default:
295								throw new \DomainException('Unknown element: ' . $field->getName());
296
297						}
298					}
299					break;
300				default:
301					throw new \DomainException('Unknown element: ' . $child->getName());
302
303			}
304		}
305		if (!empty($fields)) {
306			if (isset($primary) && $primary) {
307				if ($table->hasPrimaryKey()) {
308					return;
309				}
310				$table->setPrimaryKey($fields, $name);
311			} else {
312				if (isset($unique) && $unique) {
313					$table->addUniqueIndex($fields, $name);
314				} else {
315					$table->addIndex($fields, $name);
316				}
317			}
318		} else {
319			throw new \DomainException('Empty index definition: ' . $name . ' options:' . \print_r($fields, true));
320		}
321	}
322
323	/**
324	 * @param \SimpleXMLElement|string $xml
325	 * @return bool
326	 */
327	private function asBool($xml) {
328		$result = (string)$xml;
329		if ($result == 'true') {
330			$result = true;
331		} elseif ($result == 'false') {
332			$result = false;
333		}
334		return (bool)$result;
335	}
336}
337