diff options
Diffstat (limited to 'install.php')
-rw-r--r-- | install.php | 178 |
1 files changed, 132 insertions, 46 deletions
diff --git a/install.php b/install.php index e566b5a4..6cbbe91f 100644 --- a/install.php +++ b/install.php @@ -19,6 +19,13 @@ * they might depend on some tables that do not exist yet. ;) */ +use JetBrains\PhpStorm\NoReturn; + +if (PHP_INT_SIZE < 8) { + echo "32bit platforms no longer supported\n"; + exit(1); +} + /** * Report back the update status to the browser/client and terminate execution. * This has to be called by an update module at some point to signal the result @@ -27,7 +34,8 @@ * @param string $status one of the UPDATE_* status codes * @param string $message Human readable description of the status (optional) */ -function finalResponse($status, $message = '') +#[NoReturn] +function finalResponse(string $status, string $message = '') { if (!DIRECT_MODE && AJAX) { echo json_encode(array('status' => $status, 'message' => $message)); @@ -41,65 +49,103 @@ function finalResponse($status, $message = '') define('UPDATE_DONE', 'UPDATE_DONE'); // Process completed successfully. This is a success return code. define('UPDATE_NOOP', 'UPDATE_NOOP'); // Nothing had to be done, everything is up to date. This is also a success code. define('UPDATE_RETRY', 'UPDATE_RETRY'); // Install/update process failed, but should be retried later. -define('UPDATE_FAILED', 'UPDATE_FAILED'); // Fatal error occured, retry will not resolve the issue. +define('UPDATE_FAILED', 'UPDATE_FAILED'); // Fatal error occurred, retry will not resolve the issue. + +/** + * Take the return value of a Database::exec() call and emit failure + * if it's false. + */ +function handleUpdateResult($res) +{ + if ($res !== false) + return; + finalResponse(UPDATE_FAILED, Database::lastError()); +} /* * Helper functions for dealing with the database */ -function tableHasColumn($table, $column) +function tableHasColumn(string $table, string $column): bool +{ + return tableColumnType($table, $column) !== false; +} + +/** + * Get type of column, as reported by DESCRIBE <table>; + */ +function tableColumnType(string $table, string $column) +{ + return tableGetDescribeColumn($table, $column, 'Type'); +} + +function tableColumnKeyType($table, $column) +{ + return tableGetDescribeColumn($table, $column, 'Key'); +} + +/** + * For internal use + * @param string|string[] $column + * @return string|false + */ +function tableGetDescribeColumn(string $table, $column, string $what) { $table = preg_replace('/\W/', '', $table); $res = Database::simpleQuery("DESCRIBE `$table`", array(), true); if ($res !== false) { - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if ((is_array($column) && in_array($row['Field'], $column)) || (is_string($column) && $row['Field'] === $column)) - return true; + return $row[$what]; } } return false; } -function tableGetIndex($table, $index) +/** + * Return name of index that spans all the columns given, in the same order. + * Returns false if not found + * + * @param string[] $columns + * @return false|string + */ +function tableGetIndex(string $table, array $columns) { $table = preg_replace('/\W/', '', $table); - if (!is_array($index)) { - $index = [$index]; - } $res = Database::simpleQuery("SHOW INDEX FROM `$table`", array(), true); if ($res !== false) { $matches = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $i = $row['Seq_in_index'] - 1; - if (isset($index[$i]) && $index[$i] === $row['Column_name']) { + if (isset($columns[$i]) && $columns[$i] === $row['Column_name']) { if (!isset($matches[$row['Key_name']])) { $matches[$row['Key_name']] = 0; } $matches[$row['Key_name']]++; } } - } - foreach ($matches as $key => $m) { - if ($m === count($index)) - return $key; + foreach ($matches as $key => $m) { + if ($m === count($columns)) + return $key; + } } return false; } -function tableDropColumn($table, $column) +function tableDropColumn(string $table, string $column): void { $table = preg_replace('/\W/', '', $table); $column = preg_replace('/\W/', '', $column); $res = Database::simpleQuery("DESCRIBE `$table`", array(), true); if ($res !== false) { - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if ((is_array($column) && in_array($row['Field'], $column)) || (is_string($column) && $row['Field'] === $column)) Database::exec("ALTER TABLE `$table` DROP `{$row['Field']}`"); } } } -function tableExists($table) +function tableExists(string $table): bool { $res = Database::simpleQuery("SHOW TABLES", array(), true); while ($row = $res->fetch(PDO::FETCH_NUM)) { @@ -109,9 +155,9 @@ function tableExists($table) return false; } -function tableRename($old, $new) { - $res = Database::simpleQuery("RENAME TABLE $old TO $new", []); - return $res; +function tableRename(string $old, string $new): bool +{ + return Database::simpleQuery("RENAME TABLE $old TO $new", []) !== false; } @@ -124,7 +170,7 @@ function tableRename($old, $new) { * @param string $refColumn referenced column * @return false|string[] false == doesn't exist, assoc array otherwise */ -function tableGetConstraints($table, $column, $refTable, $refColumn) +function tableGetConstraints(string $table, string $column, string $refTable, string $refColumn) { $db = 'openslx'; if (defined('CONFIG_SQL_DB')) { @@ -157,21 +203,22 @@ function tableGetConstraints($table, $column, $refTable, $refColumn) * @param string $actions "ON xxx ON yyy" string * @return string UPDATE_* result code */ -function tableAddConstraint($table, $column, $refTable, $refColumn, $actions, $ignoreError = false, $name = '') +function tableAddConstraint(string $table, string $column, string $refTable, string $refColumn, string $actions, + bool $ignoreError = false, $name = ''): string { $test = tableExists($refTable) && tableHasColumn($refTable, $refColumn); if ($test === false) { - // Most likely, destination table does not exist yet or isn't up to date + // Most likely, destination table does not exist yet or isn't up-to-date return UPDATE_RETRY; } // TODO: Refactor function, make this two args $update = 'RESTRICT'; $delete = 'RESTRICT'; - if (preg_match('/on\s+update\s+(RESTRICT|SET\s+NULL|CASCADE)/ims', $actions, $out)) { - $update = preg_replace('/\s+/ms', ' ', strtoupper($out[1])); + if (preg_match('/on\s+update\s+(RESTRICT|SET\s+NULL|CASCADE)/im', $actions, $out)) { + $update = preg_replace('/\s+/m', ' ', strtoupper($out[1])); } - if (preg_match('/on\s+delete\s+(RESTRICT|SET\s+NULL|CASCADE)/ims', $actions, $out)) { - $delete = preg_replace('/\s+/ms', ' ', strtoupper($out[1])); + if (preg_match('/on\s+delete\s+(RESTRICT|SET\s+NULL|CASCADE)/im', $actions, $out)) { + $delete = preg_replace('/\s+/m', ' ', strtoupper($out[1])); } $test = tableGetConstraints($table, $column, $refTable, $refColumn); if ($test !== false) { @@ -180,8 +227,10 @@ function tableAddConstraint($table, $column, $refTable, $refColumn, $actions, $i // Yep, nothing more to do here return UPDATE_NOOP; } + error_log("$table, $column, $refTable, $refColumn, $actions"); + error_log("Have: {$test['UPDATE_RULE']} want: $update && Have: {$test['DELETE_RULE']} want: $delete"); // Kill the old one - $ret = tableDeleteConstraint($table, $test['CONSTRAINT_NAME']); + tableDeleteConstraint($table, $test['CONSTRAINT_NAME']); } if ($delete === 'CASCADE') { // Deletes are cascaded, so make sure first that all rows get purged that would @@ -206,9 +255,8 @@ function tableAddConstraint($table, $column, $refTable, $refColumn, $actions, $i if ($ret === false) { if ($ignoreError) { return UPDATE_FAILED; - } else { - finalResponse(UPDATE_FAILED, "Cannot add constraint $table.$column -> $refTable.$refColumn: " . Database::lastError()); } + finalResponse(UPDATE_FAILED, "Cannot add constraint $table.$column -> $refTable.$refColumn: " . Database::lastError()); } return UPDATE_DONE; } @@ -220,17 +268,17 @@ function tableAddConstraint($table, $column, $refTable, $refColumn, $actions, $i * @param string $constraint constraint name * @return bool success indicator */ -function tableDeleteConstraint($table, $constraint) +function tableDeleteConstraint(string $table, string $constraint): bool { return Database::exec("ALTER TABLE `$table` DROP FOREIGN KEY `$constraint`") !== false; } -function tableCreate($table, $structure, $fatalOnError = true) +function tableCreate(string $table, string $structure, bool $fatalOnError = true): string { if (tableExists($table)) { return UPDATE_NOOP; } - $ret = Database::exec("CREATE TABLE IF NOT EXISTS `{$table}` ( {$structure} ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + $ret = Database::exec("CREATE TABLE IF NOT EXISTS `{$table}` ( {$structure} ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); if ($ret !== false) { return UPDATE_DONE; } @@ -240,7 +288,8 @@ function tableCreate($table, $structure, $fatalOnError = true) return UPDATE_FAILED; } -function responseFromArray($array) +#[NoReturn] +function responseFromArray(array $array) { if (in_array(UPDATE_FAILED, $array)) { finalResponse(UPDATE_FAILED, 'Update failed!'); @@ -253,7 +302,6 @@ function responseFromArray($array) } finalResponse(UPDATE_NOOP, 'Everything already up to date'); - } /* @@ -296,23 +344,60 @@ if (!Database::init(true)) { // Good to go so far -/** - * @param \Module $module - * @return bool - */ -function hasUpdateScript($module) +function hasUpdateScript(Module $module): bool { return is_readable($module->getDir() . '/install.inc.php'); } -/** - * @param Module $module - */ -function runUpdateScript($module) +function runUpdateScript(Module $module): void { require_once $module->getDir() . '/install.inc.php'; } +// Update collation/encoding etc +$charsetUpdate = ''; +$COLLATION = 'utf8mb4_unicode_520_ci'; +$res = Database::queryFirst("SELECT @@character_set_database, @@collation_database"); +if ($res['@@character_set_database'] !== 'utf8mb4' || $res['@@collation_database'] !== $COLLATION) { + if (!preg_match('/dbname=(\w+)/', CONFIG_SQL_DSN, $out)) { + $charsetUpdate = 'Cannot update charset: DB Name unknown'; + } else { + $db = $out[1]; + $columns = Database::simpleQuery("SELECT + TABLE_NAME, COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE, EXTRA, COLUMN_COMMENT + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = :db AND COLLATION_NAME LIKE 'utf8%' AND COLLATION_NAME <> :collation", + ['db' => $db, 'collation' => $COLLATION]); + $idx = 0; + foreach ($columns as $c) { + $idx++; + $args = []; + $str = $c['COLUMN_TYPE'] . ' CHARACTER SET utf8mb4 ' . $c['EXTRA']; + if ($c['IS_NULLABLE'] === 'NO') { + $str .= ' NOT NULL'; + } + if (!($c['IS_NULLABLE'] === 'NO' && $c['COLUMN_DEFAULT'] === null)) { + $str .= " DEFAULT :def_$idx"; + $args["def_$idx"] = $c['COLUMN_DEFAULT']; + } + if (!empty($c['COLUMN_COMMENT'])) { + $str .= ' COMMENT :comment'; + $args['comment'] = $c['COLUMN_COMMENT']; + } + $str .= ' COLLATE ' . $COLLATION; + $query = "ALTER TABLE `{$c['TABLE_NAME']}` MODIFY `{$c['COLUMN_NAME']}` $str"; + if (Database::exec($query, $args) === false) { + $charsetUpdate .= "\n\n--------------------------\n" . + "+++ {$c['TABLE_NAME']}.{$c['COLUMN_NAME']} failed: " . Database::lastError(); + $charsetUpdate .= "\n$query"; + } + } + if (empty($charsetUpdate) && Database::exec("ALTER DATABASE `$db` CHARACTER SET utf8mb4 COLLATE $COLLATION") === false) { + $charsetUpdate .= "\nCannot update database charset or collation: " . Database::lastError(); + } + } +} // End utf8 stuff + // Build dependency tree Module::init(); $modules = Module::getEnabled(); @@ -379,7 +464,7 @@ if (DIRECT_MODE) { <title>Install/Update SLXadmin</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <style type="text/css"> + <style> body, html { color: #000; background: #fff; @@ -396,6 +481,7 @@ if (DIRECT_MODE) { </style> </head> <body> + <pre>$charsetUpdate</pre> <h1>Modules</h1> <button onclick="slxRunInstall()" class="install-btn">Install/Upgrade</button> <br> |