feat: backup and restore (#288)

This commit is contained in:
Miguel Ribeiro 2024-04-26 17:41:19 +02:00 committed by GitHub
parent 65cc376dff
commit 7b509d2b3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 717 additions and 95 deletions

2
.tmp/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -6,11 +6,11 @@ WORKDIR /var/www/html
# Update packages and install dependencies
RUN apk upgrade --no-cache && \
apk add --no-cache sqlite-dev libpng libpng-dev libjpeg-turbo libjpeg-turbo-dev freetype freetype-dev curl autoconf libgomp icu-dev nginx dcron tzdata imagemagick imagemagick-dev && \
apk add --no-cache sqlite-dev libpng libpng-dev libjpeg-turbo libjpeg-turbo-dev freetype freetype-dev curl autoconf libgomp icu-dev nginx dcron tzdata imagemagick imagemagick-dev libzip-dev && \
docker-php-ext-install pdo pdo_sqlite && \
docker-php-ext-enable pdo pdo_sqlite && \
docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install -j$(nproc) gd intl && \
docker-php-ext-install -j$(nproc) gd intl zip && \
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS && \
pecl install imagick && \
docker-php-ext-enable imagick && \

View File

@ -61,6 +61,7 @@ See instructions to run Wallos below.
- intl
- openssl
- sqlite3
- zip
#### Docker

71
endpoints/db/backup.php Normal file
View File

@ -0,0 +1,71 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
function addFolderToZip($dir, $zipArchive, $zipdir = ''){
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
//Add the directory
if(!empty($zipdir)) $zipArchive->addEmptyDir($zipdir);
while (($file = readdir($dh)) !== false) {
// Skip '.' and '..'
if ($file == "." || $file == "..") {
continue;
}
//If it's a folder, run the function again!
if(is_dir($dir . $file)){
$newdir = $dir . $file . '/';
addFolderToZip($newdir, $zipArchive, $zipdir . $file . '/');
}else{
//Add the files
$zipArchive->addFile($dir . $file, $zipdir . $file);
}
}
}
} else {
die(json_encode([
"success" => false,
"message" => "Directory does not exist: $dir"
]));
}
}
$zip = new ZipArchive();
$filename = "backup_" . uniqid() . ".zip";
$zipname = "../../.tmp/" . $filename;
if ($zip->open($zipname, ZipArchive::CREATE)!==TRUE) {
die(json_encode([
"success" => false,
"message" => translate('cannot_open_zip', $i18n)
]));
}
addFolderToZip('../../db/', $zip);
addFolderToZip('../../images/uploads/', $zip);
$numberOfFilesAdded = $zip->numFiles;
if ($zip->close() === false) {
die(json_encode([
"success" => false,
"message" => "Failed to finalize the zip file"
]));
} else {
flush();
die(json_encode([
"success" => true,
"message" => "Zip file created successfully",
"numFiles" => $numberOfFilesAdded,
"file" => $filename
]));
}
?>

112
endpoints/db/import.php Normal file
View File

