diff --git a/_doc/install/setting.default.php b/_doc/install/settings.default.php similarity index 100% rename from _doc/install/setting.default.php rename to _doc/install/settings.default.php diff --git a/composer.json b/composer.json index b0f43514..c056d296 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,10 @@ }, "require": { "php": "^7.2|^8", + "ext-gd": "*", + "ext-mbstring": "*", + "ext-mysqli": "*", + "ext-json": "*", "swiftmailer/swiftmailer": "^6.2", "guzzlehttp/guzzle": "~6.0", "imdbphp/imdbphp": "^6.4" diff --git a/include/core.php b/include/core.php index 420e3a76..c2d15b36 100644 --- a/include/core.php +++ b/include/core.php @@ -2,8 +2,12 @@ if(!defined('IN_TRACKER')) { die('Hacking attempt!'); } +if (!file_exists($rootpath . '.env')) { + header('Location: ' . getBaseUrl() . '/install/install.php'); + exit(0); +} error_reporting(E_ALL); -ini_set('display_errors', 1); +ini_set('display_errors', 0); if (!empty($_SERVER['HTTP_X_REQUEST_ID'])) { define('REQUEST_ID', $_SERVER['HTTP_X_REQUEST_ID']); } else { @@ -12,13 +16,8 @@ if (!empty($_SERVER['HTTP_X_REQUEST_ID'])) { define('ROOT_PATH', $rootpath); define('VERSION_NUMBER', '1.6.0'); define('IS_ANNOUNCE', (basename($_SERVER['SCRIPT_FILENAME']) == 'announce.php')); - -require $rootpath . 'include/database/interface_db.php'; -require $rootpath . 'include/database/class_db_mysqli.php'; -require $rootpath . 'include/database/class_db.php'; -require $rootpath . 'include/database/helpers.php'; -require $rootpath . 'include/database/class_exception.php'; - +require $rootpath . 'vendor/autoload.php'; +require $rootpath . 'nexus/Database/helpers.php'; require $rootpath . 'classes/class_advertisement.php'; require $rootpath . 'classes/class_cache_redis.php'; require $rootpath . 'include/config.php'; @@ -52,5 +51,3 @@ define ("UC_SYSOP",15); define ("UC_STAFFLEADER",16); ignore_user_abort(1); @set_time_limit(60); - -require dirname(__DIR__) . '/vendor/autoload.php'; diff --git a/include/functions.php b/include/functions.php index d6a9a0a8..259aabd3 100644 --- a/include/functions.php +++ b/include/functions.php @@ -1752,17 +1752,7 @@ function getExportedValue($input,$t = null) { function dbconn($autoclean = false, $doLogin = true) { - if (DB::getInstance()->isConnected()) { - return; - } - $config = config('database.mysql'); - if (!mysql_connect($config['host'], $config['username'], $config['password'], $config['database'], $config['port'])) - { - die("[" . mysql_errno() . "] dbconn: mysql_connect: " . mysql_error()); - } - mysql_query("SET NAMES UTF8"); - mysql_query("SET collation_connection = 'utf8_general_ci'"); - mysql_query("SET sql_mode=''"); + \Nexus\Database\DB::getInstance()->autoConnect(); if ($doLogin) { userlogin(); diff --git a/include/database/class_db.php b/nexus/Database/DB.php similarity index 78% rename from include/database/class_db.php rename to nexus/Database/DB.php index 69df470c..b606b84d 100644 --- a/include/database/class_db.php +++ b/nexus/Database/DB.php @@ -1,5 +1,7 @@ isConnected) { + if (!$this->isConnected()) { $this->driver->connect($host, $username, $password, $database, $port); $this->isConnected = true; } return true; } + public function autoConnect() + { + if ($this->isConnected()) { + return; + } + $config = config('database.mysql'); + if (!mysql_connect($config['host'], $config['username'], $config['password'], $config['database'], $config['port'])) { + throw new DatabaseException(sprintf("mysql connect error: [%s] %s", mysql_errno(), mysql_error())); + } + mysql_query("SET NAMES UTF8"); + mysql_query("SET collation_connection = 'utf8_general_ci'"); + mysql_query("SET sql_mode=''"); + $this->isConnected = true; + } + public function query(string $sql) { try { + $this->autoConnect(); return $this->driver->query($sql); } catch (\Exception $e) { do_log(sprintf("%s [%s] %s", $e->getMessage(), $sql, $e->getTraceAsString())); - throw new \DatabaseException($sql, $e->getMessage()); + throw new DatabaseException($e->getMessage(), $sql); } } @@ -101,6 +119,7 @@ class DB public function escapeString(string $string) { + $this->autoConnect(); return $this->driver->escapeString($string); } diff --git a/include/database/interface_db.php b/nexus/Database/DBInterface.php similarity index 95% rename from include/database/interface_db.php rename to nexus/Database/DBInterface.php index 1fed7776..5f41457a 100644 --- a/include/database/interface_db.php +++ b/nexus/Database/DBInterface.php @@ -1,4 +1,5 @@ report_mode = MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX; return $this->mysqli = $mysqli; diff --git a/include/database/class_exception.php b/nexus/Database/DatabaseException.php similarity index 58% rename from include/database/class_exception.php rename to nexus/Database/DatabaseException.php index b66eb8e6..2ed21cec 100644 --- a/include/database/class_exception.php +++ b/nexus/Database/DatabaseException.php @@ -1,9 +1,10 @@ currentStep = min(intval($_REQUEST['step'] ?? 1) ?: 1, count($this->steps) + 1); + } + + public function currentStep() + { + return $this->currentStep; + } + + public function getLogFile() + { + return sprintf('%s/nexus_install_%s.log', sys_get_temp_dir(), date('Ymd')); + } + + public function getInsallDirectory() + { + return ROOT_PATH . 'public/install'; + } + + public function doLog($log) + { + $log = sprintf('[%s] [%s] %s%s', date('Y-m-d H:i:s'), $this->currentStep, $log, PHP_EOL); + file_put_contents($this->getLogFile(), $log, FILE_APPEND); + } + + public function listAllTableCreate($sqlFile = '') + { + if (empty($sqlFile)) { + $sqlFile = ROOT_PATH . '_db/dbstructure_v1.6.sql'; + } + $pattern = '/CREATE TABLE `(.*)`.*;/isU'; + $string = file_get_contents($sqlFile); + if ($string === false) { + throw new \RuntimeException("sql file: $sqlFile can not read, make sure it exits and can be read."); + } + $count = preg_match_all($pattern, $string, $matches, PREG_SET_ORDER); + if ($count == 0) { + return []; + } + return array_column($matches, 0, 1); + } + + public function listExistsTable() + { + dbconn(false, false); + $sql = 'show tables'; + $res = sql_query($sql); + $data = []; + while ($row = mysql_fetch_row($res)) { + $data[] = $row[0]; + } + return $data; + } + + public function listShouldAlterTableTableRows() + { + $tables = $this->listExistsTable(); + $data = []; + foreach ($tables as $table) { + $sql = "desc $table"; + $res = sql_query($sql); + while ($row = mysql_fetch_assoc($res)) { + if ($row['Type'] == 'datetime' && $row['Default'] == '0000-00-00 00:00:00') { + $data[$table][] = $row['Field']; + $data[] = [ + 'label' => "$table." . $row['Field'], + 'required' => 'default null', + 'current' => '0000-00-00 00:00:00', + 'result' => 'NO', + ]; + } + } + } + return $data; + } + + public function listRequirementTableRows() + { + $gdInfo = gd_info(); + $tableRows = [ + [ + 'label' => 'PHP version', + 'required' => '>= ' . $this->minimumPhpVersion, + 'current' => PHP_VERSION, + 'result' => $this->yesOrNo(version_compare(PHP_VERSION, $this->minimumPhpVersion, '>=')), + ], + [ + 'label' => 'PHP extension redis', + 'required' => 'optional', + 'current' => extension_loaded('redis'), + 'result' => $this->yesOrNo(extension_loaded('redis')), + ], + [ + 'label' => 'PHP extension mysqli', + 'required' => 'enabled', + 'current' => extension_loaded('mysqli'), + 'result' => $this->yesOrNo(extension_loaded('mysqli')), + ], + [ + 'label' => 'PHP extension mbstring', + 'required' => 'enabled', + 'current' => extension_loaded('mbstring'), + 'result' => $this->yesOrNo(extension_loaded('mbstring')), + ], + [ + 'label' => 'PHP extension gd', + 'required' => 'enabled', + 'current' => extension_loaded('gd'), + 'result' => $this->yesOrNo(extension_loaded('gd')), + ], + [ + 'label' => 'PHP extension gd JPEG Support', + 'required' => 'true', + 'current' => $gdInfo['JPEG Support'], + 'result' => $this->yesOrNo($gdInfo['JPEG Support']), + ], + [ + 'label' => 'PHP extension gd PNG Support', + 'required' => 'true', + 'current' => $gdInfo['PNG Support'], + 'result' => $this->yesOrNo($gdInfo['PNG Support']), + ], + [ + 'label' => 'PHP extension gd GIF Read Support', + 'required' => 'true', + 'current' => $gdInfo['GIF Read Support'], + 'result' => $this->yesOrNo($gdInfo['GIF Read Support']), + ], + ]; + $fails = array_filter($tableRows, function ($value) {return $value['required'] == 'true' && $value['result'] == 'NO';}); + $pass = empty($fails); + + return [ + 'table_rows' => $tableRows, + 'pass' => $pass, + ]; + } + + public function listSettingTableRows() + { + $defaultSettingsFile = ROOT_PATH . '_doc/install/settings.default.php'; + $originalConfigFile = ROOT_PATH . 'config/allconfig.php'; + if (!file_exists($defaultSettingsFile)) { + throw new \RuntimeException("default setting file: $defaultSettingsFile not exists."); + } + if (!file_exists($originalConfigFile)) { + throw new \RuntimeException("original setting file: $originalConfigFile not exists."); + } + $tableRows = [ + [ + 'label' => basename($defaultSettingsFile), + 'required' => 'exists && readable', + 'current' => $defaultSettingsFile, + 'result' => $this->yesOrNo(file_exists($defaultSettingsFile) && is_readable($defaultSettingsFile)), + ], + [ + 'label' => basename($originalConfigFile), + 'required' => 'exists && readable', + 'current' => $originalConfigFile, + 'result' => $this->yesOrNo(file_exists($originalConfigFile) && is_readable($originalConfigFile)), + ], + ]; + $requireDirs = [ + 'main' => ['bitbucket', 'torrent_dir'], + 'attachment' => ['savedirectory', ], + ]; + $symbolicLinks = []; + require $originalConfigFile; + $settings = require $defaultSettingsFile; + foreach ($settings as $prefix => &$group) { + $prefixUpperCase = strtoupper($prefix); + $oldGroupValues = $$prefixUpperCase ?? null; + foreach ($group as $key => &$value) { + //merge original config to default setting + if (isset($oldGroupValues) && isset($oldGroupValues[$key])) { + $value = $oldGroupValues[$key];; + } + if (isset($requireDirs[$prefix]) && in_array($key, $requireDirs[$prefix])) { + $dir = getFullDirectory($value); + $tableRows[] = [ + 'label' => "{$prefix}.{$key}", + 'required' => 'exists && readable', + 'current' => $dir, + 'result' => $this->yesOrNo(is_dir($dir) && is_readable($dir)), + ]; + $symbolicLinks[] = $dir; + } + } + } + $fails = array_filter($tableRows, function ($value) {return $value['required'] == 'true' && $value['result'] == 'NO';}); + $pass = empty($fails); + return [ + 'table_rows' => $tableRows, + 'symbolic_links' => $symbolicLinks, + 'settings' => $settings, + 'pass' => $pass, + ]; + } + + public function nextStep() + { + $this->gotoStep($this->currentStep + 1); + } + + public function gotoStep($step) + { + header('Location: ' . getBaseUrl() . "?step=$step"); + exit(0); + } + + public function maxStep() + { + return count($this->steps); + } + + public function yesOrNo($condition) { + if ($condition) { + return 'YES'; + } + return 'NO'; + } + + public function renderTable($header, $data) + { + $table = '
'; + $table .= '
'; + $table .= '
'; + foreach ($header as $value) { + $table .= '
' . $value . '
'; + } + $table .= '
'; + foreach ($data as $value) { + $table .= '
'; + $table .= '
' . $value['label'] . '
'; + $table .= '
' . $value['required'] . '
'; + $table .= '
' . $value['current'] . '
'; + $table .= '
' . $value['result'] . '
'; + $table .= '
'; + } + $table .= '
'; + $table .= '
'; + + return $table; + + } + + public function renderForm($formControls, $formWidth = '1/2', $labelWidth = '1/3', $valueWidth = '2/3') + { + $form = '
'; + foreach ($formControls as $value) { + $form .= '
'; + $form .= sprintf('
%s
', $labelWidth, $value['label']); + $form .= sprintf( + '
', + $valueWidth, $value['name'], $value['value'] ?? '' + ); + $form .= '
'; + } + $form .= '
'; + return $form; + } + + public function renderSteps() + { + $steps = '
'; + $currentStep = $this->currentStep(); + foreach ($this->steps as $key => $value) { + $steps .= sprintf('
', $currentStep > $key + 1 ? 'done' : ($currentStep < $key + 1 ? 'none' : '')); + $steps .= sprintf('
第%s步
', $key + 1); + $steps .= sprintf('
%s
', $value); + $steps .= '
'; + } + $steps .= '
'; + return $steps; + } + + public function listEnvFormControls() + { + $envExampleFile = ROOT_PATH . ".env.example"; + $envExampleData = readEnvFile($envExampleFile); + $envFile = ROOT_PATH . '.env'; + $envData = []; + if (file_exists($envFile) && is_readable($envFile)) { + //already exists, read it ,and merge + $envData = readEnvFile($envFile); + } + $mergeData = array_merge($envExampleData, $envData); + $formControls = []; + foreach ($mergeData as $key => $value) { + if (isset($_POST[$key])) { + $value = $_POST[$key]; + } + $formControls[] = [ + 'label' => $key, + 'name' => $key, + 'value' => $value, + ]; + } + + return $formControls; + } + + public function createAdministrator($username, $email, $password, $confirmPassword) + { + if (!validusername($username)) { + throw new \InvalidArgumentException("Innvalid username: $username"); + } + $email = htmlspecialchars(trim($email)); + $email = safe_email($email); + if (!check_email($email)) { + throw new \InvalidArgumentException("Innvalid email: $email"); + } + $res = sql_query("SELECT id FROM users WHERE email=" . sqlesc($email)); + $arr = mysql_fetch_row($res); + if ($arr) { + throw new \InvalidArgumentException("The email address: $email is already in use"); + } + if (mb_strlen($password) < 6 || mb_strlen($password) > 40) { + throw new \InvalidArgumentException("Innvalid password: $password, it should be more than 6 character and less than 40 character"); + } + if ($password != $confirmPassword) { + throw new \InvalidArgumentException("confirmPassword: $confirmPassword != password"); + } + $setting = get_setting('main'); + $secret = mksecret(); + $passhash = md5($secret . $password . $secret); + $insert = [ + 'username' => $username, + 'passhash' => $passhash, + 'secret' => $secret, + 'email' => $email, + 'stylesheet' => $setting['defstylesheet'], + 'class' => 16, + 'status' => 'confirmed', + 'added' => date('Y-m-d H:i:s'), + ]; + $this->doLog("insert user: " . json_encode($insert)); + return DB::insert('users', $insert); + } + + + +} \ No newline at end of file diff --git a/public/install/install.php b/public/install/install.php new file mode 100644 index 00000000..2e65fcb1 --- /dev/null +++ b/public/install/install.php @@ -0,0 +1,311 @@ +currentStep(); +$maxStep = $install->maxStep(); + +//step 1 +if ($currentStep == 1) { + $requirements = $install->listRequirementTableRows(); + $pass = $requirements['pass']; + if ($isPost) { + $install->nextStep(); + } +} + +if ($currentStep == 2) { + $envExampleFile = "$rootpath.env.example"; + $envExampleData = readEnvFile($envExampleFile); + $envFormControls = $install->listEnvFormControls(); + $newData = array_column($envFormControls, 'value', 'name'); + while ($isPost) { +// $envFile = "$rootpath.env." . time(); +// $newData = $formData = []; +// if (file_exists($envFile) && is_readable($envFile)) { +// //already exists, read it ,and merge post data +// $newData = readEnvFile($envFile); +// } +// foreach ($envExampleData as $key => $value) { +// $postValue = trim($_POST[$key] ?? ''); +// if ($postValue) { +// $value = $postValue; +// } +// $newData[$key] = $value; +// $envExampleData[] = [ +// 'label' => $key, +// 'name' => $key, +// 'value' => $value, +// ]; +// } +// unset($key); + //check + try { + $connectMysql = mysql_connect($newData['MYSQL_HOST'], $newData['MYSQL_USERNAME'], $newData['MYSQL_PASSWORD'], $newData['MYSQL_DATABASE'], $newData['MYSQL_PORT']); + } catch (\Exception $e) { + $_SESSION['error'] = "Mysql: " . $e->getMessage(); + break; + } + if (extension_loaded('redis') && !empty($newData['REDIS_HOST'])) { + try { + $redis = new Redis(); + $redis->connect($newData['REDIS_HOST'], $newData['REDIS_PORT'] ?: 6379); + } catch (\Exception $e) { + $_SESSION['error'] = "Redis: " . $e->getMessage(); + break; + } + if (!ctype_digit($newData['REDIS_DATABASE']) || $newData['REDIS_DATABASE'] < 0 || $newData['REDIS_DATABASE'] > 15) { + $_SESSION['error'] = "invalid REDIS_DATABASE"; + break; + } + } + +// $content = ""; +// foreach ($newData as $key => $value) { +// $content .= "{$key}={$value}\n"; +// } +// $fp = @fopen($envFile, 'w'); +// if ($fp === false) { +// $msg = "文件无法打开, 确保 PHP 有权限在根目录创建文件"; +// $msg .= "\n也可以复制以下内容保存到 $envFile 中"; +// $_SESSION['error'] = $msg; +// $_SESSION['copy'] = $content; +// } +// fwrite($fp, $content); +// fclose($fp); + $install->nextStep(); + break; + } + + $tableRows = [ + [ + 'label' => '.env.example', + 'required' => 'exists && readable', + 'current' => $envExampleFile, + 'result' => $install->yesOrNo(file_exists($envExampleFile) && is_readable($envExampleFile)), + ], + ]; + $fails = array_filter($tableRows, function ($value) {return $value['result'] == 'NO';}); + $pass = empty($fails); +} + +if ($currentStep == 3) { + $pass = true; + $existsTable = $install->listExistsTable(); + $tableCreate = $install->listAllTableCreate(); + $shouldCreateTable = []; + foreach ($tableCreate as $table => $sql) { + if (in_array($table, $existsTable)) { + continue; + } + $shouldCreateTable[$table] = $sql; + } + + while (true) { +// $sqlFile = $rootpath . '_db/dbstructure_v1.6.sql'; +// try { +// $tableCreate = listAllTableCreate($sqlFile); +// } catch (\Exception $e) { +// $_SESSION['error'] = $e->getMessage(); +// break; +// } +// if (empty($tableCreate)) { +// $_SESSION['error'] = "no table create, make sure sql file is correct"; +// break; +// } + + if ($isPost) { + try { + foreach ($shouldCreateTable as $table => $sql) { + sql_query($sql); + } + } catch (\Exception $e) { + $_SESSION['error'] = $e->getMessage(); + break; + } + $install->nextStep(); + } + + break; + } +} + +//if ($currentStep == 4) { +// $pass = true; +// while (true) { +// $shouldAlterTable = listShouldAlterTable(); +// if ($isPost) { +// if (!empty($shouldAlterTable)) { +// try { +// sql_query('SET sql_mode=(SELECT REPLACE(@@sql_mode,"NO_ZERO_DATE", ""));'); +// foreach ($shouldAlterTable as $table => $fields) { +// $sqlAlter = "alter table $table"; +// $sqlUpdate = "update $table"; +// $updateWhere = []; +// foreach ($fields as $field) { +// $sqlAlter .= " modify $field datetime default null,"; +// $sqlUpdate .= " set $field = null,"; +// $updateWhere[] = "$field = '0000-00-00 00:00:00'"; +// } +// $sqlAlter = rtrim($sqlAlter, ','); +// $sqlUpdate = rtrim($sqlUpdate, ',') . " where " . implode(' or ', $updateWhere); +// sql_query($sqlUpdate); +// sql_query($sqlAlter); +// } +// } catch (\Exception $e) { +// $_SESSION['error'] = $e->getMessage(); +// break; +// } +// } +// goStep($currentStep + 1); +// } +// break; +// } +//} + +if ($currentStep == 4) { + $settingTableRows = $install->listSettingTableRows(); + $settings = $settingTableRows['settings']; + $symbolicLinks = $settingTableRows['symbolic_links']; + $tableRows = $settingTableRows['table_rows']; + $pass = $settingTableRows['pass']; + while (true) { + try { + foreach ($settings as $prefix => &$group) { + if ($isPost) { + $install->doLog("[SAVE] prefix: $prefix, nameAndValues: " . json_encode($group)); + foreach ($symbolicLinks as $path) { + $linkName = ROOT_PATH . 'public/' . basename($path); + if (is_dir($linkName)) { + $install->doLog("path: $linkName already exits, skip create symbolic link $linkName -> $path"); + continue; + } + //@todo +// $linkResult = symlink($path, $linkName); +// if ($linkResult === false) { +// throw new \Exception("can't not make symbolic link: $linkName -> $path"); +// } + } + //@todo +// saveSetting($prefix, $group); + } + } + } catch (\Exception $e) { + $_SESSION['error'] = $e->getMessage(); + break; + } + $isPost && $install->nextStep(); + break; + } +} + +if ($currentStep == 5) { + if ($isPost) { + try { + $install->createAdministrator($_POST['username'], $_POST['email'], $_POST['password'], $_POST['confirm_password']); + $install->nextStep(); + } catch (\Exception $exception) { + $_SESSION['error'] = $exception->getMessage(); + } + } + $pass = true; + $userFormControls = [ + ['label' => '用户名', 'name' => 'username', 'value' => $_POST['username'] ?? ''], + ['label' => '邮箱', 'name' => 'email', 'value' => $_POST['email'] ?? ''], + ['label' => '密码', 'name' => 'password', 'value' => $_POST['password'] ?? ''], + ['label' => '重复密码', 'name' => 'confirm_password', 'value' => $_POST['confirm_password'] ?? ''], + ]; +} +?> + + + + + + + + + Update NexusPHP | step <?php echo $currentStep?> + + + +
+ renderSteps()?> +
+
+ + '; + $header = ['项目', '要求', '当前', '结果']; + if ($currentStep == 1) { + echo $install->renderTable($header, $requirements['table_rows']); + } elseif ($currentStep == 2) { + echo $install->renderTable($header, $tableRows); + echo '
若 Redis 不启用,相关项目留空
'; + echo $install->renderForm($envFormControls); + + } elseif ($currentStep == 3) { + echo '

需要新建以下数据表

'; + if (empty($shouldCreateTable)) { + echo '
恭喜,需要的表均已创建!
'; + } else { + echo sprintf('
%s
', implode(', ', array_keys($shouldCreateTable))); + } + } elseif ($currentStep == 4) { + echo $install->renderTable($header, $tableRows); + echo '
'; + echo sprintf('这一步会把 %s 的数据合并到 %s, 然后插入数据库中。', $tableRows[1]['label'], $tableRows[0]['label']); + echo '
'; + } elseif ($currentStep == 5) { + echo $install->renderForm($userFormControls, '1/3', '1/4', '3/4'); + } elseif ($currentStep > $maxStep) { + echo '
恭喜,一切就绪!
'; + echo '
有问题可查阅安装日志:' . $install->getLogFile() . '
'; + echo '
为安全起见,请删除以下目录
'; + echo '
' . $install->getInsallDirectory() . '
'; + } + echo'
'; + + if (!empty($_SESSION['error'])) { + echo sprintf('
Error: %s
', nl2br($_SESSION['error'])); + unset($_SESSION['error']); + } + if (!empty($_SESSION['copy'])) { + echo sprintf('
', $_SESSION['copy']); + unset($_SESSION['copy']); + } + ?> +
+ + + + + 回首页 + +
+ +
+ + + + \ No newline at end of file