feat: add indonesian language (#842)

feat: add first and last names to the user profile
feat: enable IPv6 environments by configuring a dual-stack listen in nginx 
feat: add new currency
feat: add button to auto fill the next payment date
fix: vulnerability on test webhook endpoint
This commit is contained in:
Miguel Ribeiro
2025-06-08 18:52:21 +02:00
committed by GitHub
parent 2e74c4bcb7
commit 48db4e300d
39 changed files with 195 additions and 22 deletions

View File

@@ -27,6 +27,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$token = $data["token"];
$ignore_ssl = $data["ignore_ssl"];
// Validate URL scheme
$parsedUrl = parse_url($url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
$query = "SELECT COUNT(*) FROM gotify_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -29,6 +29,20 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$headers = $data["headers"];
$ignore_ssl = $data["ignore_ssl"];
$url = rtrim($host, '/') . '/' . ltrim($topic, '/');
// Validate URL scheme
$parsedUrl = parse_url($url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
$query = "SELECT COUNT(*) FROM ntfy_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -28,6 +28,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$cancelation_payload = $data["cancelation_payload"];
$ignore_ssl = $data["ignore_ssl"];
// Validate URL scheme
$parsedUrl = parse_url($url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
$query = "SELECT COUNT(*) FROM webhook_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -30,6 +30,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$bot_username = $data["bot_username"];
$bot_avatar_url = $data["bot_avatar"];
// Validate URL scheme
$parsedUrl = parse_url($webhook_url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($webhook_url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
$postfields = [
'content' => $message,
'embeds' => [

View File

@@ -31,6 +31,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$token = $data["token"];
$ignore_ssl = $data["ignore_ssl"];
// Validate URL scheme
$parsedUrl = parse_url($url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
$ch = curl_init();
// Set the URL and other options

View File

@@ -33,9 +33,22 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
return "$key: $value";
}, array_keys($headers), $headers);
$url = "$host/$topic";
$url = rtrim($host, '/') . '/' . ltrim($topic, '/');
$ignore_ssl = $data["ignore_ssl"];
// Validate URL scheme
$parsedUrl = parse_url($url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
// Set the message parameters
$message = translate('test_notification', $i18n);

View File

@@ -42,6 +42,19 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$url = $data["url"];
$payload = $data["payload"];
// Validate URL scheme
$parsedUrl = parse_url($url);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($url, FILTER_VALIDATE_URL)
) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
// Replace placeholders in the payload with fake subscription data
foreach ($fakeSubscription as $key => $value) {
$placeholder = "{{" . $key . "}}";

View File

@@ -198,8 +198,8 @@ function resizeAndUploadAvatar($uploadedFile, $uploadDir, $name)
if (
isset($_SESSION['username'])
&& isset($_POST['firstname']) && $_POST['firstname'] !== ""
&& isset($_POST['lastname']) && $_POST['lastname'] !== ""
&& isset($_POST['firstname'])
&& isset($_POST['lastname'])
&& isset($_POST['email']) && $_POST['email'] !== ""
&& isset($_POST['avatar']) && $_POST['avatar'] !== ""
&& isset($_POST['main_currency']) && $_POST['main_currency'] !== ""

View File

@@ -163,7 +163,7 @@ $mobileNavigation = $settings['mobile_nav'] ? "mobile-navigation" : "";
<div class="dropdown">
<button class="dropbtn" onClick="toggleDropdown()">
<img src="<?= htmlspecialchars($userData['avatar'], ENT_QUOTES, 'UTF-8') ?>" alt="me" id="avatar">
<span id="user" class="mobileNavigationHideOnMobile"><?= $userData['firstname'] ?></span>
<span id="user" class="mobileNavigationHideOnMobile"><?= $userData['username'] ?></span>
</button>
<div class="dropdown-content">
<a href="profile.php" class="mobileNavigationHideOnMobile">

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Žádné odpovídající předplatné",
"clone" => "Klonovat",
"renew" => "Obnovit",
"calculate_next_payment_date" => "Vypočítat datum další platby",
// Subscription form
"add_subscription" => "Přidat předplatné",
"edit_subscription" => "Upravit předplatné",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Ingen matchende abonnementer",
"clone" => "Klon",
"renew" => "Forny",
"calculate_next_payment_date" => "Beregn næste betalingsdato",
// Subscription form
"add_subscription" => "Tilføj abonnement",
"edit_subscription" => "Rediger abonnement",

View File

@@ -79,7 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Keine passenden Abonnements gefunden",
"clone" => "Klonen",
"renew" => "Verlängern",
"autofill_next_payment" => "Nächste Zahlung automatisch ausfüllen anhand von Startdatum und Interval",
"calculate_next_payment_date" => "Berechne nächstes Zahlungsdatum",
// Subscription form
"add_subscription" => "Abonnement hinzufügen",
"edit_subscription" => "Abonnement editieren",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Δεν υπάρχουν συνδρομές που ταιριάζουν με τα φίλτρα σου",
"clone" => "Κλώνος",
"renew" => "Ανανέωση",
"calculate_next_payment_date" => "Υπολογισμός ημερομηνίας επόμενης πληρωμής",
// Subscription form
"add_subscription" => "Προσθήκη συνδρομής",
"edit_subscription" => "Επεξεργασία συνδρομής",

View File

@@ -79,7 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "No matching subscriptions",
"clone" => "Clone",
"renew" => "Renew",
"autofill_next_payment" => "Autofill Next Payment by Interval and Start Date",
"calculate_next_payment_date" => "Calculate Next Payment Date",
// Subscription form
"add_subscription" => "Add subscription",
"edit_subscription" => "Edit subscription",

View File

@@ -6,7 +6,7 @@ $i18n = [
"username" => "Nombre de Usuario",
"password" => "Contraseña",
"email" => "Correo Electrónico",
"firstname" => "Nombre de pila",
"firstname" => "Nombre",
"lastname" => "Apellido",
"confirm_password" => "Confirmar Contraseña",
"main_currency" => "Moneda Principal",
@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "No hay suscripciones que coincidan con los filtros",
"clone" => "Clonar",
"renew" => "Renovar",
"calculate_next_payment_date" => "Calcular Fecha del Próximo Pago",
// Subscription form
"add_subscription" => "Añadir suscripción",
"edit_subscription" => "Editar suscripción",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Aucun abonnement ne correspond à vos critères de recherche",
"clone" => "Cloner",
"renew" => "Renouveler",
"calculate_next_payment_date" => "Calculer la date du prochain paiement",
// Formulaire d'abonnement
"add_subscription" => "Ajouter un abonnement",
"edit_subscription" => "Modifier l'abonnement",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Tidak ada langganan yang cocok",
"clone" => "Duplikat",
"renew" => "Perpanjang",
"calculate_next_payment_date" => "Hitung Tanggal Pembayaran Berikutnya",
// Subscription form
"add_subscription" => "Tambahkan langganan",
"edit_subscription" => "Edit langganan",

View File

@@ -83,6 +83,7 @@ $i18n = [
"no_matching_subscriptions" => 'Nessun abbonamento corrispondente',
"clone" => "Clona",
"renew" => "Rinnova",
"calculate_next_payment_date" => "Calcola la prossima data di pagamento",
// Add/Edit Subscription
"add_subscription" => 'Aggiungi abbonamento',

View File

@@ -6,7 +6,7 @@ $i18n = [
"username" => "ユーザー名",
"password" => "パスワード",
"email" => "メール",
"firstname" => "ファーストネーム",
"firstname" => "",
"lastname" => "苗字",
"confirm_password" => "パスワードの確認",
"main_currency" => "主要通貨",
@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "一致する定期購入がありません",
"clone" => "複製",
"renew" => "更新",
"calculate_next_payment_date" => "次回支払い日を計算",
// Subscription form
"add_subscription" => "定期購入の追加",
"edit_subscription" => "定期購入の編集",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "해당하는 구독이 없습니다.",
"clone" => "복제",
"renew" => "갱신",
"calculate_next_payment_date" => "다음 결제일 계산",
// Subscription form
"add_subscription" => "구독 추가",
"edit_subscription" => "구독 편집",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Geen overeenkomende abonnementen",
"clone" => "Klonen",
"renew" => "Verlengen",
"calculate_next_payment_date" => "Bereken volgende betalingsdatum",
// Subscription form
"add_subscription" => "Abonnement toevoegen",
"edit_subscription" => "Abonnement bewerken",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Brak pasujących subskrypcji",
"clone" => "Klonuj",
"renew" => "Odnów",
"calculate_next_payment_date" => "Oblicz datę następnej płatności",
// Subscription form
"add_subscription" => "Dodaj subskrypcję",
"edit_subscription" => "Edytuj subskrypcję",

View File

@@ -7,7 +7,7 @@ $i18n = [
"password" => "Password",
"email" => "Email",
"firstname" => "Nome próprio",
"lastname" => "Último nome",
"lastname" => "Apelido",
"confirm_password" => "Confirmar Password",
"main_currency" => "Moeda Principal",
"language" => "Linguagem",
@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Sem subscrições correspondentes",
"clone" => "Clonar",
"renew" => "Renovar",
"calculate_next_payment_date" => "Calcular Próxima Data de Pagamento",
// Subscription form
"add_subscription" => "Adicionar subscrição",
"edit_subscription" => "Modificar subscrição",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Nenhuma assinatura encontrada",
"clone" => "Clonar",
"renew" => "Renovar",
"calculate_next_payment_date" => "Calcular próxima data de pagamento",
// Subscription form
"add_subscription" => "Adicionar assinatura",
"edit_subscription" => "Editar assinatura",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Нет подходящих подписок",
"clone" => "Клонировать",
"renew" => "Продлить",
"calculate_next_payment_date" => "Рассчитать дату следующего платежа",
// Subscription form
"add_subscription" => "Добавить подписку",
"edit_subscription" => "Изменить подписку",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Ni ustreznih naročnin",
"clone" => "Klon",
"renew" => "Obnovi",
"calculate_next_payment_date" => "Izračunaj datum naslednjega plačila",
// Subscription form
"add_subscription" => "Dodaj naročnino",
"edit_subscription" => "Uredi naročnino",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Нема подударајућих претплата",
"clone" => "Клонирај",
"renew" => "Обнови",
"calculate_next_payment_date" => "Израчунајте датум следеће уплате",
// Форма за претплату
"add_subscription" => "Додај претплату",
"edit_subscription" => "Уреди претплату",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Nema podudarajućih pretplata",
"clone" => "Kloniraj",
"renew" => "Obnovi",
"calculate_next_payment_date" => "Izračunaj datum sledeće uplate",
// Forma za pretplatu
"add_subscription" => "Dodaj pretplatu",
"edit_subscription" => "Uredi pretplatu",

View File

@@ -6,7 +6,7 @@ $i18n = [
"username" => "Kullanıcı Adı",
"password" => "Şifre",
"email" => "E-posta",
"firstname" => "İlk adı",
"firstname" => "Ad",
"lastname" => "Soy isim",
"confirm_password" => "Şifreyi Onayla",
"main_currency" => "Ana Para Birimi",
@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Eşleşen abonelik bulunamadı",
"clone" => "Kopyala",
"renew" => "Yenile",
"calculate_next_payment_date" => "Sonraki ödeme tarihini hesapla",
// Subscription form
"add_subscription" => "Abonelik ekle",
"edit_subscription" => "Aboneliği düzenle",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Немає відповідних підписок",
"clone" => "Клонувати",
"renew" => "Продовжити",
"calculate_next_payment_date" => "Розрахувати дату наступного платежу",
// Subscription form
"add_subscription" => "Додати підписку",
"edit_subscription" => "Змінити підписку",

View File

@@ -6,7 +6,7 @@ $i18n = [
"username" => "Tên người dùng",
"password" => "Mật khẩu",
"email" => "Email",
"firstname" => "Tên đầu tiên",
"firstname" => "Tên",
"lastname" => "Họ",
"confirm_password" => "Xác nhận mật khẩu",
"main_currency" => "Tiền tệ chính",
@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "Không có đăng ký phù hợp",
"clone" => "Nhân bản",
"renew" => "Gia hạn",
"calculate_next_payment_date" => "Tính toán ngày thanh toán tiếp theo",
// Subscription form
"add_subscription" => "Thêm đăng ký",
"edit_subscription" => "Chỉnh sửa đăng ký",

View File

@@ -83,6 +83,7 @@ $i18n = [
"no_matching_subscriptions" => "没有匹配的订阅",
"clone" => "克隆",
"renew" => "续订",
"calculate_next_payment_date" => "计算下次支付日期",
// 订阅表单
"add_subscription" => "添加订阅",

View File

@@ -79,6 +79,7 @@ $i18n = [
"no_matching_subscriptions" => "沒有符合的訂閱服務",
"clone" => "複製",
"renew" => "續訂",
"calculate_next_payment_date" => "計算下次付款日期",
// 訂閱表單
"add_subscription" => "新增訂閱服務",
"edit_subscription" => "編輯訂閱服務",

View File

@@ -1,3 +1,3 @@
<?php
$version = "v3.1.1";
$version = "v3.2.0";
?>

View File

@@ -387,12 +387,21 @@ $headerClass = count($subscriptions) > 0 ? "main-actions" : "main-actions hidden
<label for="start_date"><?= translate('start_date', $i18n) ?></label>
<input type="date" id="start_date" name="start_date">
</div>
<div id="autofill-next-payment-button" class="image-button medium disabled autofill-next-payment-button" title="<?= translate('autofill_next_payment', $i18n) ?>" onClick="autoFillNextPaymentDate()">
<i class="fa-solid fa-wand-magic-sparkles"></i>
</div>
<button type="button" id="autofill-next-payment-button" class="button secondary-button autofill-next-payment hideOnMobile"
title="<?= translate('calculate_next_payment_date', $i18n) ?>" onClick="autoFillNextPaymentDate(event)">
<i class="fa-solid fa-wand-magic-sparkles"></i>
</button>
<div class="split50">
<label for="next_payment"><?= translate('next_payment', $i18n) ?></label>
<input type="date" id="next_payment" name="next_payment" required>
<label for="next_payment" class="split-label">
<?= translate('next_payment', $i18n) ?>
<div id="autofill-next-payment-button" class="autofill-next-payment hideOnDesktop"
title="<?= translate('calculate_next_payment_date', $i18n) ?>" onClick="autoFillNextPaymentDate(event)">
<i class="fa-solid fa-wand-magic-sparkles"></i>
</div>
</label>
<div>
<input type="date" id="next_payment" name="next_payment" required>
</div>
</div>
</div>
</div>

View File

@@ -358,11 +358,11 @@ if (isset($_POST['username'])) {
</div>
<div class="form-group">
<label for="firstname"><?= translate('firstname', $i18n) ?>:</label>
<input type="text" id="firstname" name="firstname" required>
<input type="text" id="firstname" name="firstname">
</div>
<div class="form-group">
<label for="lastname"><?= translate('lastname', $i18n) ?>:</label>
<input type="text" id="lastname" name="lastname" required>
<input type="text" id="lastname" name="lastname">
</div>
<div class="form-group">
<label for="email"><?= translate('email', $i18n) ?>:</label>
@@ -456,6 +456,12 @@ if (isset($_POST['username'])) {
accept=".zip">
</div>
<?php
} else {
?>
<div class="separator">
<input id="goToLoginButton" type="button" class="secondary-button" value="<?= translate('login', $i18n) ?>">
</div>
<?php
}
?>
</section>

View File

@@ -799,11 +799,18 @@ function swipeHintAnimation() {
}
}
function autoFillNextPaymentDate() {
function autoFillNextPaymentDate(e) {
e.preventDefault();
const frequencySelect = document.querySelector("#frequency");
const cycleSelect = document.querySelector("#cycle");
const startDate = document.querySelector("#start_date");
const nextPayment = document.querySelector("#next_payment");
// Do nothing if frequency, cycle, or start date is not set
if (!frequencySelect.value || !cycleSelect.value || !startDate.value || isNaN(Date.parse(startDate.value))) {
console.log(frequencySelect.value, cycleSelect.value, startDate.value);
return;
}
const today = new Date();
const cycle = cycleSelect.value;

View File

@@ -176,9 +176,19 @@ function checkThemeNeedsUpdate() {
}
}
function enableGoToLoginButton() {
const goToLoginButton = document.getElementById('goToLoginButton');
if (goToLoginButton) {
goToLoginButton.addEventListener('click', function () {
window.location.href = 'login.php';
});
}
}
window.onload = function () {
restoreFormFields();
removeFromStorage();
runDatabaseMigration();
checkThemeNeedsUpdate();
enableGoToLoginButton();
};

View File

@@ -1280,6 +1280,12 @@ label {
cursor: pointer;
}
label.split-label {
display: flex;
flex-direction: row;
justify-content: space-between;
}
input {
box-sizing: border-box;
}
@@ -2832,6 +2838,27 @@ input[type="radio"]:checked+label::after {
}
}
.autofill-next-payment-button {
margin-top: 20px;
.button.autofill-next-payment {
padding: 15px 15px !important;
margin-top: 22px;
}
.autofill-next-payment {
color: var(--main-color);
cursor: pointer;
}
.autofill-next-payment.hideOnDesktop {
display: none;
}
@media (max-width: 768px) {
.button.autofill-next-payment.hideOnMobile {
display: none !important;
}
.autofill-next-payment.hideOnDesktop {
display: block;
}
}