@ -0,0 +1,112 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
$result = $db->query("SELECT COUNT(*) as count FROM user");
$row = $result->fetchArray(SQLITE3_NUM);
if ($row[0] > 0) {
die(json_encode([
"success" => false,
"message" => "Denied"
]));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file'])) {
$file = $_FILES['file'];
$fileTmpName = $file['tmp_name'];
$fileError = $file['error'];
if ($fileError === 0) {
$fileDestination = '../../.tmp/restore.zip';
move_uploaded_file($fileTmpName, $fileDestination);
$zip = new ZipArchive();
if ($zip->open($fileDestination) === true) {
$zip->extractTo('../../.tmp/restore/');
$zip->close();
} else {
die(json_encode([
"success" => false,
"message" => "Failed to extract the uploaded file"
]));
}
if (file_exists('../../.tmp/restore/wallos.db')) {
if (file_exists('../../db/wallos.db')) {
unlink('../../db/wallos.db');
}
rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');
if (file_exists('../../.tmp/restore/logos/')) {
$dir = '../../images/uploads/logos/';
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
foreach ( $ri as $file ) {
if ( $file->isDir() ) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
$dir = new RecursiveDirectoryIterator('../../.tmp/restore/logos/');
$ite = new RecursiveIteratorIterator($dir);
$allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
foreach ($ite as $filePath) {
if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {
$destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);
$destinationDir = pathinfo($destination, PATHINFO_DIRNAME);
if (!is_dir($destinationDir)) {
mkdir($destinationDir, 0755, true);
}
copy($filePath, $destination);
}
}
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
$removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
$removeFunction($fileinfo->getRealPath());
}
echo json_encode([
"success" => true,
"message" => translate("success", $i18n)
]);
} else {
die(json_encode([
"success" => false,
"message" => "wallos.db does not exist in the backup file"
]));
}
} else {
echo json_encode([
"success" => false,
"message" => "Failed to upload file"
]);
}
} else {
echo json_encode([
"success" => false,
"message" => "No file uploaded"
]);
}
} else {
echo json_encode([
"success" => false,
"message" => "Invalid request method"
]);
}
?>

109
endpoints/db/restore.php Normal file
View File

@ -0,0 +1,109 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['file'])) {
$file = $_FILES['file'];
$fileTmpName = $file['tmp_name'];
$fileError = $file['error'];
if ($fileError === 0) {
$fileDestination = '../../.tmp/restore.zip';
move_uploaded_file($fileTmpName, $fileDestination);
$zip = new ZipArchive();
if ($zip->open($fileDestination) === true) {
$zip->extractTo('../../.tmp/restore/');
$zip->close();
} else {
die(json_encode([
"success" => false,
"message" => "Failed to extract the uploaded file"
]));
}
if (file_exists('../../.tmp/restore/wallos.db')) {
if (file_exists('../../db/wallos.db')) {
unlink('../../db/wallos.db');
}
rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');
if (file_exists('../../.tmp/restore/logos/')) {
$dir = '../../images/uploads/logos/';
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
foreach ( $ri as $file ) {
if ( $file->isDir() ) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
$dir = new RecursiveDirectoryIterator('../../.tmp/restore/logos/');
$ite = new RecursiveIteratorIterator($dir);
$allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
foreach ($ite as $filePath) {
if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {
$destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);
$destinationDir = pathinfo($destination, PATHINFO_DIRNAME);
if (!is_dir($destinationDir)) {
mkdir($destinationDir, 0755, true);
}
copy($filePath, $destination);
}
}
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
$removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
$removeFunction($fileinfo->getRealPath());
}
echo json_encode([
"success" => true,
"message" => translate("success", $i18n)
]);
} else {
die(json_encode([
"success" => false,
"message" => "wallos.db does not exist in the backup file"
]));
}
} else {
echo json_encode([
"success" => false,
"message" => "Failed to upload file"
]);
}
} else {
echo json_encode([
"success" => false,
"message" => "No file uploaded"
]);
}
} else {
echo json_encode([
"success" => false,
"message" => "Invalid request method"
]);
}
?>

View File

@ -1,48 +0,0 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
require_once '../../includes/getdbkeys.php';
$query = "SELECT * FROM subscriptions";
$result = $db->query($query);
if ($result) {
$subscriptions = array();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
// Map foreign keys to their corresponding values
$row['currency'] = $currencies[$row['currency_id']];
$row['payment_method'] = $payment_methods[$row['payment_method_id']];
$row['payer_user'] = $members[$row['payer_user_id']];
$row['category'] = $categories[$row['category_id']];
$row['cycle'] = $cycles[$row['cycle']];
$row['frequency'] = $frequencies[$row['frequency']];
$subscriptions[] = $row;
}
// Output JSON
$json = json_encode($subscriptions, JSON_PRETTY_PRINT);
// Set headers for file download
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="subscriptions.json"');
header('Pragma: no-cache');
header('Expires: 0');
// Output JSON for download
echo $json;
} else {
echo json_encode(array('error' => 'Failed to fetch subscriptions.'));
}
?>

