Skip to content

Commit ffd41bf

Browse files
committed
feat: Cifratura backup tramite password
1 parent 4bd6dbf commit ffd41bf

6 files changed

Lines changed: 205 additions & 30 deletions

File tree

backup/.htaccess

Lines changed: 0 additions & 1 deletion
This file was deleted.

config.example.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828
// 'sort_buffer_size' => '2M',
2929
];
3030

31-
// Percorso della cartella di backup
32-
$backup_dir = __DIR__.'/backup/';
33-
3431
// Tema selezionato per il front-end
3532
$theme = 'default';
3633

modules/backups/actions.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
if ($result) {
7171
flash()->info(tr('Nuovo backup creato correttamente!'));
7272
} else {
73+
$backup_dir = Backup::getDirectory();
7374
flash()->error(tr('Errore durante la creazione del backup!').' '.str_replace('_DIR_', '"'.$backup_dir.'"', tr('Verifica che la cartella _DIR_ abbia i permessi di scrittura!')));
7475
}
7576
} catch (Exception $e) {
@@ -83,7 +84,7 @@
8384
$number = intval($number);
8485

8586
$backups = Backup::getList();
86-
$backup = $backups[$number] ?: $backup_dir;
87+
$backup = $backups[$number] ?: Backup::getDirectory();
8788

8889
echo Util\FileSystem::size($backup);
8990

@@ -111,7 +112,10 @@
111112
}
112113

113114
try {
114-
$result = Backup::restore($path, is_file($path));
115+
// Ottieni la password per i backup esterni se impostata
116+
$password = setting('Password backup esterni');
117+
118+
$result = Backup::restore($path, is_file($path), $password);
115119
$database->beginTransaction();
116120

117121
if ($result) {

src/Backup.php

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,46 @@ class Backup
4545
*/
4646
public static function getDirectory()
4747
{
48-
$result = App::getConfig()['backup_dir'];
48+
// Ottieni l'adattatore di archiviazione selezionato
49+
$adapter = self::getStorageAdapter();
50+
$backup_dir = base_dir().'/backups';
51+
52+
// Estrai la directory dalle opzioni dell'adattatore
53+
if (!empty($adapter)) {
54+
$options = $adapter->options;
55+
// Se options è una stringa JSON, decodificala
56+
if (is_string($options)) {
57+
// Prova a decodificare il JSON normalmente
58+
$decoded = json_decode($options, true);
59+
60+
// Se la decodifica fallisce, prova a gestire il caso specifico con $ nella password
61+
if ($decoded === null && strpos($options, 'password') !== false) {
62+
// Estrai manualmente il valore di root usando espressioni regolari
63+
if (preg_match('/"root":"([^"]+)"/', $options, $matches)) {
64+
$backup_dir = $matches[1];
65+
}
66+
} else {
67+
$options = $decoded ?: [];
68+
69+
// Verifica se esiste la chiave 'directory' o 'root'
70+
if (!empty($options)) {
71+
if (isset($options['directory'])) {
72+
$backup_dir = base_dir().$options['directory'];
73+
} elseif (isset($options['root'])) {
74+
$backup_dir = $options['root'];
75+
}
76+
}
77+
}
78+
}
79+
}
80+
81+
// Fallback al percorso di configurazione se disponibile
82+
if (empty($backup_dir)) {
83+
$config = App::getConfig();
84+
$backup_dir = isset($config['backup_dir']) ? $config['backup_dir'] : base_dir().'/backups';
85+
}
4986

50-
$result = rtrim((string) $result, '/');
87+
$result = rtrim((string) $backup_dir, '/');
5188
if (!directory($result) || !is_writable($result)) {
5289
// throw new UnexpectedValueException();
5390
}
@@ -177,6 +214,7 @@ public static function create($ignores)
177214
'tmp',
178215
'.git',
179216
'.github',
217+
'.config', // Aggiungi la directory .config per evitare errori di permesso
180218
],
181219
];
182220

@@ -187,20 +225,79 @@ public static function create($ignores)
187225
$ignores['dirs'][] = basename($backup_dir);
188226
}
189227

