diff --git a/.tmp/.gitignore b/.tmp/.gitignore new file mode 100644 index 000000000..a3a0c8b5f --- /dev/null +++ b/.tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c3fa63fd2..2b3ae8064 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 && \ diff --git a/README.md b/README.md index 93da5076d..e6353c984 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ See instructions to run Wallos below. - intl - openssl - sqlite3 + - zip #### Docker diff --git a/endpoints/db/backup.php b/endpoints/db/backup.php new file mode 100644 index 000000000..9f7948c1c --- /dev/null +++ b/endpoints/db/backup.php @@ -0,0 +1,71 @@ + 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 + ])); +} + + +?> \ No newline at end of file diff --git a/endpoints/db/import.php b/endpoints/db/import.php new file mode 100644 index 000000000..38672e1d0 --- /dev/null +++ b/endpoints/db/import.php @@ -0,0 +1,112 @@ +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" + ]); +} +?> \ No newline at end of file diff --git a/endpoints/db/restore.php b/endpoints/db/restore.php new file mode 100644 index 000000000..60a271e41 --- /dev/null +++ b/endpoints/db/restore.php @@ -0,0 +1,109 @@ + 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" + ]); +} +?> \ No newline at end of file diff --git a/endpoints/subscriptions/export.php b/endpoints/subscriptions/export.php deleted file mode 100644 index 2b8df91d0..000000000 --- a/endpoints/subscriptions/export.php +++ /dev/null @@ -1,48 +0,0 @@ - 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.')); -} - -?> \ No newline at end of file diff --git a/includes/checksession.php b/includes/checksession.php index f7c8610bd..7962690e1 100644 --- a/includes/checksession.php +++ b/includes/checksession.php @@ -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"; } diff --git a/includes/footer.php b/includes/footer.php index b15b6186e..55c8038a0 100644 --- a/includes/footer.php +++ b/includes/footer.php @@ -24,5 +24,11 @@
+ close(); + } + ?> + \ No newline at end of file diff --git a/includes/i18n/de.php b/includes/i18n/de.php index c7cd0d5e6..d32f30fa1 100644 --- a/includes/i18n/de.php +++ b/includes/i18n/de.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/el.php b/includes/i18n/el.php index bae9ed335..436ade2d6 100644 --- a/includes/i18n/el.php +++ b/includes/i18n/el.php @@ -12,6 +12,7 @@ "passwords_dont_match" => "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "registration_failed" => "Η εγγραφή απέτυχε, παρακαλώ προσπάθησε ξανά.", "register" => "Εγγραφή", + "restore_database" => "Επαναφορά βάσης δεδομένων", // Login Page 'please_login' => "Παρακαλώ συνδέσου", 'stay_logged_in' => "Μείνε συνδεδεμένος (30 ημέρες)", @@ -166,8 +167,10 @@ "add" => "Προσθήκη", "save" => "Αποθήκευση", "reset" => "Επαναφορά", - "export_subscriptions" => "Εξαγωγή συνδρομών", - "export_to_json" => "Εξαγωγή σε JSON", + "backup_and_restore" => "Αντίγραφο ασφαλείας και επαναφορά", + "backup" => "Αντίγραφο ασφαλείας", + "restore" => "Επαναφορά", + "restore_info" => "Η επαναφορά της βάσης δεδομένων θα ακυρώσει όλα τα τρέχοντα δεδομένα. Μετά την επαναφορά θα αποσυνδεθείτε.", // Filters menu "filter" => "Φίλτρο", "clear" => "Καθαρισμός", diff --git a/includes/i18n/en.php b/includes/i18n/en.php index a48f2410e..6c901073f 100644 --- a/includes/i18n/en.php +++ b/includes/i18n/en.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/es.php b/includes/i18n/es.php index 0be9babd5..618bf69df 100644 --- a/includes/i18n/es.php +++ b/includes/i18n/es.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php index c661ea2af..ab5c00f72 100644 --- a/includes/i18n/fr.php +++ b/includes/i18n/fr.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/it.php b/includes/i18n/it.php index d1204e2c7..8688c378a 100644 --- a/includes/i18n/it.php +++ b/includes/i18n/it.php @@ -12,6 +12,7 @@ '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 @@ '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', diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php index 2a4d5438c..fe677fd9c 100644 --- a/includes/i18n/jp.php +++ b/includes/i18n/jp.php @@ -12,6 +12,7 @@ "passwords_dont_match" => "パスワードが違います", "registration_failed" => "登録に失敗しました。もう一度お試しください。", "register" => "登録する", + "restore_database" => "データベースをリストア", // Login Page 'please_login' => "ログインしてください", 'stay_logged_in' => "ログインしたままにする (30日)", @@ -166,8 +167,10 @@ "add" => "追加", "save" => "保存", "reset" => "リセット", - "export_subscriptions" => "購読をエクスポート", - "export_to_json" => "JSONにエクスポート", + "backup_and_restore" => "バックアップとリストア", + "backup" => "バックアップ", + "restore" => "リストア", + "restore_info" => "データベースをリストアすると、現在のデータがすべて上書きされます。リストア後はサインアウトされます。", // Filters menu "filter" => "フィルタ", "clear" => "クリア", diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php index 7b9cfe9b5..1b58a427e 100644 --- a/includes/i18n/pl.php +++ b/includes/i18n/pl.php @@ -12,6 +12,7 @@ "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 @@ "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ść", diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php index 00b3146be..eb119fb1e 100644 --- a/includes/i18n/pt.php +++ b/includes/i18n/pt.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php index 2ee754bc1..0e0fd30c3 100644 --- a/includes/i18n/pt_br.php +++ b/includes/i18n/pt_br.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php index 3306079db..ba82a40d0 100644 --- a/includes/i18n/sr.php +++ b/includes/i18n/sr.php @@ -12,6 +12,7 @@ "passwords_dont_match" => "Лозинке се не поклапају", "registration_failed" => "Регистрација није успела, покушајте поново.", "register" => "Региструј се", + "restore_database" => "Врати базу података", // Страница за пријаву 'please_login' => "Молимо вас да се пријавите", 'stay_logged_in' => "Остани пријављен (30 дана)", @@ -166,8 +167,10 @@ "add" => "Додај", "save" => "Сачувај", "reset" => "Ресетуј", - "export_subscriptions" => "Извоз претплата", - "export_to_json" => "Извоз у JSON формат", + "backup_and_restore" => "Бекап и ресторе", + "backup" => "Бекап", + "restore" => "Ресторе", + "restore_info" => "Враћање базе података ће заменити све тренутне податке. Бићете одјављени након враћања.", // Мени са филтерима "filter" => "Филтер", "clear" => "Очисти", diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php index 99db3d479..8fb2edbb0 100644 --- a/includes/i18n/sr_lat.php +++ b/includes/i18n/sr_lat.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php index d29c3c133..a2897ef73 100644 --- a/includes/i18n/tr.php +++ b/includes/i18n/tr.php @@ -12,6 +12,7 @@ "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 @@ "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", diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php index e43fa659d..37ed3e415 100644 --- a/includes/i18n/zh_cn.php +++ b/includes/i18n/zh_cn.php @@ -12,6 +12,7 @@ "passwords_dont_match" => "密码不匹配", "registration_failed" => "注册失败,请重试。", "register" => "注册", + "restore_database" => "恢复数据库", // 登录页面 'please_login' => "请登录", @@ -173,9 +174,10 @@ "add" => "添加", "save" => "保存", "reset" => "重置", - "export_subscriptions" => "导出订阅", - "export_to_json" => "导出为 JSON", - + "backup_and_restore" => "备份和恢复", + "backup" => "备份", + "restore" => "恢复", + "restore_info" => "还原数据库将覆盖所有当前数据。还原后,您将退出登录。", // Filters menu "filter" => "筛选", "clear" => "清除", diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php index c21fd9ba5..70224c1b7 100644 --- a/includes/i18n/zh_tw.php +++ b/includes/i18n/zh_tw.php @@ -12,6 +12,7 @@ "passwords_dont_match" => "密碼不一致", "registration_failed" => "註冊失敗,請再試一次。", "register" => "註冊", + "restore_database" => "還原資料庫", // 登入頁面 'please_login' => "請先登入", 'stay_logged_in' => "保持登入 30 天", @@ -166,8 +167,10 @@ "add" => "新增", "save" => "儲存", "reset" => "重設", - "export_subscriptions" => "匯出訂閱", - "export_to_json" => "匯出為 JSON 檔案", + "backup_and_restore" => "備份與還原", + "backup" => "備份", + "restore" => "還原", + "restore_info" => "復原資料庫將覆蓋所有目前資料。 恢復後您將被註銷。", // Filters menu "filter" => "篩選", "clear" => "清除", diff --git a/includes/version.php b/includes/version.php index 09db156b5..44ad6d2d7 100644 --- a/includes/version.php +++ b/includes/version.php @@ -1,3 +1,3 @@ diff --git a/login.php b/login.php index 45ba92790..20bb0dd48 100644 --- a/login.php +++ b/login.php @@ -88,7 +88,7 @@ > - > + > > > diff --git a/registration.php b/registration.php index 723dda333..0b13f074d 100644 --- a/registration.php +++ b/registration.php @@ -26,6 +26,11 @@ function validate($value) { $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 @@ function validate($value) { Wallos - Subscription Tracker - + + > + > + > > @@ -107,9 +115,9 @@ function validate($value) {
Wallos Logo Wallos Logo Wallos Logo Wallos Logo

@@ -180,8 +188,14 @@ function validate($value) { +

+ + +
- + \ No newline at end of file diff --git a/scripts/registration.js b/scripts/registration.js index 90fc8e6fa..364682c31 100644 --- a/scripts/registration.js +++ b/scripts/registration.js @@ -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(); diff --git a/scripts/settings.js b/scripts/settings.js index 96193cdaf..9b3901cbd 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -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() { diff --git a/settings.php b/settings.php index cce58699a..1a69eb84b 100644 --- a/settings.php +++ b/settings.php @@ -681,11 +681,23 @@
-

+

-
- -
+
+
+ +
+
+ + +
+
+
+

+ + +

+
diff --git a/styles/login.css b/styles/login.css index 53d26778a..5508cdd70 100644 --- a/styles/login.css +++ b/styles/login.css @@ -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 */ \ No newline at end of file