View File

@ -8,6 +8,12 @@
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$result = $stmt->execute();
$userData = $result->fetchArray(SQLITE3_ASSOC);
if ($userData === false) {
header('Location: logout.php');
exit();
}
if ($userData['avatar'] == "") {
$userData['avatar'] = "0";
}

View File

@ -24,5 +24,11 @@
<div class="progress success"></div>
</div>
<?php
if (isset($db)) {
$db->close();
}
?>
</body>
</html>

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Die Passwörter stimmen nicht überein",
"registration_failed" => "Registrierung fehlgeschlagen, bitte erneut versuchen.",
"register" => "Registrieren",
"restore_database" => "Datenbank wiederherstellen",
// Login Page
'please_login' => "Bitte einloggen",
'stay_logged_in' => "Angemeldet bleiben (30 Tage)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Hinzufügen",
"save" => "Speichern",
"reset" => "Zurücksetzen",
"export_subscriptions" => "Abonnements exportieren",
"export_to_json" => "Nach JSON exportieren",
"backup_and_restore" => "Backup und Wiederherstellung",
"backup" => "Backup",
"restore" => "Wiederherstellen",
"restore_info" => "Durch die Wiederherstellung der Datenbank werden alle aktuellen Daten überschrieben. Nach der Wiederherstellung werden Sie abgemeldet.",
// Filters menu
"filter" => "Filter",
"clear" => "Leeren",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"registration_failed" => "Η εγγραφή απέτυχε, παρακαλώ προσπάθησε ξανά.",
"register" => "Εγγραφή",
"restore_database" => "Επαναφορά βάσης δεδομένων",
// Login Page
'please_login' => "Παρακαλώ συνδέσου",
'stay_logged_in' => "Μείνε συνδεδεμένος (30 ημέρες)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Προσθήκη",
"save" => "Αποθήκευση",
"reset" => "Επαναφορά",
"export_subscriptions" => "Εξαγωγή συνδρομών",
"export_to_json" => "Εξαγωγή σε JSON",
"backup_and_restore" => "Αντίγραφο ασφαλείας και επαναφορά",
"backup" => "Αντίγραφο ασφαλείας",
"restore" => "Επαναφορά",
"restore_info" => "Η επαναφορά της βάσης δεδομένων θα ακυρώσει όλα τα τρέχοντα δεδομένα. Μετά την επαναφορά θα αποσυνδεθείτε.",
// Filters menu
"filter" => "Φίλτρο",
"clear" => "Καθαρισμός",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Passwords do not match",
"registration_failed" => "Registration failed, please try again.",
"register" => "Register",
"restore_database" => "Restore Database",
// Login Page
'please_login' => "Please login",
'stay_logged_in' => "Stay logged in (30 days)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Add",
"save" => "Save",
"reset" => "Reset",
"export_subscriptions" => "Export Subscriptions",
"export_to_json" => "Export to JSON",
"backup_and_restore" => "Backup and Restore",
"backup" => "Backup",
"restore" => "Restore",
"restore_info" => "Restoring the database will override all current data. You will be signed out after the restore.",
// Filters menu
"filter" => "Filter",
"clear" => "Clear",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Las contraseñas no coinciden",
"registration_failed" => "Error en el registro, por favor inténtalo de nuevo.",
"register" => "Registrar",
"restore_database" => "Restaurar Base de Datos",
// Login Page
'please_login' => "Por favor, inicia sesión",
'stay_logged_in' => "Mantener sesión iniciada (30 días)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Agregar",
"save" => "Guardar",
"reset" => "Restablecer",
"export_subscriptions" => "Exportar suscripciones",
"export_to_json" => "Exportar a JSON",
"backup_and_restore" => "Copia de Seguridad y Restauración",
"backup" => "Copia de Seguridad",
"restore" => "Restaurar",
"restore_info" => "La restauración de la base de datos anulará todos los datos actuales. Se cerrará la sesión después de la restauración.",
// Filters menu
"filter" => "Filtrar",
"clear" => "Limpiar",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Les mots de passe ne correspondent pas",
"registration_failed" => "L'inscription a échoué, veuillez réessayer.",
"register" => "S'inscrire",
"restore_database" => "Restaurer la base de données",
// Page de connexion
'please_login' => "Veuillez vous connecter",
'stay_logged_in' => "Rester connecté (30 jours)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Ajouter",
"save" => "Enregistrer",
"reset" => "Réinitialiser",
"export_subscriptions" => "Exporter les abonnements",
"export_to_json" => "Exporter en JSON",
"backup_and_restore" => "Sauvegarde et restauration",
"backup" => "Sauvegarde",
"restore" => "Restauration",
"restore_info" => "La restauration de la base de données annulera toutes les données actuelles. Vous serez déconnecté après la restauration.",
// Menu des filtes
"filter" => "Filtre",
"clear" => "Effacer",

