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
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']
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
- );
- }
-}
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;
}
diff --git a/bridges/FabBridge.php b/bridges/FabBridge.php
index bed849be..00130ad2 100644
--- a/bridges/FabBridge.php
+++ b/bridges/FabBridge.php
@@ -10,6 +10,7 @@ class FabBridge extends BridgeAbstract
public function collectData()
{
+
$responseheaders = get_headers(static::URI);
$csrf = "";
@@ -27,8 +28,8 @@ class FabBridge extends BridgeAbstract
'Accept: application/json, text/plain, */*',
'Accept-Language: en',
'Accept-Encoding: gzip, deflate, br, zstd',
- 'Referer: ' . static::URI,
'Cookie: ' . $csrf,
+ 'Referer: ' . static::URI
];
$json = getContents($url, $header);
diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
index 968c2c4d..8e78ce5e 100644
--- a/bridges/GoComicsBridge.php
+++ b/bridges/GoComicsBridge.php
@@ -31,25 +31,33 @@ class GoComicsBridge extends BridgeAbstract
public function collectData()
{
$link = $this->getURI();
+ $landingpage = getSimpleHTMLDOM($link);
+ $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);
- // 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 +65,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;
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;
}
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);
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;
- }
-}
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;
+ }
+ }
+}
+
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)) {
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);
}
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 = [];