228+
// Nome del file di backup
229+
$backup_filename = $backup_name.'.zip';
230+
$backup_path = $backup_dir.'/'.$backup_filename;
231+
190232
// Creazione backup in formato ZIP
191233
if (extension_loaded('zip')) {
192-
$result = Zip::create([
193-
base_dir(),
194-
self::getDatabaseDirectory(),
195-
], $backup_dir.'/'.$backup_name.'.zip', $ignores);
234+
// Verifica se è impostata una password per il backup
235+
$password = setting('Password di protezione backup');
236+
237+
// Se è impostata una password e ZipArchive è disponibile, crea un backup protetto da password
238+
if (!empty($password) && class_exists('ZipArchive')) {
239+
// Crea un percorso temporaneo per il backup
240+
$temp_path = $backup_path . '.tmp';
241+
242+
// Crea prima un backup normale
243+
$result = Zip::create([
244+
base_dir(),
245+
self::getDatabaseDirectory(),
246+
], $temp_path, $ignores);
247+
248+
if ($result) {
249+
// Crea un nuovo ZIP protetto da password
250+
$zip = new \ZipArchive();
251+
if ($zip->open($backup_path, \ZipArchive::CREATE) === true) {
252+
// Apri il file ZIP originale in lettura
253+
$original_zip = new \ZipArchive();
254+
if ($original_zip->open($temp_path) === true) {
255+
// Imposta la password per il nuovo ZIP
256+
$zip->setPassword($password);
257+
258+
// Copia tutti i file dal backup originale al nuovo backup protetto da password
259+
for ($i = 0; $i < $original_zip->numFiles; $i++) {
260+
$stat = $original_zip->statIndex($i);
261+
$file_content = $original_zip->getFromIndex($i);
262+
263+
// Aggiungi il file al nuovo ZIP
264+
$zip->addFromString($stat['name'], $file_content);
265+
266+
// Cripta il file con AES-256
267+
$zip->setEncryptionIndex($i, \ZipArchive::EM_AES_256, $password);
268+
}
269+
270+
// Chiudi i file ZIP
271+
$original_zip->close();
272+
$zip->close();
273+
274+
// Rimuovi il file temporaneo
275+
unlink($temp_path);
276+
} else {
277+
// Fallback al metodo normale
278+
$zip->close();
279+
unlink($backup_path);
280+
rename($temp_path, $backup_path);
281+
}
282+
} else {
283+
// Fallback al metodo normale
284+
rename($temp_path, $backup_path);
285+
}
286+
}
287+
} else {
288+
// Crea un backup normale senza password
289+
$result = Zip::create([
290+
base_dir(),
291+
self::getDatabaseDirectory(),
292+
], $backup_path, $ignores);
293+
}
196294
}
197-
198295
// Creazione backup attraverso la copia dei file
199296
else {
200297
$result = copyr([
201298
base_dir(),
202299
self::getDatabaseDirectory(),
203-
], $backup_dir.'/'.$backup_name.'.zip', $ignores);
300+
], $backup_path, $ignores);
204301
}
205302

