]> git.mxchange.org Git - friendica.git/blob - src/Database/DBStructure.php
864f2ded3f19cedbe1f62e9b7ee459830b07121f
[friendica.git] / src / Database / DBStructure.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Database;
23
24 use Exception;
25 use Friendica\Core\Hook;
26 use Friendica\Core\Logger;
27 use Friendica\Core\Renderer;
28 use Friendica\DI;
29 use Friendica\Model\Item;
30 use Friendica\Model\User;
31 use Friendica\Util\DateTimeFormat;
32
33 /**
34  * This class contains functions that doesn't need to know if pdo, mysqli or whatever is used.
35  */
36 class DBStructure
37 {
38         const UPDATE_NOT_CHECKED = 0; // Database check wasn't executed before
39         const UPDATE_SUCCESSFUL  = 1; // Database check was successful
40         const UPDATE_FAILED      = 2; // Database check failed
41
42         const RENAME_COLUMN      = 0;
43         const RENAME_PRIMARY_KEY = 1;
44
45         /**
46          * Database structure definition loaded from config/dbstructure.config.php
47          *
48          * @var array
49          */
50         private static $definition = [];
51
52         /**
53          * Set a database version to trigger update functions
54          *
55          * @param string $version
56          * @return void
57          */
58         public static function setDatabaseVersion(string $version)
59         {
60                 if (!is_numeric($version)) {
61                         throw new \Asika\SimpleConsole\CommandArgsException('The version number must be numeric');
62                 }
63
64                 DI::config()->set('system', 'build', $version);
65                 echo DI::l10n()->t('The database version had been set to %s.', $version);
66         }
67
68         /**
69          * Drop unused tables
70          *
71          * @param boolean $execute
72          * @return void
73          */
74         public static function dropTables(bool $execute)
75         {
76                 $postupdate = DI::config()->get("system", "post_update_version", PostUpdate::VERSION);
77                 if ($postupdate < PostUpdate::VERSION) {
78                         echo DI::l10n()->t('The post update is at version %d, it has to be at %d to safely drop the tables.', $postupdate, PostUpdate::VERSION);
79                         return;
80                 }
81
82                 $old_tables = ['fserver', 'gcign', 'gcontact', 'gcontact-relation', 'gfollower' ,'glink', 'item-delivery-data',
83                         'item-activity', 'item-content', 'item_id', 'participation', 'poll', 'poll_result', 'queue', 'retriever_rule',
84                         'deliverq', 'dsprphotoq', 'ffinder', 'sign', 'spam', 'term', 'user-item', 'thread', 'item'];
85
86                 $tables = DBA::selectToArray(['INFORMATION_SCHEMA' => 'TABLES'], ['TABLE_NAME'],
87                         ['TABLE_SCHEMA' => DBA::databaseName(), 'TABLE_TYPE' => 'BASE TABLE']);
88
89                 if (empty($tables)) {
90                         echo DI::l10n()->t('No unused tables found.');
91                         return;
92                 }
93
94                 if (!$execute) {
95                         echo DI::l10n()->t('These tables are not used for friendica and will be deleted when you execute "dbstructure drop -e":') . "\n\n";
96                 }
97
98                 foreach ($tables as $table) {
99                         if (in_array($table['TABLE_NAME'], $old_tables)) {
100                                 if ($execute) {
101                                         $sql = 'DROP TABLE ' . DBA::quoteIdentifier($table['TABLE_NAME']) . ';';
102                                         echo $sql . "\n";
103
104                                         $result = DBA::e($sql);
105                                         if (!DBA::isResult($result)) {
106                                                 self::printUpdateError($sql);
107                                         }
108                                 } else {
109                                         echo $table['TABLE_NAME'] . "\n";
110                                 }
111                         }
112                 }
113         }
114
115         /**
116          * Converts all tables from MyISAM/InnoDB Antelope to InnoDB Barracuda
117          */
118         public static function convertToInnoDB()
119         {
120                 $tables = DBA::selectToArray(
121                         ['information_schema' => 'tables'],
122                         ['table_name'],
123                         ['engine' => 'MyISAM', 'table_schema' => DBA::databaseName()]
124                 );
125
126                 $tables = array_merge($tables, DBA::selectToArray(
127                         ['information_schema' => 'tables'],
128                         ['table_name'],
129                         ['engine' => 'InnoDB', 'ROW_FORMAT' => ['COMPACT', 'REDUNDANT'], 'table_schema' => DBA::databaseName()]
130                 ));
131
132                 if (!DBA::isResult($tables)) {
133                         echo DI::l10n()->t('There are no tables on MyISAM or InnoDB with the Antelope file format.') . "\n";
134                         return;
135                 }
136
137                 foreach ($tables AS $table) {
138                         $sql = "ALTER TABLE " . DBA::quoteIdentifier($table['table_name']) . " ENGINE=InnoDB ROW_FORMAT=DYNAMIC;";
139                         echo $sql . "\n";
140
141                         $result = DBA::e($sql);
142                         if (!DBA::isResult($result)) {
143                                 self::printUpdateError($sql);
144                         }
145                 }
146         }
147
148         /**
149          * Print out database error messages
150          *
151          * @param string $message Message to be added to the error message
152          *
153          * @return string Error message
154          */
155         private static function printUpdateError($message)
156         {
157                 echo DI::l10n()->t("\nError %d occurred during database update:\n%s\n",
158                         DBA::errorNo(), DBA::errorMessage());
159
160                 return DI::l10n()->t('Errors encountered performing database changes: ') . $message . EOL;
161         }
162
163         public static function writeStructure()
164         {
165                 Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
166
167                 $tables = [];
168                 foreach (self::definition(null) as $name => $definition) {
169                         $indexes = [];
170                         foreach ($definition['indexes'] as $key => $value) {
171                                 $indexes[] = ['name' => $key, 'fields' => implode(', ', $value)];
172                         }
173
174                         $foreign = [];
175                         $fields  = [[
176                                 'name'    => 'Field',
177                                 'comment' => 'Description',
178                                 'type'    => 'Type',
179                                 'null'    => 'Null',
180                                 'primary' => 'Key',
181                                 'default' => 'Default',
182                                 'extra'   => 'Extra',
183                         ],
184                         [
185                                 'name'    => '-',
186                                 'comment' => '-',
187                                 'type'    => '-',
188                                 'null'    => '-',
189                                 'primary' => '-',
190                                 'default' => '-',
191                                 'extra'   => '-',
192                         ]];
193                         $lengths = [
194                                 'name'    => 5,
195                                 'comment' => 11,
196                                 'type'    => 4,
197                                 'null'    => 4,
198                                 'primary' => 3,
199                                 'default' => 7,
200                                 'extra'   => 5,
201                         ];
202                         foreach ($definition['fields'] as $key => $value) {
203                                 $field = [];
204                                 $field['name']    = $key;
205                                 $field['comment'] = $value['comment'] ?? '';
206                                 $field['type']    = $value['type'];
207                                 $field['null']    = ($value['not null'] ?? false) ? 'NO' : 'YES';
208                                 $field['primary'] = ($value['primary'] ?? false) ? 'PRI' : '';
209                                 $field['default'] = $value['default'] ?? 'NULL';
210                                 $field['extra']   = $value['extra'] ?? '';
211
212                                 foreach ($field as $fieldname => $fieldvalue) {
213                                         $lengths[$fieldname] = max($lengths[$fieldname] ?? 0, strlen($fieldvalue));
214                                 }
215                                 $fields[] = $field;
216
217                                 if (!empty($value['foreign'])) {
218                                         $foreign[] = [
219                                                 'field'       => $key,
220                                                 'targettable' => array_keys($value['foreign'])[0],
221                                                 'targetfield' => array_values($value['foreign'])[0]
222                                         ];
223                                 }
224                         }
225
226                         array_walk_recursive($fields, function(&$value, $key) use ($lengths)
227                         {
228                                 $value = str_pad($value, $lengths[$key], $value === '-' ? '-' : ' ');
229                         });
230
231                         $tables[] = ['name' => $name, 'comment' => $definition['comment']];
232                         $content = Renderer::replaceMacros(Renderer::getMarkupTemplate('structure.tpl'), [
233                                 '$name'    => $name,
234                                 '$comment' => $definition['comment'],
235                                 '$fields'  => $fields,
236                                 '$indexes' => $indexes,
237                                 '$foreign' => $foreign,
238                         ]);
239                         $filename = DI::basePath() . '/doc/database/db_' . $name . '.md';
240                         file_put_contents($filename, $content);
241                 }
242                 asort($tables);
243                 $content = Renderer::replaceMacros(Renderer::getMarkupTemplate('tables.tpl'), [
244                         '$tables'  => $tables,
245                 ]);
246                 $filename = DI::basePath() . '/doc/database.md';
247                 file_put_contents($filename, $content);
248         }
249
250         public static function printStructure($basePath)
251         {
252                 $database = self::definition($basePath, false);
253
254                 echo "-- ------------------------------------------\n";
255                 echo "-- " . FRIENDICA_PLATFORM . " " . FRIENDICA_VERSION . " (" . FRIENDICA_CODENAME, ")\n";
256                 echo "-- DB_UPDATE_VERSION " . DB_UPDATE_VERSION . "\n";
257                 echo "-- ------------------------------------------\n\n\n";
258                 foreach ($database AS $name => $structure) {
259                         echo "--\n";
260                         echo "-- TABLE $name\n";
261                         echo "--\n";
262                         self::createTable($name, $structure, true, false);
263
264                         echo "\n";
265                 }
266
267                 View::printStructure($basePath);
268         }
269
270         /**
271          * Loads the database structure definition from the static/dbstructure.config.php file.
272          * On first pass, defines DB_UPDATE_VERSION constant.
273          *
274          * @see static/dbstructure.config.php
275          * @param boolean $with_addons_structure Whether to tack on addons additional tables
276          * @param string  $basePath              The base path of this application
277          * @return array
278          * @throws Exception
279          */
280         public static function definition($basePath, $with_addons_structure = true)
281         {
282                 if (!self::$definition) {
283                         if (empty($basePath)) {
284                                 $basePath = DI::app()->getBasePath();
285                         }
286
287                         $filename = $basePath . '/static/dbstructure.config.php';
288
289                         if (!is_readable($filename)) {
290                                 throw new Exception('Missing database structure config file static/dbstructure.config.php');
291                         }
292
293                         $definition = require $filename;
294
295                         if (!$definition) {
296                                 throw new Exception('Corrupted database structure config file static/dbstructure.config.php');
297                         }
298
299                         self::$definition = $definition;
300                 } else {
301                         $definition = self::$definition;
302                 }
303
304                 if ($with_addons_structure) {
305                         Hook::callAll('dbstructure_definition', $definition);
306                 }
307
308                 return $definition;
309         }
310
311         /**
312          * Get field data for the given table
313          *
314          * @param string $table
315          * @param array $data data fields
316          * @return array fields for the given
317          */
318         public static function getFieldsForTable(string $table, array $data = [])
319         {
320                 $definition = DBStructure::definition('', false);
321                 if (empty($definition[$table])) {
322                         return [];
323                 }
324
325                 $fieldnames = array_keys($definition[$table]['fields']);
326
327                 $fields = [];
328
329                 // Assign all field that are present in the table
330                 foreach ($fieldnames as $field) {
331                         if (isset($data[$field])) {
332                                 // Limit the length of varchar, varbinary, char and binrary fields
333                                 if (is_string($data[$field]) && preg_match("/char\((\d*)\)/", $definition[$table]['fields'][$field]['type'], $result)) {
334                                         $data[$field] = mb_substr($data[$field], 0, $result[1]);
335                                 } elseif (is_string($data[$field]) && preg_match("/binary\((\d*)\)/", $definition[$table]['fields'][$field]['type'], $result)) {
336                                         $data[$field] = substr($data[$field], 0, $result[1]);
337                                 }
338                                 $fields[$field] = $data[$field];
339                         }
340                 }
341
342                 return $fields;
343         }
344
345         private static function createTable($name, $structure, $verbose, $action)
346         {
347                 $r = true;
348
349                 $engine = "";
350                 $comment = "";
351                 $sql_rows = [];
352                 $primary_keys = [];
353                 $foreign_keys = [];
354
355                 foreach ($structure["fields"] AS $fieldname => $field) {
356                         $sql_rows[] = "`" . DBA::escape($fieldname) . "` " . self::FieldCommand($field);
357                         if (!empty($field['primary'])) {
358                                 $primary_keys[] = $fieldname;
359                         }
360                         if (!empty($field['foreign'])) {
361                                 $foreign_keys[$fieldname] = $field;
362                         }
363                 }
364
365                 if (!empty($structure["indexes"])) {
366                         foreach ($structure["indexes"] AS $indexname => $fieldnames) {
367                                 $sql_index = self::createIndex($indexname, $fieldnames, "");
368                                 if (!is_null($sql_index)) {
369                                         $sql_rows[] = $sql_index;
370                                 }
371                         }
372                 }
373
374                 foreach ($foreign_keys AS $fieldname => $parameters) {
375                         $sql_rows[] = self::foreignCommand($name, $fieldname, $parameters);
376                 }
377
378                 if (isset($structure["engine"])) {
379                         $engine = " ENGINE=" . $structure["engine"];
380                 }
381
382                 if (isset($structure["comment"])) {
383                         $comment = " COMMENT='" . DBA::escape($structure["comment"]) . "'";
384                 }
385
386                 $sql = implode(",\n\t", $sql_rows);
387
388                 $sql = sprintf("CREATE TABLE IF NOT EXISTS `%s` (\n\t", DBA::escape($name)) . $sql .
389                         "\n)" . $engine . " DEFAULT COLLATE utf8mb4_general_ci" . $comment;
390                 if ($verbose) {
391                         echo $sql . ";\n";
392                 }
393
394                 if ($action) {
395                         $r = DBA::e($sql);
396                 }
397
398                 return $r;
399         }
400
401         private static function FieldCommand($parameters, $create = true)
402         {
403                 $fieldstruct = $parameters["type"];
404
405                 if (isset($parameters["Collation"])) {
406                         $fieldstruct .= " COLLATE " . $parameters["Collation"];
407                 }
408
409                 if (isset($parameters["not null"])) {
410                         $fieldstruct .= " NOT NULL";
411                 }
412
413                 if (isset($parameters["default"])) {
414                         if (strpos(strtolower($parameters["type"]), "int") !== false) {
415                                 $fieldstruct .= " DEFAULT " . $parameters["default"];
416                         } else {
417                                 $fieldstruct .= " DEFAULT '" . $parameters["default"] . "'";
418                         }
419                 }
420                 if (isset($parameters["extra"])) {
421                         $fieldstruct .= " " . $parameters["extra"];
422                 }
423
424                 if (isset($parameters["comment"])) {
425                         $fieldstruct .= " COMMENT '" . DBA::escape($parameters["comment"]) . "'";
426                 }
427
428                 /*if (($parameters["primary"] != "") && $create)
429                         $fieldstruct .= " PRIMARY KEY";*/
430
431                 return ($fieldstruct);
432         }
433
434         private static function createIndex($indexname, $fieldnames, $method = "ADD")
435         {
436                 $method = strtoupper(trim($method));
437                 if ($method != "" && $method != "ADD") {
438                         throw new Exception("Invalid parameter 'method' in self::createIndex(): '$method'");
439                 }
440
441                 if (in_array($fieldnames[0], ["UNIQUE", "FULLTEXT"])) {
442                         $index_type = array_shift($fieldnames);
443                         $method .= " " . $index_type;
444                 }
445
446                 $names = "";
447                 foreach ($fieldnames AS $fieldname) {
448                         if ($names != "") {
449                                 $names .= ",";
450                         }
451
452                         if (preg_match('|(.+)\((\d+)\)|', $fieldname, $matches)) {
453                                 $names .= "`" . DBA::escape($matches[1]) . "`(" . intval($matches[2]) . ")";
454                         } else {
455                                 $names .= "`" . DBA::escape($fieldname) . "`";
456                         }
457                 }
458
459                 if ($indexname == "PRIMARY") {
460                         return sprintf("%s PRIMARY KEY(%s)", $method, $names);
461                 }
462
463
464                 $sql = sprintf("%s INDEX `%s` (%s)", $method, DBA::escape($indexname), $names);
465                 return ($sql);
466         }
467
468         /**
469          * Perform a database structure dryrun (means: just simulating)
470          *
471          * @throws Exception
472          */
473         public static function dryRun()
474         {
475                 self::update(DI::app()->getBasePath(), true, false);
476         }
477
478         /**
479          * Updates DB structure and returns eventual errors messages
480          *
481          * @param bool $enable_maintenance_mode Set the maintenance mode
482          * @param bool $verbose                 Display the SQL commands
483          *
484          * @return string Empty string if the update is successful, error messages otherwise
485          * @throws Exception
486          */
487         public static function performUpdate(bool $enable_maintenance_mode = true, bool $verbose = false)
488         {
489                 if ($enable_maintenance_mode) {
490                         DI::config()->set('system', 'maintenance', 1);
491                 }
492
493                 $status = self::update(DI::app()->getBasePath(), $verbose, true);
494
495                 if ($enable_maintenance_mode) {
496                         DI::config()->set('system', 'maintenance', 0);
497                         DI::config()->set('system', 'maintenance_reason', '');
498                 }
499
500                 return $status;
501         }
502
503         /**
504          * Updates DB structure from the installation and returns eventual errors messages
505          *
506          * @param string $basePath   The base path of this application
507          *
508          * @return string Empty string if the update is successful, error messages otherwise
509          * @throws Exception
510          */
511         public static function install(string $basePath)
512         {
513                 return self::update($basePath, false, true, true);
514         }
515
516         /**
517          * Updates DB structure and returns eventual errors messages
518          *
519          * @param string $basePath   The base path of this application
520          * @param bool   $verbose
521          * @param bool   $action     Whether to actually apply the update
522          * @param bool   $install    Is this the initial update during the installation?
523          * @param array  $tables     An array of the database tables
524          * @param array  $definition An array of the definition tables
525          * @return string Empty string if the update is successful, error messages otherwise
526          * @throws Exception
527          */
528         private static function update($basePath, $verbose, $action, $install = false, array $tables = null, array $definition = null)
529         {
530                 $in_maintenance_mode = DI::config()->get('system', 'maintenance');
531
532                 if ($action && !$install && self::isUpdating()) {
533                         return DI::l10n()->t('Another database update is currently running.');
534                 }
535
536                 if ($in_maintenance_mode) {
537                         DI::config()->set('system', 'maintenance_reason', DI::l10n()->t('%s: Database update', DateTimeFormat::utcNow() . ' ' . date('e')));
538                 }
539
540                 // ensure that all initial values exist. This test has to be done prior and after the structure check.
541                 // Prior is needed if the specific tables already exists - after is needed when they had been created.
542                 self::checkInitialValues();
543
544                 $errors = '';
545
546                 Logger::info('updating structure');
547
548                 // Get the current structure
549                 $database = [];
550
551                 if (is_null($tables)) {
552                         $tables = DBA::toArray(DBA::p("SHOW TABLES"));
553                 }
554
555                 if (DBA::isResult($tables)) {
556                         foreach ($tables AS $table) {
557                                 $table = current($table);
558
559                                 Logger::info('updating structure', ['table' => $table]);
560                                 $database[$table] = self::tableStructure($table);
561                         }
562                 }
563
564                 // Get the definition
565                 if (is_null($definition)) {
566                         $definition = self::definition($basePath);
567                 }
568
569                 // MySQL >= 5.7.4 doesn't support the IGNORE keyword in ALTER TABLE statements
570                 if ((version_compare(DBA::serverInfo(), '5.7.4') >= 0) &&
571                         !(strpos(DBA::serverInfo(), 'MariaDB') !== false)) {
572                         $ignore = '';
573                 } else {
574                         $ignore = ' IGNORE';
575                 }
576
577                 // Compare it
578                 foreach ($definition AS $name => $structure) {
579                         $is_new_table = false;
580                         $group_by = "";
581                         $sql3 = "";
582                         $is_unique = false;
583                         $temp_name = $name;
584                         if (!isset($database[$name])) {
585                                 $r = self::createTable($name, $structure, $verbose, $action);
586                                 if (!DBA::isResult($r)) {
587                                         $errors .= self::printUpdateError($name);
588                                 }
589                                 $is_new_table = true;
590                         } else {
591                                 foreach ($structure["indexes"] AS $indexname => $fieldnames) {
592                                         if (isset($database[$name]["indexes"][$indexname])) {
593                                                 $current_index_definition = implode(",", $database[$name]["indexes"][$indexname]);
594                                         } else {
595                                                 $current_index_definition = "__NOT_SET__";
596                                         }
597                                         $new_index_definition = implode(",", $fieldnames);
598                                         if ($current_index_definition != $new_index_definition) {
599                                                 if ($fieldnames[0] == "UNIQUE") {
600                                                         $is_unique = true;
601                                                         if ($ignore == "") {
602                                                                 $temp_name = "temp-" . $name;
603                                                         }
604                                                 }
605                                         }
606                                 }
607
608                                 /*
609                                  * Drop the index if it isn't present in the definition
610                                  * or the definition differ from current status
611                                  * and index name doesn't start with "local_"
612                                  */
613                                 foreach ($database[$name]["indexes"] as $indexname => $fieldnames) {
614                                         $current_index_definition = implode(",", $fieldnames);
615                                         if (isset($structure["indexes"][$indexname])) {
616                                                 $new_index_definition = implode(",", $structure["indexes"][$indexname]);
617                                         } else {
618                                                 $new_index_definition = "__NOT_SET__";
619                                         }
620                                         if ($current_index_definition != $new_index_definition && substr($indexname, 0, 6) != 'local_') {
621                                                 $sql2 = self::dropIndex($indexname);
622                                                 if ($sql3 == "") {
623                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
624                                                 } else {
625                                                         $sql3 .= ", " . $sql2;
626                                                 }
627                                         }
628                                 }
629                                 // Compare the field structure field by field
630                                 foreach ($structure["fields"] AS $fieldname => $parameters) {
631                                         if (!isset($database[$name]["fields"][$fieldname])) {
632                                                 $sql2 = self::addTableField($fieldname, $parameters);
633                                                 if ($sql3 == "") {
634                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
635                                                 } else {
636                                                         $sql3 .= ", " . $sql2;
637                                                 }
638                                         } else {
639                                                 // Compare the field definition
640                                                 $field_definition = $database[$name]["fields"][$fieldname];
641
642                                                 // Remove the relation data that is used for the referential integrity
643                                                 unset($parameters['relation']);
644                                                 unset($parameters['foreign']);
645
646                                                 // We change the collation after the indexes had been changed.
647                                                 // This is done to avoid index length problems.
648                                                 // So here we always ensure that there is no need to change it.
649                                                 unset($parameters['Collation']);
650                                                 unset($field_definition['Collation']);
651
652                                                 // Only update the comment when it is defined
653                                                 if (!isset($parameters['comment'])) {
654                                                         $parameters['comment'] = "";
655                                                 }
656
657                                                 $current_field_definition = DBA::cleanQuery(implode(",", $field_definition));
658                                                 $new_field_definition = DBA::cleanQuery(implode(",", $parameters));
659                                                 if ($current_field_definition != $new_field_definition) {
660                                                         $sql2 = self::modifyTableField($fieldname, $parameters);
661                                                         if ($sql3 == "") {
662                                                                 $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
663                                                         } else {
664                                                                 $sql3 .= ", " . $sql2;
665                                                         }
666                                                 }
667                                         }
668                                 }
669                         }
670
671                         /*
672                          * Create the index if the index don't exists in database
673                          * or the definition differ from the current status.
674                          * Don't create keys if table is new
675                          */
676                         if (!$is_new_table) {
677                                 foreach ($structure["indexes"] AS $indexname => $fieldnames) {
678                                         if (isset($database[$name]["indexes"][$indexname])) {
679                                                 $current_index_definition = implode(",", $database[$name]["indexes"][$indexname]);
680                                         } else {
681                                                 $current_index_definition = "__NOT_SET__";
682                                         }
683                                         $new_index_definition = implode(",", $fieldnames);
684                                         if ($current_index_definition != $new_index_definition) {
685                                                 $sql2 = self::createIndex($indexname, $fieldnames);
686
687                                                 // Fetch the "group by" fields for unique indexes
688                                                 $group_by = self::groupBy($fieldnames);
689                                                 if ($sql2 != "") {
690                                                         if ($sql3 == "") {
691                                                                 $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
692                                                         } else {
693                                                                 $sql3 .= ", " . $sql2;
694                                                         }
695                                                 }
696                                         }
697                                 }
698
699                                 $existing_foreign_keys = $database[$name]['foreign_keys'];
700
701                                 // Foreign keys
702                                 // Compare the field structure field by field
703                                 foreach ($structure["fields"] AS $fieldname => $parameters) {
704                                         if (empty($parameters['foreign'])) {
705                                                 continue;
706                                         }
707
708                                         $constraint = self::getConstraintName($name, $fieldname, $parameters);
709
710                                         unset($existing_foreign_keys[$constraint]);
711
712                                         if (empty($database[$name]['foreign_keys'][$constraint])) {
713                                                 $sql2 = self::addForeignKey($name, $fieldname, $parameters);
714
715                                                 if ($sql3 == "") {
716                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
717                                                 } else {
718                                                         $sql3 .= ", " . $sql2;
719                                                 }
720                                         }
721                                 }
722
723                                 foreach ($existing_foreign_keys as $param) {
724                                         $sql2 = self::dropForeignKey($param['CONSTRAINT_NAME']);
725
726                                         if ($sql3 == "") {
727                                                 $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
728                                         } else {
729                                                 $sql3 .= ", " . $sql2;
730                                         }
731                                 }
732
733                                 if (isset($database[$name]["table_status"]["TABLE_COMMENT"])) {
734                                         $structurecomment = $structure["comment"] ?? '';
735                                         if ($database[$name]["table_status"]["TABLE_COMMENT"] != $structurecomment) {
736                                                 $sql2 = "COMMENT = '" . DBA::escape($structurecomment) . "'";
737
738                                                 if ($sql3 == "") {
739                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
740                                                 } else {
741                                                         $sql3 .= ", " . $sql2;
742                                                 }
743                                         }
744                                 }
745
746                                 if (isset($database[$name]["table_status"]["ENGINE"]) && isset($structure['engine'])) {
747                                         if ($database[$name]["table_status"]["ENGINE"] != $structure['engine']) {
748                                                 $sql2 = "ENGINE = '" . DBA::escape($structure['engine']) . "'";
749
750                                                 if ($sql3 == "") {
751                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
752                                                 } else {
753                                                         $sql3 .= ", " . $sql2;
754                                                 }
755                                         }
756                                 }
757
758                                 if (isset($database[$name]["table_status"]["TABLE_COLLATION"])) {
759                                         if ($database[$name]["table_status"]["TABLE_COLLATION"] != 'utf8mb4_general_ci') {
760                                                 $sql2 = "DEFAULT COLLATE utf8mb4_general_ci";
761
762                                                 if ($sql3 == "") {
763                                                         $sql3 = "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
764                                                 } else {
765                                                         $sql3 .= ", " . $sql2;
766                                                 }
767                                         }
768                                 }
769
770                                 if ($sql3 != "") {
771                                         $sql3 .= "; ";
772                                 }
773
774                                 // Now have a look at the field collations
775                                 // Compare the field structure field by field
776                                 foreach ($structure["fields"] AS $fieldname => $parameters) {
777                                         // Compare the field definition
778                                         $field_definition = ($database[$name]["fields"][$fieldname] ?? '') ?: ['Collation' => ''];
779
780                                         // Define the default collation if not given
781                                         if (!isset($parameters['Collation']) && !empty($field_definition['Collation'])) {
782                                                 $parameters['Collation'] = 'utf8mb4_general_ci';
783                                         } else {
784                                                 $parameters['Collation'] = null;
785                                         }
786
787                                         if ($field_definition['Collation'] != $parameters['Collation']) {
788                                                 $sql2 = self::modifyTableField($fieldname, $parameters);
789                                                 if (($sql3 == "") || (substr($sql3, -2, 2) == "; ")) {
790                                                         $sql3 .= "ALTER" . $ignore . " TABLE `" . $temp_name . "` " . $sql2;
791                                                 } else {
792                                                         $sql3 .= ", " . $sql2;
793                                                 }
794                                         }
795                                 }
796                         }
797
798                         if ($sql3 != "") {
799                                 if (substr($sql3, -2, 2) != "; ") {
800                                         $sql3 .= ";";
801                                 }
802
803                                 $field_list = '';
804                                 if ($is_unique && $ignore == '') {
805                                         foreach ($database[$name]["fields"] AS $fieldname => $parameters) {
806                                                 $field_list .= 'ANY_VALUE(`' . $fieldname . '`),';
807                                         }
808                                         $field_list = rtrim($field_list, ',');
809                                 }
810
811                                 if ($verbose) {
812                                         // Ensure index conversion to unique removes duplicates
813                                         if ($is_unique && ($temp_name != $name)) {
814                                                 if ($ignore != "") {
815                                                         echo "SET session old_alter_table=1;\n";
816                                                 } else {
817                                                         echo "DROP TABLE IF EXISTS `" . $temp_name . "`;\n";
818                                                         echo "CREATE TABLE `" . $temp_name . "` LIKE `" . $name . "`;\n";
819                                                 }
820                                         }
821
822                                         echo $sql3 . "\n";
823
824                                         if ($is_unique && ($temp_name != $name)) {
825                                                 if ($ignore != "") {
826                                                         echo "SET session old_alter_table=0;\n";
827                                                 } else {
828                                                         echo "INSERT INTO `" . $temp_name . "` SELECT " . DBA::anyValueFallback($field_list) . " FROM `" . $name . "`" . $group_by . ";\n";
829                                                         echo "DROP TABLE `" . $name . "`;\n";
830                                                         echo "RENAME TABLE `" . $temp_name . "` TO `" . $name . "`;\n";
831                                                 }
832                                         }
833                                 }
834
835                                 if ($action) {
836                                         if ($in_maintenance_mode) {
837                                                 DI::config()->set('system', 'maintenance_reason', DI::l10n()->t('%s: updating %s table.', DateTimeFormat::utcNow() . ' ' . date('e'), $name));
838                                         }
839
840                                         // Ensure index conversion to unique removes duplicates
841                                         if ($is_unique && ($temp_name != $name)) {
842                                                 if ($ignore != "") {
843                                                         DBA::e("SET session old_alter_table=1;");
844                                                 } else {
845                                                         $r = DBA::e("DROP TABLE IF EXISTS `" . $temp_name . "`;");
846                                                         if (!DBA::isResult($r)) {
847                                                                 $errors .= self::printUpdateError($sql3);
848                                                                 return $errors;
849                                                         }
850
851                                                         $r = DBA::e("CREATE TABLE `" . $temp_name . "` LIKE `" . $name . "`;");
852                                                         if (!DBA::isResult($r)) {
853                                                                 $errors .= self::printUpdateError($sql3);
854                                                                 return $errors;
855                                                         }
856                                                 }
857                                         }
858
859                                         $r = DBA::e($sql3);
860                                         if (!DBA::isResult($r)) {
861                                                 $errors .= self::printUpdateError($sql3);
862                                         }
863                                         if ($is_unique && ($temp_name != $name)) {
864                                                 if ($ignore != "") {
865                                                         DBA::e("SET session old_alter_table=0;");
866                                                 } else {
867                                                         $r = DBA::e("INSERT INTO `" . $temp_name . "` SELECT " . $field_list . " FROM `" . $name . "`" . $group_by . ";");
868                                                         if (!DBA::isResult($r)) {
869                                                                 $errors .= self::printUpdateError($sql3);
870                                                                 return $errors;
871                                                         }
872                                                         $r = DBA::e("DROP TABLE `" . $name . "`;");
873                                                         if (!DBA::isResult($r)) {
874                                                                 $errors .= self::printUpdateError($sql3);
875                                                                 return $errors;
876                                                         }
877                                                         $r = DBA::e("RENAME TABLE `" . $temp_name . "` TO `" . $name . "`;");
878                                                         if (!DBA::isResult($r)) {
879                                                                 $errors .= self::printUpdateError($sql3);
880                                                                 return $errors;
881                                                         }
882                                                 }
883                                         }
884                                 }
885                         }
886                 }
887
888                 View::create(false, $action);
889
890                 self::checkInitialValues();
891
892                 if ($action && !$install) {
893                         if ($errors) {
894                                 DI::config()->set('system', 'dbupdate', self::UPDATE_FAILED);
895                         } else {
896                                 DI::config()->set('system', 'dbupdate', self::UPDATE_SUCCESSFUL);
897                         }
898                 }
899
900                 return $errors;
901         }
902
903         private static function tableStructure($table)
904         {
905                 // This query doesn't seem to be executable as a prepared statement
906                 $indexes = DBA::toArray(DBA::p("SHOW INDEX FROM " . DBA::quoteIdentifier($table)));
907
908                 $fields = DBA::selectToArray(['INFORMATION_SCHEMA' => 'COLUMNS'],
909                         ['COLUMN_NAME', 'COLUMN_TYPE', 'IS_NULLABLE', 'COLUMN_DEFAULT', 'EXTRA',
910                         'COLUMN_KEY', 'COLLATION_NAME', 'COLUMN_COMMENT'],
911                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?",
912                         DBA::databaseName(), $table]);
913
914                 $foreign_keys = DBA::selectToArray(['INFORMATION_SCHEMA' => 'KEY_COLUMN_USAGE'],
915                         ['COLUMN_NAME', 'CONSTRAINT_NAME', 'REFERENCED_TABLE_NAME', 'REFERENCED_COLUMN_NAME'],
916                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? AND `REFERENCED_TABLE_SCHEMA` IS NOT NULL",
917                         DBA::databaseName(), $table]);
918
919                 $table_status = DBA::selectFirst(['INFORMATION_SCHEMA' => 'TABLES'],
920                         ['ENGINE', 'TABLE_COLLATION', 'TABLE_COMMENT'],
921                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?",
922                         DBA::databaseName(), $table]);
923
924                 $fielddata = [];
925                 $indexdata = [];
926                 $foreigndata = [];
927
928                 if (DBA::isResult($foreign_keys)) {
929                         foreach ($foreign_keys as $foreign_key) {
930                                 $parameters = ['foreign' => [$foreign_key['REFERENCED_TABLE_NAME'] => $foreign_key['REFERENCED_COLUMN_NAME']]];
931                                 $constraint = self::getConstraintName($table, $foreign_key['COLUMN_NAME'], $parameters);
932                                 $foreigndata[$constraint] = $foreign_key;
933                         }
934                 }
935
936                 if (DBA::isResult($indexes)) {
937                         foreach ($indexes AS $index) {
938                                 if ($index["Key_name"] != "PRIMARY" && $index["Non_unique"] == "0" && !isset($indexdata[$index["Key_name"]])) {
939                                         $indexdata[$index["Key_name"]] = ["UNIQUE"];
940                                 }
941
942                                 if ($index["Index_type"] == "FULLTEXT" && !isset($indexdata[$index["Key_name"]])) {
943                                         $indexdata[$index["Key_name"]] = ["FULLTEXT"];
944                                 }
945
946                                 $column = $index["Column_name"];
947
948                                 if ($index["Sub_part"] != "") {
949                                         $column .= "(" . $index["Sub_part"] . ")";
950                                 }
951
952                                 $indexdata[$index["Key_name"]][] = $column;
953                         }
954                 }
955
956                 $fielddata = [];
957                 if (DBA::isResult($fields)) {
958                         foreach ($fields AS $field) {
959                                 $search = ['tinyint(1)', 'tinyint(3) unsigned', 'tinyint(4)', 'smallint(5) unsigned', 'smallint(6)', 'mediumint(8) unsigned', 'mediumint(9)', 'bigint(20)', 'int(10) unsigned', 'int(11)'];
960                                 $replace = ['boolean', 'tinyint unsigned', 'tinyint', 'smallint unsigned', 'smallint', 'mediumint unsigned', 'mediumint', 'bigint', 'int unsigned', 'int'];
961                                 $field['COLUMN_TYPE'] = str_replace($search, $replace, $field['COLUMN_TYPE']);
962
963                                 $fielddata[$field['COLUMN_NAME']]['type'] = $field['COLUMN_TYPE'];
964
965                                 if ($field['IS_NULLABLE'] == 'NO') {
966                                         $fielddata[$field['COLUMN_NAME']]['not null'] = true;
967                                 }
968
969                                 if (isset($field['COLUMN_DEFAULT']) && ($field['COLUMN_DEFAULT'] != 'NULL')) {
970                                         $fielddata[$field['COLUMN_NAME']]['default'] = trim($field['COLUMN_DEFAULT'], "'");
971                                 }
972
973                                 if (!empty($field['EXTRA'])) {
974                                         $fielddata[$field['COLUMN_NAME']]['extra'] = $field['EXTRA'];
975                                 }
976
977                                 if ($field['COLUMN_KEY'] == 'PRI') {
978                                         $fielddata[$field['COLUMN_NAME']]['primary'] = true;
979                                 }
980
981                                 $fielddata[$field['COLUMN_NAME']]['Collation'] = $field['COLLATION_NAME'];
982                                 $fielddata[$field['COLUMN_NAME']]['comment'] = $field['COLUMN_COMMENT'];
983                         }
984                 }
985
986                 return ["fields" => $fielddata, "indexes" => $indexdata,
987                         "foreign_keys" => $foreigndata, "table_status" => $table_status];
988         }
989
990         private static function dropIndex($indexname)
991         {
992                 $sql = sprintf("DROP INDEX `%s`", DBA::escape($indexname));
993                 return ($sql);
994         }
995
996         private static function addTableField($fieldname, $parameters)
997         {
998                 $sql = sprintf("ADD `%s` %s", DBA::escape($fieldname), self::FieldCommand($parameters));
999                 return ($sql);
1000         }
1001
1002         private static function modifyTableField($fieldname, $parameters)
1003         {
1004                 $sql = sprintf("MODIFY `%s` %s", DBA::escape($fieldname), self::FieldCommand($parameters, false));
1005                 return ($sql);
1006         }
1007
1008         private static function getConstraintName(string $tablename, string $fieldname, array $parameters)
1009         {
1010                 $foreign_table = array_keys($parameters['foreign'])[0];
1011                 $foreign_field = array_values($parameters['foreign'])[0];
1012
1013                 return $tablename . "-" . $fieldname. "-" . $foreign_table. "-" . $foreign_field;
1014         }
1015
1016         private static function foreignCommand(string $tablename, string $fieldname, array $parameters) {
1017                 $foreign_table = array_keys($parameters['foreign'])[0];
1018                 $foreign_field = array_values($parameters['foreign'])[0];
1019
1020                 $sql = "FOREIGN KEY (`" . $fieldname . "`) REFERENCES `" . $foreign_table . "` (`" . $foreign_field . "`)";
1021
1022                 if (!empty($parameters['foreign']['on update'])) {
1023                         $sql .= " ON UPDATE " . strtoupper($parameters['foreign']['on update']);
1024                 } else {
1025                         $sql .= " ON UPDATE RESTRICT";
1026                 }
1027
1028                 if (!empty($parameters['foreign']['on delete'])) {
1029                         $sql .= " ON DELETE " . strtoupper($parameters['foreign']['on delete']);
1030                 } else {
1031                         $sql .= " ON DELETE CASCADE";
1032                 }
1033
1034                 return $sql;
1035         }
1036
1037         private static function addForeignKey(string $tablename, string $fieldname, array $parameters)
1038         {
1039                 return sprintf("ADD %s", self::foreignCommand($tablename, $fieldname, $parameters));
1040         }
1041
1042         private static function dropForeignKey(string $constraint)
1043         {
1044                 return sprintf("DROP FOREIGN KEY `%s`", $constraint);
1045         }
1046
1047         /**
1048          * Constructs a GROUP BY clause from a UNIQUE index definition.
1049          *
1050          * @param array $fieldnames
1051          * @return string
1052          */
1053         private static function groupBy(array $fieldnames)
1054         {
1055                 if ($fieldnames[0] != "UNIQUE") {
1056                         return "";
1057                 }
1058
1059                 array_shift($fieldnames);
1060
1061                 $names = "";
1062                 foreach ($fieldnames AS $fieldname) {
1063                         if ($names != "") {
1064                                 $names .= ",";
1065                         }
1066
1067                         if (preg_match('|(.+)\((\d+)\)|', $fieldname, $matches)) {
1068                                 $names .= "`" . DBA::escape($matches[1]) . "`";
1069                         } else {
1070                                 $names .= "`" . DBA::escape($fieldname) . "`";
1071                         }
1072                 }
1073
1074                 $sql = sprintf(" GROUP BY %s", $names);
1075                 return $sql;
1076         }
1077
1078         /**
1079          * Renames columns or the primary key of a table
1080          *
1081          * @todo You cannot rename a primary key if "auto increment" is set
1082          *
1083          * @param string $table            Table name
1084          * @param array  $columns          Columns Syntax for Rename: [ $old1 => [ $new1, $type1 ], $old2 => [ $new2, $type2 ], ... ]
1085          *                                 Syntax for Primary Key: [ $col1, $col2, ...]
1086          * @param int    $type             The type of renaming (Default is Column)
1087          *
1088          * @return boolean Was the renaming successful?
1089          * @throws Exception
1090          */
1091         public static function rename($table, $columns, $type = self::RENAME_COLUMN)
1092         {
1093                 if (empty($table) || empty($columns)) {
1094                         return false;
1095                 }
1096
1097                 if (!is_array($columns)) {
1098                         return false;
1099                 }
1100
1101                 $table = DBA::escape($table);
1102
1103                 $sql = "ALTER TABLE `" . $table . "`";
1104                 switch ($type) {
1105                         case self::RENAME_COLUMN:
1106                                 if (!self::existsColumn($table, array_keys($columns))) {
1107                                         return false;
1108                                 }
1109                                 $sql .= implode(',', array_map(
1110                                         function ($to, $from) {
1111                                                 return " CHANGE `" . $from . "` `" . $to[0] . "` " . $to[1];
1112                                         },
1113                                         $columns,
1114                                         array_keys($columns)
1115                                 ));
1116                                 break;
1117                         case self::RENAME_PRIMARY_KEY:
1118                                 if (!self::existsColumn($table, $columns)) {
1119                                         return false;
1120                                 }
1121                                 $sql .= " DROP PRIMARY KEY, ADD PRIMARY KEY(`" . implode('`, `', $columns) . "`)";
1122                                 break;
1123                         default:
1124                                 return false;
1125                 }
1126
1127                 $sql .= ";";
1128
1129                 $stmt = DBA::p($sql);
1130
1131                 if (is_bool($stmt)) {
1132                         $retval = $stmt;
1133                 } else {
1134                         $retval = true;
1135                 }
1136
1137                 DBA::close($stmt);
1138
1139                 return $retval;
1140         }
1141
1142         /**
1143          *    Check if the columns of the table exists
1144          *
1145          * @param string $table   Table name
1146          * @param array  $columns Columns to check ( Syntax: [ $col1, $col2, .. ] )
1147          *
1148          * @return boolean Does the table exist?
1149          * @throws Exception
1150          */
1151         public static function existsColumn($table, $columns = [])
1152         {
1153                 if (empty($table)) {
1154                         return false;
1155                 }
1156
1157                 if (is_null($columns) || empty($columns)) {
1158                         return self::existsTable($table);
1159                 }
1160
1161                 $table = DBA::escape($table);
1162
1163                 foreach ($columns AS $column) {
1164                         $sql = "SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "';";
1165
1166                         $stmt = DBA::p($sql);
1167
1168                         if (is_bool($stmt)) {
1169                                 $retval = $stmt;
1170                         } else {
1171                                 $retval = (DBA::numRows($stmt) > 0);
1172                         }
1173
1174                         DBA::close($stmt);
1175
1176                         if (!$retval) {
1177                                 return false;
1178                         }
1179                 }
1180
1181                 return true;
1182         }
1183
1184         /**
1185          * Check if a foreign key exists for the given table field
1186          *
1187          * @param string $table
1188          * @param string $field
1189          * @return boolean
1190          */
1191         public static function existsForeignKeyForField(string $table, string $field)
1192         {
1193                 return DBA::exists(['INFORMATION_SCHEMA' => 'KEY_COLUMN_USAGE'],
1194                         ["`TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? AND `COLUMN_NAME` = ? AND `REFERENCED_TABLE_SCHEMA` IS NOT NULL",
1195                         DBA::databaseName(), $table, $field]);
1196         }
1197         /**
1198          *    Check if a table exists
1199          *
1200          * @param string|array $table Table name
1201          *
1202          * @return boolean Does the table exist?
1203          * @throws Exception
1204          */
1205         public static function existsTable($table)
1206         {
1207                 if (empty($table)) {
1208                         return false;
1209                 }
1210
1211                 if (is_array($table)) {
1212                         $condition = ['table_schema' => key($table), 'table_name' => current($table)];
1213                 } else {
1214                         $condition = ['table_schema' => DBA::databaseName(), 'table_name' => $table];
1215                 }
1216
1217                 $result = DBA::exists(['information_schema' => 'tables'], $condition);
1218
1219                 return $result;
1220         }
1221
1222         /**
1223          * Returns the columns of a table
1224          *
1225          * @param string $table Table name
1226          *
1227          * @return array An array of the table columns
1228          * @throws Exception
1229          */
1230         public static function getColumns($table)
1231         {
1232                 $stmtColumns = DBA::p("SHOW COLUMNS FROM `" . $table . "`");
1233                 return DBA::toArray($stmtColumns);
1234         }
1235
1236         /**
1237          * Check if initial database values do exist - or create them
1238          */
1239         public static function checkInitialValues(bool $verbose = false)
1240         {
1241                 if (self::existsTable('verb')) {
1242                         if (!DBA::exists('verb', ['id' => 1])) {
1243                                 foreach (Item::ACTIVITIES as $index => $activity) {
1244                                         DBA::insert('verb', ['id' => $index + 1, 'name' => $activity], Database::INSERT_IGNORE);
1245                                 }
1246                                 if ($verbose) {
1247                                         echo "verb: activities added\n";
1248                                 }
1249                         } elseif ($verbose) {
1250                                 echo "verb: activities already added\n";
1251                         }
1252
1253                         if (!DBA::exists('verb', ['id' => 0])) {
1254                                 DBA::insert('verb', ['name' => '']);
1255                                 $lastid = DBA::lastInsertId();
1256                                 if ($lastid != 0) {
1257                                         DBA::update('verb', ['id' => 0], ['id' => $lastid]);
1258                                         if ($verbose) {
1259                                                 echo "Zero verb added\n";
1260                                         }
1261                                 }
1262                         } elseif ($verbose) {
1263                                 echo "Zero verb already added\n";
1264                         }
1265                 } elseif ($verbose) {
1266                         echo "verb: Table not found\n";
1267                 }
1268
1269                 if (self::existsTable('user') && !DBA::exists('user', ['uid' => 0])) {
1270                         $user = [
1271                                 "verified" => true,
1272                                 "page-flags" => User::PAGE_FLAGS_SOAPBOX,
1273                                 "account-type" => User::ACCOUNT_TYPE_RELAY,
1274                         ];
1275                         DBA::insert('user', $user);
1276                         $lastid = DBA::lastInsertId();
1277                         if ($lastid != 0) {
1278                                 DBA::update('user', ['uid' => 0], ['uid' => $lastid]);
1279                                 if ($verbose) {
1280                                         echo "Zero user added\n";
1281                                 }
1282                         }
1283                 } elseif (self::existsTable('user') && $verbose) {
1284                         echo "Zero user already added\n";
1285                 } elseif ($verbose) {
1286                         echo "user: Table not found\n";
1287                 }
1288
1289                 if (self::existsTable('contact') && !DBA::exists('contact', ['id' => 0])) {
1290                         DBA::insert('contact', ['nurl' => '']);
1291                         $lastid = DBA::lastInsertId();
1292                         if ($lastid != 0) {
1293                                 DBA::update('contact', ['id' => 0], ['id' => $lastid]);
1294                                 if ($verbose) {
1295                                         echo "Zero contact added\n";
1296                                 }
1297                         }
1298                 } elseif (self::existsTable('contact') && $verbose) {
1299                         echo "Zero contact already added\n";
1300                 } elseif ($verbose) {
1301                         echo "contact: Table not found\n";
1302                 }
1303
1304                 if (self::existsTable('tag') && !DBA::exists('tag', ['id' => 0])) {
1305                         DBA::insert('tag', ['name' => '']);
1306                         $lastid = DBA::lastInsertId();
1307                         if ($lastid != 0) {
1308                                 DBA::update('tag', ['id' => 0], ['id' => $lastid]);
1309                                 if ($verbose) {
1310                                         echo "Zero tag added\n";
1311                                 }
1312                         }
1313                 } elseif (self::existsTable('tag') && $verbose) {
1314                         echo "Zero tag already added\n";
1315                 } elseif ($verbose) {
1316                         echo "tag: Table not found\n";
1317                 }
1318
1319                 if (self::existsTable('permissionset')) {
1320                         if (!DBA::exists('permissionset', ['id' => 0])) {
1321                                 DBA::insert('permissionset', ['allow_cid' => '', 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => '']);
1322                                 $lastid = DBA::lastInsertId();
1323                                 if ($lastid != 0) {
1324                                         DBA::update('permissionset', ['id' => 0], ['id' => $lastid]);
1325                                         if ($verbose) {
1326                                                 echo "Zero permissionset added\n";
1327                                         }
1328                                 }
1329                         } elseif ($verbose) {
1330                                 echo "Zero permissionset already added\n";
1331                         }
1332                         if (self::existsTable('item') && !self::existsForeignKeyForField('item', 'psid')) {
1333                                 $sets = DBA::p("SELECT `psid`, `item`.`uid`, `item`.`private` FROM `item`
1334                                         LEFT JOIN `permissionset` ON `permissionset`.`id` = `item`.`psid`
1335                                         WHERE `permissionset`.`id` IS NULL AND NOT `psid` IS NULL");
1336                                 while ($set = DBA::fetch($sets)) {
1337                                         if (($set['private'] == Item::PRIVATE) && ($set['uid'] != 0)) {
1338                                                 $owner = User::getOwnerDataById($set['uid']);
1339                                                 if ($owner) {
1340                                                         $permission = '<' . $owner['id'] . '>';
1341                                                 } else {
1342                                                         $permission = '<>';
1343                                                 }
1344                                         } else {
1345                                                 $permission = '';
1346                                         }
1347                                         $fields = ['id' => $set['psid'], 'uid' => $set['uid'], 'allow_cid' => $permission,
1348                                                 'allow_gid' => '', 'deny_cid' => '', 'deny_gid' => ''];
1349                                         DBA::insert('permissionset', $fields);
1350                                 }
1351                                 DBA::close($sets);
1352                         }
1353                 } elseif ($verbose) {
1354                         echo "permissionset: Table not found\n";
1355                 }
1356
1357                 if (!self::existsForeignKeyForField('tokens', 'client_id')) {
1358                         $tokens = DBA::p("SELECT `tokens`.`id` FROM `tokens`
1359                                 LEFT JOIN `clients` ON `clients`.`client_id` = `tokens`.`client_id`
1360                                 WHERE `clients`.`client_id` IS NULL");
1361                         while ($token = DBA::fetch($tokens)) {
1362                                 DBA::delete('tokens', ['id' => $token['id']]);
1363                         }
1364                         DBA::close($tokens);
1365                 }
1366         }
1367
1368         /**
1369          * Checks if a database update is currently running
1370          *
1371          * @return boolean
1372          */
1373         private static function isUpdating()
1374         {
1375                 $isUpdate = false;
1376
1377                 $processes = DBA::select(['information_schema' => 'processlist'], ['info'],
1378                         ['db' => DBA::databaseName(), 'command' => ['Query', 'Execute']]);
1379
1380                 while ($process = DBA::fetch($processes)) {
1381                         $parts = explode(' ', $process['info']);
1382                         if (in_array(strtolower(array_shift($parts)), ['alter', 'create', 'drop', 'rename'])) {
1383                                 $isUpdate = true;
1384                         }
1385                 }
1386
1387                 DBA::close($processes);
1388
1389                 return $isUpdate;
1390         }
1391 }