Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"geoip2/geoip2": "^3.0",
"jenssegers/agent": "^2.6",
"php-di/php-di": "^7.0",
"twig/twig": "^3.0"
"twig/twig": "^3.0",
"symfony/process": "^7.2"
},
"require-dev": {
"phpstan/phpstan": "1.6.9",
Expand Down
6 changes: 5 additions & 1 deletion core/classes/Queue/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,11 @@ public function getData(): array
*/
public function setOutput(array $output = [])
{
$this->_output = $output;
if (isset($this->_output)) {
$this->_output = array_merge($this->_output, $output);
} else {
$this->_output = $output;
}
}

/**
Expand Down
71 changes: 0 additions & 71 deletions core/includes/updates/230.php

This file was deleted.

81 changes: 81 additions & 0 deletions core/migrations/20250614172305_convert_cache_uses_to_settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class ConvertCacheUsesToSettings extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$cache = new Cache([
'name' => 'nameless',
'extension' => '.cache',
'path' => ROOT_PATH . '/cache/'
]);

// Convert templatecache to use settings table
$cache->setCache('templatecache');
$default_template = $cache->fetch('default', 'DefaultRevamp');
$default_panel_template = $cache->fetch('panel_default', 'Default');
Settings::set('default_template', $default_template);
Settings::set('default_panel_template', $default_panel_template);

// Convert template_settings to use settings table
$cache->setCache('template_settings');
$darkMode = $cache->fetch('darkMode', '0');
$navbarColour = $cache->fetch('navbarColour', 'white');
Settings::set('dark_mode', $darkMode);
Settings::set('default_revamp_navbar_color', $navbarColour);

// Convert backgroundcache to use settings table
$cache->setCache('backgroundcache');
$logo_image = $cache->fetch('logo_image', '');
$banner_image = $cache->fetch('banner_image', '');
$og_image = $cache->fetch('og_image', '');
$favicon_image = $cache->fetch('favicon_image', '');
Settings::set('logo_image_path', $logo_image);
Settings::set('banner_image_path', $banner_image);
Settings::set('og_image_path', $og_image);
Settings::set('favicon_image_path', $favicon_image);

// Convert avatar_settings_cache to use settings table
$cache->setCache('avatar_settings_cache');
$custom_avatars = $cache->fetch('custom_avatars', '0');
$default_avatar_type = $cache->fetch('default_avatar_type', 'minecraft');
$default_avatar_image = $cache->fetch('default_avatar_image', '');
$default_avatar_source = $cache->fetch('avatar_source', 'cravatar');
if ($default_avatar_source === 'Nameless') {
$default_avatar_source = 'cravatar';
}
$default_avatar_perspective = $cache->fetch('avatar_perspective', 'face');
Settings::set('custom_avatars', $custom_avatars);
Settings::set('default_avatar_type', $default_avatar_type);
Settings::set('default_avatar_image', $default_avatar_image);
Settings::set('default_avatar_source', $default_avatar_source);
Settings::set('default_avatar_perspective', $default_avatar_perspective);

// Convert OnlineUsersWidget to use settings table
$cache->setCache('online_members');
$use_nickname_show = $cache->fetch('show_nickname_instead', '0');
$include_staff = $cache->fetch('include_staff_in_users', '0');
Settings::set('online_users_widget_use_nicknames', $use_nickname_show);
Settings::set('online_users_widget_include_staff', $include_staff);

// Convert social_media to use settings table
$cache->setCache('social_media');
$discord_widget_theme = $cache->fetch('discord_widget_theme', 'dark');
Settings::set('discord_widget_theme', $discord_widget_theme, 'Discord Integration');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class ScheduleConvertProfilePostsTask extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
ConvertProfilePosts::schedule();
}
}
1 change: 0 additions & 1 deletion custom/panel_templates/Default/core/update.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
<h4>{$INSTRUCTIONS}</h4>
<p>{$INSTRUCTIONS_VALUE}</p>
<hr />
<a href="{$DOWNLOAD_LINK}" class="btn btn-primary">{$DOWNLOAD}</a>
<button class="btn btn-primary" type="button" onclick="showConfirmModal()">{$UPDATE}</button>
{elseif isset($UPDATE_CHECK_ERROR)}
<div class="alert bg-danger text-white">
Expand Down
181 changes: 181 additions & 0 deletions modules/Core/classes/Tasks/Upgrade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php
/**
* Download and execute update task for NamelessMC
*
* @package NamelessMC\Tasks
* @author Aberdeener
* @version 2.3.0
* @license MIT
*/
class Upgrade extends Task
{
private const DOWNLOAD_TIMEOUT = 30;

public function run(): string
{
try {
// Acquire lock to prevent concurrent upgrades
$this->acquireLock();

Settings::set('maintenance', '1');

$updateCheck = $this->validateUpdateAvailable();

$upgradeZipPath = $this->downloadUpgradePackage($updateCheck);

$this->extractUpgradePackage($upgradeZipPath);

$this->executeMigrations();

Settings::set('nameless_version', $updateCheck->versionTag());
Settings::set('version_update', null);

$cache = $this->_container->get(Cache::class);
$cache->setCache('update_check');
$cache->store('update_check', null);
} catch (Exception $e) {
$this->setOutput(['error' => $e->getMessage()]);

return Task::STATUS_FAILED;
} finally {
// Ensure the lock is released even if an error occurs
$this->releaseLock();

Settings::set('maintenance', '0');
}

return Task::STATUS_COMPLETED;
}

private function acquireLock(): void
{
$lockFile = ROOT_PATH . '/cache/upgrade.lock';
if (file_exists($lockFile)) {
throw new Exception('Upgrade is already running');
}

if (!file_put_contents($lockFile, 'locked')) {
throw new Exception('Failed to create lock file');
}
}

private function releaseLock(): void
{
$lockFile = ROOT_PATH . '/cache/upgrade.lock';
if (file_exists($lockFile)) {
unlink($lockFile);
}
}

private function validateUpdateAvailable(): UpdateCheck
{
$cache = $this->_container->get(Cache::class);

$cache->setCache('update_check');
$updateCheck = $cache->retrieve('update_check');

if (!$updateCheck) {
throw new Exception('No update found');
}

$this->setOutput(['update_check' => "Found update: {$updateCheck->versionTag()}"]);
return $updateCheck;
}

private function downloadUpgradePackage(UpdateCheck $updateCheck): string
{
$upgradeZipPath = $this->getTempDirectory() . "/namelessmc-upgrade-{$updateCheck->versionTag()}.zip";

$downloadResponse = HttpClient::get($updateCheck->upgradeZipLink(), [
'sink' => $upgradeZipPath,
'timeout' => self::DOWNLOAD_TIMEOUT,
]);

if ($downloadResponse->hasError()) {
throw new Exception("Error downloading upgrade zip: {$downloadResponse->getError()}");
}

$this->setOutput([
'zip_download' => "Downloaded upgrade zip to: {$upgradeZipPath}"
]);

$this->verifyChecksum($upgradeZipPath, $updateCheck);

return $upgradeZipPath;
}

public function extractUpgradePackage(string $upgradeZipPath): void
{
$zip = new ZipArchive();
if ($zip->open($upgradeZipPath) !== true) {
throw new Exception("Failed to open upgrade zip: {$upgradeZipPath}");
}

if (!$zip->extractTo(ROOT_PATH)) {
throw new Exception("Failed to extract upgrade zip: {$upgradeZipPath}");
}

$zip->close();

// Remove the zip file after extraction
unlink($upgradeZipPath);

$this->setOutput(['zip_extract' => 'Upgrade package extracted successfully']);
}

private function verifyChecksum(string $upgradeZipPath, UpdateCheck $updateCheck): void
{
$expectedChecksum = $updateCheck->checksum();

// TODO: Remove before merging
if (empty($expectedChecksum)) {
$this->setOutput([
'checksum_verify' => 'No checksum provided for verification, skipping checksum check'
]);
return;
}

$actualChecksum = hash_file('sha256', $upgradeZipPath);
if ($actualChecksum === false) {
throw new Exception("Failed to calculate checksum for file: {$upgradeZipPath}");
}

if (hash_equals($expectedChecksum, $actualChecksum)) {
$this->setOutput([
'checksum_verify' => "Checksum verification passed (SHA256: {$actualChecksum})"
]);
return;
}

// Remove the corrupted file
unlink($upgradeZipPath);

throw new Exception("Checksum verification failed: expected {$expectedChecksum}, got {$actualChecksum}");
}

private function executeMigrations(): void
{
$phinxWrapper = new Phinx\Wrapper\TextWrapper(
new Phinx\Console\PhinxApplication(),
[
'configuration' => ROOT_PATH . '/core/migrations/phinx.php',
]
);

$output = $phinxWrapper->getMigrate();

if ($phinxWrapper->getExitCode() !== 0) {
throw new Exception("Migrations failed: {$output}");
}

$this->setOutput([
'migrations' => $output,
]);
}

private function getTempDirectory(): string
{
$tmpDir = ini_get('upload_tmp_dir');
return $tmpDir ?: sys_get_temp_dir();
}
}
Loading
Loading