mirror of
https://github.com/ellite/Wallos.git
synced 2026-04-18 14:09:13 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.']);
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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"],
|
||||
|
||||
]
|
||||
|
||||
?>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
$version = "v4.6.2";
|
||||
$version = "v4.7.0";
|
||||
?>
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user