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