206303
// Rimozione cartella temporanea
@@ -248,11 +345,37 @@ public static function cleanup()
248345
* Ripristina un backup esistente.
249346
*
250347
* @param string $path
348+
* @param bool $cleanup
349+
* @param string|null $password Password per decriptare il backup (se criptato)
251350
*/
252-
public static function restore($path, $cleanup = true)
351+
public static function restore($path, $cleanup = true, $password = null)
253352
{
254353
$database = database();
255-
$extraction_dir = is_dir($path) ? $path : Zip::extract($path);
354+
355+
// Se il backup non è una directory e è stata fornita una password, prova a estrarlo con la password
356+
if (!is_dir($path) && !empty($password) && class_exists('ZipArchive')) {
357+
$zip = new \ZipArchive();
358+
if ($zip->open($path) === true) {
359+
// Imposta la password per l'estrazione
360+
$zip->setPassword($password);
361+
362+
// Estrai il backup in una directory temporanea
363+
$extraction_dir = sys_get_temp_dir().'/'.basename($path, '.zip');
364+
if (!directory($extraction_dir)) {
365+
mkdir($extraction_dir, 0777, true);
366+
}
367+
368+
// Estrai tutti i file
369+
$zip->extractTo($extraction_dir);
370+
$zip->close();
371+
} else {
372+
// Se non è possibile aprire il file ZIP con la password, prova a estrarlo normalmente
373+
$extraction_dir = Zip::extract($path);
374+
}
375+
} else {
376+
// Estrai il backup normalmente
377+
$extraction_dir = is_dir($path) ? $path : Zip::extract($path);
378+
}
256379

257380
// TODO: Forzo il log out di tutti gli utenti e ne impedisco il login
258381
// fino a ripristino ultimato
@@ -340,6 +463,8 @@ protected static function getDatabaseDirectory()
340463
return slashes($result);
341464
}
342465

466+
467+
343468
/**
344469
* Restituisce l'elenco delle variabili da sostituire normalizzato per l'utilizzo.
345470
*/
@@ -348,6 +473,23 @@ protected static function getReplaces()
348473
return Generator::getReplaces();
349474
}
350475

