Add import/export and update system

Implement CSV/JSON import and export for domains, notification groups and tags (with masking for sensitive channel data), including size/format validation, in-memory CSV building, and logging. Add tag transfer and bulk transfer actions (admin-only). Introduce a new update system: Add UpdateController and UpdateService, migration 025_add_update_system_v1.1.3.sql, and installer changes to include the new migration and version handling; provide endpoints to check, apply, rollback and configure updates. Update helpers and UI bits: add getUpdateBadgeInfo in LayoutHelper, update notification icons/redirects, and add getMaxUploadSize in ViewHelper. Misc: add NotificationGroup::findByName, tweak .gitignore backups path, and update related views and routes.
This commit is contained in:
Hosteroid
2026-02-11 17:43:23 +02:00
parent 0c759cdd1d
commit 3688c8b71b
32 changed files with 4268 additions and 350 deletions

View File

@@ -19,6 +19,7 @@ class ErrorHandler
private Logger $logger;
private ?ErrorLog $errorLogModel = null;
private bool $isDevelopment;
private bool $handling = false; // Recursion guard
public function __construct()
{
@@ -29,9 +30,8 @@ class ErrorHandler
// Initialize ErrorLog model if database is available
try {
$this->errorLogModel = new ErrorLog();
} catch (\Exception $e) {
} catch (\Throwable $e) {
// Database not available, will only use file logging
// Don't use error_log as it might fail too
}
}
@@ -40,6 +40,22 @@ class ErrorHandler
*/
public function handleException(\Throwable $exception): void
{
// Prevent infinite recursion if error handling itself triggers an error
if ($this->handling) {
// Fallback: just log to file and stop
try {
$this->logger->critical('Recursive error detected', [
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine()
]);
} catch (\Throwable $e) {
// Last resort
}
return;
}
$this->handling = true;
$errorData = $this->captureError($exception);
// Log to file
@@ -62,8 +78,8 @@ class ErrorHandler
return false;
}
// Ignore certain non-critical errors during error handling itself
if (error_reporting() === 0) {
// Prevent recursive handling (e.g. if logToDatabase triggers a warning)
if ($this->handling) {
return false;
}
@@ -114,7 +130,7 @@ class ErrorHandler
'error_message' => $exception->getMessage(),
'error_file' => $exception->getFile(),
'error_line' => $exception->getLine(),
'stack_trace' => json_encode($exception->getTrace()),
'stack_trace' => json_encode($exception->getTrace(), JSON_PARTIAL_OUTPUT_ON_ERROR) ?: '[]',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'N/A',
'request_data' => json_encode($requestData),
@@ -228,9 +244,19 @@ class ErrorHandler
try {
return $this->errorLogModel->logError($errorData);
} catch (\Exception $e) {
// Database logging failed, continue with file logging only
error_log("Failed to log error to database: " . $e->getMessage());
} catch (\Throwable $e) {
// Database logging failed — log to file so it's visible in the app's /logs folder
try {
$this->logger->error('Failed to log error to database', [
'db_error' => $e->getMessage(),
'db_error_file' => $e->getFile(),
'db_error_line' => $e->getLine(),
'original_error_id' => $errorData['error_id'] ?? 'unknown'
]);
} catch (\Throwable $e2) {
// Last resort — use PHP's native error_log
error_log("ErrorHandler: DB log failed: " . $e->getMessage());
}
return null;
}
}

View File

@@ -569,30 +569,36 @@ class NotificationService
/**
* Create system upgrade notification for admins (in-app)
* @param bool $composerManualRequired If true, appends a note to run composer install manually (e.g. when exec is disabled on cPanel)
*/
public function notifySystemUpgrade(int $userId, string $fromVersion, string $toVersion, int $migrationsCount): void
public function notifySystemUpgrade(int $userId, string $fromVersion, string $toVersion, int $migrationsCount, bool $composerManualRequired = false): void
{
$message = "Domain Monitor upgraded from v{$fromVersion} to v{$toVersion} ({$migrationsCount} migration" . ($migrationsCount > 1 ? 's' : '') . " applied)";
if ($composerManualRequired) {
$message .= ". Composer could not be run here (e.g. exec disabled). If dependencies changed, run \"composer install --no-dev\" manually via SSH or Terminal.";
}
$notificationModel = new \App\Models\Notification();
$notificationModel->createNotification(
$userId,
'system_upgrade',
'System Upgraded Successfully',
"Domain Monitor upgraded from v{$fromVersion} to v{$toVersion} ({$migrationsCount} migration" . ($migrationsCount > 1 ? 's' : '') . " applied)",
$message,
null
);
}
/**
* Notify all admins about system upgrade (in-app)
* @param bool $composerManualRequired If true, in-app message will include a note to run composer install manually
*/
public function notifyAdminsUpgrade(string $fromVersion, string $toVersion, int $migrationsCount): void
public function notifyAdminsUpgrade(string $fromVersion, string $toVersion, int $migrationsCount, bool $composerManualRequired = false): void
{
try {
$userModel = new \App\Models\User();
$admins = $userModel->getAllAdmins();
foreach ($admins as $admin) {
$this->notifySystemUpgrade($admin['id'], $fromVersion, $toVersion, $migrationsCount);
$this->notifySystemUpgrade($admin['id'], $fromVersion, $toVersion, $migrationsCount, $composerManualRequired);
}
} catch (\Exception $e) {
$logger = new \App\Services\Logger();
@@ -602,6 +608,50 @@ class NotificationService
}
}
/**
* Create "update available" in-app notification for one user
*/
public function notifyUpdateAvailable(int $userId, string $currentVersion, string $latestVersion, string $type = 'release', ?int $commitsBehind = null): void
{
$notificationModel = new \App\Models\Notification();
$title = 'Update Available';
if ($type === 'release') {
$message = "A new version of Domain Monitor is available: v{$latestVersion} (you have v{$currentVersion}). Go to Settings → Updates to apply.";
} else {
$msg = $commitsBehind
? "{$commitsBehind} new commit(s) are available on the main branch. Go to Settings → Updates to apply the hotfix."
: "New commits are available. Go to Settings → Updates to apply the hotfix.";
$message = $msg;
}
$notificationModel->createNotification(
$userId,
'update_available',
$title,
$message,
null
);
}
/**
* Notify all admins that an update is available (in-app)
* Used by cron when it detects a new version or hotfix.
*/
public function notifyAdminsUpdateAvailable(string $currentVersion, string $latestVersionOrLabel, string $type = 'release', ?int $commitsBehind = null): void
{
try {
$userModel = new \App\Models\User();
$admins = $userModel->getAllAdmins();
foreach ($admins as $admin) {
$this->notifyUpdateAvailable($admin['id'], $currentVersion, $latestVersionOrLabel, $type, $commitsBehind);
}
} catch (\Exception $e) {
$logger = new \App\Services\Logger();
$logger->error("Failed to notify admins about available update", [
'error' => $e->getMessage()
]);
}
}
/**
* Delete old read notifications (cleanup)
*/

File diff suppressed because it is too large Load Diff