Files
nexusphp/nexus/Install/Update.php
2025-10-31 20:07:58 +07:00

603 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Nexus\Install;
use App\Models\Attendance;
use App\Models\BonusLogs;
use App\Models\Category;
use App\Models\Exam;
use App\Models\ExamUser;
use App\Models\HitAndRun;
use App\Models\Icon;
use App\Models\Language;
use App\Models\SearchBox;
use App\Models\Setting;
use App\Models\Tag;
use App\Models\Torrent;
use App\Models\TorrentTag;
use App\Models\TrackerUrl;
use App\Models\User;
use App\Models\UserBanLog;
use App\Repositories\AttendanceRepository;
use App\Repositories\BonusRepository;
use App\Repositories\ExamRepository;
use App\Repositories\SearchBoxRepository;
use App\Repositories\TagRepository;
use App\Repositories\TokenRepository;
use App\Repositories\ToolRepository;
use App\Repositories\TorrentRepository;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Nexus\Database\NexusDB;
class Update extends Install
{
protected $steps = ['Env check', 'Get files', 'Update .env', 'Perform updates'];
protected string $lockFile = 'update.lock';
public function getLogFile()
{
return getLogFile("update");
}
public function getUpdateDirectory()
{
return ROOT_PATH . 'public/update';
}
public function listTableFieldsFromCreateTable($createTableSql)
{
$arr = preg_split("/[\r\n]+/", $createTableSql);
$result = [];
foreach ($arr as $value) {
$value = trim($value);
if (substr($value, 0, 1) != '`') {
continue;
}
$pos = strpos($value, '`', 1);
$field = substr($value, 1, $pos - 1);
$result[$field] = rtrim($value, ',');
}
return $result;
}
public function listTableFieldsFromDb($table)
{
$sql = "desc $table";
$res = sql_query($sql);
$data = [];
while ($row = mysql_fetch_assoc($res)) {
$data[$row['Field']] = $row;
}
return $data;
}
private function addSetting($name, $value)
{
$attributes = [
'name' => $name,
];
$now = Carbon::now()->toDateTimeString();
$values = [
'value' => $value,
'created_at' => $now,
'updated_at' => $now,
];
return Setting::query()->firstOrCreate($attributes, $values);
}
public function runExtraQueries()
{
$toolRep = new ToolRepository();
$redis = NExusDB::redis();
/**
* @since 1.7.13
*/
foreach (['adminpanel', 'modpanel', 'sysoppanel'] as $table) {
$columnInfo = NexusDB::getMysqlColumnInfo($table, 'id');
if ($columnInfo['DATA_TYPE'] == 'tinyint' || empty($columnInfo['EXTRA']) || $columnInfo['EXTRA'] != 'auto_increment') {
sql_query("alter table $table modify id int(11) unsigned not null AUTO_INCREMENT");
}
}
//custom field menu
$url = 'fields.php';
$table = 'adminpanel';
$count = get_row_count($table, "where url = " . sqlesc($url));
if ($count == 0) {
$insert = [
'name' => 'Custom Field Manage',
'url' => $url,
'info' => 'Manage custom fields',
];
$id = NexusDB::insert($table, $insert);
$this->doLog("[ADD CUSTOM FIELD MENU] insert: " . json_encode($insert) . " to table: $table, id: $id");
}
//since beta8
if (WITH_LARAVEL && !NexusDB::hasColumn('categories', 'icon_id')) {
$this->doLog('[INIT CATEGORY ICON_ID]');
$this->runMigrate('database/migrations/2022_03_08_040415_add_icon_id_to_categories_table.php');
$icon = Icon::query()->orderBy('id', 'asc')->first();
if ($icon) {
Category::query()->where('icon_id', 0)->update(['icon_id' => $icon->id]);
}
}
//fix base url, since beta8
if (WITH_LARAVEL && NexusDB::hasTable('settings')) {
$settingBasic = get_setting('basic');
if (isset($settingBasic['BASEURL']) && Str::startsWith($settingBasic['BASEURL'], 'localhost')) {
$this->doLog('[RESET CONFIG basic.BASEURL]');
Setting::query()->where('name', 'basic.BASEURL')->update(['value' => '']);
}
if (isset($settingBasic['announce_url']) && Str::startsWith($settingBasic['announce_url'], 'localhost')) {
$this->doLog('[RESET CONFIG basic.announce_url]');
Setting::query()->where('name', 'basic.announce_url')->update(['value' => '']);
}
}
//torrent support sticky second level
if (WITH_LARAVEL) {
$columnInfo = NexusDB::getMysqlColumnInfo('torrents', 'pos_state');
$this->doLog("[TORRENT POS_STATE], column info: " . json_encode($columnInfo));
if ($columnInfo['DATA_TYPE'] == 'enum') {
$sql = "alter table torrents modify `pos_state` varchar(32) NOT NULL DEFAULT 'normal'";
$this->doLog("[ALTER TORRENT POS_STATE TYPE TO VARCHAR], $sql");
sql_query($sql);
}
}
/**
* @since 1.6.0-beta9
*
* attendance change, do migrate
*/
if (WITH_LARAVEL) {
if (!NexusDB::hasTable('attendance')) {
//no table yet, no need to migrate
$this->runMigrate('database/migrations/2021_06_08_113437_create_attendance_table.php');
}
if (!NexusDB::hasColumn('attendance', 'total_days')) {
$this->runMigrate('database/migrations/2021_06_13_215440_add_total_days_to_attendance_table.php');
$attendanceRep = new AttendanceRepository();
$count = $attendanceRep->migrateAttendance();
$this->doLog("[MIGRATE_ATTENDANCE] $count");
}
}
/**
* @since 1.6.0-beta13
*
* add seed points to user
*/
if (WITH_LARAVEL && !NexusDB::hasColumn('users', 'seed_points')) {
$this->runMigrate('database/migrations/2021_06_24_013107_add_seed_points_to_users_table.php');
//Don't do this, initial seed points = 0;
// $result = $this->initSeedPoints();
$this->doLog("[INIT SEED POINTS]");
}
/**
* @since 1.6.0-beta14
*
* add id to agent_allowed_exception
*/
if (WITH_LARAVEL && !NexusDB::hasColumn('agent_allowed_exception', 'id')) {
$this->runMigrate('database/migrations/2022_02_25_021356_add_id_to_agent_allowed_exception_table.php');
$this->doLog("[ADD_ID_TO_AGENT_ALLOWED_EXCEPTION]");
}
/**
* @since 1.6.0
*
* init tag
*/
if (WITH_LARAVEL && !NexusDB::hasTable('tags')) {
$this->runMigrate('database/migrations/2022_03_07_012545_create_tags_table.php');
$this->initTag();
$this->doLog("[INIT_TAG]");
}
/**
* @since 1.6.3
*
* add usersearch.php and unco.php
*/
$menus = [
['name' => 'Search user', 'url' => 'usersearch.php', 'info' => 'Search user'],
['name' => 'Confirm user', 'url' => 'unco.php', 'info' => 'Confirm user to complete registration'],
];
$table = 'modpanel';
foreach ($menus as $menu) {
$count = get_row_count($table, "where url = " . sqlesc($menu['url']));
if ($count == 0) {
$id = NexusDB::insert($table, $menu);
$this->doLog("[ADD MENU] insert: " . json_encode($menu) . " to table: $table, id: $id");
}
}
/**
* @since 1.7.0
*
* add attendance_card to users
*/
if (WITH_LARAVEL && !NexusDB::hasColumn('users', 'attendance_card')) {
$this->runMigrate('database/migrations/2022_04_02_163930_create_attendance_logs_table.php');
$this->runMigrate('database/migrations/2022_04_03_041642_add_attendance_card_to_users_table.php');
$rep = new AttendanceRepository();
$count = $rep->migrateAttendanceLogs();
$this->doLog("[ADD_ATTENDANCE_CARD_TO_USERS], migrateAttendanceLogs: $count");
}
/**
* @since 1.7.12
*/
$menus = [
['name' => 'Add Bonus/Attend card/Invite/upload', 'url' => 'increment-bulk.php', 'info' => 'Add Bonus/Attend card/Invite/upload to certain classes'],
];
$table = 'sysoppanel';
$this->addMenu($table, $menus);
$menuToDel = ['amountupload.php', 'amountattendancecard.php', 'amountbonus.php', 'deletedisabled.php'];
$this->removeMenu($menuToDel);
/**
* @since 1.7.19
*/
$this->removeMenu(['freeleech.php']);
NexusDB::cache_del('nexus_rss');
NexusDB::cache_del('nexus_is_ip_seed_box');
/**
* @since 1.7.24
*/
if (!NexusDB::hasColumn('searchbox', 'extra')) {
$this->runMigrate('database/migrations/2022_09_02_031539_add_extra_to_searchbox_table.php');
SearchBox::query()->update(['extra' => [
SearchBox::EXTRA_DISPLAY_COVER_ON_TORRENT_LIST => 1,
SearchBox::EXTRA_DISPLAY_SEED_BOX_ICON_ON_TORRENT_LIST => 1,
]]);
}
/**
* @since 1.8.0
*/
$shouldMigrateSearchBox = false;
if (!NexusDB::hasColumn('searchbox', 'section_name')) {
$shouldMigrateSearchBox = true;
$searchBoxLog = "no section_name field";
} else {
$columnInfo = NexusDB::getMysqlColumnInfo('searchbox', 'section_name');
$searchBoxLog = "has section_name, searchbox.section DATA_TYPE: " . $columnInfo['DATA_TYPE'];
if ($columnInfo['DATA_TYPE'] != 'json') {
$searchBoxLog .= ", not json";
$shouldMigrateSearchBox = true;
}
}
$this->doLog("$searchBoxLog, shouldMigrateSearchBox: $shouldMigrateSearchBox");
if ($shouldMigrateSearchBox) {
$this->runMigrate('database/migrations/2021_06_08_113437_create_searchbox_table.php');
$this->runMigrate('database/migrations/2022_03_08_041951_add_custom_fields_to_searchbox_table.php');
$this->runMigrate('database/migrations/2022_09_02_031539_add_extra_to_searchbox_table.php');
$this->runMigrate('database/migrations/2022_09_05_230532_add_mode_to_section_related.php');
$this->runMigrate('database/migrations/2022_09_06_004318_add_section_name_to_searchbox_table.php');
$this->runMigrate('database/migrations/2022_09_06_030324_change_searchbox_field_extra_to_json.php');
$this->migrateSearchBoxModeRelated();
$this->doLog("[MIGRATE_TAXONOMY_TO_MODE_RELATED]");
}
$this->removeMenu(['catmanage.php']);
if (!NexusDB::hasColumn('users', 'seed_points_updated_at')) {
$this->runMigrate('database/migrations/2022_11_23_042152_add_seed_points_seed_times_update_time_to_users_table.php');
foreach (User::$notificationOptions as $option) {
$sql = "update users set notifs = concat(notifs, '[$option]') where instr(notifs, '[$option]') = 0";
NexusDB::statement($sql);
}
}
if (!$this->isSnatchedTableTorrentUserUnique()) {
$toolRep->removeDuplicateSnatch();
$this->runMigrate('database/migrations/2023_03_29_021950_handle_snatched_user_torrent_unique.php');
$this->doLog("removeDuplicateSnatch and migrate 2023_03_29_021950_handle_snatched_user_torrent_unique");
}
if (!NexusDB::hasIndex("peers", "unique_torrent_peer_user")) {
$toolRep->removeDuplicatePeer();
$this->runMigrate('database/migrations/2023_04_01_005409_add_unique_torrent_peer_user_to_peers_table.php');
$this->doLog("removeDuplicatePeer and migrate 2023_04_01_005409_add_unique_torrent_peer_user_to_peers_table");
}
/**
* @since 1.8.3
*/
$hasTableSetting = NexusDB::hasTable('settings');
if ($hasTableSetting) {
$updateSettings = [];
if (get_setting("system.meilisearch_enabled") == 'yes') {
$updateSettings["enabled"] = "yes";
}
if (get_setting("system.meilisearch_search_description") == 'yes') {
$updateSettings["search_description"] = "yes";
}
if (!empty($updateSettings)) {
$this->saveSettings(['meilisearch' => $updateSettings]);
}
}
/**
* @since 1.8.10
*/
if ($hasTableSetting) {
Setting::query()->firstOrCreate(
["name" => "system.alarm_email_receiver"],
["value" => User::query()->where("class", User::CLASS_STAFF_LEADER)->first(["id"])->id]
);
}
/**
* @since 1.9.0
*/
if (!Schema::hasTable("torrent_extras")) {
$this->runMigrate("database/migrations/2025_01_08_133552_create_torrent_extra_table.php");
Artisan::call("upgrade:migrate_torrents_table_text_column");
Language::updateTransStatus();
$this->addSetting('main.complain_enabled', 'yes');
$this->addSetting('image_hosting.driver', 'local');
$this->addSetting('permission.user_token_allowed', json_encode(TokenRepository::listUserTokenPermissions(false)));
}
if (!$redis->exists(Setting::USER_TOKEN_PERMISSION_ALLOWED_CACHE_KRY)) {
Setting::updateUserTokenPermissionAllowedCache(TokenRepository::listUserTokenPermissions(false));
}
/**
* @since 1.9.5
*/
if (!Schema::hasColumn("snatched", "hit_and_run_id")) {
$this->runMigrate("database/migrations/2025_06_09_222012_add_hr_and_buy_id_to_snatched_table.php");
Artisan::call("upgrade:migrate_snatched_hr_id");
Artisan::call("upgrade:migrate_snatched_buy_log_id");
}
if (!Schema::hasTable("tracker_urls")) {
$this->runMigrate("database/migrations/2025_06_19_194137_create_tracker_urls_table.php");
$this->initTrackerUrl('update');
NexusDB::cache_del("nexus_plugin_store_all");
}
}
public function runExtraMigrate()
{
if (!WITH_LARAVEL) {
$this->doLog(__METHOD__ . ", laravel is not available");
return;
}
if (NexusDB::hasColumn('torrents', 'tags')) {
if (Torrent::query()->where('tags', '>', 0)->count() > 0 && TorrentTag::query()->count() == 0) {
$this->doLog("[MIGRATE_TORRENT_TAG]...");
$tagRep = new TagRepository();
$tagRep->migrateTorrentTag();
$this->doLog("[MIGRATE_TORRENT_TAG] done!");
}
$sql = 'alter table torrents drop column tags';
sql_query($sql);
$this->doLog($sql);
} else {
$this->doLog("torrents table does not has column: tags");
}
clear_setting_cache();
}
private function addMenu($table, array $menus)
{
foreach ($menus as $menu) {
$count = get_row_count($table, "where url = " . sqlesc($menu['url']));
if ($count == 0) {
$id = NexusDB::insert($table, $menu);
$this->doLog("[ADD MENU] insert: " . json_encode($menu) . " to table: $table, id: $id");
}
}
}
private function removeMenu(array $menus, array $tables = ['sysoppanel', 'adminpanel', 'modpanel'])
{
$this->doLog("[REMOVE MENU]: " . json_encode($menus));
if (empty($menus)) {
return;
}
foreach ($tables as $table) {
NexusDB::table($table)->whereIn('url', $menus)->delete();
}
}
public function listVersions()
{
$url = "https://api.github.com/repos/xiaomlove/nexusphp/releases";
$versions = $this->requestGithub($url);
return array_reverse($versions);
}
public function getLatestCommit()
{
$url = "https://api.github.com/repos/xiaomlove/nexusphp/commits/php8";
return $this->requestGithub($url);
}
public function requestGithub($url)
{
$client = new Client();
$logPrefix = "Request github: $url";
$response = $client->get($url, ['timeout' => 10,]);
if (($statusCode = $response->getStatusCode()) != 200) {
throw new \RuntimeException("$logPrefix fail, status code$statusCode");
}
if ($response->getBody()->getSize() <= 0) {
throw new \RuntimeException("$logPrefix fail, response empty");
}
$bodyString = $response->getBody()->getContents();
$this->doLog("[REQUEST_GITHUB_RESPONSE]: $bodyString");
$results = json_decode($bodyString, true);
if (empty($results) || !is_array($results)) {
throw new \RuntimeException("$logPrefix response invalid");
}
return $results;
}
public function downAndExtractCode($url, array $includes = []): string
{
$requireCommand = 'rsync';
if (!command_exists($requireCommand)) {
throw new \RuntimeException("command: $requireCommand not exists!");
}
$arr = explode('/', $url);
$basename = last($arr);
$isZip = false;
if (Str::contains($basename,'.zip')) {
$isZip = true;
$basename = strstr($basename, '.zip', true);
$suffix = ".zip";
} else {
$suffix = '.tar.gz';
}
$filename = sprintf('%s/nexusphp-%s-%s%s', sys_get_temp_dir(), $basename, date('YmdHis'), $suffix);
$this->doLog("download from: $url, save to filename: $filename");
$client = new Client();
$response = $client->request('GET', $url, ['sink' => $filename]);
if (($statusCode = $response->getStatusCode()) != 200) {
throw new \RuntimeException("Download fail, status code$statusCode");
}
if (($bodySize = $response->getBody()->getSize()) <= 0) {
throw new \RuntimeException("Download fail, file size$bodySize");
}
if (!file_exists($filename)) {
throw new \RuntimeException("Download fail, file not exists$filename");
}
if (filesize($filename) <= 0) {
throw new \RuntimeException("Download fail, file: $filename size = 0");
}
$this->doLog('SUCCESS_DOWNLOAD');
$extractDir = str_replace($suffix, "", $filename);
$command = "mkdir -p $extractDir";
$this->executeCommand($command);
if ($isZip) {
$command = "unzip -q $filename -d $extractDir";
} else {
$command = "tar -xf $filename -C $extractDir";
}
$this->executeCommand($command);
foreach (glob("$extractDir/*") as $path) {
if (is_dir($path)) {
$excludes = array_merge(ToolRepository::BACKUP_EXCLUDES, ['public/favicon.ico', '.env', 'public/pic/category/chd/*']);
if (!in_array('composer', $includes)) {
$excludes[] = 'composer.lock';
$excludes[] = 'composer.json';
}
// $command = sprintf('cp -raf %s/. %s', $path, ROOT_PATH);
$command = "rsync -rvq $path/ " . ROOT_PATH;
$command .= " --include=public/vendor";
foreach ($excludes as $exclude) {
$command .= " --exclude=$exclude";
}
$this->executeCommand($command);
//remove original file
unlink($filename);
break;
}
}
$this->doLog('SUCCESS_EXTRACT');
return $extractDir;
}
public function initSeedPoints(): int
{
$size = 10000;
$tableName = (new User())->getTable();
$result = 0;
do {
$affectedRows = NexusDB::table($tableName)
->whereNull('seed_points')
->limit($size)
->update([
'seed_points' => NexusDB::raw('seedbonus')
]);
$result += $affectedRows;
$this->doLog("affectedRows: $affectedRows, query: " . last_query());
} while ($affectedRows > 0);
return $result;
}
public function updateDependencies()
{
$command = "composer install -d " . ROOT_PATH;
$this->executeCommand($command);
$this->doLog("[COMPOSER INSTALL] SUCCESS");
}
public function initTag()
{
$priority = count(Tag::DEFAULTS);
$dateTimeStringNow = date('Y-m-d H:i:s');
foreach (Tag::DEFAULTS as $value) {
$attributes = [
'name' => $value['name'],
];
$values = [
'priority' => $priority,
'color' => $value['color'],
'created_at' => $dateTimeStringNow,
'updated_at' => $dateTimeStringNow,
];
Tag::query()->firstOrCreate($attributes, $values);
$priority--;
}
}
private function isSnatchedTableTorrentUserUnique(): bool
{
$tableName = 'snatched';
$result = NexusDB::select('show index from ' . $tableName);
foreach ($result as $item) {
if (in_array($item['Column_name'], ['torrentid', 'userid']) && $item['Non_unique'] == 0) {
return true;
}
}
return false;
}
public function updateEnvFile()
{
$envFile = ROOT_PATH . '.env';
$envExample = ROOT_PATH . '.env.example';
$envData = readEnvFile($envFile);
$envExampleData = readEnvFile($envExample);
foreach ($envExampleData as $key => $value) {
if (!isset($envData[$key])) {
$envData[$key] = $value;
}
}
$fp = @fopen($envFile, 'w');
if ($fp === false) {
throw new \RuntimeException("can't create env file, make sure php has permission to create file at: " . ROOT_PATH);
}
$content = "";
foreach ($envData as $key => $value) {
$content .= "{$key}={$value}\n";
}
fwrite($fp, $content);
fclose($fp);
}
}