476+
/**
477+
* Restituisce l'adattatore di archiviazione da utilizzare per i backup.
478+
*
479+
* @return \Modules\FileAdapters\FileAdapter
480+
*/
481+
public static function getStorageAdapter()
482+
{
483+
$adapter_id = setting('Adattatore archiviazione backup');
484+
485+
// Se non è stato selezionato un adattatore, utilizzo quello predefinito
486+
if (empty($adapter_id)) {
487+
return \Modules\FileAdapters\FileAdapter::getDefaultConnector();
488+
}
489+
490+
return \Modules\FileAdapters\FileAdapter::find($adapter_id);
491+
}
492+
351493
/**
352494
* Restituisce il nome previsto per il backup successivo.
353495
*

update/2_8.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
<?php
22

3+
// Spostamento backup
4+
$directory = 'backup/';
5+
$files = glob($directory.'*.{zip}', GLOB_BRACE);
6+
$new_folder = 'files/backups/';
7+
directory($new_folder);
8+
9+
foreach ($files as $file) {
10+
$filename = basename($file);
11+
rename($file, $new_folder.$filename);
12+
}
13+
314
// File e cartelle deprecate
415
$files = [
516
'templates/bilancio/settings.php',
@@ -12,10 +23,11 @@
1223
'templates/preventivi/settings.php',
1324
'templates/prima_nota/settings.php',
1425
'templates/scadenzario/settings.php',
26+
'backup/',
1527
];
1628

1729
foreach ($files as $key => $value) {
1830
$files[$key] = realpath(base_dir().'/'.$value);
1931
}
2032

21-
delete($files);
33+
delete($files);

update/2_8.sql

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`) VALUES (1, (SEL
66
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`) VALUES (1, (SELECT id FROM zz_settings WHERE `nome`='Api key ibanapi.com'), 'Api key ibanapi.com');
77

88
-- Aggiunta impostazione per OpenRouter API Key
9-
INSERT INTO `zz_settings` (`id`, `nome`, `valore`, `tipo`, `editable`, `sezione`, `order`) VALUES
9+
INSERT INTO `zz_settings` (`id`, `nome`, `valore`, `tipo`, `editable`, `sezione`, `order`) VALUES
1010
(NULL, 'OpenRouter API Key', '', 'string', 1, 'API', NULL);
1111

12-
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
12+
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
1313
(1, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'OpenRouter API Key'),
1414
'OpenRouter API Key',
1515
'API Key per l''integrazione con OpenRouter AI. Ottieni la tua chiave da https://openrouter.ai/keys');
1616

17-
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
17+
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
1818
(2, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'OpenRouter API Key'),
1919
'OpenRouter API Key',
2020
'API Key for OpenRouter AI integration. Get your key from https://openrouter.ai/keys');
@@ -54,23 +54,23 @@ INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
5454
INSERT INTO `zz_modules` (`name`, `directory`, `options`, `options2`, `icon`, `version`, `compatibility`, `order`, `parent`, `default`, `enabled`, `use_notes`, `use_checklists`) VALUES ('Descrizioni predefinite', 'descrizioni_predefinite', 'SELECT |select| FROM `zz_default_description` WHERE 1=1 HAVING 2=2', '', 'fa fa-circle-o', '2.8', '2.8', '8', (SELECT `id` FROM `zz_modules` AS `t` WHERE `name` = 'Tabelle'), '1', '1', '1', '1');
5555

5656
SELECT @id_module := `id` FROM `zz_modules` WHERE `name` = 'Descrizioni predefinite';
57-
INSERT INTO `zz_modules_lang` (`id_lang`, `id_record`, `title`, `meta_title`) VALUES
57+
INSERT INTO `zz_modules_lang` (`id_lang`, `id_record`, `title`, `meta_title`) VALUES
5858
('1', @id_module, 'Descrizioni predefinite', 'Descrizioni predefinite'),
5959
('2', @id_module, 'Descrizioni predefinite', 'Descrizioni predefinite');
6060

6161
SELECT @id_module := `id` FROM `zz_modules` WHERE `name` = 'Descrizioni predefinite';
62-
INSERT INTO `zz_views` (`id_module`, `name`, `query`, `order`, `search`, `slow`, `format`, `html_format`, `search_inside`, `order_by`, `visible`, `summable`, `avg`, `default`) VALUES
62+
INSERT INTO `zz_views` (`id_module`, `name`, `query`, `order`, `search`, `slow`, `format`, `html_format`, `search_inside`, `order_by`, `visible`, `summable`, `avg`, `default`) VALUES
6363
(@id_module, 'Descrizione', '`zz_default_description`.`descrizione`', '3', '1', '0', '0', '0', NULL, NULL, '1', '0', '0', '1'),
6464
(@id_module, 'Nome', 'zz_default_description.name', '2', '1', '0', '0', '0', NULL, NULL, '1', '0', '0', '1'),
65-
(@id_module, 'id', '`zz_default_description`.`id`', '1', '0', '0', '0', '0', NULL, NULL, '0', '0', '0', '1');
65+
(@id_module, 'id', '`zz_default_description`.`id`', '1', '0', '0', '0', '0', NULL, NULL, '0', '0', '0', '1');
6666

6767
SELECT @id_module := `id` FROM `zz_modules` WHERE `name` = 'Descrizioni predefinite';
68-
INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) VALUES
69-
('1', (SELECT `id` FROM `zz_views` WHERE `name` = 'Descrizione' AND `id_module` = @id_module), 'Descrizione'),
70-
('2', (SELECT `id` FROM `zz_views` WHERE `name` = 'Descrizione' AND `id_module` = @id_module), 'Description'),
68+
INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) VALUES
69+
('1', (SELECT `id` FROM `zz_views` WHERE `name` = 'Descrizione' AND `id_module` = @id_module), 'Descrizione'),
70+
('2', (SELECT `id` FROM `zz_views` WHERE `name` = 'Descrizione' AND `id_module` = @id_module), 'Description'),
7171
('1', (SELECT `id` FROM `zz_views` WHERE `name` = 'Nome' AND `id_module` = @id_module), 'Nome'),
72-
('2', (SELECT `id` FROM `zz_views` WHERE `name` = 'Nome' AND `id_module` = @id_module), 'Name'),
73-
('1', (SELECT `id` FROM `zz_views` WHERE `name` = 'id' AND `id_module` = @id_module), 'id'),
72+
('2', (SELECT `id` FROM `zz_views` WHERE `name` = 'Nome' AND `id_module` = @id_module), 'Name'),
73+
('1', (SELECT `id` FROM `zz_views` WHERE `name` = 'id' AND `id_module` = @id_module), 'id'),
7474
('2', (SELECT `id` FROM `zz_views` WHERE `name` = 'id' AND `id_module` = @id_module), 'id');
7575

7676
CREATE TABLE `zz_default_description` (`id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `descrizione` TEXT NOT NULL , `note` TEXT NOT NULL , PRIMARY KEY (`id`));
@@ -92,7 +92,7 @@ INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) VALUES
9292
-- Miglioria plugin Assicurazione crediti
9393
UPDATE `zz_plugins` SET `options` = '{ \"main_query\": [ { \"type\": \"table\", \"fields\": \"Fido assicurato, Data inizio, Data fine, Totale, Residuo\", \"query\": \"SELECT id, DATE_FORMAT(data_inizio,\'%d/%m/%Y\') AS \'Data inizio\', DATE_FORMAT(data_fine,\'%d/%m/%Y\') AS \'Data fine\', ROUND(fido_assicurato, 2) AS \'Fido assicurato\', ROUND(totale, 2) AS Totale, ROUND(fido_assicurato - totale, 2) AS Residuo, IF((fido_assicurato - totale) < 0, \'#f4af1b\', \'#4dc347\') AS _bg_ FROM an_assicurazione_crediti WHERE 1=1 AND id_anagrafica = |id_parent| HAVING 2=2 ORDER BY an_assicurazione_crediti.id DESC\"} ]}' WHERE `zz_plugins`.`name` = 'Assicurazione crediti';
9494

95-
ALTER TABLE `my_impianti` ADD `note` VARCHAR(255) NULL AFTER `descrizione`;
95+
ALTER TABLE `my_impianti` ADD `note` VARCHAR(255) NULL AFTER `descrizione`;
9696

9797
-- Aggiunta colonne Marchio e Modello nella vista Articoli (nascoste di default)
9898
SELECT @id_module := `id` FROM `zz_modules` WHERE `name` = 'Articoli';
@@ -105,4 +105,25 @@ INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) VALUES
105105
(1, @id-1, 'Marchio'),
106106
(2, @id-1, 'Brand'),
107107
(1, @id, 'Modello'),
108-
(2, @id, 'Model');
108+
(2, @id, 'Model');
109+
110+
INSERT INTO `zz_storage_adapters` (`name`, `class`, `options`, `can_delete`, `is_default`, `is_local`) VALUES
111+
('Backup', '\\Modules\\FileAdapters\\Adapters\\LocalAdapter', '{ \"directory\":\"/files/backups\" }', 1, 0, 1);
112+
113+
ALTER TABLE `zz_settings` CHANGE `is_user_setting` `is_user_setting` TINYINT(1) NOT NULL DEFAULT '0';
114+
115+
-- Aggiunta impostazione per l'adattatore di archiviazione per i backup
116+
INSERT INTO `zz_settings` (`id`, `nome`, `valore`, `tipo`, `editable`, `sezione`, `order`) VALUES
117+
(NULL, 'Adattatore archiviazione backup', (SELECT `id` FROM `zz_storage_adapters` WHERE name = 'Backup'), 'query=SELECT `id`, `name` AS descrizione FROM `zz_storage_adapters` WHERE `deleted_at` IS NULL ORDER BY `name`', 1, 'Backup', NULL);
118+
119+
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
120+
(1, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Adattatore archiviazione backup'), 'Adattatore archiviazione backup', 'Adattatore di archiviazione da utilizzare per i backup'),
121+
(2, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Adattatore archiviazione backup'), 'Backup storage adapter', 'Storage adapter to use for backups');
122+
123+
-- Aggiunta impostazione per la password dei backup esterni
124+
INSERT INTO `zz_settings` (`id`, `nome`, `valore`, `tipo`, `editable`, `sezione`, `order`) VALUES
125+
(NULL, 'Password di protezione backup', '', 'password', 1, 'Backup', NULL);
126+
127+
INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
128+
(1, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Password di protezione backup'), 'Password di protezione backup', 'Password da utilizzare per proteggere i backup in formato zip'),
129+
(2, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Password di protezione backup'), 'Backup protection password', 'Password to use for protecting zip backups');

0 commit comments

Comments
 (0)