From 0130adcd6c96e34a1c965d611641fb2e194e1cee Mon Sep 17 00:00:00 2001 From: Dag Date: Fri, 23 May 2025 22:55:41 +0200 Subject: [PATCH 01/14] fix: deprecation warning (#4567) --- lib/CacheFactory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/CacheFactory.php b/lib/CacheFactory.php index 47bbbf72..50d8cef9 100644 --- a/lib/CacheFactory.php +++ b/lib/CacheFactory.php @@ -92,6 +92,7 @@ class CacheFactory if (empty($port)) { throw new \Exception('"port" param is not set for ' . $section); } + $port = (string) $port; if (!ctype_digit($port)) { throw new \Exception('"port" param is invalid for ' . $section); } From ec5b32c551e4888eb514857a7a659f2df47955db Mon Sep 17 00:00:00 2001 From: Dag Date: Sat, 24 May 2025 19:14:53 +0200 Subject: [PATCH 02/14] ci: fix broken ci (#4568) * fix: deprecation warning * ci: fix broken ci --- .github/workflows/lint.yml | 6 +++--- .github/workflows/tests.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 206b53de..79201edd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: jobs: phpcs: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php-versions: ['7.4'] @@ -21,7 +21,7 @@ jobs: - run: phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p phpcompatibility: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php-versions: ['7.4'] @@ -36,7 +36,7 @@ jobs: - run: ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p executable_php_files_check: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 93f07b0f..96128fc2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: jobs: phpunit8: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php-versions: ['7.4', '8.0', '8.1'] From daef06c6dd445c12a45f60bcd8d1fa7eadf023a0 Mon Sep 17 00:00:00 2001 From: Thiago Ferreira Date: Sat, 24 May 2025 14:18:52 -0300 Subject: [PATCH 03/14] devcontainer: Fixed Dev Containers setup (#4556) The current setup for Dev Containers was not working, with multiple different errors. So, in order to restore its funcionality (and allow for things like linting and debugging), the following changes were made: - The Dockerfile was severely alterered. Now, the `docker-php-ext-enable` binary is installed before its usage, it points to the correct PHP binary, and we install Composer for for loading dev-dependencies later-on. - Moved the "postCreateCommand" section (defined on the `devcontainer.json` file) into its own script file (for a more readable experience) - On the post-creation script, moved the `xdebug.ini` to the correct directory (alongside the PHP-FPM bin), installed PHPUnit, PHPCodesniffer (and the 'PHP Compatibility' sniffer) with Composer on a global location, and changed owner of the `cache` directory - Changed VSCode-specific customization setting in order to point to the update some binary paths. Also made sure globally-installed composer packages binaries are accessible via PATHdocker-php-ext-enable --- .devcontainer/Dockerfile | 25 +++++++++++++++++++------ .devcontainer/devcontainer.json | 11 +++++++---- .devcontainer/launch.json | 3 ++- .devcontainer/post-create-command.sh | 27 +++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100755 .devcontainer/post-create-command.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index faec9f09..15297cc3 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,21 @@ FROM rssbridge/rss-bridge:latest -RUN apt-get update && \ - apt-get install --yes --no-install-recommends \ - git && \ - pecl install xdebug && \ - pear install PHP_CodeSniffer && \ - docker-php-ext-enable xdebug \ No newline at end of file +COPY --chmod=755 post-create-command.sh /usr/local/bin/post-create-command + +ADD https://raw.githubusercontent.com/docker-library/php/master/docker-php-ext-enable /usr/local/bin/docker-php-ext-enable +RUN chmod u+x /usr/local/bin/docker-php-ext-enable + +ADD https://getcomposer.org/installer /usr/local/bin/composer-installer.php +RUN chmod u+x /usr/local/bin/composer-installer.php +RUN php /usr/local/bin/composer-installer.php --check && \ + php /usr/local/bin/composer-installer.php --filename=composer --install-dir=/usr/local/bin + +RUN apt-get update && \ + apt-get install -y \ + git \ + php-dev \ + make \ + unzip + +RUN pecl install xdebug && \ + PHP_INI_DIR=/etc/php/8.2/fpm docker-php-ext-enable xdebug diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6e625b8a..564da38a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,9 +6,9 @@ "vscode": { // Set *default* container specific settings.json values on container create. "settings": { - "php.validate.executablePath": "/usr/local/bin/php", - "phpSniffer.executablesFolder": "/usr/local/bin/", - "phpcs.executablePath": "/usr/local/bin/phpcs", + "php.validate.executablePath": "/usr/bin/php", + "phpSniffer.executablesFolder": "/root/.config/composer/vendor/bin", + "phpcs.executablePath": "/root/.config/composer/vendor/bin/phpcs", "phpcs.lintOnType": false }, @@ -22,6 +22,9 @@ ] } }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/root/.config/composer/vendor/bin", + }, "forwardPorts": [3100, 9000, 9003], - "postCreateCommand": "cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf && cp .devcontainer/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && mkdir .vscode && cp .devcontainer/launch.json .vscode && echo '*' > whitelist.txt && chmod a+x \"$(pwd)\" && rm -rf /var/www/html && ln -s \"$(pwd)\" /var/www/html && nginx && php-fpm -D" + "postCreateCommand": "/usr/local/bin/post-create-command" } \ No newline at end of file diff --git a/.devcontainer/launch.json b/.devcontainer/launch.json index e1b473b8..7fe617e9 100644 --- a/.devcontainer/launch.json +++ b/.devcontainer/launch.json @@ -9,7 +9,8 @@ "type": "php", "request": "launch", "port": 9003, - "auto": true + "auto": true, + "log": true }, { "name": "Launch currently open script", diff --git a/.devcontainer/post-create-command.sh b/.devcontainer/post-create-command.sh new file mode 100755 index 00000000..aa3819eb --- /dev/null +++ b/.devcontainer/post-create-command.sh @@ -0,0 +1,27 @@ +#/bin/sh + +cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf +cp .devcontainer/xdebug.ini /etc/php/8.2/fpm/conf.d/xdebug.ini + +# This should download some dev-dependencies, like phpunit and the PHP code sniffers +composer global require "phpunit/phpunit:^9" +composer global require "squizlabs/php_codesniffer:^3.6" +composer global require "phpcompatibility/php-compatibility:^9.3" + +# We need to this manually for running the PHPCompatibility ruleset +phpcs --config-set installed_paths /root/.config/composer/vendor/phpcompatibility/php-compatibility + +mkdir -p .vscode +cp .devcontainer/launch.json .vscode + +echo '*' > whitelist.txt + +chmod a+x $(pwd) +rm -rf /var/www/html +ln -s $(pwd) /var/www/html + +# Solves possible issue of cache directory not being accessible +chown www-data:www-data -R $(pwd)/cache + +nginx +php-fpm8.2 -D \ No newline at end of file From 7397cabeee606e6360d1a8b854a9898a8178784e Mon Sep 17 00:00:00 2001 From: Dag Date: Sat, 24 May 2025 19:29:04 +0200 Subject: [PATCH 04/14] fix(telegram): remove meta message (#4569) --- bridges/TelegramBridge.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bridges/TelegramBridge.php b/bridges/TelegramBridge.php index 54a089bc..0a3f1a74 100644 --- a/bridges/TelegramBridge.php +++ b/bridges/TelegramBridge.php @@ -104,7 +104,7 @@ class TelegramBridge extends BridgeAbstract $notSupported = $messageDiv->find('div.message_media_not_supported_wrap', 0); if ($notSupported) { // For unknown reasons, the telegram preview page omits the content of this post - $message = 'RSS-Bridge was unable to find the content of this post.

' . $notSupported->innertext; + $message = (string) $notSupported->innertext; } if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) { From 7b55eb3824d6bdad898ec7d8172b6313950343f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Sun, 25 May 2025 20:46:50 +0200 Subject: [PATCH 05/14] Adding a bridge for Paul Graham's essays (#4570) * Adding a bridge for Paul Graham's essays * lint --------- Co-authored-by: Dag --- bridges/PaulGrahamBridge.php | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 bridges/PaulGrahamBridge.php diff --git a/bridges/PaulGrahamBridge.php b/bridges/PaulGrahamBridge.php new file mode 100644 index 00000000..928eea35 --- /dev/null +++ b/bridges/PaulGrahamBridge.php @@ -0,0 +1,95 @@ +find('body table'); + if (!isset($tables[0])) { + return; + } + + $tds = $tables[0]->find('td'); + if (!isset($tds[2])) { + return; + } + + $contentTd = $tds[2]; + + // Find all inner tables (each one holds a single essay link) + $essayTables = $contentTd->find('table'); + if (!isset($essayTables[1])) { + return; + } + + $essayTable = $essayTables[1]; + + // /html/body/table/tbody/tr/td[3]/table[2]/tbody/tr[2]/td/font/a + + $links = $essayTable->find('font'); + + $essayLinks = []; + foreach ($links as $t) { + $link = $t->find('a', 0); + if (!$link) { + continue; + } + + $href = trim($link->href); + $title = trim($link->plaintext); + + if (empty($href) || strpos($href, 'http') === 0 || !preg_match('/\.html$/', $href)) { + continue; + } + + $essayLinks[] = [ + 'title' => $title, + 'url' => 'https://www.paulgraham.com/' . $href, + ]; + } + + // Only fetch the first 10 (in display order) + $essayLinks = array_slice($essayLinks, 0, 10); + + foreach ($essayLinks as $essay) { + $item = [ + 'uri' => $essay['url'], + 'title' => $essay['title'], + 'uid' => $essay['url'], + 'content' => '', + ]; + + $essayHtml = getSimpleHTMLDOMCached($essay['url']); + if ($essayHtml) { + $essayTables = $essayHtml->find('body table'); + if (isset($essayTables[0])) { + $essayTds = $essayTables[0]->find('td'); + if (isset($essayTds[2])) { + $mainContent = $essayTds[2]->innertext; + $mainDom = str_get_html($mainContent); + + // Strip unwanted layout elements + foreach ($mainDom->find('map, img, script') as $el) { + $el->outertext = ''; + } + + $item['content'] = $mainDom->save(); + } + } + } + + $this->items[] = $item; + } + } +} + From e5b3ec85d9d07b2bc06b7561e21eb1358f68e805 Mon Sep 17 00:00:00 2001 From: Joseph Date: Mon, 26 May 2025 21:46:28 +0100 Subject: [PATCH 06/14] Delete CuriousCatBridge.php (#4571) --- bridges/CuriousCatBridge.php | 113 ----------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 bridges/CuriousCatBridge.php diff --git a/bridges/CuriousCatBridge.php b/bridges/CuriousCatBridge.php deleted file mode 100644 index 3d6e87d0..00000000 --- a/bridges/CuriousCatBridge.php +++ /dev/null @@ -1,113 +0,0 @@ - [ - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'koethekoethe', - ] - ]]; - - const CACHE_TIMEOUT = 3600; - - public function collectData() - { - $url = self::URI . '/api/v2/profile?username=' . urlencode($this->getInput('username')); - - $apiJson = getContents($url); - - $apiData = Json::decode($apiJson); - if (isset($apiData['error'])) { - throw new \Exception($apiData['error_code']); - } - - foreach ($apiData['posts'] as $post) { - $item = []; - - $item['author'] = 'Anonymous'; - - if ($post['senderData']['id'] !== false) { - $item['author'] = $post['senderData']['username']; - } - - $item['uri'] = $this->getURI() . '/post/' . $post['id']; - $item['title'] = $this->ellipsisTitle($post['comment']); - - $item['content'] = $this->processContent($post); - $item['timestamp'] = $post['timestamp']; - - $this->items[] = $item; - } - } - - public function getURI() - { - if (!is_null($this->getInput('username'))) { - return self::URI . '/' . $this->getInput('username'); - } - - return parent::getURI(); - } - - public function getName() - { - if (!is_null($this->getInput('username'))) { - return $this->getInput('username') . ' - Curious Cat'; - } - - return parent::getName(); - } - - private function processContent($post) - { - $author = 'Anonymous'; - - if ($post['senderData']['id'] !== false) { - $authorUrl = self::URI . '/' . $post['senderData']['username']; - - $author = <<{$post['senderData']['username']} -EOD; - } - - $question = $this->formatUrls($post['comment']); - $answer = $this->formatUrls($post['reply']); - - $content = <<{$author} asked:

-
{$question}

-

{$post['addresseeData']['username']} answered:

-
{$answer}
-EOD; - - return $content; - } - - private function ellipsisTitle($text) - { - $length = 150; - - if (strlen($text) > $length) { - $text = explode('
', wordwrap($text, $length, '
')); - return $text[0] . '...'; - } - - return $text; - } - - private function formatUrls($content) - { - return preg_replace( - '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', - '$1 ', - $content - ); - } -} From 419844f01002f18b654c35bed717e3a5e78635ee Mon Sep 17 00:00:00 2001 From: Joseph Date: Mon, 26 May 2025 21:46:42 +0100 Subject: [PATCH 07/14] Delete OpenlyBridge.php (#4572) --- bridges/OpenlyBridge.php | 255 --------------------------------------- 1 file changed, 255 deletions(-) delete mode 100644 bridges/OpenlyBridge.php diff --git a/bridges/OpenlyBridge.php b/bridges/OpenlyBridge.php deleted file mode 100644 index 9f54e22a..00000000 --- a/bridges/OpenlyBridge.php +++ /dev/null @@ -1,255 +0,0 @@ - [], - 'All Opinion' => [], - 'By Region' => [ - 'region' => [ - 'name' => 'Region', - 'type' => 'list', - 'values' => [ - 'Africa' => 'africa', - 'Asia Pacific' => 'asia-pacific', - 'Europe' => 'europe', - 'Latin America' => 'latin-america', - 'Middle Easta' => 'middle-east', - 'North America' => 'north-america' - ] - ], - 'content' => [ - 'name' => 'Content', - 'type' => 'list', - 'values' => [ - 'News' => 'news', - 'Opinion' => 'people' - ], - 'defaultValue' => 'news' - ] - ], - 'By Tag' => [ - 'tag' => [ - 'name' => 'Tag', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'lgbt-law', - ], - 'content' => [ - 'name' => 'Content', - 'type' => 'list', - 'values' => [ - 'News' => 'news', - 'Opinion' => 'people' - ], - 'defaultValue' => 'news' - ] - ], - 'By Author' => [ - 'profileId' => [ - 'name' => 'Profile ID', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '003D000002WZGYRIA5', - ] - ] - ]; - - const TEST_DETECT_PARAMETERS = [ - 'https://www.openlynews.com/profile/?id=0033z00002XUTepAAH' => [ - 'context' => 'By Author', 'profileId' => '0033z00002XUTepAAH' - ], - 'https://www.openlynews.com/news/?page=1&theme=lgbt-law' => [ - 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law' - ], - 'https://www.openlynews.com/news/?page=1®ion=north-america' => [ - 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america' - ], - 'https://www.openlynews.com/news/?theme=lgbt-law' => [ - 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law' - ], - 'https://www.openlynews.com/news/?region=north-america' => [ - 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america' - ] - ]; - - const CACHE_TIMEOUT = 900; // 15 mins - const ARTICLE_CACHE_TIMEOUT = 3600; // 1 hour - - private $feedTitle = ''; - private $itemLimit = 10; - - private $profileUrlRegex = '/openlynews\.com\/profile\/\?id=([a-zA-Z0-9]+)/'; - private $tagUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?theme=([\w-]+)/'; - private $regionUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?region=([\w-]+)/'; - - public function detectParameters($url) - { - $params = []; - - if (preg_match($this->profileUrlRegex, $url, $matches) > 0) { - $params['context'] = 'By Author'; - $params['profileId'] = $matches[1]; - return $params; - } - - if (preg_match($this->tagUrlRegex, $url, $matches) > 0) { - $params['context'] = 'By Tag'; - $params['content'] = $matches[1]; - $params['tag'] = $matches[2]; - return $params; - } - - if (preg_match($this->regionUrlRegex, $url, $matches) > 0) { - $params['context'] = 'By Region'; - $params['content'] = $matches[1]; - $params['region'] = $matches[2]; - return $params; - } - - return null; - } - - public function collectData() - { - $url = $this->getAjaxURI(); - - if ($this->queriedContext === 'By Author') { - $url = $this->getURI(); - } - - $html = getSimpleHTMLDOM($url); - $html = defaultLinkTo($html, $this->getURI()); - - if ($html->find('h1', 0)) { - $this->feedTitle = $html->find('h1', 0)->plaintext; - } - - if ($html->find('h2.title-v4', 0)) { - $html->find('span.tooltiptext', 0)->innertext = ''; - $this->feedTitle = $html->find('a.tooltipitem', 0)->plaintext; - } - - $items = $html->find('div.item'); - $limit = 5; - foreach (array_slice($items, 0, $limit) as $div) { - $this->items[] = $this->getArticle($div->find('a', 0)->href); - - if (count($this->items) >= $this->itemLimit) { - break; - } - } - } - - public function getURI() - { - switch ($this->queriedContext) { - case 'All News': - return self::URI . 'news'; - break; - case 'All Opinion': - return self::URI . 'people'; - break; - case 'By Tag': - return self::URI . $this->getInput('content') . '/?theme=' . $this->getInput('tag'); - case 'By Region': - return self::URI . $this->getInput('content') . '/?region=' . $this->getInput('region'); - break; - case 'By Author': - return self::URI . 'profile/?id=' . $this->getInput('profileId'); - break; - default: - return parent::getURI(); - } - } - - public function getName() - { - switch ($this->queriedContext) { - case 'All News': - return 'News - Openly'; - break; - case 'All Opinion': - return 'Opinion - Openly'; - break; - case 'By Tag': - if (empty($this->feedTitle)) { - $this->feedTitle = $this->getInput('tag'); - } - - if ($this->getInput('content') === 'people') { - return $this->feedTitle . ' - Opinion - Openly'; - } - - return $this->feedTitle . ' - Openly'; - break; - case 'By Region': - if (empty($this->feedTitle)) { - $this->feedTitle = $this->getInput('region'); - } - - if ($this->getInput('content') === 'people') { - return $this->feedTitle . ' - Opinion - Openly'; - } - - return $this->feedTitle . ' - Openly'; - break; - case 'By Author': - if (empty($this->feedTitle)) { - $this->feedTitle = $this->getInput('profileId'); - } - - return $this->feedTitle . ' - Author - Openly'; - break; - default: - return parent::getName(); - } - } - - private function getAjaxURI() - { - $part = '/ajax.html?'; - - switch ($this->queriedContext) { - case 'All News': - return self::URI . 'news' . $part; - break; - case 'All Opinion': - return self::URI . 'people' . $part; - break; - case 'By Tag': - return self::URI . $this->getInput('content') . $part . 'theme=' . $this->getInput('tag'); - break; - case 'By Region': - return self::URI . $this->getInput('content') . $part . 'region=' . $this->getInput('region'); - break; - } - } - - private function getArticle($url) - { - $article = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT); - $article = defaultLinkTo($article, $this->getURI()); - - $item = []; - $item['title'] = $article->find('h1', 0)->plaintext; - $item['uri'] = $url; - $item['content'] = $article->find('div.body-text', 0); - $item['enclosures'][] = $article->find('meta[name="twitter:image"]', 0)->content; - $item['timestamp'] = $article->find('div.meta.small', 0)->plaintext; - - if ($article->find('div.meta a', 0)) { - $item['author'] = $article->find('div.meta a', 0)->plaintext; - } - - foreach ($article->find('div.themes li') as $li) { - $item['categories'][] = trim(htmlspecialchars($li->plaintext, ENT_QUOTES)); - } - - return $item; - } -} From 976217111cd99837116f48aa69a9aff462286e23 Mon Sep 17 00:00:00 2001 From: Mynacol Date: Wed, 28 May 2025 19:12:00 +0000 Subject: [PATCH 08/14] [GolemBridge] Add code elements The extractor missed
 elements for code snippets.
For example the code line in
https://www.golem.de/news/falsch-deklarierte-hdds-betrug-bei-festplatten-bleibt-ein-problem-2505-196675.html
---
 bridges/GolemBridge.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bridges/GolemBridge.php b/bridges/GolemBridge.php
index 219233f4..fbd154e3 100644
--- a/bridges/GolemBridge.php
+++ b/bridges/GolemBridge.php
@@ -152,7 +152,7 @@ class GolemBridge extends FeedExpander
             $img->src = $img->getAttribute('data-src-full');
         }
 
-        foreach ($content->find('p, h1, h2, h3, img[src*="."], iframe, video') as $element) {
+        foreach ($content->find('p, h1, h2, h3, pre, img[src*="."], iframe, video') as $element) {
             $item .= $element;
         }
 

From b8064d9dfe210cc20fad303222e104460413071d Mon Sep 17 00:00:00 2001
From: Anton Smirnov 
Date: Fri, 30 May 2025 12:05:36 +0300
Subject: [PATCH 09/14] [EpicGamesFree] Fixes: url not set, other promos shown
 (#4575)

* URI was not set because of the typo

* Filter out other promos
---
 bridges/EpicGamesFreeBridge.php | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/bridges/EpicGamesFreeBridge.php b/bridges/EpicGamesFreeBridge.php
index 087b95be..3b16cd5b 100644
--- a/bridges/EpicGamesFreeBridge.php
+++ b/bridges/EpicGamesFreeBridge.php
@@ -27,7 +27,7 @@ class EpicGamesFreeBridge extends BridgeAbstract
                 'Türkçe' => 'tr',
                 '简体中文' => 'zh-CN',
                 '繁體中文' => 'zh-Hant',
-             ],
+            ],
             'title' => 'Language for game information',
             'defaultValue' => 'en-US',
         ],
@@ -51,16 +51,21 @@ class EpicGamesFreeBridge extends BridgeAbstract
 
         $data = $json['data']['Catalog']['searchStore']['elements'];
         foreach ($data as $element) {
-            if (!isset($element['promotions']['promotionalOffers'][0])) {
+            $promo = $element['promotions']['promotionalOffers'][0]['promotionalOffers'][0] ?? false;
+            if (
+                !$promo ||
+                $promo['discountSetting']['discountType'] !== 'PERCENTAGE' ||
+                $promo['discountSetting']['discountPercentage'] !== 0
+            ) {
                 continue;
             }
             $item = [
                 'author' => $element['seller']['name'],
                 'content' => $element['description'],
                 'enclosures' => array_map(fn($item) => $item['url'], $element['keyImages']),
-                'timestamp' => strtotime($element['promotions']['promotionalOffers'][0]['promotionalOffers'][0]['startDate']),
+                'timestamp' => strtotime($promo['startDate']),
                 'title' => $element['title'],
-                'url' => parent::getURI() . $this->getInput('locale') . '/p/' . $element['urlSlug'],
+                'uri' => parent::getURI() . $this->getInput('locale') . '/p/' . $element['productSlug'],
             ];
             $this->items[] = $item;
         }

From 98e03011dbb80ea9cea3ef8c5e29a1778f169cc9 Mon Sep 17 00:00:00 2001
From: Dag 
Date: Tue, 3 Jun 2025 21:24:35 +0200
Subject: [PATCH 10/14] chore: prepare for 2025-06-03 release (#4583)

---
 lib/Configuration.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/Configuration.php b/lib/Configuration.php
index 60bf80fb..79f2922c 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -7,7 +7,7 @@
  */
 final class Configuration
 {
-    private const VERSION = '2025-01-26';
+    private const VERSION = '2025-06-03';
 
     private static $config = [];
 

From 7aa54602cfe0b5ed2ea91c0588fd80057fa6eb59 Mon Sep 17 00:00:00 2001
From: Tobias Alexander Franke 
Date: Wed, 4 Jun 2025 22:15:28 +0200
Subject: [PATCH 11/14] [FabBridge] Pull 100% discounted items via Fab API
 (#4584)

* [FabBridge] Pull 100% discounted items via Fab API

* [FabBridge] Linter fixes
---
 bridges/FabBridge.php | 43 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100644 bridges/FabBridge.php

diff --git a/bridges/FabBridge.php b/bridges/FabBridge.php
new file mode 100644
index 00000000..6596f869
--- /dev/null
+++ b/bridges/FabBridge.php
@@ -0,0 +1,43 @@
+results as $item) {
+            $thumbnail = $item->thumbnails[0]->mediaUrl;
+            $itemurl = static::URI . '/listings/' . $item->uid;
+
+            $itemapiurl = static::URI . '/i/listings/' . $item->uid;
+            $itemjson = getContents($itemapiurl, $header);
+            $itemjson = json_decode($itemjson);
+
+            $this->items[] = [
+                'title' => $item->title,
+                'author' => $item->user->sellerName,
+                'uri' => $itemurl,
+                'timestamp' => strtotime($item->lastUpdatedAt),
+                'content' => '' . $itemjson->description,
+            ];
+        }
+    }
+}

From 514b3edf0b85c37c69ada3980d355bd6a3c3b052 Mon Sep 17 00:00:00 2001
From: Jonathan Kay 
Date: Thu, 5 Jun 2025 17:41:20 -0400
Subject: [PATCH 12/14] [GoComicsBridge] Fix for JSON being removed (#4585)

- Now redirects to first comic from landing page
- Switched to meta tags
---
 bridges/GoComicsBridge.php | 26 +++++++++++---------------
 1 file changed, 11 insertions(+), 15 deletions(-)

diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
index 968c2c4d..a5784091 100644
--- a/bridges/GoComicsBridge.php
+++ b/bridges/GoComicsBridge.php
@@ -31,25 +31,21 @@ class GoComicsBridge extends BridgeAbstract
     public function collectData()
     {
         $link = $this->getURI();
+        $landingpage = getSimpleHTMLDOM($link);
+
+        $link = $landingpage->find('div[data-post-url]', 0)->getAttribute('data-post-url');
 
         for ($i = 0; $i < $this->getInput('limit'); $i++) {
             $html = getSimpleHTMLDOM($link);
-            // get json data from the first page
-            $json = $html->find('div[class^="ShowComicViewer_showComicViewer__comic__"] script[type="application/ld+json"]', 0)->innertext;
-            $data = json_decode($json, false);
+
+            $imagelink = $html->find('meta[property="og:image"]', 0)->content;
+            $parts = explode('/', $link);
+            $date = DateTime::createFromFormat('Y/m/d', implode('/', array_slice($parts, -3)));
+            $title = $html->find('meta[property="og:title"]', 0)->content;
+            preg_match('/by (.*?) for/', $title, $authormatches);
+            $author = $authormatches[1] ?? 'GoComics';
 
             $item = [];
-
-            $author = $data->author->name;
-            $imagelink = $data->contentUrl;
-            $date = $data->datePublished;
-            $title = $data->name . ' - GoComics';
-
-            // get a permlink for this day's comic if there isn't one specified
-            if ($link === $this->getURI()) {
-                $link = $this->getURI() . '/' . DateTime::createFromFormat('F j, Y', $date)->format('Y/m/d');
-            }
-
             $item['id'] = $imagelink;
             $item['uri'] = $link;
             $item['author'] = $author;
@@ -57,7 +53,7 @@ class GoComicsBridge extends BridgeAbstract
             if ($this->getInput('date-in-title') === true) {
                 $item['title'] = $title;
             }
-            $item['timestamp'] = DateTime::createFromFormat('F j, Y', $date)->setTime(0, 0, 0)->getTimestamp();
+            $item['timestamp'] = $date->setTime(0, 0, 0)->getTimestamp();
             $item['content'] = '';
 
             $link = rtrim(self::URI, '/') . $html->find('a[class*="ComicNavigation_controls__button_previous__"]', 0)->href;

From 8dada08e6920eacf387f2b8e0086350760556290 Mon Sep 17 00:00:00 2001
From: sysadminstory 
Date: Sat, 7 Jun 2025 23:31:02 +0200
Subject: [PATCH 13/14] [IdealoBridge] Bypass bot protection (#4588)

Add some headers (User-Agent, Accept, Accept-Language) and activate
compression to bypass the bot protection
---
 bridges/IdealoBridge.php | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/bridges/IdealoBridge.php b/bridges/IdealoBridge.php
index 55cee467..05a2ebb8 100644
--- a/bridges/IdealoBridge.php
+++ b/bridges/IdealoBridge.php
@@ -35,6 +35,16 @@ class IdealoBridge extends BridgeAbstract
         ]
     ];
 
+    private $headers = [
+        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0',
+        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
+        'Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.5,en;q=0.3'
+    ];
+    private $options = [
+        CURLOPT_TRANSFER_ENCODING => 1,
+        CURLOPT_ACCEPT_ENCODING => 'gzip, deflate, br'
+    ];
+
     public function getIcon()
     {
         return 'https://cdn.idealo.com/storage/ids-assets/ico/favicon.ico';
@@ -53,10 +63,7 @@ class IdealoBridge extends BridgeAbstract
 
         // The cache does not contain the title of the bridge, we must get it and save it in the cache
         if ($product === null) {
-            $header = [
-                'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15'
-            ];
-            $html = getSimpleHTMLDOM($link, $header);
+            $html = getSimpleHTMLDOM($link, $this->headers, $this->options);
             $product = $html->find('.oopStage-title', 0)->find('span', 0)->plaintext;
             $this->saveCacheValue($keyTITLE, $product);
         }
@@ -123,13 +130,8 @@ class IdealoBridge extends BridgeAbstract
     }
     public function collectData()
     {
-        // Needs header with user-agent to function properly.
-        $header = [
-            'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15'
-        ];
-
         $link = $this->getInput('Link');
-        $html = getSimpleHTMLDOM($link, $header);
+        $html = getSimpleHTMLDOM($link, $this->headers, $this->options);
 
         // Get Productname
         $titleobj = $html->find('.oopStage-title', 0);

From 354cea09a7e8fa4bc83c9e118083891a678bad1b Mon Sep 17 00:00:00 2001
From: Jonathan Kay 
Date: Sun, 8 Jun 2025 15:57:41 -0400
Subject: [PATCH 14/14] [GoComicsBridge] Add fallback when link to current
 comic is missing (#4589)

---
 bridges/GoComicsBridge.php | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
index a5784091..8e78ce5e 100644
--- a/bridges/GoComicsBridge.php
+++ b/bridges/GoComicsBridge.php
@@ -32,8 +32,20 @@ class GoComicsBridge extends BridgeAbstract
     {
         $link = $this->getURI();
         $landingpage = getSimpleHTMLDOM($link);
-
-        $link = $landingpage->find('div[data-post-url]', 0)->getAttribute('data-post-url');
+        $element = $landingpage->find('div[data-post-url]', 0);
+        if ($element) {
+            $link = $element->getAttribute('data-post-url');
+        } else { // fallback for comics without data-post-url (assumes daily comic)
+            $nextcomiclink = $landingpage->find('a[class*="ComicNavigation_controls__button_previous__"]', 0)->href;
+            preg_match('/(\d{4}\/\d{2}\/\d{2})/', $nextcomiclink, $nclmatches);
+            if (!empty($nclmatches[1])) {
+                $nextdate = new DateTime($nclmatches[1]);
+                $nextdate = $nextdate->modify('+1 day')->format('Y/m/d');
+                $link = $link . '/' . $nextdate;
+            } else {
+                throw new \Exception('Could not find the first comic URL. Please create a new GitHub issue.');
+            }
+        }
 
         for ($i = 0; $i < $this->getInput('limit'); $i++) {
             $html = getSimpleHTMLDOM($link);