Merge branch 'master' into next
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / package / PackageInstallationSQLParser.class.php
1 <?php
2 declare(strict_types=1);
3 namespace wcf\system\package;
4 use wcf\data\package\Package;
5 use wcf\system\database\util\SQLParser;
6 use wcf\system\exception\SystemException;
7 use wcf\system\WCF;
8
9 /**
10 * Extends SQLParser by testing and logging functions.
11 *
12 * @author Marcel Werk
13 * @copyright 2001-2018 WoltLab GmbH
14 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
15 * @package WoltLabSuite\Core\System\Package
16 */
17 class PackageInstallationSQLParser extends SQLParser {
18 /**
19 * package object
20 * @var Package
21 */
22 protected $package = null;
23
24 /**
25 * activates the testing mode
26 * @var boolean
27 */
28 protected $test = false;
29
30 /**
31 * installation type
32 * @var string
33 */
34 protected $action = 'install';
35
36 /**
37 * list of existing database tables
38 * @var array
39 */
40 protected $existingTables = [];
41
42 /**
43 * list of logged tables
44 * @var array
45 */
46 protected $knownTables = [];
47
48 /**
49 * list of conflicted database tables
50 * @var array
51 */
52 protected $conflicts = [];
53
54 /**
55 * list of created/deleted tables
56 * @var array
57 */
58 protected $tableLog = [];
59
60 /**
61 * list of created/deleted columns
62 * @var array
63 */
64 protected $columnLog = [];
65
66 /**
67 * list of created/deleted indices
68 * @var array
69 */
70 protected $indexLog = [];
71
72 /**
73 * Creates a new PackageInstallationSQLParser object.
74 *
75 * @param string $queries
76 * @param Package $package
77 * @param string $action
78 */
79 public function __construct($queries, Package $package, $action = 'install') {
80 $this->package = $package;
81 $this->action = $action;
82
83 parent::__construct($queries);
84 }
85
86 /**
87 * Performs a test of the given queries.
88 *
89 * @return array conflicts
90 */
91 public function test() {
92 $this->conflicts = [];
93
94 // get all existing tables from database
95 $this->existingTables = WCF::getDB()->getEditor()->getTableNames();
96
97 // get logged tables
98 $this->getKnownTables();
99
100 // enable testing mode
101 $this->test = true;
102
103 // run test
104 $this->execute();
105
106 // disable testing mode
107 $this->test = false;
108
109 // return conflicts
110 return $this->conflicts;
111 }
112
113 /**
114 * Logs executed sql queries
115 */
116 public function log() {
117 // tables
118 foreach ($this->tableLog as $logEntry) {
119 $sql = "DELETE FROM wcf".WCF_N."_package_installation_sql_log
120 WHERE sqlTable = ?";
121 $statement = WCF::getDB()->prepareStatement($sql);
122 $statement->execute([$logEntry['tableName']]);
123
124 if ($logEntry['action'] == 'insert') {
125 $sql = "INSERT INTO wcf".WCF_N."_package_installation_sql_log
126 (packageID, sqlTable)
127 VALUES (?, ?)";
128 $statement = WCF::getDB()->prepareStatement($sql);
129 $statement->execute([
130 $logEntry['packageID'],
131 $logEntry['tableName']
132 ]);
133 }
134 }
135
136 // columns
137 if (!empty($this->columnLog)) {
138 $sql = "DELETE FROM wcf".WCF_N."_package_installation_sql_log
139 WHERE sqlTable = ?
140 AND sqlColumn = ?";
141 $deleteStatement = WCF::getDB()->prepareStatement($sql);
142
143 $sql = "INSERT INTO wcf".WCF_N."_package_installation_sql_log
144 (packageID, sqlTable, sqlColumn)
145 VALUES (?, ?, ?)";
146 $insertStatement = WCF::getDB()->prepareStatement($sql);
147
148 foreach ($this->columnLog as $logEntry) {
149 $deleteStatement->execute([
150 $logEntry['tableName'],
151 $logEntry['columnName']
152 ]);
153
154 if ($logEntry['action'] == 'insert') {
155 $insertStatement->execute([
156 $logEntry['packageID'],
157 $logEntry['tableName'],
158 $logEntry['columnName']
159 ]);
160 }
161 }
162 }
163
164 // indices
165 if (!empty($this->indexLog)) {
166 $sql = "DELETE FROM wcf".WCF_N."_package_installation_sql_log
167 WHERE sqlTable = ?
168 AND sqlIndex = ?";
169 $deleteStatement = WCF::getDB()->prepareStatement($sql);
170
171 $sql = "INSERT INTO wcf".WCF_N."_package_installation_sql_log
172 (packageID, sqlTable, sqlIndex)
173 VALUES (?, ?, ?)";
174 $insertStatement = WCF::getDB()->prepareStatement($sql);
175
176 foreach ($this->indexLog as $logEntry) {
177 $deleteStatement->execute([
178 $logEntry['tableName'],
179 $logEntry['indexName']
180 ]);
181
182 if ($logEntry['action'] == 'insert') {
183 $insertStatement->execute([
184 $logEntry['packageID'],
185 $logEntry['tableName'],
186 $logEntry['indexName']
187 ]);
188 }
189 }
190 }
191 }
192
193 /**
194 * Fetches known sql tables and their owners from installation log.
195 */
196 protected function getKnownTables() {
197 $sql = "SELECT packageID, sqlTable
198 FROM wcf".WCF_N."_package_installation_sql_log
199 WHERE sqlColumn = ''
200 AND sqlIndex = ''";
201 $statement = WCF::getDB()->prepareStatement($sql);
202 $statement->execute();
203 $this->knownTables = $statement->fetchMap('sqlTable', 'packageID');
204 }
205
206 /**
207 * Returns the owner of a specific database table column.
208 *
209 * @param string $tableName
210 * @param string $columnName
211 * @return integer package id
212 */
213 protected function getColumnOwnerID($tableName, $columnName) {
214 $sql = "SELECT packageID
215 FROM wcf".WCF_N."_package_installation_sql_log
216 WHERE sqlTable = ?
217 AND sqlColumn = ?";
218 $statement = WCF::getDB()->prepareStatement($sql);
219 $statement->execute([
220 $tableName,
221 $columnName
222 ]);
223 $row = $statement->fetchArray();
224 if (!empty($row['packageID'])) return $row['packageID'];
225 else if (isset($this->knownTables[$tableName])) return $this->knownTables[$tableName];
226 else return null;
227 }
228
229 /**
230 * Returns the owner of a specific database index.
231 *
232 * @param string $tableName
233 * @param string $indexName
234 * @return integer package id
235 */
236 protected function getIndexOwnerID($tableName, $indexName) {
237 $sql = "SELECT packageID
238 FROM wcf".WCF_N."_package_installation_sql_log
239 WHERE sqlTable = ?
240 AND sqlIndex = ?";
241 $statement = WCF::getDB()->prepareStatement($sql);
242 $statement->execute([
243 $tableName,
244 $indexName
245 ]);
246 $row = $statement->fetchArray();
247 if (!empty($row['packageID'])) return $row['packageID'];
248 else if (isset($this->knownTables[$tableName])) return $this->knownTables[$tableName];
249 else return null;
250 }
251
252 /**
253 * @inheritDoc
254 */
255 protected function executeCreateTableStatement($tableName, $columns, $indices = []) {
256 if ($this->test) {
257 if (in_array($tableName, $this->existingTables)) {
258 if (isset($this->knownTables[$tableName]) && $this->knownTables[$tableName] != $this->package->packageID) {
259 throw new SystemException("Cannot recreate table '".$tableName."'. A package can only overwrite own tables.");
260 }
261 else {
262 if (!isset($this->conflicts['CREATE TABLE'])) $this->conflicts['CREATE TABLE'] = [];
263 $this->conflicts['CREATE TABLE'][] = $tableName;
264 }
265 }
266 }
267 else {
268 // log
269 $this->tableLog[] = ['tableName' => $tableName, 'packageID' => $this->package->packageID, 'action' => 'insert'];
270
271 // execute
272 parent::executeCreateTableStatement($tableName, $columns, $indices);
273 }
274 }
275
276 /**
277 * @inheritDoc
278 */
279 protected function executeAddColumnStatement($tableName, $columnName, $columnData) {
280 if ($this->test) {
281 if (!isset($this->knownTables[$tableName])) {
282 throw new SystemException("Cannot add column '".$columnName."' to table '".$tableName."'.");
283 }
284 }
285 else {
286 // log
287 $this->columnLog[] = ['tableName' => $tableName, 'columnName' => $columnName, 'packageID' => $this->package->packageID, 'action' => 'insert'];
288
289 // execute
290 parent::executeAddColumnStatement($tableName, $columnName, $columnData);
291 }
292 }
293
294 /**
295 * @inheritDoc
296 */
297 protected function executeAlterColumnStatement($tableName, $oldColumnName, $newColumnName, $newColumnData) {
298 if ($this->test) {
299 if ($ownerPackageID = $this->getColumnOwnerID($tableName, $oldColumnName)) {
300 if ($ownerPackageID != $this->package->packageID) {
301 throw new SystemException("Cannot alter column '".$oldColumnName."'. A package can only change own columns.");
302 }
303 }
304 }
305 else {
306 // log
307 if ($oldColumnName != $newColumnName) {
308 $this->columnLog[] = ['tableName' => $tableName, 'columnName' => $oldColumnName, 'packageID' => $this->package->packageID, 'action' => 'delete'];
309 $this->columnLog[] = ['tableName' => $tableName, 'columnName' => $newColumnName, 'packageID' => $this->package->packageID, 'action' => 'insert'];
310 }
311
312 // execute
313 parent::executeAlterColumnStatement($tableName, $oldColumnName, $newColumnName, $newColumnData);
314 }
315 }
316
317 /**
318 * @inheritDoc
319 */
320 protected function executeAddIndexStatement($tableName, $indexName, $indexData) {
321 if (!$this->test) {
322 // log
323 $this->indexLog[] = ['tableName' => $tableName, 'indexName' => $indexName, 'packageID' => $this->package->packageID, 'action' => 'insert'];
324
325 // execute
326 parent::executeAddIndexStatement($tableName, $indexName, $indexData);
327 }
328 }
329
330 /**
331 * @inheritDoc
332 */
333 protected function executeAddForeignKeyStatement($tableName, $indexName, $indexData) {
334 if (!$this->test) {
335 // log
336 $this->indexLog[] = ['tableName' => $tableName, 'indexName' => $indexName, 'packageID' => $this->package->packageID, 'action' => 'insert'];
337
338 // execute
339 parent::executeAddForeignKeyStatement($tableName, $indexName, $indexData);
340 }
341 }
342
343 /**
344 * @inheritDoc
345 */
346 protected function executeDropColumnStatement($tableName, $columnName) {
347 if ($this->test) {
348 if ($ownerPackageID = $this->getColumnOwnerID($tableName, $columnName)) {
349 if ($ownerPackageID != $this->package->packageID) {
350 throw new SystemException("Cannot drop column '".$columnName."'. A package can only drop own columns.");
351 }
352 }
353 }
354 else {
355 // log
356 $this->columnLog[] = ['tableName' => $tableName, 'columnName' => $columnName, 'packageID' => $this->package->packageID, 'action' => 'delete'];
357
358 // execute
359 parent::executeDropColumnStatement($tableName, $columnName);
360 }
361 }
362
363 /**
364 * @inheritDoc
365 */
366 protected function executeDropIndexStatement($tableName, $indexName) {
367 if ($this->test) {
368 if ($ownerPackageID = $this->getIndexOwnerID($tableName, $indexName)) {
369 if ($ownerPackageID != $this->package->packageID) {
370 throw new SystemException("Cannot drop index '".$indexName."'. A package can only drop own indices.");
371 }
372 }
373 }
374 else {
375 // log
376 $this->indexLog[] = ['tableName' => $tableName, 'indexName' => $indexName, 'packageID' => $this->package->packageID, 'action' => 'delete'];
377
378 // execute
379 parent::executeDropIndexStatement($tableName, $indexName);
380 }
381 }
382
383 /**
384 * @inheritDoc
385 */
386 protected function executeDropPrimaryKeyStatement($tableName) {
387 if ($this->test) {
388 if ($ownerPackageID = $this->getIndexOwnerID($tableName, '')) {
389 if ($ownerPackageID != $this->package->packageID) {
390 throw new SystemException("Cannot drop primary key from '".$tableName."'. A package can only drop own indices.");
391 }
392 }
393 }
394 else {
395 // // log
396 // $this->indexLog[] = array('tableName' => $tableName, 'indexName' => '', 'packageID' => $this->package->packageID, 'action' => 'delete');
397
398 // execute
399 parent::executeDropPrimaryKeyStatement($tableName);
400 }
401 }
402
403 /**
404 * @inheritDoc
405 */
406 protected function executeDropForeignKeyStatement($tableName, $indexName) {
407 if ($this->test) {
408 if ($ownerPackageID = $this->getIndexOwnerID($tableName, $indexName)) {
409 if ($ownerPackageID != $this->package->packageID) {
410 throw new SystemException("Cannot drop index '".$indexName."'. A package can only drop own indices.");
411 }
412 }
413 }
414 else {
415 // log
416 $this->indexLog[] = ['tableName' => $tableName, 'indexName' => $indexName, 'packageID' => $this->package->packageID, 'action' => 'delete'];
417
418 // execute
419 parent::executeDropForeignKeyStatement($tableName, $indexName);
420 }
421 }
422
423 /**
424 * @inheritDoc
425 */
426 protected function executeDropTableStatement($tableName) {
427 if ($this->test) {
428 if (in_array($tableName, $this->existingTables)) {
429 if (isset($this->knownTables[$tableName]) && $this->knownTables[$tableName] != $this->package->packageID) {
430 throw new SystemException("Cannot drop table '".$tableName."'. A package can only drop own tables.");
431 }
432 }
433 }
434 else {
435 // log
436 $this->tableLog[] = ['tableName' => $tableName, 'packageID' => $this->package->packageID, 'action' => 'delete'];
437
438 // execute
439 parent::executeDropTableStatement($tableName);
440 }
441 }
442
443 /**
444 * @inheritDoc
445 */
446 protected function executeStandardStatement($query) {
447 if (!$this->test) {
448 parent::executeStandardStatement($query);
449 }
450 }
451 }