View File

@ -12,6 +12,7 @@ $i18n = [
'passwords_dont_match' => 'Le password non corrispondono',
'registration_failed' => 'Registrazione fallita, riprova.',
'register' => 'Registrati',
"restore_database" => 'Ripristina database',
// Login
'please_login' => 'Per favore, accedi',
@ -171,9 +172,10 @@ $i18n = [
'add' => 'Aggiungi',
'save' => 'Salva',
"reset" => 'Ripristina',
'export_subscriptions' => 'Esporta abbonamenti',
'export_to_json' => 'Esporta in JSON',
"backup_and_restore" => 'Backup e ripristino',
"backup" => 'Backup',
"restore" => 'Ripristina',
"restore_info" => "Il ripristino del database annullerà tutti i dati correnti. Al termine del ripristino, l'utente verrà disconnesso.",
// Filters
'filter' => 'Filtra',
'clear' => 'Pulisci',

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "パスワードが違います",
"registration_failed" => "登録に失敗しました。もう一度お試しください。",
"register" => "登録する",
"restore_database" => "データベースをリストア",
// Login Page
'please_login' => "ログインしてください",
'stay_logged_in' => "ログインしたままにする (30日)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "追加",
"save" => "保存",
"reset" => "リセット",
"export_subscriptions" => "購読をエクスポート",
"export_to_json" => "JSONにエクスポート",
"backup_and_restore" => "バックアップとリストア",
"backup" => "バックアップ",
"restore" => "リストア",
"restore_info" => "データベースをリストアすると、現在のデータがすべて上書きされます。リストア後はサインアウトされます。",
// Filters menu
"filter" => "フィルタ",
"clear" => "クリア",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Hasła nie pasują",
"registration_failed" => "Rejestracja nie powiodła się, spróbuj ponownie.",
"register" => "Rejestracja",
"restore_database" => "Przywróć bazę danych",
// Login Page
'please_login' => "Proszę się zalogować",
'stay_logged_in' => "Pozostań zalogowany (30 dni)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Dodaj",
"save" => "Zapisz",
"reset" => "Resetuj",
"export_subscriptions" => "Eksportuj subskrypcje",
"export_to_json" => "Eksportuj do JSON",
"backup_and_restore" => "Kopia zapasowa i przywracanie",
"backup" => "Kopia zapasowa",
"restore" => "Przywróć",
"restore_info" => "Przywrócenie bazy danych zastąpi wszystkie bieżące dane. Po przywróceniu zostaniesz wylogowany.",
// Filters menu
"filter" => "Filtr",
"clear" => "Wyczyść",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "As passwords não coincidem",
"registration_failed" => "O registo falhou. Tente novamente",
"register" => "Registar",
"restore_database" => "Restaurar base de dados",
// Login Page
'please_login' => "Por favor inicie sessão",
'stay_logged_in' => "Manter sessão (30 dias)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Adicionar",
"save" => "Guardar",
"reset" => "Repor",
"export_subscriptions" => "Exportar Subscrições",
"export_to_json" => "Exportar para JSON",
"backup_and_restore" => "Backup e Restauro",
"backup" => "Backup",
"restore" => "Restauro",
"restore_info" => "O restauro da base de dados apagará todos os dados actuais. A sua sessão irá terminar após o restauro.",
// Filters menu
"filter" => "Filtro",
"clear" => "Limpar",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "As senhas não são iguais",
"registration_failed" => "O registro falhou. Por favor, tente novamente",
"register" => "Registrar",
"restore_database" => "Restaurar banco de dados",
// Login Page
'please_login' => "Por favor, faça o login",
'stay_logged_in' => "Me manter logado (30 dias)",
@ -164,8 +165,10 @@ $i18n = [
"add" => "Adicionar",
"save" => "Salvar",
"reset" => "Redefinir",
"export_subscriptions" => "Exportar assinaturas",
"export_to_json" => "Exportar para JSON",
"backup_and_restore" => "Backup e Restauração",
"backup" => "Backup",
"restore" => "Restaurar",
"restore_info" => "A restauração do banco de dados substituirá todos os dados atuais. Você será desconectado após a restauração.",
// Filters menu
"filter" => "Filtrar",
"clear" => "Limpar",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Лозинке се не поклапају",
"registration_failed" => "Регистрација није успела, покушајте поново.",
"register" => "Региструј се",
"restore_database" => "Врати базу података",
// Страница за пријаву
'please_login' => "Молимо вас да се пријавите",
'stay_logged_in' => "Остани пријављен (30 дана)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Додај",
"save" => "Сачувај",
"reset" => "Ресетуј",
"export_subscriptions" => "Извоз претплата",
"export_to_json" => "Извоз у JSON формат",
"backup_and_restore" => "Бекап и ресторе",
"backup" => "Бекап",
"restore" => "Ресторе",
"restore_info" => "Враћање базе података ће заменити све тренутне податке. Бићете одјављени након враћања.",
// Мени са филтерима
"filter" => "Филтер",
"clear" => "Очисти",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Lozinke se ne poklapaju",
"registration_failed" => "Registracija nije uspela, pokušajte ponovo.",
"register" => "Registruj se",
"restore_database" => "Vrati bazu podataka",
// Stranica za prijavu
'please_login' => "Molimo vas da se prijavite",
'stay_logged_in' => "Ostani prijavljen (30 dana)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Dodaj",
"save" => "Sačuvaj",
"reset" => "Resetuj",
"export_subscriptions" => "Izvezi pretplate",
"export_to_json" => "Izvezi u JSON format",
"backup_and_restore" => "Backup i restore",
"backup" => "Backup",
"restore" => "Restore",
"restore_info" => "Vraćanje baze podataka će zameniti sve trenutne podatke. Bićete odjavljeni nakon vraćanja.",
// Meni sa filterima
"filter" => "Filter",
"clear" => "Očisti",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "Şifreler eşleşmiyor",
"registration_failed" => "Kayıt başarısız, lütfen tekrar deneyin.",
"register" => "Kayıt Ol",
"restore_database" => "Veritabanını geri yükle",
// Login Page
'please_login' => "Lütfen giriş yapın",
'stay_logged_in' => "Oturumu açık tut (30 gün)",
@ -166,8 +167,10 @@ $i18n = [
"add" => "Ekle",
"save" => "Kaydet",
"reset" => "Sıfırla",
"export_subscriptions" => "Abonelikleri Dışa Aktar",
"export_to_json" => "JSON'a dışa aktar",
"backup_and_restore" => "Yedekle ve Geri Yükle",
"backup" => "Yedekle",
"restore" => "Geri Yükle",
"restore_info" => "Veritabanının geri yüklenmesi tüm mevcut verileri geçersiz kılacaktır. Geri yüklemeden sonra oturumunuz kapatılacaktır.",
// Filters menu
"filter" => "Filtre",
"clear" => "Temizle",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "密码不匹配",
"registration_failed" => "注册失败,请重试。",
"register" => "注册",
"restore_database" => "恢复数据库",
// 登录页面
'please_login' => "请登录",
@ -173,9 +174,10 @@ $i18n = [
"add" => "添加",
"save" => "保存",
"reset" => "重置",
"export_subscriptions" => "导出订阅",
"export_to_json" => "导出为 JSON",
"backup_and_restore" => "备份和恢复",
"backup" => "备份",
"restore" => "恢复",
"restore_info" => "还原数据库将覆盖所有当前数据。还原后,您将退出登录。",
// Filters menu
"filter" => "筛选",
"clear" => "清除",

View File

@ -12,6 +12,7 @@ $i18n = [
"passwords_dont_match" => "密碼不一致",
"registration_failed" => "註冊失敗,請再試一次。",
"register" => "註冊",
"restore_database" => "還原資料庫",
// 登入頁面
'please_login' => "請先登入",
'stay_logged_in' => "保持登入 30 天",
@ -166,8 +167,10 @@ $i18n = [
"add" => "新增",
"save" => "儲存",
"reset" => "重設",
"export_subscriptions" => "匯出訂閱",
"export_to_json" => "匯出為 JSON 檔案",
"backup_and_restore" => "備份與還原",
"backup" => "備份",
"restore" => "還原",
"restore_info" => "復原資料庫將覆蓋所有目前資料。 恢復後您將被註銷。",
// Filters menu
"filter" => "篩選",
"clear" => "清除",

View File

@ -1,3 +1,3 @@
<?php
$version = "v1.22.0";
$version = "v1.23.0";
?>

View File

@ -88,7 +88,7 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
<link rel="stylesheet" href="styles/theme.css?<?= $version ?>">
<link rel="stylesheet" href="styles/login.css?<?= $version ?>">
<link rel="stylesheet" href="styles/themes/red.css?<?= $version ?>" id="red-theme" <?= $colorTheme != "red" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/yellow.css?<?= $version ?>" id="yellow-theme" <?= $colorTheme != "yellow" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/barlow.css">
<link rel="stylesheet" href="styles/login-dark-theme.css?<?= $version ?>" id="dark-theme" <?= $theme == "light" ? "disabled" : "" ?>>

View File

@ -26,6 +26,11 @@ if (isset($_COOKIE['theme'])) {
$theme = $_COOKIE['theme'];
}
$colorTheme = "blue";
if (isset($_COOKIE['colorTheme'])) {
$colorTheme = $_COOKIE['colorTheme'];
}
$currencies = array();
$query = "SELECT * FROM currencies";
$result = $db->query($query);
@ -94,9 +99,12 @@ if (isset($_POST['username'])) {
<title>Wallos - Subscription Tracker</title>
<link rel="icon" type="image/png" href="images/icon/favicon.ico" sizes="16x16">
<link rel="apple-touch-icon" sizes="180x180" href="images/icon/apple-touch-icon.png">
<link rel="manifest" href="manifes.json">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="styles/theme.css?<?= $version ?>">
<link rel="stylesheet" href="styles/login.css?<?= $version ?>">
<link rel="stylesheet" href="styles/themes/red.css?<?= $version ?>" id="red-theme" <?= $colorTheme != "red" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/yellow.css?<?= $version ?>" id="yellow-theme" <?= $colorTheme != "yellow" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/login-dark-theme.css?<?= $version ?>" id="dark-theme" <?= $theme == "light" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/barlow.css">
<script type="text/javascript" src="scripts/registration.js?<?= $version ?>"></script>
@ -107,9 +115,9 @@ if (isset($_POST['username'])) {
<header>
<?php
if ($theme == "light") {
?> <img src="images/siteicons/blue/wallos.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" /> <?php
?> <img src="images/siteicons/<?= $colorTheme ?>/wallos.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" width="215" /> <?php
} else {
?> <img src="images/siteicons/blue/walloswhite.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" /> <?php
?> <img src="images/siteicons/<?= $colorTheme ?>/walloswhite.png" alt="Wallos Logo" title="Wallos - Subscription Tracker" width="215" /> <?php
}
?>
<p>
@ -180,8 +188,14 @@ if (isset($_POST['username'])) {
<input type="submit" value="<?= translate('register', $i18n) ?>">
</div>
</form>
<div class="separator">
<input type="button" class="secondary-button" value="<?= translate('restore_database', $i18n) ?>" id="restoreDB" onClick="openRestoreDBFileSelect()" />
<input type="file" name="restoreDBFile" id="restoreDBFile" style="display: none;" onChange="restoreDB()" accept=".zip">
</div>
</section>
</div>
<?php
require_once 'includes/footer.php';
?>
</body>
</html>

View File

@ -62,6 +62,101 @@ function runDatabaseMigration() {
});
}
function showErrorMessage(message) {
const toast = document.querySelector(".toast#errorToast");
(closeIcon = document.querySelector(".close-error")),
(errorMessage = document.querySelector(".errorMessage")),
(progress = document.querySelector(".progress.error"));
let timer1, timer2;
errorMessage.textContent = message;
toast.classList.add("active");
progress.classList.add("active");
timer1 = setTimeout(() => {
toast.classList.remove("active");
closeIcon.removeEventListener("click", () => {});
}, 5000);
timer2 = setTimeout(() => {
progress.classList.remove("active");
}, 5300);
closeIcon.addEventListener("click", () => {
toast.classList.remove("active");
setTimeout(() => {
progress.classList.remove("active");
}, 300);
clearTimeout(timer1);
clearTimeout(timer2);
closeIcon.removeEventListener("click", () => {});
});
}
function showSuccessMessage(message) {
const toast = document.querySelector(".toast#successToast");
(closeIcon = document.querySelector(".close-success")),
(successMessage = document.querySelector(".successMessage")),
(progress = document.querySelector(".progress.success"));
let timer1, timer2;
successMessage.textContent = message;
toast.classList.add("active");
progress.classList.add("active");
timer1 = setTimeout(() => {
toast.classList.remove("active");
closeIcon.removeEventListener("click", () => {});
}, 5000);
timer2 = setTimeout(() => {
progress.classList.remove("active");
}, 5300);
closeIcon.addEventListener("click", () => {
toast.classList.remove("active");
setTimeout(() => {
progress.classList.remove("active");
}, 300);
clearTimeout(timer1);
clearTimeout(timer2);
closeIcon.removeEventListener("click", () => {});
});
}
function openRestoreDBFileSelect() {
document.getElementById('restoreDBFile').click();
};
function restoreDB() {
const input = document.getElementById('restoreDBFile');
const file = input.files[0];
if (!file) {
console.error('No file selected');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('endpoints/db/import.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message)
window.location.href = 'logout.php';
} else {
showErrorMessage(data.message);
}
})
.catch(error => showErrorMessage('Error:', error));
}
window.onload = function () {
restoreFormFields();
removeFromStorage();

View File

@ -1008,8 +1008,64 @@ function setHideDisabled() {
storeSettingsOnDB('hide_disabled', value);
}
function exportToJson() {
window.location.href = "endpoints/subscriptions/export.php";
function backupDB() {
const button = document.getElementById("backupDB");
button.disabled = true;
fetch('endpoints/db/backup.php')
.then(response => response.json())
.then(data => {
if (data.success) {
const link = document.createElement('a');
const filename = data.file;
link.href = '.tmp/' + filename;
link.download = 'backup.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
button.disabled = false;
} else {
showErrorMessage(data.errorMessage);
button.disabled = false;
}
})
.catch(error => {
showErrorMessage(error);
button.disabled = false;
});
}
function openRestoreDBFileSelect() {
document.getElementById('restoreDBFile').click();
};
function restoreDB() {
const input = document.getElementById('restoreDBFile');
const file = input.files[0];
if (!file) {
console.error('No file selected');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('endpoints/db/restore.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message)
window.location.href = 'logout.php';
} else {
showErrorMessage(data.message);
}
})
.catch(error => showErrorMessage('Error:', error));
}
function saveCategorySorting() {

View File

@ -681,11 +681,23 @@
<section class="account-section">
<header>
<h2><?= translate('export_subscriptions', $i18n) ?></h2>
<h2><?= translate('backup_and_restore', $i18n) ?></h2>
</header>
<div>
<input type="button" class="button thin" value="<?= translate('export_to_json', $i18n) ?>" id="exportToJson" onClick="exportToJson()"/>
<div>
<div class="form-group-inline">
<div>
<input type="button" class="button thin" value="<?= translate('backup', $i18n) ?>" id="backupDB" onClick="backupDB()"/>
</div>
<div>
<input type="button" class="secondary-button thin" value="<?= translate('restore', $i18n) ?>" id="restoreDB" onClick="openRestoreDBFileSelect()" />
<input type="file" name="restoreDBFile" id="restoreDBFile" style="display: none;" onChange="restoreDB()" accept=".zip">
</div>
</div>
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i>
<?= translate('restore_info', $i18n) ?>
</p>
</div>
</section>
</section>

View File

@ -90,7 +90,8 @@ select {
outline: none;
}
input[type="submit"] {
input[type="submit"],
input[type="button"] {
width: 100%;
padding: 15px;
font-size: 16px;
@ -105,6 +106,20 @@ input[type="submit"]:hover {
background-color: var(--hover-color);
}
input[type="button"].secondary-button,
button.button.secondary-button {
background-color: #FFFFFF;
color: var(--main-color);
border: 2px solid var(--main-color);
}
input[type="button"].secondary-button:hover,
button.button.secondary-button:hover {
background-color: #EEEEEE;
color: var(--hover-color);
border-color: var(--hover-color);
}
input[type="checkbox"] {
cursor: pointer;
width: 25px;
@ -123,3 +138,131 @@ input[type="checkbox"] {
color: var(--error-color);
margin-bottom: 20px;
}
.separator {
border-top: 1px solid #ccc;
padding-top: 20px;
}
/* TOAST MESSAGE */
.toast {
position: fixed;
bottom: 25px;
right: 30px;
border-radius: 12px;
border: 1px solid #eeeeee;
background: #fff;
padding: 20px 35px 20px 25px;
box-shadow: 0 6px 20px -5px rgba(0, 0, 0, 0.1);
overflow: hidden;
transform: translateX(calc(100% + 30px));
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.35);
box-sizing: border-box;
}
@media (max-width: 768px) {
.toast {
bottom: 0px;
right: 0px;
left: 0px;
width: 100%;
}
}
.toast.active {
transform: translateX(0%);
}
.toast .toast-content {
display: flex;
align-items: center;
}
.toast-content .toast-icon {
display: flex;
align-items: center;
justify-content: center;
height: 35px;
min-width: 35px;
color: #fff;
font-size: 20px;
border-radius: 50%;
}
.toast-content .toast-icon.error {
background-color: var(--error-color);
}
.toast-content .toast-icon.success {
background-color: var(--success-color);
}
.toast-content .message {
display: flex;
flex-direction: column;
margin: 0 20px;
}
.toast-content .message .text {
font-size: 16px;
font-weight: 400;
color: #666666;
}
.toast-content .message .text.text-1 {
font-weight: 600;
color: #333;
}
.toast .close {
position: absolute;
top: 10px;
right: 15px;
padding: 5px;
cursor: pointer;
opacity: 0.7;
}
.toast .close:hover {
opacity: 1;
}
.toast .progress {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
width: 100%;
}
.toast .progress:before {
content: "";
position: absolute;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
}
.toast .progress.error:before {
background-color: var(--error-color);
}
.toast .progress.success:before {
background-color: var(--success-color);
}
.progress.active:before {
animation: progress 5s linear forwards;
}
@keyframes progress {
100% {
right: 100%;
}
}
/* TOAST END */