feat: add romanian translations (#1017)

fix: ai recommendation numbering when deleting a recommendation
feat: mask ai api key on the settings page
fix: unicode character on the css file
fix: retain first and last name when switching language during registration
fix: calendar ocurrences to respect subscriptions start date
fix: ssrf vulnerability on several endpoints
fix: logo search
fix: xss vulnerability on payment method rename endpoint
fix: set login cookie to httponly
This commit is contained in:
Miguel Ribeiro
2026-03-19 00:41:55 +00:00
committed by GitHub
parent 69613766bc
commit e87387f0eb
20 changed files with 834 additions and 546 deletions

View File

@@ -2,6 +2,7 @@
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$chatgptModelsApiUrl = 'https://api.openai.com/v1/models';
$geminiModelsApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
@@ -59,6 +60,21 @@ if ($aiType === 'chatgpt') {
echo json_encode($response);
exit;
}
// Scheme check
$parsedUrl = parse_url($aiOllamaHost);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($aiOllamaHost, FILTER_VALIDATE_URL)
) {
echo json_encode(["success" => false, "message" => translate('invalid_host', $i18n)]);
exit;
}
// SSRF check — dies automatically if private IP not in allowlist
$ssrf = validate_webhook_url_for_ssrf($aiOllamaHost, $db, $i18n);
$apiUrl = $aiOllamaHost . '/api/tags';
}
// Initialize cURL

View File

