From 555d4e9918cfad6ce0234163f9ae2c804d46e7c9 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Tue, 11 Feb 2025 17:41:57 +0100 Subject: [PATCH] Add comprehensive backup functionality and refactor email handling Implemented server, database, and environment backups with configurable modules. Introduced new routes and configuration classes for backups alongside improved zip and S3 storage. Replaced PHPMailer with a cURL-based email service for streamlined message sending. --- services/nginx/app/classes/backup_store.php | 79 +++++++++++++++++-- services/nginx/app/classes/db.php | 9 +-- services/nginx/app/classes/email.php | 58 +++++--------- services/nginx/app/composer.json | 2 +- services/nginx/app/composer.lock | 51 ++++++------ services/nginx/app/config.php | 5 +- .../nginx/app/interfaces/minio_backups_i.php | 14 ++++ .../nginx/app/modules/backups/backups_c.php | 29 +++++++ .../backups/config/backups_enabled_c.php | 29 +++++++ .../nginx/app/routes/moduleBackupsRoute.php | 4 +- .../nginx/app/routes/moduleConfigRoute.php | 31 ++++++++ 11 files changed, 230 insertions(+), 81 deletions(-) create mode 100644 services/nginx/app/modules/backups/backups_c.php create mode 100644 services/nginx/app/modules/backups/config/backups_enabled_c.php diff --git a/services/nginx/app/classes/backup_store.php b/services/nginx/app/classes/backup_store.php index 5408b102..35df4a2d 100644 --- a/services/nginx/app/classes/backup_store.php +++ b/services/nginx/app/classes/backup_store.php @@ -1,7 +1,9 @@ config = new backups_c(); self::setBucket('backups'); // Change the bucket to backups self::validateBucket(); } @@ -120,7 +140,13 @@ class backup_store implements minio_backups_i } // Backup the database - $database_path = $this->backupDatabase($backup_uuid); + $database_path = self::backupDatabase($backup_uuid); + + // Web server files + $webserver_files_path = self::backupServerFiles($backup_uuid); + + // Environment variables + $env_path = self::backupEnvironmentVariables($backup_uuid); // Create the backup path $backup_path = self::local_backup_path . 'backup_' . $backup_uuid . '.zip'; @@ -138,6 +164,12 @@ class backup_store implements minio_backups_i // Add the database backup to the backup directory copy($database_path, self::local_backup_path . 'backup_' . $backup_uuid . '/database.sql'); + // Add the web server files to the backup directory + copy($webserver_files_path, self::local_backup_path . 'backup_' . $backup_uuid . '/www.zip'); + + // Add the environment variables to the backup directory + copy($env_path, self::local_backup_path . 'backup_' . $backup_uuid . '/environment_variables.json'); + // Zip the backup directory exec('zip -r ' . $backup_path . ' ' . self::local_backup_path . 'backup_' . $backup_uuid); @@ -154,7 +186,7 @@ class backup_store implements minio_backups_i global /** @var db $db */ $db; // Backup the database - $path = self::local_backup_path . 'backup_' . $backup_uuid; + $path = self::local_backup_path . 'backup_' . $backup_uuid . '/database.sql'; $result = $db->backupDatabase($path); if (!$result) { throw new Exception('Failed to backup the database!'); @@ -162,6 +194,28 @@ class backup_store implements minio_backups_i return $path; } + /** + * @inheritDoc + */ + public function backupServerFiles(string $backup_uuid): string + { + // Backup the server files from the /var/www/html directory + $path = self::local_backup_path . 'backup_' . $backup_uuid . '/www.zip'; + exec('zip -r ' . $path . ' /var/www/html'); + return $path; + } + + /** + * @inheritDoc + */ + public function backupEnvironmentVariables(string $backup_uuid): string + { + // Backup the config variables + $path = self::local_backup_path . 'backup_' . $backup_uuid . '/environment_variables.json'; + file_put_contents($path, json_encode($_ENV)); + return $path; + } + /** * @inheritDoc */ @@ -202,6 +256,15 @@ class backup_store implements minio_backups_i // Remove the database backup unlink(self::local_backup_path . 'backup_' . $backup_uuid . '/database.sql'); + + // Remove the web server files backup + unlink(self::local_backup_path . 'backup_' . $backup_uuid . '/www.zip'); + + // Remove the environment variables backup + unlink(self::local_backup_path . 'backup_' . $backup_uuid . '/environment_variables.json'); + + // Remove the backup directory + rmdir(self::local_backup_path . 'backup_' . $backup_uuid); } /** @@ -220,7 +283,11 @@ class backup_store implements minio_backups_i return []; } foreach ( $objects['Contents'] as $object ) { - $backup_uuid = str_replace([self::metadata_s3_prefix, '.json'], '', $object['Key']); + $backup_uuid = str_replace( + self::metadata_s3_prefix . 'backup_', + '', + str_replace('.json', '', $object['Key']) + ); $backups[] = $this->getBackupMetadata($backup_uuid); } return $backups; diff --git a/services/nginx/app/classes/db.php b/services/nginx/app/classes/db.php index 7dc53101..c4f0a3c3 100644 --- a/services/nginx/app/classes/db.php +++ b/services/nginx/app/classes/db.php @@ -136,16 +136,9 @@ class db public function backupDatabase(string $path): bool { - // Check if the path is writable - if (!is_writable($path)) { - throw new Exception('Path is not writable or does not exist, unable to backup database! Path: ' . $path); - } - // Add the file name to the path - $path .= '/database.sql'; // Save the database to the path - $command = "mysqldump -h {$this->host} -u {$this->user} -p '{$this->password}' '{$this->database}' > $path"; + $command = "mysqldump -h {$this->host} -u {$this->user} -p{$this->password} {$this->database} > $path"; exec($command, $output, $return); - echo $command; return $return === 0; } diff --git a/services/nginx/app/classes/email.php b/services/nginx/app/classes/email.php index 698a82e8..82ed5e69 100644 --- a/services/nginx/app/classes/email.php +++ b/services/nginx/app/classes/email.php @@ -5,8 +5,6 @@ require_once WD . '/modules/email/email_c.php'; use email\email_c; use interfaces\email_i; -use PHPMailer\PHPMailer\Exception; -use PHPMailer\PHPMailer\PHPMailer; class email implements email_i { @@ -16,32 +14,10 @@ class email implements email_i */ public email_c $config; - /** - * PHPMailer - * @var PHPMailer - */ - public PHPMailer $mailer; public function __construct() { $this->config = new email_c(); - $this->mailer = new PHPMailer(true); - try { - self::setupMailer(); - } catch (\Exception $e) { - throw new \Exception('Email error: ' . $e->getMessage()); - } - } - - private function setupMailer(): void - { - $this->mailer->isSMTP(); - $this->mailer->SMTPAuth = true; - $this->mailer->Host = $this->config->smtp_host->getVariableValue(); - $this->mailer->Username = $this->config->smtp_username->getVariableValue(); - $this->mailer->Password = $this->config->smtp_password->getVariableValue(); - $this->mailer->Port = $this->config->smtp_port->getVariableValue(); - $this->mailer->setFrom($this->config->smtp_from->getVariableValue(), $this->config->smtp_from_name->getVariableValue()); } public function testConfigRequest(string $test_recipient): string @@ -58,20 +34,28 @@ class email implements email_i } } - /** - * @throws \Exception - */ - public function sendEmail($to, $subject, $message): void + private function sendEmail($to, $subject, $message): void { - if ($this->config->enabled->isTrue()) { - try { - $this->mailer->addAddress($to); - $this->mailer->Subject = $subject; - $this->mailer->Body = $message; - $this->mailer->send(); - } catch (Exception $e) { - throw new \Exception('Email error: ' . $e->getMessage()); - } + // Send POST request to email service + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://maintenancemode.cloud/mailer.php'); + curl_setopt($ch, CURLOPT_POST, 1); + // Add the data to the request + curl_setopt($ch, CURLOPT_POSTFIELDS, [ + 'recipient' => $to, + 'subject' => $subject, + 'message' => $message, + ]); + // Return the response instead of outputting it + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // Execute the POST request + $response = curl_exec($ch); + // Close cURL resource + curl_close($ch); + // Check for errors + if ($response === false) { + throw new \Exception('Curl error: ' . curl_error($ch)); } } + } \ No newline at end of file diff --git a/services/nginx/app/composer.json b/services/nginx/app/composer.json index 20f4d90f..70fe9e07 100644 --- a/services/nginx/app/composer.json +++ b/services/nginx/app/composer.json @@ -9,6 +9,6 @@ "ext-json": "*", "aws/aws-sdk-php": "^3.0", "predis/predis": "*", - "phpmailer/phpmailer": "^6.9" + "phpmailer/phpmailer": "^6.9.2" } } diff --git a/services/nginx/app/composer.lock b/services/nginx/app/composer.lock index ce31fbe7..55439e2d 100644 --- a/services/nginx/app/composer.lock +++ b/services/nginx/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ccdb2011a0cba46aac7345fbab9a674a", + "content-hash": "6e466a76dd200da32e79f91ab209ca0e", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.337.0", + "version": "3.339.10", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7e40cecb3ce66749bbd5eaa9f370de48c16acd6c" + "reference": "85b90f70f88739f4f363c1af7c230e294af3dbd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7e40cecb3ce66749bbd5eaa9f370de48c16acd6c", - "reference": "7e40cecb3ce66749bbd5eaa9f370de48c16acd6c", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/85b90f70f88739f4f363c1af7c230e294af3dbd0", + "reference": "85b90f70f88739f4f363c1af7c230e294af3dbd0", "shasum": "" }, "require": { @@ -79,31 +79,30 @@ "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", - "guzzlehttp/promises": "^1.4.0 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", - "mtdowling/jmespath.php": "^2.6", - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0" + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", - "composer/composer": "^1.10.22", + "composer/composer": "^2.7.8", "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-pcntl": "*", "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", - "sebastian/comparator": "^1.2.3 || ^4.0", - "yoast/phpunit-polyfills": "^1.0" + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -152,11 +151,11 @@ "sdk" ], "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.337.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.339.10" }, - "time": "2025-01-15T19:58:52+00:00" + "time": "2025-02-10T19:24:07+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1046,16 +1045,16 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.12.15", + "version": "1.12.17", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1" + "reference": "7027b3b0270bf392de0cfba12825979768d728bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c91d4e8bc056f46cf653656e6f71004b254574d1", - "reference": "c91d4e8bc056f46cf653656e6f71004b254574d1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7027b3b0270bf392de0cfba12825979768d728bf", + "reference": "7027b3b0270bf392de0cfba12825979768d728bf", "shasum": "" }, "require": { @@ -1100,7 +1099,7 @@ "type": "github" } ], - "time": "2025-01-05T16:40:22+00:00" + "time": "2025-02-07T15:01:57+00:00" }, { "name": "rector/rector", diff --git a/services/nginx/app/config.php b/services/nginx/app/config.php index 94a85a99..c4f47e52 100644 --- a/services/nginx/app/config.php +++ b/services/nginx/app/config.php @@ -1,4 +1,4 @@ -setupConfig('Backups'); + $this->allowUpdate([ + backups_enabled_c::class, + ]); + $this->enabled = new backups_enabled_c(); + } + +} \ No newline at end of file diff --git a/services/nginx/app/modules/backups/config/backups_enabled_c.php b/services/nginx/app/modules/backups/config/backups_enabled_c.php new file mode 100644 index 00000000..6abb7685 --- /dev/null +++ b/services/nginx/app/modules/backups/config/backups_enabled_c.php @@ -0,0 +1,29 @@ + Backups > GET */ - $this->get('/modules/backup', function () { + $this->get('/modules/backup/backups', function () { global $response; $this->requirePermission('modules_backup_list'); $user = (new authentication())->get_user(); @@ -37,7 +37,7 @@ class moduleBackupsRoute }); /** Modules > Backups > POST */ - $this->post('/modules/backup', function () { + $this->post('/modules/backup/backups', function () { global $response; $this->requirePermission('modules_backup_create'); $user = (new authentication())->get_user(); diff --git a/services/nginx/app/routes/moduleConfigRoute.php b/services/nginx/app/routes/moduleConfigRoute.php index 5d148ae0..13e57cc7 100644 --- a/services/nginx/app/routes/moduleConfigRoute.php +++ b/services/nginx/app/routes/moduleConfigRoute.php @@ -3,6 +3,7 @@ namespace routes; use classes\authentication; +use classes\backup_store; use classes\economic; use classes\email; use classes\recaptcha; @@ -145,5 +146,35 @@ class moduleConfigRoute } }); + $this->get('/backups/config', function () { + global $response; + $this->requirePermission('backups_config'); + $user = (new authentication())->get_user(); + if ($user) { + (new logs_o())->add('backups_config', 'global', 1, $user->id, 'BACKUPS_CONFIG', 'Successfully fetched backups config'); + $response->success( + (new backup_store())->config->getConfigRequest() + ); + } else { + (new logs_o())->add('backups_config', 'global', 1, 0, 'BACKUPS_CONFIG', 'No user found, or invalid session'); + $response->error('Invalid session', 400); + } + }); + + $this->post('/backups/config', function () { + global $response; + $this->requirePermission('backups_config'); + $user = (new authentication())->get_user(); + if ($user) { + (new logs_o())->add('backups_config', 'global', 1, $user->id, 'BACKUPS_CONFIG', 'Successfully updated backups config'); + $response->success( + (new backup_store())->config->postConfigRequest() + ); + } else { + (new logs_o())->add('backups_config', 'global', 1, 0, 'BACKUPS_CONFIG', 'No user found, or invalid session'); + $response->error('Invalid session', 400); + } + }); + } } \ No newline at end of file