+
attributeBool('mark_as_read') ? 'checked="checked"' : '' ?> />
diff --git a/xExtension-Webhook/extension.php b/xExtension-Webhook/extension.php
index e4f13aee..753cd79e 100644
--- a/xExtension-Webhook/extension.php
+++ b/xExtension-Webhook/extension.php
@@ -32,7 +32,7 @@ enum HTTP_METHOD: string {
* configured keyword filters.
*
* @author Lukas Melega, Ryahn
- * @version 0.2.0
+ * @version 0.3.0
* @since FreshRSS 1.20.0
*/
final class WebhookExtension extends Minz_Extension {
@@ -49,6 +49,8 @@ final class WebhookExtension extends Minz_Extension {
}';
private const DEFAULT_METHOD = HTTP_METHOD::POST;
private const DEFAULT_BODY_TYPE = BODY_TYPE::JSON;
+ private const MATCH_MODE_BASIC = 'basic';
+ private const MATCH_MODE_ADVANCED = 'advanced';
private const JSON_LOG_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
private const PLAINTEXT_MAX_LENGTH = 360;
@@ -125,13 +127,15 @@ public function processArticle($entry): FreshRSS_Entry {
return $entry;
}
- $patterns = $config['keywords'];
- if ($patterns === []) {
+ if (!$this->hasConfiguredKeywords($config)) {
logWarning($this->logsEnabled, 'No keywords defined in Webhook extension settings.');
return $entry;
}
- $matchLog = $this->findMatchLog($entry, $patterns, $config);
+ $matchLog = $config['match_mode'] === self::MATCH_MODE_ADVANCED
+ ? $this->findMatchLogAdvanced($entry, $config)
+ : $this->findMatchLogBasic($entry, $config);
+
if ($matchLog === null) {
return $entry;
}
@@ -146,12 +150,16 @@ public function processArticle($entry): FreshRSS_Entry {
}
/**
- * Try to find a pattern that matches this entry.
+ * Try to find a pattern that matches this entry in basic mode.
*
- * @param array $patterns
* @param array $config
*/
- private function findMatchLog(FreshRSS_Entry $entry, array $patterns, array $config): ?string {
+ private function findMatchLogBasic(FreshRSS_Entry $entry, array $config): ?string {
+ $patterns = $config['keywords'];
+ if ($patterns === []) {
+ return null;
+ }
+
$title = (string) $entry->title();
$link = (string) $entry->link();
$feed = $entry->feed();
@@ -197,6 +205,83 @@ private function findMatchLog(FreshRSS_Entry $entry, array $patterns, array $con
return null;
}
+ /**
+ * Try to find a pattern that matches this entry in advanced mode.
+ *
+ * @param array $config
+ */
+ private function findMatchLogAdvanced(FreshRSS_Entry $entry, array $config): ?string {
+ $title = (string) $entry->title();
+ $link = (string) $entry->link();
+ $feed = $entry->feed();
+ $feedName = (is_object($feed) && method_exists($feed, 'name')) ? (string) $feed->name() : '';
+ $authors = trim((string) $entry->authors(true));
+ $content = (string) $entry->content();
+
+ $advancedLists = [
+ 'title' => $config['keywords_title'] ?? [],
+ 'feed' => $config['keywords_feed'] ?? [],
+ 'authors' => $config['keywords_authors'] ?? [],
+ 'content' => $config['keywords_content'] ?? [],
+ ];
+
+ foreach ($advancedLists['title'] as $pattern) {
+ $normalizedPattern = $this->normalizePattern($pattern);
+ if ($normalizedPattern !== null && $this->isPatternFound($normalizedPattern, $title, $pattern)) {
+ return "Matched by title ⮕ pattern: {$pattern} ♦ title: {$title} ♦ link: {$link}";
+ }
+ }
+
+ foreach ($advancedLists['feed'] as $pattern) {
+ $normalizedPattern = $this->normalizePattern($pattern);
+ if (
+ $normalizedPattern !== null
+ && $feedName !== ''
+ && $this->isPatternFound($normalizedPattern, $feedName, $pattern)
+ ) {
+ return "Matched by feed ⮕ pattern: {$pattern} ♦ feed: {$feedName} ♦ link: {$link}";
+ }
+ }
+
+ foreach ($advancedLists['authors'] as $pattern) {
+ $normalizedPattern = $this->normalizePattern($pattern);
+ if (
+ $normalizedPattern !== null
+ && $authors !== ''
+ && $this->isPatternFound($normalizedPattern, $authors, $pattern)
+ ) {
+ return "Matched by authors ⮕ pattern: {$pattern} ♦ authors: {$authors} ♦ link: {$link}";
+ }
+ }
+
+ foreach ($advancedLists['content'] as $pattern) {
+ $normalizedPattern = $this->normalizePattern($pattern);
+ if (
+ $normalizedPattern !== null
+ && $content !== ''
+ && $this->isPatternFound($normalizedPattern, $content, $pattern)
+ ) {
+ return "Matched by content ⮕ pattern: {$pattern} ♦ title: {$title} ♦ link: {$link}";
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array $config
+ */
+ private function hasConfiguredKeywords(array $config): bool {
+ if (($config['match_mode'] ?? self::MATCH_MODE_BASIC) === self::MATCH_MODE_ADVANCED) {
+ return ($config['keywords_title'] ?? []) !== []
+ || ($config['keywords_feed'] ?? []) !== []
+ || ($config['keywords_authors'] ?? []) !== []
+ || ($config['keywords_content'] ?? []) !== [];
+ }
+
+ return ($config['keywords'] ?? []) !== [];
+ }
+
/**
* Send article data via webhook.
*
@@ -305,6 +390,26 @@ private function ensureConfigurationDefaults(): void {
$userConf->_attribute('keywords', []);
$needsSave = true;
}
+ if ($userConf->attributeString('match_mode') === null) {
+ $userConf->_attribute('match_mode', self::MATCH_MODE_BASIC);
+ $needsSave = true;
+ }
+ if ($userConf->attributeArray('keywords_title') === null) {
+ $userConf->_attribute('keywords_title', []);
+ $needsSave = true;
+ }
+ if ($userConf->attributeArray('keywords_feed') === null) {
+ $userConf->_attribute('keywords_feed', []);
+ $needsSave = true;
+ }
+ if ($userConf->attributeArray('keywords_authors') === null) {
+ $userConf->_attribute('keywords_authors', []);
+ $needsSave = true;
+ }
+ if ($userConf->attributeArray('keywords_content') === null) {
+ $userConf->_attribute('keywords_content', []);
+ $needsSave = true;
+ }
$needsSave = $this->ensureBoolDefault($userConf, 'search_in_title', true) || $needsSave;
$needsSave = $this->ensureBoolDefault($userConf, 'search_in_feed', false) || $needsSave;
@@ -360,6 +465,11 @@ private function migrateLegacyConfiguration(FreshRSS_UserConfiguration $userConf
$needsSave = false;
$map = [
'keywords' => ['type' => 'array'],
+ 'match_mode' => ['type' => 'string'],
+ 'keywords_title' => ['type' => 'array'],
+ 'keywords_feed' => ['type' => 'array'],
+ 'keywords_authors' => ['type' => 'array'],
+ 'keywords_content' => ['type' => 'array'],
'search_in_title' => ['type' => 'bool'],
'search_in_feed' => ['type' => 'bool'],
'search_in_authors' => ['type' => 'bool'],
@@ -430,14 +540,27 @@ private function getUserConf(): ?FreshRSS_UserConfiguration {
*/
private function collectConfigurationFromRequest(): array {
$keywords = $this->normalizeListInput(Minz_Request::paramTextToArray('keywords'));
+ $keywordsTitle = $this->normalizeListInput(Minz_Request::paramTextToArray('keywords_title'));
+ $keywordsFeed = $this->normalizeListInput(Minz_Request::paramTextToArray('keywords_feed'));
+ $keywordsAuthors = $this->normalizeListInput(Minz_Request::paramTextToArray('keywords_authors'));
+ $keywordsContent = $this->normalizeListInput(Minz_Request::paramTextToArray('keywords_content'));
$headers = $this->normalizeListInput(Minz_Request::paramTextToArray('webhook_headers'));
$headers = $headers === [] ? self::DEFAULT_HEADERS : $headers;
$methodValue = HTTP_METHOD::tryFrom(strtoupper(Minz_Request::paramString('webhook_method')));
$bodyTypeValue = BODY_TYPE::tryFrom(strtolower(Minz_Request::paramString('webhook_body_type')));
+ $matchMode = Minz_Request::paramString('match_mode');
+ if (!in_array($matchMode, [self::MATCH_MODE_BASIC, self::MATCH_MODE_ADVANCED], true)) {
+ $matchMode = self::MATCH_MODE_BASIC;
+ }
return [
'keywords' => $keywords,
+ 'match_mode' => $matchMode,
+ 'keywords_title' => $keywordsTitle,
+ 'keywords_feed' => $keywordsFeed,
+ 'keywords_authors' => $keywordsAuthors,
+ 'keywords_content' => $keywordsContent,
'search_in_title' => Minz_Request::paramBoolean('search_in_title'),
'search_in_feed' => Minz_Request::paramBoolean('search_in_feed'),
'search_in_authors' => Minz_Request::paramBoolean('search_in_authors'),
@@ -470,6 +593,11 @@ private function getSnapshot(): ?array {
return [
'keywords' => $this->getArrayAttribute($userConf, 'keywords', []),
+ 'match_mode' => $this->normalizeMatchMode($userConf->attributeString('match_mode')),
+ 'keywords_title' => $this->getArrayAttribute($userConf, 'keywords_title', []),
+ 'keywords_feed' => $this->getArrayAttribute($userConf, 'keywords_feed', []),
+ 'keywords_authors' => $this->getArrayAttribute($userConf, 'keywords_authors', []),
+ 'keywords_content' => $this->getArrayAttribute($userConf, 'keywords_content', []),
'search_in_title' => $this->getBoolAttribute($userConf, 'search_in_title', true),
'search_in_feed' => $this->getBoolAttribute($userConf, 'search_in_feed', false),
'search_in_authors' => $this->getBoolAttribute($userConf, 'search_in_authors', false),
@@ -493,6 +621,14 @@ private function normalizeBodyTypeValue(?string $bodyType): string {
return (BODY_TYPE::tryFrom(strtolower((string) $bodyType)) ?? self::DEFAULT_BODY_TYPE)->value;
}
+ private function normalizeMatchMode(?string $matchMode): string {
+ if ($matchMode === self::MATCH_MODE_ADVANCED) {
+ return self::MATCH_MODE_ADVANCED;
+ }
+
+ return self::MATCH_MODE_BASIC;
+ }
+
/**
* @return string[]
*/
@@ -666,6 +802,22 @@ public function getKeywordsData(): string {
return implode(PHP_EOL, $keywords);
}
+ public function getKeywordDataByField(string $field): string {
+ $config = $this->getSnapshot();
+ return match ($field) {
+ 'title' => implode(PHP_EOL, $config['keywords_title'] ?? []),
+ 'feed' => implode(PHP_EOL, $config['keywords_feed'] ?? []),
+ 'authors' => implode(PHP_EOL, $config['keywords_authors'] ?? []),
+ 'content' => implode(PHP_EOL, $config['keywords_content'] ?? []),
+ default => '',
+ };
+ }
+
+ public function getMatchMode(): string {
+ $config = $this->getSnapshot();
+ return $config['match_mode'] ?? self::MATCH_MODE_BASIC;
+ }
+
public function getWebhookHeaders(): string {
$config = $this->getSnapshot();
$headers = $config['webhook_headers'] ?? self::DEFAULT_HEADERS;
diff --git a/xExtension-Webhook/i18n/en/ext.php b/xExtension-Webhook/i18n/en/ext.php
index 2819c3d8..71cdec5b 100644
--- a/xExtension-Webhook/i18n/en/ext.php
+++ b/xExtension-Webhook/i18n/en/ext.php
@@ -9,6 +9,17 @@
'save_and_send_test_req' => 'Save and send test request',
'description' => 'Webhooks allow external services to be notified when certain events happen.\nWhen the specified events happen, we\'ll send a HTTP request (usually POST) to the URL you provide.',
'keywords' => 'Keywords in the new article',
+ 'match_mode' => 'Matching mode',
+ 'match_mode_basic' => 'Basic matching',
+ 'match_mode_advanced' => 'Advanced matching',
+ 'match_mode_description' => 'Basic mode uses one keyword list with shared scopes. Advanced mode uses dedicated keyword lists per field.',
+ 'basic_matching' => 'Basic matching',
+ 'advanced_matching' => 'Advanced matching (field-specific)',
+ 'advanced_matching_description' => 'These lists are used only when matching mode is set to Advanced.',
+ 'advanced_title' => 'Title keywords',
+ 'advanced_feed' => 'Feed keywords',
+ 'advanced_authors' => 'Author keywords',
+ 'advanced_content' => 'Content keywords',
'search_in' => 'Search in article\'s:',
'search_in_title' => 'title',
'search_in_feed' => 'feed',
diff --git a/xExtension-Webhook/metadata.json b/xExtension-Webhook/metadata.json
index 6fd13bf4..0bd5fbd5 100644
--- a/xExtension-Webhook/metadata.json
+++ b/xExtension-Webhook/metadata.json
@@ -2,7 +2,7 @@
"name": "Webhook",
"author": "Lukas Melega, Ryahn",
"description": "Send custom webhook when new article appears (and matches custom criteria)",
- "version": "0.2.0",
+ "version": "0.3.0",
"entrypoint": "Webhook",
"type": "system"
}
From 4904bf674ad0159e728d8e75866d33f19c6c4064 Mon Sep 17 00:00:00 2001
From: Jp302109 <15944448389@163.com>
Date: Sat, 7 Feb 2026 19:39:17 +0800
Subject: [PATCH 4/7] Fix webhook extension typing and request validation
---
xExtension-Webhook/extension.php | 401 ++++++++++++++++++++++---------
xExtension-Webhook/request.php | 78 +++---
2 files changed, 330 insertions(+), 149 deletions(-)
diff --git a/xExtension-Webhook/extension.php b/xExtension-Webhook/extension.php
index 753cd79e..819ba298 100644
--- a/xExtension-Webhook/extension.php
+++ b/xExtension-Webhook/extension.php
@@ -35,9 +35,10 @@ enum HTTP_METHOD: string {
* @version 0.3.0
* @since FreshRSS 1.20.0
*/
-final class WebhookExtension extends Minz_Extension {
- private const DEFAULT_URL = 'http://';
- private const DEFAULT_HEADERS = [
+ final class WebhookExtension extends Minz_Extension {
+ private const DEFAULT_URL = 'http://';
+ /** @var list */
+ private const DEFAULT_HEADERS = [
'User-Agent: FreshRSS',
'Content-Type: application/json',
];
@@ -63,6 +64,9 @@ public function init(): void {
$this->registerHook('entry_before_insert', [$this, 'processArticle']);
}
+ /**
+ * @throws Minz_PermissionDeniedException
+ */
#[\Override]
public function handleConfigureAction(): void {
$this->registerTranslates();
@@ -84,7 +88,7 @@ public function handleConfigureAction(): void {
$userConf->save();
- $this->logsEnabled = (bool) ($config['enable_logging'] ?? false);
+ $this->logsEnabled = $config['enable_logging'];
$loggable = $config;
$loggable['webhook_body'] = '[redacted]';
@@ -94,24 +98,14 @@ public function handleConfigureAction(): void {
);
if ($this->shouldSendTestRequest()) {
- try {
- $this->sendTestRequest($config);
- } catch (Throwable $err) {
- logError($this->logsEnabled, 'Test webhook request failed: ' . $err->getMessage());
- }
+ $this->sendTestRequest($config);
}
}
/**
* Process article and send webhook if patterns match.
- *
- * @param FreshRSS_Entry|mixed $entry
*/
- public function processArticle($entry): FreshRSS_Entry {
- if (!$entry instanceof FreshRSS_Entry) {
- return $entry;
- }
-
+ public function processArticle(FreshRSS_Entry $entry): FreshRSS_Entry {
$config = $this->getSnapshot();
if ($config === null) {
return $entry;
@@ -152,7 +146,26 @@ public function processArticle($entry): FreshRSS_Entry {
/**
* Try to find a pattern that matches this entry in basic mode.
*
- * @param array $config
+ * @param array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * } $config
*/
private function findMatchLogBasic(FreshRSS_Entry $entry, array $config): ?string {
$patterns = $config['keywords'];
@@ -160,12 +173,12 @@ private function findMatchLogBasic(FreshRSS_Entry $entry, array $config): ?strin
return null;
}
- $title = (string) $entry->title();
- $link = (string) $entry->link();
+ $title = $entry->title();
+ $link = $entry->link();
$feed = $entry->feed();
- $feedName = (is_object($feed) && method_exists($feed, 'name')) ? (string) $feed->name() : '';
- $authors = trim((string) $entry->authors(true));
- $content = (string) $entry->content();
+ $feedName = $feed instanceof FreshRSS_Feed ? $feed->name() : '';
+ $authors = trim($entry->authors(true));
+ $content = $entry->content();
foreach ($patterns as $pattern) {
$normalizedPattern = $this->normalizePattern($pattern);
@@ -208,31 +221,43 @@ private function findMatchLogBasic(FreshRSS_Entry $entry, array $config): ?strin
/**
* Try to find a pattern that matches this entry in advanced mode.
*
- * @param array $config
+ * @param array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * } $config
*/
private function findMatchLogAdvanced(FreshRSS_Entry $entry, array $config): ?string {
- $title = (string) $entry->title();
- $link = (string) $entry->link();
+ $title = $entry->title();
+ $link = $entry->link();
$feed = $entry->feed();
- $feedName = (is_object($feed) && method_exists($feed, 'name')) ? (string) $feed->name() : '';
- $authors = trim((string) $entry->authors(true));
- $content = (string) $entry->content();
-
- $advancedLists = [
- 'title' => $config['keywords_title'] ?? [],
- 'feed' => $config['keywords_feed'] ?? [],
- 'authors' => $config['keywords_authors'] ?? [],
- 'content' => $config['keywords_content'] ?? [],
- ];
+ $feedName = $feed instanceof FreshRSS_Feed ? $feed->name() : '';
+ $authors = trim($entry->authors(true));
+ $content = $entry->content();
- foreach ($advancedLists['title'] as $pattern) {
+ foreach ($config['keywords_title'] as $pattern) {
$normalizedPattern = $this->normalizePattern($pattern);
if ($normalizedPattern !== null && $this->isPatternFound($normalizedPattern, $title, $pattern)) {
return "Matched by title ⮕ pattern: {$pattern} ♦ title: {$title} ♦ link: {$link}";
}
}
- foreach ($advancedLists['feed'] as $pattern) {
+ foreach ($config['keywords_feed'] as $pattern) {
$normalizedPattern = $this->normalizePattern($pattern);
if (
$normalizedPattern !== null
@@ -243,7 +268,7 @@ private function findMatchLogAdvanced(FreshRSS_Entry $entry, array $config): ?st
}
}
- foreach ($advancedLists['authors'] as $pattern) {
+ foreach ($config['keywords_authors'] as $pattern) {
$normalizedPattern = $this->normalizePattern($pattern);
if (
$normalizedPattern !== null
@@ -254,7 +279,7 @@ private function findMatchLogAdvanced(FreshRSS_Entry $entry, array $config): ?st
}
}
- foreach ($advancedLists['content'] as $pattern) {
+ foreach ($config['keywords_content'] as $pattern) {
$normalizedPattern = $this->normalizePattern($pattern);
if (
$normalizedPattern !== null
@@ -269,40 +294,78 @@ private function findMatchLogAdvanced(FreshRSS_Entry $entry, array $config): ?st
}
/**
- * @param array $config
+ * @param array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * } $config
*/
private function hasConfiguredKeywords(array $config): bool {
- if (($config['match_mode'] ?? self::MATCH_MODE_BASIC) === self::MATCH_MODE_ADVANCED) {
- return ($config['keywords_title'] ?? []) !== []
- || ($config['keywords_feed'] ?? []) !== []
- || ($config['keywords_authors'] ?? []) !== []
- || ($config['keywords_content'] ?? []) !== [];
+ if ($config['match_mode'] === self::MATCH_MODE_ADVANCED) {
+ return $config['keywords_title'] !== []
+ || $config['keywords_feed'] !== []
+ || $config['keywords_authors'] !== []
+ || $config['keywords_content'] !== [];
}
- return ($config['keywords'] ?? []) !== [];
+ return $config['keywords'] !== [];
}
/**
* Send article data via webhook.
*
- * @param array $config
+ * @param array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * } $config
*/
private function sendArticle(FreshRSS_Entry $entry, string $additionalLog, array $config): void {
try {
- $bodyTemplate = (string) $config['webhook_body'];
+ $bodyTemplate = $config['webhook_body'];
$replacements = $this->buildReplacements($entry);
$body = str_replace(array_keys($replacements), array_values($replacements), $bodyTemplate);
sendReq(
- (string) $config['webhook_url'],
- (string) $config['webhook_method'],
- (string) $config['webhook_body_type'],
+ $config['webhook_url'],
+ $config['webhook_method'],
+ $config['webhook_body_type'],
$body,
$config['webhook_headers'],
- (bool) $config['enable_logging'],
+ $config['enable_logging'],
$additionalLog,
);
- } catch (Throwable $err) {
+ } catch (RuntimeException|InvalidArgumentException|JsonException $err) {
logError($this->logsEnabled, 'sendArticle error: ' . $err->getMessage());
}
}
@@ -310,16 +373,35 @@ private function sendArticle(FreshRSS_Entry $entry, string $additionalLog, array
/**
* Send a manual test request using configuration data.
*
- * @param array $config
+ * @param array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * } $config
*/
private function sendTestRequest(array $config): void {
sendReq(
- (string) $config['webhook_url'],
- (string) $config['webhook_method'],
- (string) $config['webhook_body_type'],
- (string) $config['webhook_body'],
+ $config['webhook_url'],
+ $config['webhook_method'],
+ $config['webhook_body_type'],
+ $config['webhook_body'],
$config['webhook_headers'],
- (bool) $config['enable_logging'],
+ $config['enable_logging'],
'Test request from configuration',
);
}
@@ -331,7 +413,7 @@ private function sendTestRequest(array $config): void {
*/
private function buildReplacements(FreshRSS_Entry $entry): array {
$feed = $entry->feed();
- $feedName = (is_object($feed) && method_exists($feed, 'name')) ? (string) $feed->name() : '';
+ $feedName = $feed instanceof FreshRSS_Feed ? $feed->name() : '';
return [
'__TITLE__' => $this->toSafeJsonStr($entry->title()),
@@ -360,7 +442,11 @@ private function toSafeJsonStr(mixed $value): string {
}
if (is_array($value)) {
- $value = implode(', ', array_map(static fn ($item): string => (string) $item, $value));
+ $items = [];
+ foreach ($value as $item) {
+ $items[] = $this->normalizeScalarToString($item);
+ }
+ $value = implode(', ', $items);
} elseif (is_object($value)) {
if (method_exists($value, '__toString')) {
$value = (string) $value;
@@ -369,12 +455,29 @@ private function toSafeJsonStr(mixed $value): string {
}
}
- $string = html_entity_decode((string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
- $string = preg_replace('/\s+/u', ' ', $string) ?? '';
+ $string = html_entity_decode($this->normalizeScalarToString($value), ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ $string = preg_replace('/\s+/u', ' ', $string) ?? '';
return addcslashes(trim($string), "\"\\");
}
+ private function normalizeScalarToString(mixed $value): string {
+ if ($value === null) {
+ return '';
+ }
+ if (is_scalar($value)) {
+ return (string) $value;
+ }
+ if ($value instanceof DateTimeInterface) {
+ return $value->format(DateTimeInterface::ATOM);
+ }
+ if (is_object($value) && method_exists($value, '__toString')) {
+ return (string) $value;
+ }
+
+ return '';
+ }
+
/**
* Ensure configuration defaults exist for the current user.
*/
@@ -453,12 +556,8 @@ private function ensureConfigurationDefaults(): void {
* Attempt to migrate settings stored via the legacy system configuration.
*/
private function migrateLegacyConfiguration(FreshRSS_UserConfiguration $userConf): bool {
- if (!method_exists($this, 'getSystemConfiguration')) {
- return false;
- }
-
$legacy = $this->getSystemConfiguration();
- if (!is_array($legacy) || $legacy === []) {
+ if ($legacy === []) {
return false;
}
@@ -505,6 +604,7 @@ private function migrateLegacyConfiguration(FreshRSS_UserConfiguration $userConf
}
private function hasAttributeValue(FreshRSS_UserConfiguration $userConf, string $key, string $type): bool {
+ /** @var non-empty-string $key */
return match ($type) {
'array' => $userConf->attributeArray($key) !== null,
'bool' => $userConf->attributeBool($key) !== null,
@@ -513,6 +613,7 @@ private function hasAttributeValue(FreshRSS_UserConfiguration $userConf, string
}
private function ensureBoolDefault(FreshRSS_UserConfiguration $userConf, string $key, bool $default): bool {
+ /** @var non-empty-string $key */
if ($userConf->attributeBool($key) === null) {
$userConf->_attribute($key, $default);
return true;
@@ -536,7 +637,26 @@ private function getUserConf(): ?FreshRSS_UserConfiguration {
/**
* Collect configuration values from the current request payload.
*
- * @return array
+ * @return array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * }
*/
private function collectConfigurationFromRequest(): array {
$keywords = $this->normalizeListInput(Minz_Request::paramTextToArray('keywords'));
@@ -583,7 +703,26 @@ private function shouldSendTestRequest(): bool {
/**
* Return the configuration snapshot for the current user.
*
- * @return array|null
+ * @return array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * }|null
*/
private function getSnapshot(): ?array {
$userConf = $this->getUserConf();
@@ -614,13 +753,18 @@ private function getSnapshot(): ?array {
}
private function normalizeMethodValue(?string $method): string {
- return (HTTP_METHOD::tryFrom(strtoupper((string) $method)) ?? self::DEFAULT_METHOD)->value;
+ $methodValue = $method ?? '';
+ return (HTTP_METHOD::tryFrom(strtoupper($methodValue)) ?? self::DEFAULT_METHOD)->value;
}
private function normalizeBodyTypeValue(?string $bodyType): string {
- return (BODY_TYPE::tryFrom(strtolower((string) $bodyType)) ?? self::DEFAULT_BODY_TYPE)->value;
+ $bodyTypeValue = $bodyType ?? '';
+ return (BODY_TYPE::tryFrom(strtolower($bodyTypeValue)) ?? self::DEFAULT_BODY_TYPE)->value;
}
+ /**
+ * @return 'basic'|'advanced'
+ */
private function normalizeMatchMode(?string $matchMode): string {
if ($matchMode === self::MATCH_MODE_ADVANCED) {
return self::MATCH_MODE_ADVANCED;
@@ -630,17 +774,19 @@ private function normalizeMatchMode(?string $matchMode): string {
}
/**
- * @return string[]
+ * @param non-empty-string $key
+ * @param list $default
+ * @return list
*/
private function getArrayAttribute(FreshRSS_UserConfiguration $userConf, string $key, array $default): array {
$value = $userConf->attributeArray($key);
- if (!is_array($value)) {
+ if ($value === null) {
return $default;
}
$normalized = [];
foreach ($value as $item) {
- $trimmed = trim((string) $item);
+ $trimmed = trim($this->normalizeScalarToString($item));
if ($trimmed !== '') {
$normalized[] = $trimmed;
}
@@ -649,11 +795,13 @@ private function getArrayAttribute(FreshRSS_UserConfiguration $userConf, string
return $normalized;
}
+ /** @param non-empty-string $key */
private function getBoolAttribute(FreshRSS_UserConfiguration $userConf, string $key, bool $default): bool {
$value = $userConf->attributeBool($key);
return $value ?? $default;
}
+ /** @param non-empty-string $key */
private function getStringAttribute(FreshRSS_UserConfiguration $userConf, string $key, string $default): string {
$value = $userConf->attributeString($key);
return ($value === null || $value === '') ? $default : $value;
@@ -663,7 +811,7 @@ private function getStringAttribute(FreshRSS_UserConfiguration $userConf, string
* Normalize a list input (textarea) into trimmed values.
*
* @param array|null $values
- * @return string[]
+ * @return list
*/
private function normalizeListInput(?array $values): array {
if (!is_array($values)) {
@@ -709,26 +857,20 @@ private function isPatternFound(string $pattern, string $text, string $fallback)
}
$fallbackNeedle = $fallback !== '' ? $fallback : $pattern;
- return $fallbackNeedle !== '' && str_contains($text, $fallbackNeedle);
+ return str_contains($text, $fallbackNeedle);
}
private function getEntryThumbnail(FreshRSS_Entry $entry): string {
- if (method_exists($entry, 'thumbnail')) {
- $thumbnail = $entry->thumbnail();
- if (is_string($thumbnail) && $thumbnail !== '') {
- return $thumbnail;
- }
+ $thumbnail = $entry->thumbnail();
+ if ($thumbnail !== null && $thumbnail['url'] !== '') {
+ return $thumbnail['url'];
}
- if (method_exists($entry, 'enclosures')) {
- $enclosures = $entry->enclosures();
- if (is_array($enclosures)) {
- foreach ($enclosures as $enclosure) {
- $url = $this->extractEnclosureUrl($enclosure);
- if ($url !== '') {
- return $url;
- }
- }
+ $enclosures = $entry->enclosures();
+ foreach ($enclosures as $enclosure) {
+ $url = $this->extractEnclosureUrl($enclosure);
+ if ($url !== '') {
+ return $url;
}
}
@@ -736,7 +878,7 @@ private function getEntryThumbnail(FreshRSS_Entry $entry): string {
}
private function getPlainTextContent(FreshRSS_Entry $entry): string {
- $content = (string) $entry->content();
+ $content = $entry->content();
if ($content === '') {
return '';
}
@@ -765,10 +907,6 @@ private function getPlainTextContent(FreshRSS_Entry $entry): string {
}
private function extractEnclosureUrl(mixed $enclosure): string {
- if (!is_object($enclosure)) {
- return '';
- }
-
$url = $this->getEnclosureValue($enclosure, 'url');
if ($url === '') {
return '';
@@ -782,15 +920,34 @@ private function extractEnclosureUrl(mixed $enclosure): string {
return '';
}
- private function getEnclosureValue(object $enclosure, string $name): string {
- if (method_exists($enclosure, $name)) {
- $value = $enclosure->{$name}();
- return is_string($value) ? $value : (string) $value;
+ private function getEnclosureValue(mixed $enclosure, string $name): string {
+ if (is_array($enclosure)) {
+ if (!array_key_exists($name, $enclosure)) {
+ return '';
+ }
+ return $this->normalizeScalarToString($enclosure[$name]);
+ }
+
+ if (!is_object($enclosure)) {
+ return '';
+ }
+
+ if ($name === 'url') {
+ if (method_exists($enclosure, 'get_link')) {
+ return $this->normalizeScalarToString($enclosure->get_link());
+ }
+ if (method_exists($enclosure, 'link')) {
+ return $this->normalizeScalarToString($enclosure->link());
+ }
}
- if (isset($enclosure->{$name})) {
- $value = $enclosure->{$name};
- return is_string($value) ? $value : (string) $value;
+ if ($name === 'type') {
+ if (method_exists($enclosure, 'get_type')) {
+ return $this->normalizeScalarToString($enclosure->get_type());
+ }
+ if (method_exists($enclosure, 'type')) {
+ return $this->normalizeScalarToString($enclosure->type());
+ }
}
return '';
@@ -798,52 +955,62 @@ private function getEnclosureValue(object $enclosure, string $name): string {
public function getKeywordsData(): string {
$config = $this->getSnapshot();
- $keywords = $config['keywords'] ?? [];
- return implode(PHP_EOL, $keywords);
+ if ($config === null) {
+ return '';
+ }
+
+ return implode(PHP_EOL, $config['keywords']);
}
public function getKeywordDataByField(string $field): string {
$config = $this->getSnapshot();
+ if ($config === null) {
+ return '';
+ }
+
return match ($field) {
- 'title' => implode(PHP_EOL, $config['keywords_title'] ?? []),
- 'feed' => implode(PHP_EOL, $config['keywords_feed'] ?? []),
- 'authors' => implode(PHP_EOL, $config['keywords_authors'] ?? []),
- 'content' => implode(PHP_EOL, $config['keywords_content'] ?? []),
+ 'title' => implode(PHP_EOL, $config['keywords_title']),
+ 'feed' => implode(PHP_EOL, $config['keywords_feed']),
+ 'authors' => implode(PHP_EOL, $config['keywords_authors']),
+ 'content' => implode(PHP_EOL, $config['keywords_content']),
default => '',
};
}
public function getMatchMode(): string {
$config = $this->getSnapshot();
- return $config['match_mode'] ?? self::MATCH_MODE_BASIC;
+ return $config === null ? self::MATCH_MODE_BASIC : $config['match_mode'];
}
public function getWebhookHeaders(): string {
$config = $this->getSnapshot();
- $headers = $config['webhook_headers'] ?? self::DEFAULT_HEADERS;
- return implode(PHP_EOL, $headers);
+ if ($config === null) {
+ return implode(PHP_EOL, self::DEFAULT_HEADERS);
+ }
+
+ return implode(PHP_EOL, $config['webhook_headers']);
}
public function getWebhookUrl(): string {
$config = $this->getSnapshot();
- return $config['webhook_url'] ?? self::DEFAULT_URL;
+ return $config === null ? self::DEFAULT_URL : $config['webhook_url'];
}
public function getWebhookBody(): string {
$config = $this->getSnapshot();
- return $config['webhook_body'] ?? self::DEFAULT_BODY_TEMPLATE;
+ return $config === null ? self::DEFAULT_BODY_TEMPLATE : $config['webhook_body'];
}
public function getWebhookBodyType(): string {
$config = $this->getSnapshot();
- return $config['webhook_body_type'] ?? self::DEFAULT_BODY_TYPE->value;
+ return $config === null ? self::DEFAULT_BODY_TYPE->value : $config['webhook_body_type'];
}
}
-function _LOG(bool $logEnabled, $data): void {
+function _LOG(bool $logEnabled, mixed $data): void {
logWarning($logEnabled, $data);
}
-function _LOG_ERR(bool $logEnabled, $data): void {
+function _LOG_ERR(bool $logEnabled, mixed $data): void {
logError($logEnabled, $data);
}
diff --git a/xExtension-Webhook/request.php b/xExtension-Webhook/request.php
index fc851289..977545f7 100644
--- a/xExtension-Webhook/request.php
+++ b/xExtension-Webhook/request.php
@@ -18,7 +18,6 @@
*
* @throws InvalidArgumentException When invalid parameters are provided
* @throws JsonException When JSON encoding/decoding fails
- * @throws Minz_PermissionDeniedException
* @throws RuntimeException When cURL operations fail
*
* @return void
@@ -33,12 +32,13 @@ function sendReq(
string $additionalLog = "",
): void {
// Validate inputs
- if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
+ if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new InvalidArgumentException("Invalid URL provided: {$url}");
}
+ $normalizedMethod = strtoupper($method);
$allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
- if (!in_array(strtoupper($method), $allowedMethods, true)) {
+ if (!in_array($normalizedMethod, $allowedMethods, true)) {
throw new InvalidArgumentException("Invalid HTTP method: {$method}");
}
@@ -54,20 +54,20 @@ function sendReq(
try {
// Configure HTTP method
- configureHttpMethod($ch, strtoupper($method));
+ configureHttpMethod($ch, $normalizedMethod);
// Process and set HTTP body
- $processedBody = processHttpBody($body, $bodyType, $method, $logEnabled);
- if ($processedBody !== null && $method !== 'GET') {
- curl_setopt($ch, CURLOPT_POSTFIELDS, $processedBody);
- }
+ $processedBody = processHttpBody($body, $bodyType, $normalizedMethod, $logEnabled);
+ if ($processedBody !== null && $normalizedMethod !== 'GET') {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $processedBody);
+ }
// Configure headers
$finalHeaders = configureHeaders($headers, $bodyType);
curl_setopt($ch, CURLOPT_HTTPHEADER, $finalHeaders);
// Log the request
- logRequest($logEnabled, $additionalLog, $method, $url, $bodyType, $processedBody, $finalHeaders);
+ logRequest($logEnabled, $additionalLog, $normalizedMethod, $url, $bodyType, $processedBody, $finalHeaders);
// Execute request
executeRequest($ch, $logEnabled);
@@ -86,7 +86,7 @@ function sendReq(
* Sets the appropriate cURL options based on the HTTP method.
*
* @param CurlHandle $ch The cURL handle
- * @param string $method HTTP method in uppercase
+ * @param non-empty-string $method HTTP method in uppercase
*
* @return void
*/
@@ -120,8 +120,6 @@ function configureHttpMethod(CurlHandle $ch, string $method): void {
*
* @throws JsonException When JSON processing fails
* @throws InvalidArgumentException When unsupported body type is provided
- * @throws Minz_PermissionDeniedException
- *
* @return string|null Processed body content or null if no body needed
*/
function processHttpBody(string $body, string $bodyType, string $method, bool $logEnabled): ?string {
@@ -131,10 +129,11 @@ function processHttpBody(string $body, string $bodyType, string $method, bool $l
try {
$bodyObject = json_decode($body, true, 256, JSON_THROW_ON_ERROR);
+ $formData = is_array($bodyObject) ? $bodyObject : [];
return match ($bodyType) {
'json' => json_encode($bodyObject, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
- 'form' => http_build_query($bodyObject ?? []),
+ 'form' => http_build_query($formData),
default => throw new InvalidArgumentException("Unsupported body type: {$bodyType}")
};
} catch (JsonException $err) {
@@ -152,7 +151,7 @@ function processHttpBody(string $body, string $bodyType, string $method, bool $l
* @param string[] $headers Array of custom headers
* @param string $bodyType Content type ('json' or 'form')
*
- * @return string[] Final array of headers to use
+ * @return list Final array of headers to use
*/
function configureHeaders(array $headers, string $bodyType): array {
$normalized = [];
@@ -201,8 +200,6 @@ function configureHeaders(array $headers, string $bodyType): array {
* @param string|null $body Processed request body
* @param string[] $headers Array of HTTP headers
*
- * @throws Minz_PermissionDeniedException
- *
* @return void
*/
function logRequest(
@@ -219,7 +216,7 @@ function logRequest(
}
$cleanUrl = urldecode($url);
- $cleanBody = $body ? str_replace('\/', '/', $body) : '';
+ $cleanBody = $body !== null ? str_replace('\/', '/', $body) : '';
$headersJson = json_encode($headers);
$logMessage = trim("{$additionalLog} ♦♦ sendReq ⏩ {$method}: {$cleanUrl} ♦♦ {$bodyType} ♦♦ {$cleanBody} ♦♦ {$headersJson}");
@@ -237,7 +234,6 @@ function logRequest(
* @param bool $logEnabled Whether logging is enabled
*
* @throws RuntimeException When cURL execution fails
- * @throws Minz_PermissionDeniedException
*
* @return void
*/
@@ -265,13 +261,15 @@ function executeRequest(CurlHandle $ch, bool $logEnabled): void {
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log (will be converted to string)
*
- * @throws Minz_PermissionDeniedException
- *
* @return void
*/
-function logWarning(bool $logEnabled, $data): void {
+function logWarning(bool $logEnabled, mixed $data): void {
if ($logEnabled && class_exists('Minz_Log')) {
- Minz_Log::warning("[WEBHOOK] " . $data);
+ try {
+ Minz_Log::warning('[WEBHOOK] ' . toLogString($data));
+ } catch (Throwable) {
+ return;
+ }
}
}
@@ -284,13 +282,15 @@ function logWarning(bool $logEnabled, $data): void {
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log (will be converted to string)
*
- * @throws Minz_PermissionDeniedException
- *
* @return void
*/
-function logError(bool $logEnabled, $data): void {
+function logError(bool $logEnabled, mixed $data): void {
if ($logEnabled && class_exists('Minz_Log')) {
- Minz_Log::error("[WEBHOOK]❌ " . $data);
+ try {
+ Minz_Log::error('[WEBHOOK]❌ ' . toLogString($data));
+ } catch (Throwable) {
+ return;
+ }
}
}
@@ -301,11 +301,9 @@ function logError(bool $logEnabled, $data): void {
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log
*
- * @throws Minz_PermissionDeniedException
- *
* @return void
*/
-function LOG_WARN(bool $logEnabled, $data): void {
+function LOG_WARN(bool $logEnabled, mixed $data): void {
logWarning($logEnabled, $data);
}
@@ -316,10 +314,26 @@ function LOG_WARN(bool $logEnabled, $data): void {
* @param bool $logEnabled Whether logging is enabled
* @param mixed $data Data to log
*
- * @throws Minz_PermissionDeniedException
- *
* @return void
*/
-function LOG_ERR(bool $logEnabled, $data): void {
+function LOG_ERR(bool $logEnabled, mixed $data): void {
logError($logEnabled, $data);
}
+
+function toLogString(mixed $data): string {
+ if ($data === null) {
+ return '';
+ }
+ if (is_scalar($data)) {
+ return (string) $data;
+ }
+ if ($data instanceof DateTimeInterface) {
+ return $data->format(DateTimeInterface::ATOM);
+ }
+ if (is_object($data) && method_exists($data, '__toString')) {
+ return (string) $data;
+ }
+
+ $encoded = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ return is_string($encoded) ? $encoded : '[unserializable]';
+}
From cd4b64fd30c101f94e7be592df570130bd8ef58a Mon Sep 17 00:00:00 2001
From: Jp302109 <15944448389@163.com>
Date: Sat, 7 Feb 2026 19:49:57 +0800
Subject: [PATCH 5/7] Keep test webhook failures from blocking config save
---
xExtension-Webhook/extension.php | 34 +++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/xExtension-Webhook/extension.php b/xExtension-Webhook/extension.php
index 819ba298..5596c85a 100644
--- a/xExtension-Webhook/extension.php
+++ b/xExtension-Webhook/extension.php
@@ -98,7 +98,7 @@ public function handleConfigureAction(): void {
);
if ($this->shouldSendTestRequest()) {
- $this->sendTestRequest($config);
+ $this->sendTestRequestSafely($config);
}
}
@@ -406,6 +406,38 @@ private function sendTestRequest(array $config): void {
);
}
+ /**
+ * Keep configuration save path resilient even when test webhook fails.
+ *
+ * @param array{
+ * keywords: list,
+ * match_mode: 'basic'|'advanced',
+ * keywords_title: list,
+ * keywords_feed: list,
+ * keywords_authors: list,
+ * keywords_content: list,
+ * search_in_title: bool,
+ * search_in_feed: bool,
+ * search_in_authors: bool,
+ * search_in_content: bool,
+ * mark_as_read: bool,
+ * ignore_updated: bool,
+ * webhook_headers: list,
+ * webhook_url: string,
+ * webhook_method: string,
+ * webhook_body: string,
+ * webhook_body_type: string,
+ * enable_logging: bool
+ * } $config
+ */
+ private function sendTestRequestSafely(array $config): void {
+ try {
+ $this->sendTestRequest($config);
+ } catch (Throwable $err) {
+ logError($this->logsEnabled, 'Test webhook request failed: ' . $err->getMessage());
+ }
+ }
+
/**
* Build placeholder replacements for the configured template.
*
From 52a42edbda4bda9b606950d517da1d3e311db05b Mon Sep 17 00:00:00 2001
From: Jp302109 <15944448389@163.com>
Date: Sat, 7 Feb 2026 20:13:03 +0800
Subject: [PATCH 6/7] complement author
---
xExtension-Webhook/extension.php | 2 +-
xExtension-Webhook/metadata.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/xExtension-Webhook/extension.php b/xExtension-Webhook/extension.php
index 5596c85a..7c3be6cc 100644
--- a/xExtension-Webhook/extension.php
+++ b/xExtension-Webhook/extension.php
@@ -31,7 +31,7 @@ enum HTTP_METHOD: string {
* Sends configurable webhook requests whenever new entries match the
* configured keyword filters.
*
- * @author Lukas Melega, Ryahn
+ * @author Lukas Melega, Ryahn, onlymykazari
* @version 0.3.0
* @since FreshRSS 1.20.0
*/
diff --git a/xExtension-Webhook/metadata.json b/xExtension-Webhook/metadata.json
index 0bd5fbd5..78fa0172 100644
--- a/xExtension-Webhook/metadata.json
+++ b/xExtension-Webhook/metadata.json
@@ -1,6 +1,6 @@
{
"name": "Webhook",
- "author": "Lukas Melega, Ryahn",
+ "author": "Lukas Melega, Ryahn, onlymykazari",
"description": "Send custom webhook when new article appears (and matches custom criteria)",
"version": "0.3.0",
"entrypoint": "Webhook",
From 1590f09b2e182f544d91f743906a689cea41298b Mon Sep 17 00:00:00 2001
From: Jp302109 <15944448389@163.com>
Date: Sat, 7 Feb 2026 21:51:19 +0800
Subject: [PATCH 7/7] Narrow test-request catch to real exception types
---
xExtension-Webhook/extension.php | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/xExtension-Webhook/extension.php b/xExtension-Webhook/extension.php
index 7c3be6cc..4b87118d 100644
--- a/xExtension-Webhook/extension.php
+++ b/xExtension-Webhook/extension.php
@@ -373,6 +373,10 @@ private function sendArticle(FreshRSS_Entry $entry, string $additionalLog, array
/**
* Send a manual test request using configuration data.
*
+ * @throws InvalidArgumentException
+ * @throws JsonException
+ * @throws RuntimeException
+ *
* @param array{
* keywords: list,
* match_mode: 'basic'|'advanced',
@@ -433,7 +437,7 @@ private function sendTestRequest(array $config): void {
private function sendTestRequestSafely(array $config): void {
try {
$this->sendTestRequest($config);
- } catch (Throwable $err) {
+ } catch (RuntimeException|InvalidArgumentException|JsonException $err) {
logError($this->logsEnabled, 'Test webhook request failed: ' . $err->getMessage());
}
}