@@ -2,6 +2,7 @@
set_time_limit(300);
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
function getPricePerMonth($cycle, $frequency, $price)
{
@@ -80,7 +81,19 @@ if ($type == 'ollama') {
echo json_encode($response);
exit;
}
$parsedUrl = parse_url($host);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($host, FILTER_VALIDATE_URL)
) {
echo json_encode(["success" => false, "message" => translate('invalid_host', $i18n)]);
exit;
}
$ssrf = validate_webhook_url_for_ssrf($host, $db, $i18n);
} else {
$ssrf = null;
$apiKey = isset($aiSettings['api_key']) ? $aiSettings['api_key'] : '';
if (empty($apiKey)) {
$response = [
@@ -216,6 +229,7 @@ $ch = curl_init();
if ($type === 'ollama') {
curl_setopt($ch, CURLOPT_URL, $host . '/api/generate');
curl_setopt($ch, CURLOPT_RESOLVE, ["{$ssrf['host']}:{$ssrf['port']}:{$ssrf['ip']}"]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['model' => $model, 'prompt' => $prompt, 'stream' => false]));
} else {

View File

@@ -1,6 +1,7 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$input = file_get_contents('php://input');
$data = json_decode($input, true);
@@ -49,6 +50,18 @@ if (empty($aiModel)) {
if ($aiType === 'ollama') {
$aiApiKey = ''; // Ollama does not require an API key
$parsedUrl = parse_url($aiOllamaHost);
if (
!isset($parsedUrl['scheme']) ||
!in_array(strtolower($parsedUrl['scheme']), ['http', 'https']) ||
!filter_var($aiOllamaHost, FILTER_VALIDATE_URL)
) {
echo json_encode(["success" => false, "message" => translate('invalid_host', $i18n)]);
exit;
}
// SSRF check — dies automatically if private IP not in allowlist
validate_webhook_url_for_ssrf($aiOllamaHost, $db, $i18n);
} else {
$aiOllamaHost = ''; // Clear Ollama host if not using Ollama
}

View File

@@ -5,6 +5,7 @@ use PHPMailer\PHPMailer\Exception;
require_once 'validate.php';
require_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';
require_once __DIR__ . '/../../includes/ssrf_helper.php';
require __DIR__ . '/../../libs/PHPMailer/PHPMailer.php';
require __DIR__ . '/../../libs/PHPMailer/SMTP.php';
@@ -273,107 +274,117 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {
// Discord notifications if enabled
if ($discordNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($discord['webhook_url'], $db);
if (!$ssrf) {
echo "Discord notification skipped: URL failed SSRF validation.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$title = translate('wallos_notification', $i18n);
$title = translate('wallos_notification', $i18n);
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for cancellation:\n";
} else {
$message = "The following subscriptions are up for cancellation:\n";
}
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for cancellation:\n";
} else {
$message = "The following subscriptions are up for cancellation:\n";
}
foreach ($perUser as $subscription) {
$message .= $subscription['name'] . " for " . $subscription['price'] . "\n";
}
foreach ($perUser as $subscription) {
$message .= $subscription['name'] . " for " . $subscription['price'] . "\n";
}
$postfields = [
'content' => $message
];
$postfields = [
'content' => $message
];
if (!empty($discord['bot_username'])) {
$postfields['username'] = $discord['bot_username'];
}
if (!empty($discord['bot_username'])) {
$postfields['username'] = $discord['bot_username'];
}
if (!empty($discord['bot_avatar_url'])) {
$postfields['avatar_url'] = $discord['bot_avatar_url'];
}
if (!empty($discord['bot_avatar_url'])) {
$postfields['avatar_url'] = $discord['bot_avatar_url'];
}
$ch = curl_init();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$response = curl_exec($ch);
curl_close($ch);
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Discord Notifications sent<br />";
if ($response === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Discord Notifications sent<br />";
}
}
}
}
// Gotify notifications if enabled
if ($gotifyNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($gotify['serverUrl'], $db);
if (!$ssrf) {
echo "Gotify notification skipped: URL failed SSRF validation.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for cancellation:\n";
} else {
$message = "The following subscriptions are up for cancellation:\n";
}
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for cancellation:\n";
} else {
$message = "The following subscriptions are up for cancellation:\n";
}
foreach ($perUser as $subscription) {
$message .= $subscription['name'] . " for " . $subscription['price'] . "\n";
}
foreach ($perUser as $subscription) {
$message .= $subscription['name'] . " for " . $subscription['price'] . "\n";
}
$data = array(
'message' => $message,
'priority' => 5
);
$data = array(
'message' => $message,
'priority' => 5
);
$data_string = json_encode($data);
$data_string = json_encode($data);
$ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data_string)
)
);
$ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data_string)
)
);
if ($gotify['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
if ($gotify['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
$result = curl_exec($ch);
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Gotify Notifications sent<br />";
$result = curl_exec($ch);
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Gotify Notifications sent<br />";
}
}
}
}
@@ -469,112 +480,122 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {
// Ntfy notifications if enabled
if ($ntfyNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($ntfy['host'], $db);
if (!$ssrf) {
echo "Ntfy notification skipped: URL failed SSRF validation.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for cancellation:\n";
} else {
$message = "The following subscriptions are up for cancellation:\n";
}
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for cancellation:\n";
} else {
$message = "The following subscriptions are up for cancellation:\n";
}
foreach ($perUser as $subscription) {
$message .= $subscription['name'] . " for " . $subscription['price'] . "\n";
}
foreach ($perUser as $subscription) {
$message .= $subscription['name'] . " for " . $subscription['price'] . "\n";
}
$headers = json_decode($ntfy["headers"], true);
$customheaders = array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers);
$headers = json_decode($ntfy["headers"], true);
$customheaders = array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers);
$ch = curl_init();
$ch = curl_init();
$ntfyHost = rtrim($ntfy["host"], '/');
$ntfyTopic = $ntfy['topic'];
$ntfyHost = rtrim($ntfy["host"], '/');
$ntfyTopic = $ntfy['topic'];
curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($ntfy['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
if ($ntfy['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
$response = curl_exec($ch);
curl_close($ch);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Ntfy Notifications sent<br />";
if ($response === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Ntfy Notifications sent<br />";
}
}
}
}
// Webhook notifications if enabled
if ($webhookNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$payer = $user['name'];
}
foreach ($perUser as $subscription) {
// Ensure the payload is reset for each subscription
$payload = $webhook['cancelation_payload'];
$payload = str_replace("{{subscription_name}}", $subscription['name'], $payload);
$payload = str_replace("{{subscription_price}}", $subscription['price'], $payload);
$payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload);
$payload = str_replace("{{subscription_category}}", $subscription['category'], $payload);
$payload = str_replace("{{subscription_payer}}", $payer, $payload);
$payload = str_replace("{{subscription_date}}", $subscription['date'], $payload);
$payload = str_replace("{{subscription_url}}", $subscription['url'], $payload);
$payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload);
// Initialize cURL for each subscription
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook['url']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Add headers if they exist
if (!empty($webhook['headers'])) {
$customheaders = preg_split("/\r\n|\n|\r/", $webhook['headers']);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
$ssrf = is_url_safe_for_ssrf($webhook['url'], $db);
if (!$ssrf) {
echo "Webhook notification skipped: URL failed SSRF validation.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$payer = $user['name'];
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Handle SSL settings
if ($webhook['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
foreach ($perUser as $subscription) {
// Ensure the payload is reset for each subscription
$payload = $webhook['cancelation_payload'];
$payload = str_replace("{{subscription_name}}", $subscription['name'], $payload);
$payload = str_replace("{{subscription_price}}", $subscription['price'], $payload);
$payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload);
$payload = str_replace("{{subscription_category}}", $subscription['category'], $payload);
$payload = str_replace("{{subscription_payer}}", $payer, $payload);
$payload = str_replace("{{subscription_date}}", $subscription['date'], $payload);
$payload = str_replace("{{subscription_url}}", $subscription['url'], $payload);
$payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload);
// Initialize cURL for each subscription
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook['url']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Add headers if they exist
if (!empty($webhook['headers'])) {
$customheaders = preg_split("/\r\n|\n|\r/", $webhook['headers']);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Handle SSL settings
if ($webhook['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
// Execute the cURL request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode >= 400) {
echo "Error sending cancellation notifications: " . curl_error($ch) . "<br />";
} else {
echo "Webhook Cancellation Notification sent for subscription: " . $subscription['name'] . "<br />";
}
usleep(1000000); // 1s delay between requests
}
// Execute the cURL request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode >= 400) {
echo "Error sending cancellation notifications: " . curl_error($ch) . "<br />";
} else {
echo "Webhook Cancellation Notification sent for subscription: " . $subscription['name'] . "<br />";
}
usleep(1000000); // 1s delay between requests
}
}
}

View File

@@ -5,6 +5,7 @@ use PHPMailer\PHPMailer\Exception;
require_once 'validate.php';
require_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';
require_once __DIR__ . '/../../includes/ssrf_helper.php';
require __DIR__ . '/../../libs/PHPMailer/PHPMailer.php';
require __DIR__ . '/../../libs/PHPMailer/SMTP.php';
@@ -376,109 +377,119 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {
// Discord notifications if enabled
if ($discordNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($discord['webhook_url'], $db);
if (!$ssrf) {
echo "SSRF attempt detected for Discord webhook URL. Notifications not sent.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$title = translate('wallos_notification', $i18n);
$title = translate('wallos_notification', $i18n);
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$message = "The following subscriptions are up for renewal:\n";
}
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$message = "The following subscriptions are up for renewal:\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
$postfields = [
'content' => $message
];
$postfields = [
'content' => $message
];
if (!empty($discord['bot_username'])) {
$postfields['username'] = $discord['bot_username'];
}
if (!empty($discord['bot_username'])) {
$postfields['username'] = $discord['bot_username'];
}
if (!empty($discord['bot_avatar_url'])) {
$postfields['avatar_url'] = $discord['bot_avatar_url'];
}
if (!empty($discord['bot_avatar_url'])) {
$postfields['avatar_url'] = $discord['bot_avatar_url'];
}
$ch = curl_init();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$response = curl_exec($ch);
curl_close($ch);
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Discord Notifications sent<br />";
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Discord Notifications sent<br />";
}
}
}
}
// Gotify notifications if enabled
if ($gotifyNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($gotify['serverUrl'], $db);
if (!$ssrf) {
echo "SSRF attempt detected for Gotify server URL. Notifications not sent.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$message = "The following subscriptions are up for renewal:\n";
}
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$message = "The following subscriptions are up for renewal:\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
$data = array(
'message' => $message,
'priority' => 5
);
$data = array(
'message' => $message,
'priority' => 5
);
$data_string = json_encode($data);
$data_string = json_encode($data);
$ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data_string)
)
);
$ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data_string)
)
);
if ($gotify['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
if ($gotify['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
$result = curl_exec($ch);
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Gotify Notifications sent<br />";
$result = curl_exec($ch);
if ($result === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Gotify Notifications sent<br />";
}
}
}
}
@@ -597,64 +608,69 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {
// Mattermost notifications if enabled
if ($mattermostNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($mattermost['webhook_url'], $db);
if (!$ssrf) {
echo "SSRF attempt detected for Mattermost webhook URL. Notifications not sent.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
// Build Message Content
$messageContent = "";
if ($user['name']) {
$messageContent = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$messageContent = "The following subscriptions are up for renewal:\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$messageContent .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
// Prepare Mattermost Data
$webhook_url = $mattermost['webhook_url'];
$data = array(
'username' => $mattermost['bot_username'],
'icon_emoji' => $mattermost['bot_icon_emoji'],
'text' => mb_convert_encoding($messageContent, 'UTF-8', 'auto'),
);
$data_string = json_encode($data);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json'
),
);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($result === false) {
echo "Error sending Mattermost notifications: " . curl_error($ch) . "<br />";
} else {
$resultData = json_decode($result, true);
if (isset($resultData['code']) && $resultData['code'] == 200) {
echo "Mattermost Notifications sent successfully<br />";
// Build Message Content
$messageContent = "";
if ($user['name']) {
$messageContent = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$errorMsg = isset($resultData['msg']) ? $resultData['msg'] : 'Unknown error';
echo "Mattermost API error: " . $errorMsg . "<br />";
$messageContent = "The following subscriptions are up for renewal:\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$messageContent .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
// Prepare Mattermost Data
$webhook_url = $mattermost['webhook_url'];
$data = array(
'username' => $mattermost['bot_username'],
'icon_emoji' => $mattermost['bot_icon_emoji'],
'text' => mb_convert_encoding($messageContent, 'UTF-8', 'auto'),
);
$data_string = json_encode($data);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json'
),
);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($result === false) {
echo "Error sending Mattermost notifications: " . curl_error($ch) . "<br />";
} else {
$resultData = json_decode($result, true);
if (isset($resultData['code']) && $resultData['code'] == 200) {
echo "Mattermost Notifications sent successfully<br />";
} else {
$errorMsg = isset($resultData['msg']) ? $resultData['msg'] : 'Unknown error';
echo "Mattermost API error: " . $errorMsg . "<br />";
}
}
curl_close($ch);
}
curl_close($ch);
}
}
@@ -702,119 +718,129 @@ while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) {
// Ntfy notifications if enabled
if ($ntfyNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$ssrf = is_url_safe_for_ssrf($ntfy['host'], $db);
if (!$ssrf) {
echo "SSRF attempt detected for Ntfy host URL. Notifications not sent.<br />";
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$message = "The following subscriptions are up for renewal:\n";
}
if ($user['name']) {
$message = $user['name'] . ", the following subscriptions are up for renewal:\n";
} else {
$message = "The following subscriptions are up for renewal:\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
foreach ($perUser as $subscription) {
$dayText = getDaysText($subscription['days']);
$message .= $subscription['name'] . " for " . $subscription['formatted_price'] . " (" . $dayText . ")\n";
}
$headers = json_decode($ntfy["headers"], true);
$customheaders = [];
$headers = json_decode($ntfy["headers"], true);
$customheaders = [];
if (is_array($headers)) {
$customheaders = array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers);
}
if (is_array($headers)) {
$customheaders = array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers);
}
$ch = curl_init();
$ch = curl_init();
$ntfyHost = rtrim($ntfy["host"], '/');
$ntfyTopic = $ntfy['topic'];
$ntfyHost = rtrim($ntfy["host"], '/');
$ntfyTopic = $ntfy['topic'];
curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($ntfy['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
if ($ntfy['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
$response = curl_exec($ch);
curl_close($ch);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Ntfy Notifications sent<br />";
if ($response === false) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Ntfy Notifications sent<br />";
}
}
}
}
// Webhook notifications if enabled
if ($webhookNotificationsEnabled) {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$payer = $user['name'];
}
foreach ($perUser as $subscription) {
// Ensure the payload is reset for each subscription
$payload = $webhook['payload'];
$payload = str_replace("{{days_until}}", $days, $payload);
$payload = str_replace("{{subscription_name}}", $subscription['name'], $payload);
$payload = str_replace("{{subscription_price}}", $subscription['formatted_price'], $payload);
$payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload);
$payload = str_replace("{{subscription_category}}", $subscription['category'], $payload);
$payload = str_replace("{{subscription_payer}}", $payer, $payload); // Use $payer instead of $subscription['payer']
$payload = str_replace("{{subscription_date}}", $subscription['date'], $payload);
$payload = str_replace("{{subscription_days_until_payment}}", $subscription['days'], $payload);
$payload = str_replace("{{subscription_url}}", $subscription['url'], $payload);
$payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload);
// Initialize cURL for each subscription
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook['url']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Add headers if they exist
if (!empty($webhook['headers'])) {
$customheaders = json_decode($webhook["headers"], true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
$ssrf = is_url_safe_for_ssrf($webhook['url'], $db);
if (!$ssrf) {
echo "SSRF attempt detected for webhook URL. Notifications not sent.<br />";;
} else {
foreach ($notify as $userId => $perUser) {
// Get name of user from household table
$stmt = $db->prepare('SELECT * FROM household WHERE id = :userId');
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user['name']) {
$payer = $user['name'];
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Handle SSL settings
if ($webhook['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
foreach ($perUser as $subscription) {
// Ensure the payload is reset for each subscription
$payload = $webhook['payload'];
$payload = str_replace("{{days_until}}", $days, $payload);
$payload = str_replace("{{subscription_name}}", $subscription['name'], $payload);
$payload = str_replace("{{subscription_price}}", $subscription['formatted_price'], $payload);
$payload = str_replace("{{subscription_currency}}", $subscription['currency'], $payload);
$payload = str_replace("{{subscription_category}}", $subscription['category'], $payload);
$payload = str_replace("{{subscription_payer}}", $payer, $payload); // Use $payer instead of $subscription['payer']
$payload = str_replace("{{subscription_date}}", $subscription['date'], $payload);
$payload = str_replace("{{subscription_days_until_payment}}", $subscription['days'], $payload);
$payload = str_replace("{{subscription_url}}", $subscription['url'], $payload);
$payload = str_replace("{{subscription_notes}}", $subscription['notes'], $payload);
// Initialize cURL for each subscription
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $webhook['url']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $webhook['request_method']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
// Add headers if they exist
if (!empty($webhook['headers'])) {
$customheaders = json_decode($webhook["headers"], true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Handle SSL settings
if ($webhook['ignore_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
// Execute the cURL request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode >= 400) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Webhook Notification sent for subscription: " . $subscription['name'] . "<br />";
}
usleep(1000000); // 1s delay between requests
}
// Execute the cURL request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode >= 400) {
echo "Error sending notifications: " . curl_error($ch) . "<br />";
} else {
echo "Webhook Notification sent for subscription: " . $subscription['name'] . "<br />";
}
usleep(1000000); // 1s delay between requests
}
}
}

View File

@@ -2,83 +2,121 @@
if (isset($_GET['search'])) {
$searchTerm = urlencode($_GET['search'] . " logo");
$url = "https://www.google.com/search?q={$searchTerm}&tbm=isch&tbs=iar:xw,ift:png";
$backupUrl = "https://search.brave.com/search?q={$searchTerm}";
function applyProxy($ch) {
$proxy = getenv('https_proxy')
?: getenv('HTTPS_PROXY')
?: getenv('http_proxy')
?: getenv('HTTP_PROXY')
?: null;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Convert all environment variable keys to lowercase
$envVars = array_change_key_case($_SERVER, CASE_LOWER);
// Check for http_proxy or https_proxy environment variables
$httpProxy = isset($envVars['http_proxy']) ? $envVars['http_proxy'] : null;
$httpsProxy = isset($envVars['https_proxy']) ? $envVars['https_proxy'] : null;
if (!empty($httpProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpProxy);
} elseif (!empty($httpsProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpsProxy);
if ($proxy) {
curl_setopt($ch, CURLOPT_PROXY, $proxy);
}
}
$response = curl_exec($ch);
if ($response === false) {
// If cURL fails to access google images, use brave image search as a backup
function curlGet($url, $headers = []) {
$allowedHosts = ['duckduckgo.com', 'search.brave.com'];
$host = parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowedHosts)) return null;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $backupUrl);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$envVars = array_change_key_case($_SERVER, CASE_LOWER);
$httpProxy = isset($envVars['http_proxy']) ? $envVars['http_proxy'] : null;
$httpsProxy = isset($envVars['https_proxy']) ? $envVars['https_proxy'] : null;
if (!empty($httpProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpProxy);
} elseif (!empty($httpsProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpsProxy);
}
$response = curl_exec($ch);
if ($response === false) {
echo json_encode(['error' => 'Failed to fetch data from Google.']);
} else {
$imageUrls = extractImageUrlsFromPage($response);
header('Content-Type: application/json');
echo json_encode(['imageUrls' => $imageUrls]);
}
} else {
// Parse the HTML response to extract image URLs
$imageUrls = extractImageUrlsFromPage($response);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
// Explicitly disable proxy by default, then re-apply only from env (not $_SERVER)
curl_setopt($ch, CURLOPT_PROXY, '');
curl_setopt($ch, CURLOPT_NOPROXY, '*');
// Pass the image URLs to the client
header('Content-Type: application/json');
echo json_encode(['imageUrls' => $imageUrls]);
if (!empty($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
applyProxy($ch);
$response = curl_exec($ch);
curl_close($ch);
return $response ?: null;
}
curl_close($ch);
} else {
echo json_encode(['error' => 'Invalid request.']);
}
function getVqdToken($query) {
$html = curlGet("https://duckduckgo.com/?q={$query}&ia=images");
if ($html && preg_match('/vqd="?([\d-]+)"?/', $html, $matches)) {
return $matches[1];
}
return null;
}
function extractImageUrlsFromPage($html)
{
$imageUrls = [];
function fetchDDGImages($query, $vqd) {
$params = http_build_query([
'l' => 'us-en',
'o' => 'json',
'q' => urldecode($query),
'vqd' => $vqd,
'f' => ',,transparent,Wide,',
'p' => '1',
]);
$response = curlGet("https://duckduckgo.com/i.js?{$params}", [
'Accept: application/json',
'Referer: https://duckduckgo.com/',
]);
if (!$response) return null;
$data = json_decode($response, true);
if (!isset($data['results']) || empty($data['results'])) return null;
return array_column($data['results'], 'image');
}
function fetchBraveImages($query) {
$url = "https://search.brave.com/images?q={$query}";
$html = curlGet($url, [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.5',
'Referer: https://search.brave.com/',
]);
if (!$html) return null;
$doc = new DOMDocument();
@$doc->loadHTML($html);
$imageUrls = [];
$imgTags = $doc->getElementsByTagName('img');
foreach ($imgTags as $imgTag) {
$src = $imgTag->getAttribute('src');
if (!strstr($imgTag->getAttribute('class'), "favicon") && !strstr($imgTag->getAttribute('class'), "logo")) {
if (filter_var($src, FILTER_VALIDATE_URL)) {
$imageUrls[] = $src;
}
}
$class = $imgTag->getAttribute('class');
if (str_contains($class, 'favicon') || str_contains($class, 'logo')) continue;
if (!filter_var($src, FILTER_VALIDATE_URL)) continue;
if (str_contains($src, 'cdn.search.brave.com')) continue; // filter Brave UI assets
$imageUrls[] = $src;
}
return $imageUrls;
return !empty($imageUrls) ? $imageUrls : null;
}
?>
// --- Main flow ---
// Try DuckDuckGo first
$vqd = getVqdToken($searchTerm);
$imageUrls = $vqd ? fetchDDGImages($searchTerm, $vqd) : null;
// Fall back to Brave if DDG failed at any step
if (!$imageUrls) {
$imageUrls = fetchBraveImages($searchTerm);
}
header('Content-Type: application/json');
if ($imageUrls) {
echo json_encode(['imageUrls' => $imageUrls]);
} else {
echo json_encode(['error' => 'Failed to fetch images from both DuckDuckGo and Brave.']);
}
} else {
echo json_encode(['error' => 'Invalid request.']);
}
?>

View File

@@ -2,6 +2,7 @@
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
@@ -20,6 +21,8 @@ if (
$bot_username = $data["bot_username"];
$bot_avatar_url = $data["bot_avatar"];
validate_webhook_url_for_ssrf($webhook_url, $db, $i18n);
$query = "SELECT COUNT(*) FROM discord_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -1,6 +1,7 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$postData = file_get_contents("php://input");
@@ -34,6 +35,8 @@ if (
]));
}
validate_webhook_url_for_ssrf($url, $db, $i18n);
$query = "SELECT COUNT(*) FROM gotify_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -1,6 +1,7 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
@@ -17,6 +18,20 @@ if (!isset($data["webhook_url"]) || $data["webhook_url"] == "") {
$bot_username = $data["bot_username"];
$bot_iconemoji = $data["bot_icon_emoji"];
$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)
]));
}
validate_webhook_url_for_ssrf($webhook_url, $db, $i18n);
$query = "SELECT COUNT(*) FROM mattermost_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -2,6 +2,7 @@
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
@@ -36,6 +37,8 @@ if (
]));
}
validate_webhook_url_for_ssrf($url, $db, $i18n);
$query = "SELECT COUNT(*) FROM ntfy_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -1,6 +1,7 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
require_once '../../includes/ssrf_helper.php';
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
@@ -34,6 +35,8 @@ if (
]));
}
validate_webhook_url_for_ssrf($url, $db, $i18n);
$query = "SELECT COUNT(*) FROM webhook_notifications WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(":userId", $userId, SQLITE3_INTEGER);

View File

@@ -3,6 +3,15 @@
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/validate_endpoint.php';
function validate($value)
{
$value = trim($value);
$value = stripslashes($value);
$value = htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return $value;
}
if (!isset($_POST['paymentId']) || !isset($_POST['name']) || $_POST['paymentId'] === '' || $_POST['name'] === '') {
die(json_encode([
"success" => false,
@@ -11,7 +20,14 @@ if (!isset($_POST['paymentId']) || !isset($_POST['name']) || $_POST['paymentId']
}
$paymentId = $_POST['paymentId'];
$name = $_POST['name'];
$name = validate($_POST['name']);
if (strlen($name) > 255) {
die(json_encode([
"success" => false,
"message" => translate('fields_missing', $i18n)
]));
}
$sql = "UPDATE payment_methods SET name = :name WHERE id = :paymentId and user_id = :userId";
$stmt = $db->prepare($sql);
@@ -20,16 +36,11 @@ $stmt->bindParam(':paymentId', $paymentId, SQLITE3_INTEGER);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
if ($result) {
echo json_encode([
"success" => true,
"message" => translate('payment_renamed', $i18n)
]);
if ($result && $db->changes() > 0) {
echo json_encode(["success" => true, "message" => translate('payment_renamed', $i18n)]);
} else {
echo json_encode([
"success" => false,
"message" => translate('payment_not_renamed', $i18n)
]);
echo json_encode(["success" => false, "message" => translate('payment_not_renamed', $i18n)]);
}
?>

View File

@@ -1,84 +1,130 @@
<?php
if (isset($_GET['search'])) {
$searchTerm = urlencode($_GET['search'] . " logo");
function applyProxy($ch) {
$proxy = getenv('https_proxy')
?: getenv('HTTPS_PROXY')
?: getenv('http_proxy')
?: getenv('HTTP_PROXY')
?: null;
$url = "https://www.google.com/search?q={$searchTerm}&tbm=isch";
$backupUrl = "https://search.brave.com/search?q={$searchTerm}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Convert all environment variable keys to lowercase
$envVars = array_change_key_case($_SERVER, CASE_LOWER);
// Check for http_proxy or https_proxy environment variables
$httpProxy = isset($envVars['http_proxy']) ? $envVars['http_proxy'] : null;
$httpsProxy = isset($envVars['https_proxy']) ? $envVars['https_proxy'] : null;
if (!empty($httpProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpProxy);
} elseif (!empty($httpsProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpsProxy);
if ($proxy) {
curl_setopt($ch, CURLOPT_PROXY, $proxy);
}
}
$response = curl_exec($ch);
function curlGet($url, $headers = []) {
$allowedHosts = ['duckduckgo.com', 'search.brave.com'];
$host = parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowedHosts, true)) {
return null;
}
if ($response === false) {
// If cURL fails to access google images, use brave image search as a backup
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $backupUrl);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$envVars = array_change_key_case($_SERVER, CASE_LOWER);
$httpProxy = isset($envVars['http_proxy']) ? $envVars['http_proxy'] : null;
$httpsProxy = isset($envVars['https_proxy']) ? $envVars['https_proxy'] : null;
if (!empty($httpProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpProxy);
} elseif (!empty($httpsProxy)) {
curl_setopt($ch, CURLOPT_PROXY, $httpsProxy);
}
$response = curl_exec($ch);
if ($response === false) {
echo json_encode(['error' => 'Failed to fetch data from Google.']);
} else {
$imageUrls = extractImageUrlsFromPage($response);
header('Content-Type: application/json');
echo json_encode(['imageUrls' => $imageUrls]);
}
} else {
// Parse the HTML response to extract image URLs
$imageUrls = extractImageUrlsFromPage($response);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36');
// Pass the image URLs to the client
header('Content-Type: application/json');
echo json_encode(['imageUrls' => $imageUrls]);
if (!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
applyProxy($ch);
$response = curl_exec($ch);
curl_close($ch);
return $response ?: null;
}
$searchTermRaw = $_GET['search'] . " logo";
$searchTerm = urlencode($searchTermRaw);
function getVqdToken($query) {
$html = curlGet("https://duckduckgo.com/?q={$query}&ia=images");
if ($html && preg_match('/vqd="?([\d-]+)"?/', $html, $matches)) {
return $matches[1];
}
return null;
}
function fetchDDGImages($query, $vqd) {
$params = http_build_query([
'l' => 'us-en',
'o' => 'json',
'q' => urldecode($query),
'vqd' => $vqd,
'f' => ',,,,', // size,color,type,layout,license → all unset
'p' => '1', // safesearch on
]);
$response = curlGet("https://duckduckgo.com/i.js?{$params}", [
'Accept: application/json',
'Referer: https://duckduckgo.com/',
]);
if (!$response) return null;
$data = json_decode($response, true);
if (!isset($data['results']) || empty($data['results'])) return null;
return array_column($data['results'], 'image');
}
function fetchBraveImages($query) {
$url = "https://search.brave.com/images?q={$query}";
$html = curlGet($url, [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.5',
'Referer: https://search.brave.com/',
]);
if (!$html) return null;
$doc = new DOMDocument();
@$doc->loadHTML($html);
$blockedDomains = ['cdn.search.brave.com', 'search.brave.com/static'];
$imageUrls = [];
$imgTags = $doc->getElementsByTagName('img');
foreach ($imgTags as $imgTag) {
$src = $imgTag->getAttribute('src');
$class = $imgTag->getAttribute('class');
if (str_contains($class, 'favicon') || str_contains($class, 'logo')) continue;
if (!filter_var($src, FILTER_VALIDATE_URL)) continue;
foreach ($blockedDomains as $blocked) {
if (str_contains($src, $blocked)) {
continue 2; // skip to next <img>
}
}
$imageUrls[] = $src;
}
return !empty($imageUrls) ? $imageUrls : null;
}
// Main flow: DDG first, Brave fallback
$vqd = getVqdToken($searchTerm);
$imageUrls = $vqd ? fetchDDGImages($searchTerm, $vqd) : null;
if (!$imageUrls) {
$imageUrls = fetchBraveImages($searchTerm);
}
header('Content-Type: application/json');
if ($imageUrls) {
echo json_encode(['imageUrls' => $imageUrls]);
} else {
echo json_encode(['error' => 'Failed to fetch images from DuckDuckGo and Brave.']);
}
curl_close($ch);
} else {
echo json_encode(['error' => 'Invalid request.']);
}
function extractImageUrlsFromPage($html)
{
$imageUrls = [];
$doc = new DOMDocument();
@$doc->loadHTML($html);
$imgTags = $doc->getElementsByTagName('img');
foreach ($imgTags as $imgTag) {
$src = $imgTag->getAttribute('src');
if (!strstr($imgTag->getAttribute('class'), "favicon") && !strstr($imgTag->getAttribute('class'), "logo")) {
if (filter_var($src, FILTER_VALIDATE_URL)) {
$imageUrls[] = $src;
}
}
}
return $imageUrls;
}
?>

View File

@@ -19,6 +19,7 @@ $languages = [
"pl" => ["name" => "Polski", "dir" => "ltr"],
"pt" => ["name" => "Português", "dir" => "ltr"],
"pt_br" => ["name" => "Português Brasileiro", "dir" => "ltr"],
"ro" => ["name" => "Română", "dir" => "ltr"],
"ru" => ["name" => "Русский", "dir" => "ltr"],
"sl" => ["name" => "Slovenščina", "dir" => "ltr"],
"sr_lat" => ["name" => "Srpski", "dir" => "ltr"],
@@ -28,8 +29,6 @@ $languages = [
"vi" => ["name" => "Tiếng Việt", "dir" => "ltr"],
"zh_cn" => ["name" => "简体中文", "dir" => "ltr"],
"zh_tw" => ["name" => "繁體中文", "dir" => "ltr"],
"ro" => ["name" => "Română", "dir" => "ltr"],
]
?>

View File

@@ -29,7 +29,8 @@ $_SESSION['token'] = $token;
$cookieValue = $username . "|" . $token . "|" . $main_currency;
setcookie('wallos_login', $cookieValue, [
'expires' => $cookieExpire,
'samesite' => 'Strict'
'samesite' => 'Strict',
'httponly' => true,
]);
// Set language cookie

View File

@@ -1,5 +1,17 @@
<?php
/**
* Checks if an IP falls in the RFC 6598 Carrier-Grade NAT range (100.64.0.0/10).
* PHP's FILTER_FLAG_NO_PRIV_RANGE does not cover this range.
* Used by Tailscale and corporate CGNAT environments.
*/
function is_cgnat_ip($ip) {
$long = ip2long($ip);
return $long !== false
&& $long >= ip2long('100.64.0.0')
&& $long <= ip2long('100.127.255.255');
}
/**
* Validates a webhook URL against SSRF attacks and checks the admin allowlist.
* If validation fails, it kills the script and outputs a JSON error response.
@@ -35,7 +47,7 @@ function validate_webhook_url_for_ssrf($url, $db, $i18n) {
$ipWithPort = $port ? $ip . ':' . $port : $ip;
// Check if it's a private IP
$is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
$is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false || is_cgnat_ip($ip);
if ($is_private) {
$stmt = $db->prepare("SELECT local_webhook_notifications_allowlist FROM admin LIMIT 1");
@@ -65,4 +77,61 @@ function validate_webhook_url_for_ssrf($url, $db, $i18n) {
'ip' => $ip,
'port' => $targetPort
];
}
}
/**
* Non-fatal variant for use in cron jobs (sendnotifications.php).
* Returns the same ['host', 'ip', 'port'] array on success, or false on failure.
* Never calls die() — caller should use continue/skip on false.
* Respects the admin allowlist for private IPs, just like the main function.
*
* @param string $url The destination URL to check
* @param SQLite3 $db The database connection
* @return array|false
*/
function is_url_safe_for_ssrf($url, $db) {
$parsedUrl = parse_url($url);
if (!$parsedUrl || !isset($parsedUrl['host'])) return false;
$scheme = strtolower($parsedUrl['scheme'] ?? '');
if (!in_array($scheme, ['http', 'https'])) return false;
$urlHost = $parsedUrl['host'];
$port = $parsedUrl['port'] ?? '';
$ip = gethostbyname($urlHost);
// DNS failure
if ($ip === $urlHost && filter_var($urlHost, FILTER_VALIDATE_IP) === false) return false;
$hostWithPort = $port ? $urlHost . ':' . $port : $urlHost;
$ipWithPort = $port ? $ip . ':' . $port : $ip;
$is_private = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|| is_cgnat_ip($ip);
if ($is_private) {
$stmt = $db->prepare("SELECT local_webhook_notifications_allowlist FROM admin LIMIT 1");
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$allowlist_str = $row ? $row['local_webhook_notifications_allowlist'] : '';
$allowlist = array_filter(array_map('trim', explode(',', $allowlist_str)));
if (
!in_array($urlHost, $allowlist) &&
!in_array($ip, $allowlist) &&
!in_array($hostWithPort, $allowlist) &&
!in_array($ipWithPort, $allowlist)
) {
return false; // private and not in allowlist — skip silently
}
}
$targetPort = $port ?: ($scheme === 'https' ? 443 : 80);
return [
'host' => $urlHost,
'ip' => $ip,
'port' => $targetPort
];
}

View File

@@ -1,3 +1,3 @@
<?php
$version = "v4.6.2";
$version = "v4.7.0";
?>

View File

@@ -72,13 +72,14 @@ if ($adminRow['login_disabled'] == 1) {
$settings = $result->fetchArray(SQLITE3_ASSOC);
setcookie('colorTheme', $settings['color_theme'], [
'expires' => $cookieExpire,
'samesite' => 'Strict'
'samesite' => 'Strict',
]);
$cookieValue = $username . "|" . "abc123ABC" . "|" . $main_currency;
setcookie('wallos_login', $cookieValue, [
'expires' => $cookieExpire,
'samesite' => 'Strict'
'samesite' => 'Strict',
'httponly' => true,
]);
$db->close();
@@ -198,7 +199,8 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
$cookieValue = $username . "|" . $token . "|" . $main_currency;
setcookie('wallos_login', $cookieValue, [
'expires' => $cookieExpire,
'samesite' => 'Strict'
'samesite' => 'Strict',
'httponly' => true,
]);
}

View File

@@ -1097,14 +1097,13 @@ $userData['currency_symbol'] = $currencies[$main_currency]['symbol'];
class="<?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>"
placeholder="<?= translate('api_key', $i18n) ?>"
value="<?= isset($aiSettings['api_key']) ? htmlspecialchars($aiSettings['api_key']) : '' ?>" />
<button type="button" id="toggleAiApiKey" class="button tiny <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>" onclick="toggleAiApiKeyVisibility()" aria-label="Toggle API key visibility">
<i class="fa-solid fa-eye"></i>
</button>
<input type="text" id="ai_ollama_host" name="ai_ollama_host" autocomplete="off"
class="<?= (!isset($aiSettings['type']) || $aiSettings['type'] != 'ollama') ? 'hidden' : '' ?>"
placeholder="<?= translate('host', $i18n) ?>"
value="<?= isset($aiSettings['url']) ? htmlspecialchars($aiSettings['url']) : '' ?>" />
<button type="button" id="toggleAiApiKey" class="button secondary-button icon-button <?= (isset($aiSettings['type']) && $aiSettings['type'] == 'ollama') ? 'hidden' : '' ?>" onclick="toggleAiApiKeyVisibility()" aria-label="Toggle API key visibility">
<i class="fa-solid fa-eye"></i>
</button>
<button type="button" id="fetchModelsButton" class="button thin" onclick="fetch_ai_models()">
<?= translate('test', $i18n) ?>
</button>

View File

@@ -28,6 +28,7 @@ textarea {
font-weight: 400;
}
button.hidden,
input.hidden {
display: none;
}
@@ -281,6 +282,11 @@ main>.contain {
font-size: 12px;
}
.button.icon-button,
.button-secondary.icon-button {
padding: 15px;
}
button:hover svg .main-color {
fill: var(--hover-color);
}