Compare commits

...

54 Commits

Author SHA1 Message Date
danoloan10 7fc5182780 [PicaroBridge] Add new bridge
ICS bridge for concerts in Sala Pícaro, Toledo
2023-05-06 22:49:51 +02:00
danoloan10 699556ca04 Add timezone support for Events
In ICS, dates are given for the GMT timezone. If the fetched times
are in a different timezone, they must be corrected.
2023-05-06 22:49:51 +02:00
danoloan10 03e5bd2304 formats: ICS Support
Events array added to BridgeAbstract.
IcsFormat format added to handle events in said array.
2023-05-06 22:49:51 +02:00
danoloan10 b9a4892baf [RivieraBridge] Fix titles, description, mantainer
Fixed the feed's elements titles and added both description and
mantainer.
2023-05-06 22:49:51 +02:00
danoloan10 5514a74b87 [RivieraBridge] Add new bridge
RSS bridge for concerts in Sala La Riviera, in Madrid
2023-05-06 22:49:51 +02:00
mrnoname1000 887f4bbe15
[BugzillaBridge] Explicitly request JSON (#3364) 2023-04-27 19:24:29 +02:00
Paul Prechtel 212c56fde5
[HeiseBridge] Handle heise+ articles better (#3358)
- Stop parsing paywalled heise+ articles, as they had garbage content
  and anyways not the full article.
- Link to archive.today to access the full article without account.
  (Automatically getting the full article from archive.ph was not feasible
  b/c of captchas and problems extracting the actual content)
2023-04-20 23:02:08 +02:00
sysadminstory 00e716d84d
[PepperBridgeAbstract] Fix "no results" check (#3357)
CSS class for "no results" text has changed, so the bridge has been
updated accordingly.
2023-04-20 11:22:53 +02:00
July f0c96008bc
[ScribbleHubBridge] Create new bridge (#3353)
* [ScribbleHubBridge] Create new bridge

* [ScribbleHubBridge] Improve 'Series' filtering

* [ScribbleHubBridge] Properly fetch feed name

* [ScribbleHubBridge] Fix feed name and set feed URI

* [ScribbleHubBridge] Fix linting violations with phpcbf

* [ScribbleHubBridge] Properly handle html encoding in titles
2023-04-19 20:35:04 +02:00
Eugene Molotov 343fd36671
[core] Remove hardcoded maximum duration of 24 hours in loadCacheValue (#3355) 2023-04-19 17:53:35 +02:00
Paul Prechtel a4a7473abb
[Docs] Fix link to SimpleHTMLDOM documentation (#3354) 2023-04-19 17:51:55 +02:00
Paul Prechtel 4068668de9
[ZeitBridge] Re-add paywall workaround (#3352)
Additionally to the Googlebot User-Agent, a Googlebot IP address has to
be used. For now, we can use `X-Forwarded-For` for this.
2023-04-18 18:41:40 +02:00
Eugene Molotov 7c4591c550
[VkBridge] Add detectParameters (#3351) 2023-04-18 18:41:11 +02:00
Paul Prechtel 0718fdc829
[ZeitBridge] Revert User-Agent (#3350)
The Googlebot User-Agent is no longer sufficient to circumvent the
paywall.
2023-04-17 15:33:14 +02:00
Dawid Wróbel 7eca527160
[eBayBridge] New bridge (#3349)
Fixes #3268
2023-04-15 18:40:49 +02:00
triatic f1c54d5d55
[TwitterV2.md] Update to reflect price change to Twitter API (#3347) 2023-04-14 21:32:13 +02:00
Korytov Pavel 1ed7bdcddf
[InternationalInstituteForStrategicStudiesBridge] Repair and improve bridge (#3338)
* [InternationalInstituteForStrategicStudiesBridge] Repair and improve bridge

* [InternationalInstituteForStrategicStudiesBridge] Fix lint
2023-04-08 22:09:07 +02:00
Paroleen 8486c0f8ca
[SpotifyBridge] Add podcasts feed (#3329)
Co-authored-by: Matteo Parolin <matteoparolin99@gmail.com>
2023-03-24 20:34:51 +01:00
Eugene Molotov 249133204e
[UI] Remove excessive top margin in all pages and logo in HTML feed preview (#3326) 2023-03-24 13:44:34 +01:00
Eugene Molotov c8af9f9055
[VkBridge] Make timestamps more accurate (#3325) 2023-03-22 20:32:15 +01:00
Dag 9bb04ba848
Prepare 2023-03-21 release (#3323)
* fix: upgrade version string in php code
2023-03-22 19:32:19 +01:00
realansgar 307f5865c0
[ARDAudiothekBridge] fix feed icon not showing in RSS feeds (#3274)
* [ARDAudiothekBridge] fix feed icon not showing in RSS feeds

* [ARDAudiothekBridge] Fix lint errors
2023-03-21 18:24:28 +01:00
DRogueRonin 36e98e8481
docs: add docker development environment example (#3319) 2023-03-20 19:13:08 +01:00
Dag 347a0e9a3d
fix: patch simple_html_dom, #3309 (#3310) 2023-03-20 19:12:13 +01:00
Dag 4c3ebb312d
feat: improve error handling ux (#3298)
* feat: improve error handling ux

* feat: add error messages for failed xml parsing
2023-03-20 19:11:51 +01:00
Dag 9e9a697b8b
feat: add config option "path" for file cache (#3297) 2023-03-20 19:10:01 +01:00
Miika Launiainen 4e616c7092
[YorushikaBridge] Replace YouTube embeds with YouTube link (#3321) 2023-03-19 12:50:04 +01:00
toineenzo fbe7cc11ec
Add more countries to App Store Bridge (#3246)
* Added more countries

* Fixed Brazil typo

* Update AppleAppStoreBridge.php

Removed whitespace line 52 for lint fix
2023-03-18 19:55:23 +01:00
sysadminstory 23fb5819cd
[FreeTelechargerBridge] New bridge (#3318)
* [FreeTelechargerBridge] New bridge

New bridge

* [FreeTelechargerBridge ] Fix CACHE_TIMEOUT value

Fixed CACHE_TIMEOUT value
2023-03-16 09:35:49 +01:00
vdbhb59 dc8ce20482
Update 06_Public_Hosts.md (#3311)
Added my instance of public hosted RSS-Bridge host.
2023-03-14 15:55:14 +01:00
mad-reyk 224cce08a8
[Sfeed] Fixed category separator and random white spaces (#3308) 2023-03-12 15:21:21 +01:00
mad-reyk c1f446fd19
[Sfeed] Added new format (#3306)
* [Sfeed] Added new format

* [Sfeed] Spaces instead of tabs

* [Sfeed] Move all global functions to class and fix phpcs warnings
2023-03-12 00:13:27 +01:00
Corentin Garcia 19fc2dc100
[GatesNotesBridge] Fix bridge (fix #3294) (#3305) 2023-03-11 23:26:22 +01:00
Dag 2c94791bcd
fix: skip yt json if absent, fix #3301 (#3302) 2023-03-11 20:06:01 +01:00
Dag 1ffb2df46d
New bridge (#3300)
Create rss feed from wallpapers published on erowall.com.

Allow fetching n latest wallpapers sorted by date, views, downloads and
tags.

Co-authored-by: Kurz Junge <kurz.junge.0xa@tutanota.com>
2023-03-11 01:41:02 +01:00
Miika Launiainen dc9530b405
[YorushikaBridge] Created the bridge (#3299) 2023-03-09 18:36:51 +01:00
Bocki 90bf5518cb
[Core] Activate live linting in codespaces (#3293)
* [Core] Add live linting to devcontainer

* Deactivate lint on type
2023-03-08 18:39:50 +01:00
Bocki 783160e715
[nginx] Add ipv6 listener (#3292) 2023-03-07 23:59:22 +01:00
Bocki 0a114c02c2
[Docu] Clarify docker instructions (#3291) 2023-03-07 23:58:21 +01:00
Bocki 2abdc7588a
[Docu] Add documentation for Codespaces (#3289)
* [Docu] Add documentation for Codespaces

* Adapt PR line
2023-03-07 21:34:02 +01:00
Bocki 84e0135959
[Core] github codespaces setup (#3287) 2023-03-07 17:10:36 +01:00
Korytov Pavel f7200756c3
[InternationalInstituteForStrategicStudiesBridge] Add bridge (#3286)
* [InternationalInstituteForStrategicStudiesBridge] Add bridge

* [InternationalInstituteForStrategicStudiesBridge] Fix lint errors
2023-03-07 17:03:50 +01:00
sysadminstory b8ad49c562
[ExtremeDownload] Remove Bridge (#3285)
The Website has been taken down, this bridge is not needed anymore.
2023-03-07 01:02:51 +01:00
Dag 058e792b8f
feat: add filecache config to enable/disable real purge (#3263)
* refactor: cachefactory

* feat: add filecache config to enable/disable real purge

* test: fix test
2023-03-06 21:50:40 +01:00
Dag 007f2b2d8a
feat: sanitize root folder also in php error messages (#3262) 2023-03-06 21:47:25 +01:00
Dag a01c1f6ab0
fix: disallow usage of default password (#3284) 2023-03-06 20:43:44 +01:00
Bocki f0e5ef0fc5
[Various] getKey replacements and docu (#3283)
* [Various] getKey replacements and docu

* more bridges and fix to the abstract

* linting

* revert bandcampdaily. doing more than i thought
2023-03-06 20:01:51 +01:00
Tone b40714079f
Create FinanzflussBridge.php (#3282)
* Create finanzflussBridge.php

new bridge for finanzfluss.de

* Pascal case

* Rename finanzflussBridge.php to FinanzflussBridge.php

* Update FinanzflussBridge.php

more spaces!
2023-03-05 23:45:45 +01:00
Simon 180c332406
Update 06_Public_Hosts.md (#3280)
Change location for bridge.easter.fr
2023-03-05 00:37:42 +01:00
Joseph 8c4dbb32de
[DockerHubBridge] Display compressed image size in items (#3279)
* [DockerHubBridge] Display compressed image size in items

* [DockerHubBridge] lint

* [DockerHubBridge] Use format_bytes()
2023-03-04 17:33:28 +01:00
Ololbu 5ab949ca55
[FicbookBridge] Fix new lines in content (#3278)
* [FicbookBridge] Fix new lines in content

Sets `$stripRN` in `getSimpleHTMLDOMCached` to `false` and replace new line to `br` through `str_replace()`.

* [FicbookBridge] Add space after comma
2023-03-04 16:12:46 +01:00
Bocki f3f98a117c
[Core] Add getKey function (#3275)
* [Core] Add getKey function
2023-03-02 13:25:57 +01:00
Bocki f0d8cfd4d4
[JustWatchBridge] New bridge (#3273)
* [JustWatchBridge] New bridge
2023-03-01 20:24:01 +01:00
Korytov Pavel 4aed05c7b6
[TldrTechBridge] Add AI section (#3272) 2023-02-28 17:28:33 +01:00
81 changed files with 2445 additions and 396 deletions

8
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
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

View File

@ -0,0 +1,27 @@
{
"name": "rss-bridge dev",
"build": { "dockerfile": "Dockerfile" },
"customizations": {
// Configure properties specific to VS Code.
"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",
"phpcs.lintOnType": false
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"xdebug.php-debug",
"bmewburn.vscode-intelephense-client",
"philfontaine.autolaunch",
"eamodio.gitlens",
"shevaua.phpcs"
]
}
},
"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"
}

49
.devcontainer/launch.json Normal file
View File

@ -0,0 +1,49 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"auto": true
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
},
{
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
}
]
}

17
.devcontainer/nginx.conf Normal file
View File

@ -0,0 +1,17 @@
server {
listen 3100 default_server;
root /workspaces/rss-bridge;
access_log /var/log/nginx/rssbridge.access.log;
error_log /var/log/nginx/rssbridge.error.log;
index index.php;
location ~ /(\.|vendor|tests) {
deny all;
return 403; # Forbidden
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass 127.0.0.1:9000;
}
}

7
.devcontainer/xdebug.ini Normal file
View File

@ -0,0 +1,7 @@
[xdebug]
xdebug.mode=develop,debug
xdebug.client_host=localhost
xdebug.client_port=9003
xdebug.start_with_request=yes
xdebug.discover_client_host=false
xdebug.log='/var/www/html/xdebug.log'

View File

@ -98,6 +98,7 @@ class DisplayAction implements ActionInterface
$cache->purgeCache(86400); // 24 hours
$cache->setKey($cache_params);
$events = [];
$items = [];
$infos = [];
$mtime = $cache->getTime();
@ -133,7 +134,6 @@ class DisplayAction implements ActionInterface
$bridge->collectData();
$items = $bridge->getItems();
if (isset($items[0]) && is_array($items[0])) {
$feedItems = [];
foreach ($items as $item) {
@ -141,6 +141,17 @@ class DisplayAction implements ActionInterface
}
$items = $feedItems;
}
// TODO duplication
$events = $bridge->getEvents();
if (isset($events[0]) && is_array($events[0])) {
$feedEvents = [];
foreach ($events as $event) {
$feedEvents[] = new FeedEvent($event);
}
$events = $feedEvents;
}
$infos = [
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
@ -178,6 +189,7 @@ class DisplayAction implements ActionInterface
}
$format->setItems($items);
$format->setEvents($events);
$format->setExtraInfos($infos);
$lastModified = $cache->getTime();
$format->setLastModified($lastModified);

View File

@ -22,6 +22,11 @@ class ARDAudiothekBridge extends BridgeAbstract
* @const IMAGEWIDTHPLACEHOLDER
*/
const IMAGEWIDTHPLACEHOLDER = '{width}';
/*
* File extension appended to image link in $this->icon
* @const IMAGEEXTENSION
*/
const IMAGEEXTENSION = '.jpg';
const PARAMETERS = [
[
@ -115,6 +120,9 @@ class ARDAudiothekBridge extends BridgeAbstract
$this->title = $processedJSON->title;
$this->uri = $processedJSON->sharingUrl;
$this->icon = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $processedJSON->image->url1X1);
// add image file extension to URL so icon is shown in generated RSS feeds, see
// https://github.com/RSS-Bridge/rss-bridge/blob/4aed05c7b678b5673386d61374bba13637d15487/formats/MrssFormat.php#L76
$this->icon = $this->icon . self::IMAGEEXTENSION;
$this->items = array_slice($this->items, 0, $limit);

View File

@ -75,11 +75,7 @@ class AllocineFRBridge extends BridgeAbstract
public function getName()
{
if (!is_null($this->getInput('category'))) {
return self::NAME . ' : '
. array_search(
$this->getInput('category'),
self::PARAMETERS[$this->queriedContext]['category']['values']
);
return self::NAME . ' : ' . $this->getKey('category');
}
return parent::getName();
@ -89,10 +85,6 @@ class AllocineFRBridge extends BridgeAbstract
{
$html = getSimpleHTMLDOM($this->getURI());
$category = array_search(
$this->getInput('category'),
self::PARAMETERS[$this->queriedContext]['category']['values']
);
foreach ($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = [];

View File

@ -25,10 +25,7 @@ class AnimeUltimeBridge extends BridgeAbstract
public function collectData()
{
//Add type filter if provided
$typeFilter = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
$typeFilter = $this->getKey('type');
//Build date and filters for making requests
$thismonth = date('mY') . $typeFilter;
@ -128,12 +125,7 @@ class AnimeUltimeBridge extends BridgeAbstract
public function getName()
{
if (!is_null($this->getInput('type'))) {
$typeFilter = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge';
return 'Latest ' . $this->getKey('type') . ' - Anime-Ultime Bridge';
}
return parent::getName();

View File

@ -37,6 +37,18 @@ class AppleAppStoreBridge extends BridgeAbstract
'India' => 'IN',
'Canada' => 'CA',
'Germany' => 'DE',
'Netherlands' => 'NL',
'Belgium (NL)' => 'BENL',
'Belgium (FR)' => 'BEFR',
'France' => 'FR',
'Italy' => 'IT',
'United Kingdom' => 'UK',
'Spain' => 'ES',
'Portugal' => 'PT',
'Australia' => 'AU',
'New Zealand' => 'NZ',
'Indonesia' => 'ID',
'Brazil' => 'BR',
],
'defaultValue' => 'US',
],

View File

@ -97,13 +97,8 @@ EOD;
public function getName()
{
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
$parameters = $this->getParameters();
$editionValues = array_flip($parameters[0]['edition']['values']);
$categoryValues = array_flip($parameters[0]['category']['values']);
return $categoryValues[$this->getInput('category')] . ' - ' .
$editionValues[$this->getInput('edition')] . ' - Brut.';
return $this->getKey('category') . ' - ' .
$this->getKey('edition') . ' - Brut.';
}
return parent::getName();

View File

@ -85,7 +85,7 @@ class BugzillaBridge extends BridgeAbstract
protected function getTitle($url)
{
// Only request the summary for a faster request
$json = json_decode(getContents($url . '?include_fields=summary'), true);
$json = self::getJSON($url . '?include_fields=summary');
$this->title = 'Bug ' . $this->bugid . ' - ' .
$json['bugs'][0]['summary'] . ' - ' .
// Remove https://
@ -94,7 +94,7 @@ class BugzillaBridge extends BridgeAbstract
protected function collectComments($url)
{
$json = json_decode(getContents($url), true);
$json = self::getJSON($url);
// Array of comments is here
if (!isset($json['bugs'][$this->bugid]['comments'])) {
@ -127,7 +127,7 @@ class BugzillaBridge extends BridgeAbstract
protected function collectUpdates($url)
{
$json = json_decode(getContents($url), true);
$json = self::getJSON($url);
// Array of changesets which contain an array of changes
if (!isset($json['bugs']['0']['history'])) {
@ -159,7 +159,7 @@ class BugzillaBridge extends BridgeAbstract
protected function getUser($user)
{
// Check if the user endpoint is available
if ($this->loadCacheValue($this->instance . 'userEndpointClosed')) {
if ($this->loadCacheValue($this->instance . 'userEndpointClosed', 86400)) {
return $user;
}
@ -170,7 +170,7 @@ class BugzillaBridge extends BridgeAbstract
$url = $this->instance . '/rest/user/' . $user . '?include_fields=real_name';
try {
$json = json_decode(getContents($url), true);
$json = self::getJSON($url);
if (isset($json['error']) and $json['error']) {
throw new Exception();
}
@ -187,4 +187,12 @@ class BugzillaBridge extends BridgeAbstract
$this->saveCacheValue($this->instance . $user, $username);
return $username;
}
protected static function getJSON($url)
{
$headers = [
'Accept: application/json',
];
return json_decode(getContents($url, $headers), true);
}
}

View File

@ -93,7 +93,7 @@ class DockerHubBridge extends BridgeAbstract
<Strong>Last pushed</strong><br>
<p>{$lastPushed}</p>
<Strong>Images</strong><br>
{$this->getImages($result)}
{$this->getImagesTable($result)}
EOD;
$this->items[] = $item;
@ -187,25 +187,37 @@ EOD;
return $url . '/tags/?&name=' . $name;
}
private function getImages($result)
private function getImagesTable($result)
{
$html = <<<EOD
<table style="width:300px;"><thead><tr><th>Digest</th><th>OS/architecture</th></tr></thead></tbody>
EOD;
$data = '';
foreach ($result->images as $image) {
$layersUrl = $this->getLayerUrl($result->name, $image->digest);
$id = $this->getShortDigestId($image->digest);
$html .= <<<EOD
<tr>
<td><a href="{$layersUrl}">{$id}</a></td>
<td>{$image->os}/{$image->architecture}</td>
</tr>
$size = format_bytes($image->size);
$data .= <<<EOD
<tr>
<td><a href="{$layersUrl}">{$id}</a></td>
<td>{$image->os}/{$image->architecture}</td>
<td>{$size}</td>
</tr>
EOD;
}
return $html . '</tbody></table>';
return <<<EOD
<table style="width:400px;">
<thead>
<tr style="text-align: left;">
<th>Digest</th>
<th>OS/architecture</th>
<th>Compressed Size</th>
</tr>
</thead>
</tbody>
{$data}
</tbody>
</table>
EOD;
}
private function getShortDigestId($digest)

97
bridges/EBayBridge.php Normal file
View File

@ -0,0 +1,97 @@
<?php
class EBayBridge extends BridgeAbstract
{
const NAME = 'eBay';
const DESCRIPTION = 'Returns the search results from the eBay auctioning platforms';
const URI = 'https://www.eBay.com';
const MAINTAINER = 'wrobelda';
const PARAMETERS = [[
'url' => [
'name' => 'Search URL',
'title' => 'Copy the URL from your browser\'s address bar after searching for your items and paste it here',
'pattern' => '^(https:\/\/)?(www.)?ebay\.(com|com\.au|at|be|ca|ch|cn|es|fr|de|com\.hk|ie|it|com\.my|nl|ph|pl|com\.sg|co\.uk).*$',
'exampleValue' => 'https://www.ebay.com/sch/i.html?_nkw=atom+rss',
'required' => true,
]
]];
public function getURI()
{
if ($this->getInput('url')) {
# make sure we order by the most recently listed offers
$uri = trim(preg_replace('/([?&])_sop=[^&]+(&|$)/', '$1', $this->getInput('url')), '?&/');
$uri .= (parse_url($uri, PHP_URL_QUERY) ? '&' : '?') . '_sop=10';
return $uri;
} else {
return parent::getURI();
}
}
public function getName()
{
$urlQueries = explode('&', parse_url($this->getInput('url'), PHP_URL_QUERY));
$searchQuery = array_reduce($urlQueries, function ($q, $p) {
if (preg_match('/^_nkw=(.+)$/i', $p, $matches)) {
$q[] = str_replace('+', ' ', urldecode($matches[1]));
}
return $q;
});
if ($searchQuery) {
return $searchQuery[0];
}
return parent::getName();
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
// Remove any unsolicited results, e.g. "Results matching fewer words"
foreach ($html->find('ul.srp-results > li.srp-river-answer--REWRITE_START ~ li') as $inexactMatches) {
$inexactMatches->remove();
}
$results = $html->find('ul.srp-results > li.s-item');
foreach ($results as $listing) {
$item = [];
// Remove "NEW LISTING" label, we sort by the newest, so this is redundant
foreach ($listing->find('.LIGHT_HIGHLIGHT') as $new_listing_label) {
$new_listing_label->remove();
}
$item['title'] = $listing->find('.s-item__title', 0)->plaintext;
$subtitle = implode('', $listing->find('.s-item__subtitle'));
$item['uri'] = $listing->find('.s-item__link', 0)->href;
preg_match('/.*\/itm\/(\d+).*/i', $item['uri'], $matches);
$item['uid'] = $matches[1];
$price = $listing->find('.s-item__details > .s-item__detail > .s-item__price', 0)->plaintext;
$shippingFree = $listing->find('.s-item__details > .s-item__detail > .s-item__freeXDays', 0)->plaintext ?? '';
$localDelivery = $listing->find('.s-item__details > .s-item__detail > .s-item__localDelivery', 0)->plaintext ?? '';
$logisticsCost = $listing->find('.s-item__details > .s-item__detail > .s-item__logisticsCost', 0)->plaintext ?? '';
$location = $listing->find('.s-item__details > .s-item__detail > .s-item__location', 0)->plaintext ?? '';
$sellerInfo = $listing->find('.s-item__seller-info-text', 0)->plaintext ?? '';
$item['enclosures'] = [ $listing->find('.s-item__image-wrapper > img', 0)->src . '#.image' ];
$item['content'] = <<<CONTENT
<p>$sellerInfo $location</p>
<p><span style="font-weight:bold">$price</span> $shippingFree $localDelivery $logisticsCost<span></span></p>
<p>$subtitle</p>
CONTENT;
$this->items[] = $item;
}
}
}

127
bridges/ErowallBridge.php Normal file
View File

@ -0,0 +1,127 @@
<?php
class ErowallBridge extends BridgeAbstract
{
const NAME = 'Erowall.com Bridge';
const URI = 'https://www.erowall.com/';
const DESCRIPTION = 'Latest wallpapers from erowall.com';
const MAINTAINER = 'kurz.junge';
const PARAMETERS = [
'global' => [
'count' => [
'type' => 'number',
'name' => 'Count',
'title' => 'How many wallpapers to fetch',
'defaultValue' => 16
]
],
'By tag' => [
'tag' => [
'type' => 'text',
'name' => 'tag',
'title' => 'Filter results by tag (e.g. playboy)',
'required' => true
]
],
'Latest' => [],
'Most viewed' => [],
'Most downloaded' => []
];
public function collectData()
{
$requestedCount = $this->getInput('count');
$count = 0;
while ($count < $requestedCount) {
# Indexing from 1
$videosURL = $this->getPagedURI($count / 16 + 1);
$website = getSimpleHTMLDOMCached($videosURL);
$nodes = $website->find('.wpmini');
foreach ($nodes as $wpmini) {
$n = $wpmini->find('a', 0);
# The href has format "/w/1234/" so we just remove all non-numeric
$uid = preg_replace('/[^0-9]/', '', $n->href);
$imageURL = self::URI . "/wallpapers/original/$uid.jpg";
$item = [
'title' => "Wallpaper $uid",
'uri' => self::URI . $n->href,
'uid' => "$uid",
'enclosures' => [ $imageURL ],
'content' => "<img src=\"$imageURL\"/>"
];
$tags = basename($n->title, ' wallpaper');
$item['categories'] = array_map(
'ucwords',
explode(',', $tags)
);
$this->items[] = $item;
$count++;
if ($count >= $requestedCount) {
break;
}
}
# In case that current page has less than 16 wallpapers, it is the
# last page and we don't iterate further
if (count($nodes) < 16) {
break;
}
}
}
private function getPagedURI($pgnum)
{
return $this->getURI() . "/page/$pgnum";
}
public function getURI()
{
$ret = self::URI;
switch ($this->queriedContext) {
case 'Most viewed':
$ret .= 'views/';
break;
case 'Most downloaded':
$ret .= 'down/';
break;
case 'Latest':
$ret .= 'dat/';
break;
default:
$tag = $this->getInput('tag');
$ret .= 'teg/' . str_replace(' ', '+', $tag);
}
return $ret;
}
public function getName()
{
$count = $this->getInput('count');
$ret = 'Erowall ';
switch ($this->queriedContext) {
case 'Most viewed':
case 'Most downloaded':
case 'Latest':
$ret .= $count . ' ' . strtolower($this->queriedContext);
break;
case 'By tag':
$tag = $this->getInput('tag');
$ret .= "$count latest " . $tag;
break;
default:
}
return $ret . ' wallpapers';
}
}

View File

@ -1,116 +0,0 @@
<?php
class ExtremeDownloadBridge extends BridgeAbstract
{
const NAME = 'Extreme Download';
const URI = 'https://www.extreme-down.plus/';
const DESCRIPTION = 'Suivi de série sur Extreme Download';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = [
'Suivre la publication des épisodes d\'une série en cours de diffusion' => [
'url' => [
'name' => 'URL de la série',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une série sans le https://www.extreme-down.plus/',
'exampleValue' => 'series-hd/hd-series-vostfr/46631-halt-and-catch-fire-saison-04-vostfr-hdtv-720p.html'],
'filter' => [
'name' => 'Type de contenu',
'type' => 'list',
'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
'values' => [
'Streaming et Téléchargement' => 'both',
'Téléchargement' => 'download',
'Streaming' => 'streaming'
]
]
]
];
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'));
$filter = $this->getInput('filter');
$typesText = [
'download' => 'Téléchargement',
'streaming' => 'Streaming'
];
// Get the TV show title
$this->showTitle = trim($html->find('span[id=news-title]', 0)->plaintext);
$list = $html->find('div[class=prez_7]');
foreach ($list as $element) {
$add = false;
// Link type is needed is needed to generate an unique link
$type = $this->findLinkType($element);
if ($filter == 'both') {
$add = true;
} else {
if ($type == $filter) {
$add = true;
}
}
if ($add == true) {
$item = [];
// Get the element name
$title = $element->plaintext;
// Get thee element links
$links = $element->next_sibling()->innertext;
$item['content'] = $links;
$item['title'] = $this->showTitle . ' ' . $title . ' - ' . $typesText[$type];
// As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element
// should geneerate unique URI to prevent confusion for RSS readers
$item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']);
$this->items[] = $item;
}
}
}
public function getName()
{
switch ($this->queriedContext) {
case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
return $this->showTitle . ' - ' . self::NAME;
break;
default:
return self::NAME;
}
}
public function getURI()
{
switch ($this->queriedContext) {
case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
return self::URI . $this->getInput('url');
break;
default:
return self::URI;
}
}
private function findLinkType($element)
{
$return = '';
// Walk through all elements in the reverse order until finding one with class 'presz_2'
while ($element->class != 'prez_2') {
$element = $element->prev_sibling();
}
$text = html_entity_decode($element->plaintext);
// Regarding the text of the element, return the according link type
if (stristr($text, 'téléchargement') != false) {
$return = 'download';
} elseif (stristr($text, 'streaming') != false) {
$return = 'streaming';
}
return $return;
}
}

View File

@ -135,8 +135,8 @@ class FicbookBridge extends BridgeAbstract
];
if ($this->getInput('include_contents')) {
$content = getSimpleHTMLDOMCached($item['uri']);
$item['content'] = $content->find('#content', 0);
$content = getSimpleHTMLDOMCached($item['uri'], 86400, [], [], true, true, DEFAULT_TARGET_CHARSET, false);
$item['content'] = str_replace("\n", '<br>', $content->find('#content', 0)->innertext);
}
$this->items[] = $item;

View File

@ -0,0 +1,50 @@
<?php
class FinanzflussBridge extends BridgeAbstract
{
const MAINTAINER = 'Tone866';
const NAME = 'finanzfluss Bridge';
const URI = 'https://www.finanzfluss.de/blog';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Feed for finanzfluss';
const LIMIT = 10;
public function collectData()
{
$baseurl = 'https://www.finanzfluss.de';
$dom = getSimpleHTMLDOM('https://www.finanzfluss.de/blog');
foreach ($dom->find('.preview-card') as $li) {
$a = $li->find('a', 0);
$title = $a->find('.title', 0);
$url = $baseurl . $a->href;
//get article
$domarticle = getSimpleHTMLDOM($url);
$content = $domarticle->find('div.content', 0);
//get header-image and set absolute src
$headerimage = $domarticle->find('div.article-header-image', 0);
$headerimageimg = $headerimage->find('img[src]', 0);
$src = $headerimageimg->src;
$headerimageimg->src = $baseurl . $src;
$headerimageimg->srcset = $baseurl . $src;
//set absolute src for all img
foreach ($content->find('img[src]') as $img) {
$src = $img->src;
$img->src = $baseurl . $src;
$img->srcset = $baseurl . $src;
}
//get author
$author = $domarticle->find('div.author-name', 0);
$this->items[] = [
'title' => $title->plaintext,
'uri' => $url,
'content' => $headerimage . '<br />' . $content,
'author' => $author->plaintext
];
}
}
}

View File

@ -0,0 +1,90 @@
<?php
class FreeTelechargerBridge extends BridgeAbstract
{
const NAME = 'Free-Telecharger';
const URI = 'https://www.free-telecharger.live/';
const DESCRIPTION = 'Suivi de série sur Free-Telecharger';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = [
'Suivi de publication de série' => [
'url' => [
'name' => 'URL de la série',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une série sans le https://www.free-telecharger.live/',
'pattern' => 'series.*\.html',
'exampleValue' => 'series-vf-hd/145458-the-last-of-us-saison-1-web-dl-720p.html'
],
]
];
const CACHE_TIMEOUT = 3600;
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'));
// Find all block content of the page
$blocks = $html->find('div[class=block1]');
// Global Infos block
$infosBlock = $blocks[0];
// Links block
$linksBlock = $blocks[2];
// Extract Global Show infos
$this->showTitle = trim($infosBlock->find('div[class=titre1]', 0)->find('font', 0)->plaintext);
$this->showTechDetails = trim($infosBlock->find('div[align=center]', 0)->find('b', 0)->plaintext);
// Get Episodes names and links
$episodes = $linksBlock->find('div[id=link]', 0)->find('font[color=#ff6600]');
$links = $linksBlock->find('div[id=link]', 0)->find('a');
foreach ($episodes as $index => $episode) {
$item = []; // Create an empty item
$item['title'] = $this->showTitle . ' ' . $this->showTechDetails . ' - ' . ltrim(trim($episode->plaintext), '-');
$item['uri'] = $links[$index]->href;
$item['content'] = '<a href="' . $item['uri'] . '">' . $item['title'] . '</a>';
$item['uid'] = hash('md5', $item['uri']);
$this->items[] = $item; // Add this item to the list
}
}
public function getName()
{
switch ($this->queriedContext) {
case 'Suivi de publication de série':
return $this->showTitle . ' ' . $this->showTechDetails . ' - ' . self::NAME;
break;
default:
return self::NAME;
}
}
public function getURI()
{
switch ($this->queriedContext) {
case 'Suivi de publication de série':
return self::URI . $this->getInput('url');
break;
default:
return self::URI;
}
}
public function detectParameters($url)
{
// Example: https://www.free-telecharger.live/series-vf-hd/145458-the-last-of-us-saison-1-web-dl-720p.html
$params = [];
$regex = '/^https:\/\/www.*\.free-telecharger\.live\/(series.*\.html)/';
if (preg_match($regex, $url, $matches) > 0) {
$params['url'] = urldecode($matches[1]);
return $params;
}
return null;
}
}

View File

@ -146,11 +146,7 @@ class GBAtempBridge extends BridgeAbstract
public function getName()
{
if (!is_null($this->getInput('type'))) {
$type = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
return 'GBAtemp ' . $type . ' Bridge';
return 'GBAtemp ' . $this->getKey('type') . ' Bridge';
}
return parent::getName();

View File

@ -1,6 +1,6 @@
<?php
class GatesNotesBridge extends FeedExpander
class GatesNotesBridge extends BridgeAbstract
{
const MAINTAINER = 'corenting';
const NAME = 'Gates Notes';
@ -8,14 +8,51 @@ class GatesNotesBridge extends FeedExpander
const DESCRIPTION = 'Returns the newest articles.';
const CACHE_TIMEOUT = 21600; // 6h
protected function parseItem($item)
public function collectData()
{
$item = parent::parseItem($item);
$params = [
'validYearsString' => 'all',
'setNumber' => '0',
'sortByVideo' => 'all',
'sortByTopic' => 'all'
];
$api_endpoint = '/api/TGNWebAPI/Get_Filtered_Article_Set?';
$apiUrl = self::URI . $api_endpoint . http_build_query($params);
$article_html = getSimpleHTMLDOMCached($item['uri']);
$rawContent = getContents($apiUrl);
$cleanedContent = str_replace('\r\n', '', substr($rawContent, 1, -1));
$cleanedContent = str_replace('\"', '"', $cleanedContent);
// The content is actually a json between quotes with \r\n inserted
$json = json_decode($cleanedContent);
foreach ($json as $article) {
$item = [];
$articleUri = self::URI . '/' . $article->{'_system_'}->name;
$item['uri'] = $articleUri;
$item['title'] = $article->headline;
$item['content'] = self::getItemContent($articleUri);
$item['timestamp'] = strtotime($article->date);
$this->items[] = $item;
}
}
protected function getItemContent($articleUri)
{
// We need to change the headers as the normal desktop website
// use canvas-based image carousels for some pictures
$headers = [
'User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
];
$article_html = getSimpleHTMLDOMCached($articleUri, 86400, $headers);
$content = '';
if (!$article_html) {
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
return $item;
$content .= '<p><em>Could not request ' . $this->getName() . ': ' . $articleUri . '</em></p>';
return $content;
}
$article_html = defaultLinkTo($article_html, $this->getURI());
@ -23,6 +60,20 @@ class GatesNotesBridge extends FeedExpander
$hero_image = '<img src=' . $article_html->find('img.article_top_DMT_Image', 0)->getAttribute('data-src') . '>';
$article_body = $article_html->find('div.TGN_Article_ReadTimeSection', 0);
// Remove the menu bar on some articles (PDF download etc.)
foreach ($article_body->find('.TGN_MenuHolder') as $found) {
$found->remove();
}
// For the carousels pictures, we still to remove the lazy-loading and force the real picture
foreach ($article_body->find('canvas') as $found) {
$found->remove();
}
foreach ($article_body->find('.TGN_PE_C_Img') as $found) {
$found->setAttribute('src', $found->getAttribute('data-src'));
}
// Convert iframe of Youtube videos to link
foreach ($article_body->find('iframe') as $found) {
$iframeUrl = $found->getAttribute('src');
@ -32,6 +83,7 @@ class GatesNotesBridge extends FeedExpander
$found->outertext = '<p><a href="' . $iframeUrl . '">' . $text . '</a></p>';
}
}
// Remove <link> CSS ressources
foreach ($article_body->find('link') as $found) {
$linkedRessourceUrl = $found->getAttribute('href');
@ -42,14 +94,8 @@ class GatesNotesBridge extends FeedExpander
}
$article_body = sanitize($article_body->innertext);
$item['content'] = $top_description . $hero_image . $article_body;
$content = $top_description . $hero_image . $article_body;
return $item;
}
public function collectData()
{
$feed = static::URI . '/rss';
$this->collectExpandableDatas($feed);
return $content;
}
}

View File

@ -625,8 +625,7 @@ class GithubTrendingBridge extends BridgeAbstract
public function getName()
{
if (!is_null($this->getInput('language'))) {
$language = array_search($this->getInput('language'), self::PARAMETERS['By language']['language']['values']);
return self::NAME . ': ' . $language;
return self::NAME . ': ' . $this->getKey('language');
}
return parent::getName();

View File

@ -118,12 +118,22 @@ class HeiseBridge extends FeedExpander
protected function parseItem($feedItem)
{
$item = parent::parseItem($feedItem);
$item['uri'] = explode('?', $item['uri'])[0] . '?seite=all';
// strip rss parameter
$item['uri'] = explode('?', $item['uri'])[0];
// ignore TechStage articles
if (strpos($item['uri'], 'https://www.heise.de') !== 0) {
return $item;
}
// abort on heise+ articles and link to archive.ph for full-text content
if (str_starts_with($item['title'], 'heise+ |')) {
$item['uri'] = 'https://archive.ph/?run=1&url=' . urlencode($item['uri']);
return $item;
}
$item['uri'] .= '?seite=all';
$article = getSimpleHTMLDOMCached($item['uri']);
if ($article) {

View File

@ -0,0 +1,98 @@
<?php
class InternationalInstituteForStrategicStudiesBridge extends BridgeAbstract
{
const MAINTAINER = 'sqrtminusone';
const NAME = 'International Institute For Strategic Studies Bridge';
const URI = 'https://www.iiss.org';
const CACHE_TIMEOUT = 3600; // 1 hour
const DESCRIPTION = 'Returns the latest blog posts from the IISS';
const TEMPLATE_ID = ['BlogArticlePage', 'BlogPage'];
const COMPONENT_ID = '9b0c6919-c78b-4910-9be9-d73e6ee40e50';
public function collectData()
{
$url = 'https://www.iiss.org/api/filteredlist/filter';
$opts = [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => json_encode([
'templateId' => self::TEMPLATE_ID,
'componentId' => self::COMPONENT_ID,
'page' => '1',
'amount' => 1,
'filter' => (object)[],
'tags' => null,
'sortType' => 'Newest',
'restrictionType' => 'Any'
])
];
$headers = [
'Accept: application/json, text/plain, */*',
'Content-Type: application/json;charset=UTF-8'
];
$json = getContents($url, $headers, $opts);
$data = json_decode($json);
foreach ($data->model->Results as $record) {
[$content, $enclosures] = $this->getContents(self::URI . $record->Link);
$this->items[] = [
'uri' => self::URI . $record->Link,
'title' => $record->Heading,
'categories' => [$record->Topic],
'author' => join(', ', array_map(function ($author) {
return $author->Name;
}, $record->Authors)),
'timestamp' => DateTime::createFromFormat('jS F Y', $record->Date)->format('U'),
'content' => $content,
'enclosures' => $enclosures
];
}
}
private function getContents($uri)
{
$html = getSimpleHTMLDOMCached($uri);
$body = $html->find('body', 0);
$scripts = $body->find('script');
$result = '';
$enclosures = [];
foreach ($scripts as $script) {
$script_text = $script->innertext;
if (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.Reading')) {
$args = $this->getRenderArguments($script_text);
$result .= $args->Html;
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.ImagePanel')) {
$args = $this->getRenderArguments($script_text);
$result .= '<figure><img src="' . self::URI . $args->Image . '"></img></figure>';
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.Intro')) {
$args = $this->getRenderArguments($script_text);
$result .= '<p>' . $args->Intro . '</p>';
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.Footnotes')) {
$args = $this->getRenderArguments($script_text);
$result .= '<p>' . $args->Content . '</p>';
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.List')) {
$args = $this->getRenderArguments($script_text);
foreach ($args->Items as $item) {
if ($item->Url != null) {
$match = preg_match('/\\"(.*)\\"/', $item->Url, $matches);
if ($match > 0) {
array_push($enclosures, self::URI . $matches[1]);
}
}
}
}
}
return [$result, $enclosures];
}
private function getRenderArguments($script_text)
{
$matches = [];
preg_match('/React\.createElement\(Components\.\w+, {(.*)}\),/', $script_text, $matches);
return json_decode('{' . $matches[1] . '}');
}
}

View File

@ -138,9 +138,7 @@ class InternetArchiveBridge extends BridgeAbstract
public function getName()
{
if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
$contentValues = array_flip(self::PARAMETERS['Account']['content']['values']);
return $contentValues[$this->getInput('content')] . ' - '
return $this->getKey('content') . ' - '
. $this->processUsername() . ' - Internet Archive';
}

219
bridges/JustWatchBridge.php Normal file
View File

@ -0,0 +1,219 @@
<?php
class JustWatchBridge extends BridgeAbstract
{
const NAME = 'JustWatch';
const URI = 'https://justwatch.com';
const DESCRIPTION = 'Returns latest releases on Streaming Platforms.';
const MAINTAINER = 'Bocki';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [[
'country' => [
'name' => 'Country',
'defaultValue' => 'us',
'type' => 'list',
'values' => [
'North America' => [
'Bermuda' => 'bm',
'Canada' => 'ca',
'Mexico' => 'mx',
'United States' => 'us'
],
'South America' => [
'Argentina' => 'ar',
'Bolivia' => 'bo',
'Brazil' => 'br',
'Chile' => 'cl',
'Colombia' => 'co',
'Ecuador' => 'ec',
'French Guiana' => 'gf',
'Paraguay' => 'py',
'Peru' => 'pe',
'Uruguay' => 'uy',
'Venezuela' => 've'
],
'Europe' => [
'Albania' => 'al',
'Andorra' => 'ad',
'Austria' => 'at',
'Belgium' => 'be',
'Bosnia Herzegovina' => 'ba',
'Bulgaria' => 'bg',
'Croatia' => 'hr',
'Czech Republic' => 'cz',
'Denmark' => 'dk',
'Estonia' => 'ee',
'Finland' => 'fi',
'France' => 'fr',
'Germany' => 'de',
'Gibraltar' => 'gi',
'Greece' => 'gr',
'Guernsey' => 'gg',
'Hungary' => 'hu',
'Iceland' => 'is',
'Ireland' => 'ie',
'Italy' => 'it',
'Kosovo' => 'xk',
'Liechtenstein' => 'li',
'Lithuania' => 'lt',
'Macedonia' => 'mk',
'Malta' => 'mt',
'Moldova' => 'md',
'Monaco' => 'mc',
'Netherlands' => 'nl',
'Norway' => 'no',
'Poland' => 'pl',
'Portugal' => 'pt',
'Romania' => 'ro',
'Russia' => 'ru',
'San Marino' => 'sm',
'Serbia' => 'rs',
'Slovakia' => 'sk',
'Slovenia' => 'si',
'Spain' => 'es',
'Sweden' => 'se',
'Switzerland' => 'ch',
'Turkey' => 'tr',
'United Kingdom' => 'uk',
'Vatican City' => 'va'
],
'Asia' => [
'Hong Kong' => 'hk',
'India' => 'in',
'Indonesia' => 'id',
'Japan' => 'jp',
'Lebanon' => 'lb',
'Malaysia' => 'my',
'Pakistan' => 'pk',
'Philippines' => 'ph',
'Singapore' => 'sg',
'South Korea' => 'kr',
'Taiwan' => 'tw',
'Thailand' => 'th'
],
'Central America' => [
'Costa Rica' => 'cr',
'El Salvador' => 'sv',
'Guatemala' => 'gt',
'Honduras' => 'hn',
'Panama' => 'pa'
],
'Africa' => [
'Algeria' => 'dz',
'Cape Verde' => 'cv',
'Equatorial Guinea' => 'gq',
'Ghana' => 'gh',
'Ivory Coast' => 'ci',
'Kenya' => 'ke',
'Libya' => 'ly',
'Mauritius' => 'mu',
'Morocco' => 'ma',
'Mozambique' => 'mz',
'Niger' => 'ne',
'Nigeria' => 'ng',
'Senegal' => 'sn',
'Seychelles' => 'sc',
'South Africa' => 'za',
'Tunisia' => 'tn',
'Uganda' => 'ug',
'Zambia' => 'zm'
],
'Pacific' => [
'Australia' => 'au',
'Fiji' => 'fj',
'French Polynesia' => 'pf',
'New Zealand' => 'nz'
],
'Middle East' => [
'Bahrain' => 'bh',
'Egypt' => 'eg',
'Iraq' => 'iq',
'Israel' => 'il',
'Jordan' => 'jo',
'Kuwait' => 'kw',
'Oman' => 'om',
'Palestine' => 'ps',
'Qatar' => 'qa',
'Saudi Arabia' => 'sa',
'United Arab Emirates' => 'ae',
'Yemen' => 'ye'
]
]
],
'mediatype' => [
'name' => 'Type',
'defaultValue' => '0',
'type' => 'list',
'values' => [
'All' => 0,
'Movies' => 1,
'Series' => 2
]
]
]
];
public function collectData()
{
$basehtml = getSimpleHTMLDOM($this->getURI());
$basehtml = defaultLinkTo($basehtml, self::URI);
$overviewhtml = getSimpleHTMLDOM($basehtml->find('.navbar__button__link', 1)->href);
$overviewhtml = defaultLinkTo($overviewhtml, self::URI);
$html = getSimpleHTMLDOM($overviewhtml->find('.filter-bar-content-type__item', $this->getInput('mediatype'))->find('a', 0)->href);
$html = defaultLinkTo($html, self::URI);
$today = $html->find('div.title-timeline', 0);
$providers = $today->find('div.provider-timeline');
foreach ($providers as $provider) {
$titles = $html->find('div.horizontal-title-list__item');
foreach ($titles as $title) {
$item = [];
$item['uri'] = $title->find('a', 0)->href;
$item['title'] = $provider->find('picture > img', 0)->alt . ' - ' . $title->find('.title-poster__image > img', 0)->alt;
$image = $title->find('.title-poster__image > img', 0)->attr['src'];
if (str_starts_with($image, 'data')) {
$image = $title->find('.title-poster__image > img', 0)->attr['data-src'];
}
$content = '<b>Provider:</b> '
. $provider->find('picture > img', 0)->alt . '<br>';
$content .= '<b>Media:</b> '
. $title->find('.title-poster__image > img', 0)->alt . '<br>';
if (isset($title->find('.title-poster__badge', 0)->plaintext)) {
$content .= '<b>Type:</b> Series<br>';
$content .= '<b>Season:</b> ' . $title->find('.title-poster__badge', 0)->plaintext . '<br>';
} else {
$content .= '<b>Type:</b> Movie<br>';
}
$content .= '<b>Poster:</b><br><a href="'
. $title->find('a', 0)->href
. '"><img src="'
. $image
. '"></a>';
$item['content'] = $content;
$this->items[] = $item;
}
}
}
public function getURI()
{
return 'https://www.justwatch.com/' . $this->getInput('country');
}
public function getName()
{
if (!is_null($this->getInput('country'))) {
return 'JustWatch - ' . $this->getKey('country') . ' - ' . $this->getKey('mediatype');
}
return parent::getName();
}
public function getIcon()
{
return 'https://www.justwatch.com/appassets/favicon.ico';
}
}

View File

@ -181,11 +181,11 @@ class NineGagBridge extends BridgeAbstract
public function getName()
{
if ($this->getInput('d')) {
$name = sprintf('%s - %s', '9GAG', $this->getParameterKey('d'));
$name = sprintf('%s - %s', '9GAG', $this->getKey('d'));
} elseif ($this->getInput('g')) {
$name = sprintf('%s - %s', '9GAG', $this->getParameterKey('g'));
$name = sprintf('%s - %s', '9GAG', $this->getKey('g'));
if ($this->getInput('t')) {
$name = sprintf('%s [%s]', $name, $this->getParameterKey('t'));
$name = sprintf('%s [%s]', $name, $this->getKey('t'));
}
}
if (!empty($name)) {
@ -236,23 +236,6 @@ class NineGagBridge extends BridgeAbstract
return $this->p;
}
protected function getParameterKey($input = '')
{
$params = $this->getParameters();
$tab = 'Sections';
if ($input === 'd') {
$tab = 'Popular';
}
if (!isset($params[$tab][$input])) {
return '';
}
return array_search(
$this->getInput($input),
$params[$tab][$input]['values']
);
}
protected static function getContent($post)
{
if ($post['type'] === 'Animated') {

View File

@ -47,13 +47,9 @@ class NpciBridge extends BridgeAbstract
public function getName()
{
$product = $this->getInput('product');
if ($product) {
$productNameMap = array_flip(self::PARAMETERS[0]['product']['values']);
$productName = $productNameMap[$product];
return "NPCI Circulars: $productName";
if ($this->getInput('product')) {
return 'NPCI Circulars: ' . $this->getKey('product');
}
return 'NPCI Circulars';
}

View File

@ -94,7 +94,7 @@ class PepperBridgeAbstract extends BridgeAbstract
);
// If there is no results, we don't parse the content because it display some random deals
$noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
$noresult = $html->find('h3[class=size--all-l]', 0);
if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
$this->items = [];
} else {
@ -592,9 +592,7 @@ HEREDOC;
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q');
break;
case $this->i8n('context-group'):
$values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
$group = array_search($this->getInput('group'), $values);
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group;
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $this->getKey('group');
break;
case $this->i8n('context-talk'):
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle();

60
bridges/PicaroBridge.php Normal file
View File

@ -0,0 +1,60 @@
<?php
class PicaroBridge extends BridgeAbstract {
const NAME = 'Sala Pícaro';
const URI = 'https://sala.picarotoledo.com/agenda/';
const DESCRIPTION = 'Concerts in Sala Pícaro, Toledo (Spain)';
const MAINTAINER = 'danoloan';
const PARAMETERS = array(); // Can be omitted!
const CACHE_TIMEOUT = 1;
private function eventToItem($node) : Event {
$event_date = $node->find('.mec-start-date-label', 0);
$event_time = $node->find('.mec-start-time', 0);
$event_title = $node->find('.mec-event-title', 0);
$date = $event_date->innertext . ' ' . strftime("%Y");
$time = $event_time->innertext;
$title = $event_title->find('a', 0)->innertext;
$link = $event_title->find('a', 0)->href;
$date = str_replace("enero", "january", $date);
$date = str_replace("febrero", "february", $date);
$date = str_replace("marzo", "march", $date);
$date = str_replace("abril", "april", $date);
$date = str_replace("mayo", "may", $date);
$date = str_replace("junio", "june", $date);
$date = str_replace("julio", "july", $date);
$date = str_replace("agosto", "august", $date);
$date = str_replace("septiembre", "september", $date);
$date = str_replace("octubre", "october", $date);
$date = str_replace("noviembre", "november", $date);
$date = str_replace("diciembre", "december", $date);
$event = new \Event();
$event->timezone = "Europe/Madrid";
$event->uid = $link;
$event->stamp = time();
$event->start = strtotime($date) + strtotime($time, 0);
$event->end = $event->start + 3600*2;
$event->summary = $title;
$event->fill = false;
return $event;
}
private function getEventItems() : array {
$page = 0;
$events = array();
$html = getSimpleHTMLDOM(self::URI);
foreach ($html->find('.mec-event-article') as $node) {
$event = $this->eventToItem($node);
$events[] = $event;
}
return $events;
}
public function collectData() {
$this->events = array_merge($this->items, $this->getEventItems());
}
}

View File

@ -76,11 +76,7 @@ class PixivBridge extends BridgeAbstract
default:
return parent::getName();
}
$mode = array_search(
$this->getInput('mode'),
self::PARAMETERS['global']['mode']['values']
);
return "Pixiv ${mode} from ${context} ${query}";
return 'Pixiv ' . $this->getKey('mode') . " from ${context} ${query}";
}
public function getURI()
@ -149,7 +145,6 @@ class PixivBridge extends BridgeAbstract
public function collectData()
{
$content = $this->collectWorksArray();
$content = array_filter($content, function ($v, $k) {
return !array_key_exists('isAdContainer', $v);
}, ARRAY_FILTER_USE_BOTH);
@ -191,6 +186,9 @@ class PixivBridge extends BridgeAbstract
}
}
/**
* todo: remove manual file cache
*/
private function cacheImage($url, $illustId, $isImage)
{
$illustId = preg_replace('/[^0-9]/', '', $illustId);

77
bridges/RivieraBridge.php Normal file
View File

@ -0,0 +1,77 @@
<?php
class RivieraBridge extends BridgeAbstract {
const NAME = 'Sala La Riviera';
const URI = 'https://salariviera.com/conciertos-riviera/';
const DESCRIPTION = 'Concerts in Sala La Riviera, Madrid';
const MAINTAINER = 'danoloan';
const PARAMETERS = array(); // Can be omitted!
const CACHE_TIMEOUT = 1;
private function eventToItem($node, string $month) : Event {
$event_date_day = $node->find('.event-date-day', 0);
$event_title = $node->find('.event-title', 0);
$event_status_wrapper = $node->find('.event-status-wrapper', 0);
$date = $event_date_day->innertext . ' ' . $month;
$title = $event_title->find('a', 0)->innertext;
$link = $event_title->find('a', 0)->href;
$date = str_replace("enero", "january", $date);
$date = str_replace("febrero", "february", $date);
$date = str_replace("marzo", "march", $date);
$date = str_replace("abril", "april", $date);
$date = str_replace("mayo", "may", $date);
$date = str_replace("junio", "june", $date);
$date = str_replace("julio", "july", $date);
$date = str_replace("agosto", "august", $date);
$date = str_replace("septiembre", "september", $date);
$date = str_replace("octubre", "october", $date);
$date = str_replace("noviembre", "november", $date);
$date = str_replace("diciembre", "december", $date);
$event = new \Event();
$event->uid = $link;
$event->stamp = time();
$event->start = strtotime($date);
$event->end = strtotime($date) + 86400;
$event->summary = $title;
$event->fill = true;
return $event;
}
private function getEventItemsPage(int $page) {
$events = array();
$html = getSimpleHTMLDOM(self::URI . '/page/' . $page);
$month = '';
foreach ($html->find('.gdlr-list-event, .gdlr-list-by-month-header')
as $node) {
if ($node->class == 'gdlr-list-by-month-header') {
$month = $node->innertext;
} else {
$event = $this->eventToItem($node, $month);
$events[] = $event;
}
}
return array_reverse($events);
}
private function getEventItems() : array {
$page = 0;
$events = array();
do {
$current = $this->getEventItemsPage($page);
$events = array_merge($current, $events);
$page++;
} while (sizeof($current) > 0);
return $events;
}
public function collectData() {
$this->events = array_merge($this->items, $this->getEventItems());
}
}

View File

@ -0,0 +1,110 @@
<?php
class ScribbleHubBridge extends FeedExpander
{
const MAINTAINER = 'phantop';
const NAME = 'Scribble Hub';
const URI = 'https://scribblehub.com/';
const DESCRIPTION = 'Returns chapters from Scribble Hub.';
const PARAMETERS = [
'All' => [],
'Author' => [
'uid' => [
'name' => 'uid',
'required' => true,
// Example: Alyson Greaves's stories
'exampleValue' => '76208',
],
],
'Series' => [
'sid' => [
'name' => 'sid',
'required' => true,
// Example: latest chapters from The Sisters of Dorley by Alyson Greaves
'exampleValue' => '421879',
],
]
];
public function getIcon()
{
return self::URI . 'favicon.ico';
}
public function collectData()
{
$url = 'https://rssscribblehub.com/rssfeed.php?type=';
if ($this->queriedContext === 'Author') {
$url = $url . 'author&uid=' . $this->getInput('uid');
} else { //All and Series use the same source feed
$url = $url . 'main';
}
$this->collectExpandableDatas($url);
}
protected function parseItem($newItem)
{
$item = parent::parseItem($newItem);
//For series, filter out other series from 'All' feed
if (
$this->queriedContext === 'Series'
&& preg_match('/read\/' . $this->getInput('sid') . '-/', $item['uri']) !== 1
) {
return [];
}
if ($item_html = getSimpleHTMLDOMCached($item['uri'])) {
//Retrieve full description from page contents
$item['content'] = $item_html->find('#chp_raw', 0);
//Retrieve image for thumbnail
$item_image = $item_html->find('.s_novel_img > img', 0)->src;
$item['enclosures'] = [$item_image];
//Restore lost categories
$item_story = html_entity_decode($item_html->find('.chp_byauthor > a', 0)->innertext);
$item_sid = $item_html->find('#mysid', 0)->value;
$item['categories'] = [$item_story, $item_sid];
//Generate UID
$item_pid = $item_html->find('#mypostid', 0)->value;
$item['uid'] = $item_sid . "/$item_pid";
}
return $item;
}
public function getName()
{
$name = parent::getName() . " $this->queriedContext";
switch ($this->queriedContext) {
case 'Author':
$page = getSimpleHTMLDOMCached(self::URI . 'profile/' . $this->getInput('uid'));
$title = html_entity_decode($page->find('.p_m_username.fp_authorname', 0)->plaintext);
break;
case 'Series':
$page = getSimpleHTMLDOMCached(self::URI . 'series/' . $this->getInput('sid') . '/a');
$title = html_entity_decode($page->find('.fic_title', 0)->plaintext);
break;
}
if (isset($title)) {
$name .= " - $title";
}
return $name;
}
public function getURI()
{
$uri = parent::getURI();
switch ($this->queriedContext) {
case 'Author':
$uri = self::URI . 'profile/' . $this->getInput('uid');
break;
case 'Series':
$uri = self::URI . 'series/' . $this->getInput('sid') . '/a';
break;
}
return $uri;
}
}

View File

@ -54,11 +54,7 @@ class SplCenterBridge extends FeedExpander
public function getName()
{
if (!is_null($this->getInput('content'))) {
$parameters = $this->getParameters();
$contentValues = array_flip($parameters[0]['content']['values']);
return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center';
return $this->getKey('content') . ' - Southern Poverty Law Center';
}
return parent::getName();

View File

@ -4,7 +4,7 @@ class SpotifyBridge extends BridgeAbstract
{
const NAME = 'Spotify';
const URI = 'https://spotify.com/';
const DESCRIPTION = 'Fetches the latest albums from one or more artists or the latest tracks from one or more playlists';
const DESCRIPTION = 'Fetches the latest items from one or more artists, playlists or podcasts';
const MAINTAINER = 'Paroleen';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [ [
@ -19,7 +19,7 @@ class SpotifyBridge extends BridgeAbstract
'required' => true
],
'country' => [
'name' => 'Country',
'name' => 'Country/Market',
'type' => 'text',
'required' => false,
'exampleValue' => 'US',
@ -36,7 +36,7 @@ class SpotifyBridge extends BridgeAbstract
'name' => 'Spotify URIs',
'type' => 'text',
'required' => true,
'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:playlist:37i9dQZF1DXcBWIGoYBM5M]',
'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:playlist:37i9dQZF1DXcBWIGoYBM5M,spotify:show:6ShFMYxeDNMo15COLObDvC]',
],
'albumtype' => [
'name' => 'Album type',
@ -47,8 +47,18 @@ class SpotifyBridge extends BridgeAbstract
]
] ];
const TOKENURI = 'https://accounts.spotify.com/api/token';
const APIURI = 'https://api.spotify.com/v1/';
const TOKEN_URI = 'https://accounts.spotify.com/api/token';
const API_URI = 'https://api.spotify.com/v1/';
const TYPE_ALBUM = 'artist';
const TYPE_PLAYLIST = 'playlist';
const TYPE_PODCAST = 'show';
const ENTRY_ALBUM = 'album';
const ENTRY_PLAYLIST = 'track';
const ENTRY_PODCAST = 'episode';
const NOT_SUPPORTED = 'Spotify URI not supported';
private $uri = '';
private $name = '';
@ -89,14 +99,31 @@ class SpotifyBridge extends BridgeAbstract
return explode(':', $uri)[2];
}
private function getEntryType($type)
{
$entry_types = [
self::TYPE_ALBUM => self::ENTRY_ALBUM,
self::TYPE_PLAYLIST => self::ENTRY_PLAYLIST,
self::TYPE_PODCAST => self::ENTRY_PODCAST,
];
if (isset($entry_types[$type])) {
return $entry_types[$type];
} else {
throw new \Exception(self::NOT_SUPPORTED);
}
}
private function getDate($entry)
{
if ($entry['type'] === 'album') {
$date = $entry['release_date'];
if (isset($entry['type'])) {
$type = 'release_date';
} else {
$date = $entry['added_at'];
$type = 'added_at';
}
$date = $entry[$type];
if (strlen($date) == 4) {
$date .= '-01-01';
} elseif (strlen($date) == 7) {
@ -148,9 +175,15 @@ class SpotifyBridge extends BridgeAbstract
private function getFirstEntry()
{
if (!is_null($this->getInput('spotifyuri')) && strpos($this->getInput('spotifyuri'), ',') === false) {
$type = $this->getUriType($this->uris[0]) . 's';
$item = $this->fetchContent(self::APIURI . $type . '/'
. $this->getId($this->uris[0]));
$type = $this->getUriType($this->uris[0]);
$uri = self::API_URI . $type . 's/' . $this->getId($this->uris[0]);
if ($type === self::TYPE_PODCAST) {
$uri = $uri . '?market=' . $this->getCountry();
}
$item = $this->fetchContent($uri);
$this->uri = $item['external_urls']['spotify'];
$this->name = $item['name'] . ' - Spotify';
} else {
@ -173,19 +206,20 @@ class SpotifyBridge extends BridgeAbstract
Debug::log('Fetching all entries');
foreach ($this->uris as $uri) {
$type = $this->getUriType($uri) . 's';
$entry_type = $type === 'artists' ? 'albums' : 'tracks';
$type = $this->getUriType($uri);
$entry_type = $this->getEntryType($type);
$fetch = true;
$offset = 0;
$api_url = self::APIURI . $type . '/'
$api_url = self::API_URI . $type . 's/'
. $this->getId($uri)
. '/' . $entry_type
. '?limit=50&country='
. $this->getCountry();
. 's?limit=50';
if ($type === 'artists') {
$api_url = $api_url . '&include_groups=' . $this->getAlbumType();
if ($type === self::TYPE_ALBUM) {
$api_url = $api_url . '&country=' . $this->getCountry() . '&include_groups=' . $this->getAlbumType();
} else {
$api_url = $api_url . '&market=' . $this->getCountry();
}
while ($fetch) {
@ -211,7 +245,7 @@ class SpotifyBridge extends BridgeAbstract
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, self::TOKENURI);
curl_setopt($curl, CURLOPT_URL, self::TOKEN_URI);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
@ -298,6 +332,33 @@ class SpotifyBridge extends BridgeAbstract
return $item;
}
private function getEpisodeData($episode)
{
$item = [];
$item['title'] = $episode['name'];
$item['uri'] = $episode['external_urls']['spotify'];
$item['timestamp'] = $this->getDate($episode);
$item['content'] = '<img style="width: 256px" src="'
. $episode['images'][0]['url']
. '">';
if (isset($episode['description'])) {
$item['content'] = $item['content'] . '<p>'
. $episode['description']
. '</p>';
}
if (isset($episode['audio_preview_url'])) {
$item['content'] = $item['content'] . '<audio controls src="'
. $episode['audio_preview_url']
. '"></audio>';
}
return $item;
}
public function collectData()
{
$offset = 0;
@ -307,10 +368,14 @@ class SpotifyBridge extends BridgeAbstract
Debug::log('Building RSS feed');
foreach ($this->entries as $entry) {
if ($entry['type'] === 'album') {
$item = $this->getAlbumData($entry);
} else {
if (! isset($entry['type'])) {
$item = $this->getTrackData($entry);
} else if ($entry['type'] === self::ENTRY_ALBUM) {
$item = $this->getAlbumData($entry);
} else if ($entry['type'] === self::ENTRY_PODCAST) {
$item = $this->getEpisodeData($entry);
} else {
throw new \Exception(self::NOT_SUPPORTED);
}
$this->items[] = $item;

View File

@ -23,6 +23,7 @@ class TldrTechBridge extends BridgeAbstract
'values' => [
'Tech' => 'tech',
'Crypto' => 'crypto',
'AI' => 'ai'
],
'defaultValue' => 'tech'
]

View File

@ -147,10 +147,7 @@ EOD;
public function getName()
{
if (!is_null($this->getInput('country'))) {
$parameters = $this->getParameters();
$values = array_flip($parameters[0]['country']['values']);
return $values[$this->getInput('country')] . ' - TwitScoop';
return $this->getKey('country') . ' - TwitScoop';
}
return parent::getName();

View File

@ -76,7 +76,7 @@ class UnogsBridge extends BridgeAbstract
if ($this->queriedContext == 'Global') {
$feedName .= 'Netflix Global - ';
} elseif ($this->queriedContext == 'Country') {
$feedName .= 'Netflix ' . $this->getParametersKey('country_code') . ' - ';
$feedName .= 'Netflix ' . $this->getKey('country_code') . ' - ';
}
if ($this->getInput('feed') == 'expiring') {
$feedName .= 'Expiring title';
@ -88,20 +88,6 @@ class UnogsBridge extends BridgeAbstract
return $feedName;
}
private function getParametersKey($input = '')
{
$params = $this->getParameters();
$tab = 'Country';
if (!isset($params[$tab][$input])) {
return '';
}
return array_search(
$this->getInput($input),
$params[$tab][$input]['values']
);
}
private function getJSON($url)
{
$header = [

View File

@ -22,8 +22,18 @@ class VkBridge extends BridgeAbstract
]
]
];
const TEST_DETECT_PARAMETERS = [
'https://vk.com/id1' => ['u' => 'id1'],
'https://vk.com/groupname' => ['u' => 'groupname'],
'https://m.vk.com/groupname' => ['u' => 'groupname'],
'https://vk.com/groupname/anythingelse' => ['u' => 'groupname'],
'https://vk.com/groupname?w=somethingelse' => ['u' => 'groupname'],
'https://vk.com/with_underscore' => ['u' => 'with_underscore'],
];
protected $pageName;
protected $tz = 0;
private $urlRegex = '/vk\.com\/([\w]+)/';
public function getURI()
{
@ -43,6 +53,15 @@ class VkBridge extends BridgeAbstract
return parent::getName();
}
public function detectParameters($url)
{
if (preg_match($this->urlRegex, $url, $matches)) {
return ['u' => $matches[1]];
}
return null;
}
public function collectData()
{
$text_html = $this->getContents();
@ -50,6 +69,13 @@ class VkBridge extends BridgeAbstract
$text_html = iconv('windows-1251', 'utf-8//ignore', $text_html);
$html = str_get_html($text_html);
foreach ($html->find('script') as $script) {
preg_match('/tz: ([0-9]+)/', $script->outertext, $matches);
if (count($matches) > 0) {
$this->tz = intval($matches[1]);
break;
}
}
$pageName = $html->find('.page_name', 0);
if (is_object($pageName)) {
$pageName = $pageName->plaintext;
@ -393,8 +419,9 @@ class VkBridge extends BridgeAbstract
private function getTime($post)
{
if ($time = $post->find('time.PostHeaderSubtitle__item', 0)->getAttribute('time')) {
return $time;
$accurateDateElement = $post->find('span.rel_date', 0);
if ($accurateDateElement) {
return $accurateDateElement->getAttribute('time');
} else {
$strdate = $post->find('time.PostHeaderSubtitle__item', 0)->plaintext;
$strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate);
@ -417,7 +444,7 @@ class VkBridge extends BridgeAbstract
$date['hour'] = $date['minute'] = '00';
}
return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
$date['hour'] . ':' . $date['minute']);
$date['hour'] . ':' . $date['minute']) - $this->tz;
}
}

View File

@ -48,10 +48,7 @@ class WebfailBridge extends BridgeAbstract
{
$html = getSimpleHTMLDOM($this->getURI() . $this->getInput('type'));
$type = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
$type = $this->getKey('type');
switch (strtolower($type)) {
case 'facebook':

View File

@ -117,7 +117,7 @@ The default URI shows the Madara demo page.';
protected function getMangaInfo($url)
{
$url_cache = 'TitleInfo_' . preg_replace('/[^\w]/', '.', rtrim($url, '/'));
$cache = $this->loadCacheValue($url_cache);
$cache = $this->loadCacheValue($url_cache, 86400);
if (isset($cache)) {
return $cache;
}

View File

@ -0,0 +1,93 @@
<?php
class YorushikaBridge extends BridgeAbstract
{
const NAME = 'Yorushika';
const URI = 'https://yorushika.com';
const DESCRIPTION = 'Return news from Yorushika\'s offical website';
const MAINTAINER = 'Miicat_47';
const PARAMETERS = [
'All categories' => [
],
'Only selected categories' => [
'yorushika' => [
'name' => 'Yorushika',
'type' => 'checkbox',
],
'suis' => [
'name' => 'suis',
'type' => 'checkbox',
],
'n-buna' => [
'name' => 'n-buna',
'type' => 'checkbox',
],
]
];
public function collectData()
{
$categories = [];
if ($this->queriedContext == 'All categories') {
array_push($categories, 'all');
} else if ($this->queriedContext == 'Only selected categories') {
if ($this->getInput('yorushika')) {
array_push($categories, 'ヨルシカ');
}
if ($this->getInput('suis')) {
array_push($categories, 'suis');
}
if ($this->getInput('n-buna')) {
array_push($categories, 'n-buna');
}
}
$html = getSimpleHTMLDOM('https://yorushika.com/news/5/')->find('.list--news', 0);
$html = defaultLinkTo($html, $this->getURI());
foreach ($html->find('.inview') as $art) {
$item = [];
// Get article category and check the filters
$art_category = $art->find('.category', 0)->plaintext;
if (!in_array('all', $categories) && !in_array($art_category, $categories)) {
// Filtering is enabled and the category is not selected, skipping
continue;
}
// Get article title
$title = $art->find('.tit', 0)->plaintext;
// Get article url
$url = $art->find('a.clearfix', 0)->href;
// Get article date
$exp_date = '/\d+\.\d+\.\d+/';
$date = $art->find('.date', 0)->plaintext;
preg_match($exp_date, $date, $matches);
$date = date_create_from_format('Y.m.d', $matches[0]);
$date = date_format($date, 'd.m.Y');
// Get article info
$art_html = getSimpleHTMLDOMCached($url)->find('.text.inview', 0);
$art_html = defaultLinkTo($art_html, $this->getURI());
// Check if article contains a embed YouTube video
$exp_youtube = '/https:\/\/[w\.]+youtube\.com\/embed\/([\w]+)/m';
if (preg_match($exp_youtube, $art_html, $matches)) {
// Replace the YouTube embed with a YouTube link
$yt_embed = $art_html->find('iframe[src*="youtube.com"]', 0);
$yt_link = sprintf('<a href="https://youtube.com/watch?v=%1$s">https://youtube.com/watch?v=%1$s</a>', $matches[1]);
$art_html = str_replace($yt_embed, $yt_link, $art_html);
}
$item['uri'] = $url;
$item['title'] = $title . ' (' . $art_category . ')';
$item['content'] = $art_html;
$item['timestamp'] = $date;
$this->items[] = $item;
}
}
}

View File

@ -99,8 +99,11 @@ class YoutubeBridge extends BridgeAbstract
}
$jsonData = $this->getJSONData($html);
$jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents;
if (! isset($jsonData->contents)) {
return;
}
$jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents;
$videoSecondaryInfo = null;
foreach ($jsonData as $item) {
if (isset($item->videoSecondaryInfoRenderer)) {

View File

@ -66,8 +66,10 @@ class ZeitBridge extends FeedExpander
$item['enclosures'] = [];
$headers = [
'User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
'X-Forwarded-For: 66.249.66.1',
'Cookie: zonconsent=' . date('Y-m-d\TH:i:s.v\Z'),
'User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'];
];
// one-page article
$article = getSimpleHTMLDOM($item['uri'], $headers);

View File

@ -2,13 +2,19 @@
class FileCache implements CacheInterface
{
protected $path;
private array $config;
protected $scope;
protected $key;
public function __construct()
public function __construct(array $config = [])
{
if (!is_writable(PATH_CACHE)) {
throw new \Exception('The cache folder is not writeable');
$this->config = $config;
if (!is_dir($this->config['path'])) {
throw new \Exception('The cache path does not exists. You probably want: mkdir cache && chown www-data:www-data cache');
}
if (!is_writable($this->config['path'])) {
throw new \Exception('The cache path is not writeable. You probably want: chown www-data:www-data cache');
}
}
@ -24,7 +30,7 @@ class FileCache implements CacheInterface
{
$writeStream = file_put_contents($this->getCacheFile(), serialize($data));
if ($writeStream === false) {
throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
throw new \Exception('The cache path is not writeable. You probably want: chown www-data:www-data cache');
}
return $this;
}
@ -46,7 +52,11 @@ class FileCache implements CacheInterface
public function purgeCache($seconds)
{
$cachePath = $this->getPath();
if (! $this->config['enable_purge']) {
return;
}
$cachePath = $this->getScope();
if (!file_exists($cachePath)) {
return;
}
@ -73,7 +83,7 @@ class FileCache implements CacheInterface
throw new \Exception('The given scope is invalid!');
}
$this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
$this->scope = $this->config['path'] . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
return $this;
}
@ -90,24 +100,24 @@ class FileCache implements CacheInterface
return $this;
}
private function getPath()
private function getScope()
{
if (is_null($this->path)) {
if (is_null($this->scope)) {
throw new \Exception('Call "setScope" first!');
}
if (!is_dir($this->path)) {
if (mkdir($this->path, 0755, true) !== true) {
if (!is_dir($this->scope)) {
if (mkdir($this->scope, 0755, true) !== true) {
throw new \Exception('mkdir: Unable to create file cache folder');
}
}
return $this->path;
return $this->scope;
}
private function getCacheFile()
{
return $this->getPath() . $this->getCacheName();
return $this->getScope() . $this->getCacheName();
}
private function getCacheName()

View File

@ -75,8 +75,8 @@ enable = false
username = "admin"
; This default password is public knowledge. Replace it.
password = "7afbf648a369b261"
; The password cannot be the empty string if authentication is enabled.
password = ""
; This will be used only for actions that require privileged access
access_token = ""
@ -95,6 +95,13 @@ report_limit = 1
; --- Cache specific configuration ---------------------------------------------
[FileCache]
; The root folder to store files in.
; "" = Use the cache folder in the repository (default)
path = ""
; Whether to actually delete files when purging. Can be useful to turn off to increase performance.
enable_purge = true
[SQLiteCache]
file = "cache.sqlite"

View File

@ -1,5 +1,6 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /app;
access_log /var/log/nginx/rssbridge.access.log;
error_log /var/log/nginx/rssbridge.error.log;

View File

@ -4,7 +4,7 @@
|:-------:|---------|--------|----------|---------|
| ![](https://iplookup.flagfox.net/images/h16/GB.png) | https://rss-bridge.org/bridge01 | ![](https://img.shields.io/website/https/rss-bridge.org/bridge01.svg) | [@dvikan](https://github.com/dvikan) | London, Digital Ocean|
| ![](https://iplookup.flagfox.net/images/h16/GB.png) | https://rss-bridge.lewd.tech | ![](https://img.shields.io/website/https/rss-bridge.lewd.tech.svg) | [@Erisa](https://github.com/Erisa) | Hosted in London, protected by Cloudflare Rate Limiting |
| ![](https://iplookup.flagfox.net/images/h16/FR.png) | https://bridge.easter.fr | ![](https://img.shields.io/website/https/bridge.easter.fr.svg) | [@chatainsim](https://github.com/chatainsim) | Hosted in Roubaix, France |
| ![](https://iplookup.flagfox.net/images/h16/FR.png) | https://bridge.easter.fr | ![](https://img.shields.io/website/https/bridge.easter.fr.svg) | [@chatainsim](https://github.com/chatainsim) | Hosted in Isère, France |
| ![](https://iplookup.flagfox.net/images/h16/FR.png) | https://wtf.roflcopter.fr/rss-bridge/ | ![](https://img.shields.io/website/https/wtf.roflcopter.fr/rss-bridge.svg) | [roflcopter.fr](https://wtf.roflcopter.fr/) | Hosted in France |
| ![](https://iplookup.flagfox.net/images/h16/DE.png) | https://rss.nixnet.services | ![](https://img.shields.io/website/https/rss.nixnet.services.svg) | [@amolith](https://nixnet.services/contact) | Hosted in Wunstorf, Germany |
| ![](https://iplookup.flagfox.net/images/h16/AT.png) | https://rss-bridge.ggc-project.de | ![](https://img.shields.io/website/https/rss-bridge.ggc-project.de) | [@ggc-project.de](https://social.dev-wiki.de/@ggc_project) | Hosted in Steyr, Austria |
@ -18,6 +18,7 @@
| ![](https://iplookup.flagfox.net/images/h16/NL.png) | https://feed.eugenemolotov.ru | ![](https://img.shields.io/website/https/feed.eugenemolotov.ru.svg) | [@em92](https://github.com/em92) | Hosted in Amsterdam, Netherlands |
| ![](https://iplookup.flagfox.net/images/h16/DE.png) | https://rss-bridge.mediani.de | ![](https://img.shields.io/website/https/rss-bridge.mediani.de.svg) | [@sokai](https://github.com/sokai) | Hosted with Netcup, Germany |
| ![](https://iplookup.flagfox.net/images/h16/US.png) | http://rb.vern.cc/ | ![](https://img.shields.io/website/https/rb.vern.cc.svg) | [@vern.cc](https://vern.cc/en/admin) | Hosted with Hetzner, US |
| ![](https://iplookup.flagfox.net/images/h16/FR.png) | https://rssbridge.flossboxin.org.in/ | ![](https://img.shields.io/badge/website-up-brightgreen) | [@vdbhb59](https://github.com/vdbhb59) | Hosted with OVH SAS (Maintained in India)
## Inactive instances

View File

@ -16,17 +16,9 @@ rssbridge/rss-bridge:latest
docker start rss-bridge
```
And access it using `http://IP_Address:3000`. If you'd like to run a specific version, you can run it by:
Access it using `http://IP_Address:3000`. If you'd like to run a specific version, you can run it by changing the ':latest' on the image to a tag listed [here](https://hub.docker.com/r/rssbridge/rss-bridge/tags/)
```bash
docker create \
--name=rss-bridge \
--volume </local/custom/path>:/config \
--publish 3000:80 \
rssbridge/rss-bridge:$version
```
Where you can get the versions published to Docker Hub at https://hub.docker.com/r/rssbridge/rss-bridge/tags/. The server runs on port 80 internally, and you can publish it on a different port (change 3000 to your choice).
The server runs on port 80 internally, map any port of your choice (in this example 3000).
You can run it using a `docker-compose.yml` as well:
@ -50,9 +42,8 @@ services:
|Realtime container logs|`docker logs -f rss-bridge`|
# Adding custom bridges and configurations
If you want to add a bridge that is not part of [`/bridges`](https://github.com/RSS-Bridge/rss-bridge/tree/master/bridges), you can specify an additional folder to copy necessary files to the `rss-bridge` container.
If you want to add a bridge that is not part of [`/bridges`](https://github.com/RSS-Bridge/rss-bridge/tree/master/bridges), you can map a folder to the `/config` folder of the `rss-bridge` container.
_Here **root** is folder where `docker-compose.yml` resides._
1. Create `custom` folder in root.
2. Copy your [bridges files](../05_Bridge_API/01_How_to_create_a_new_bridge.md) to the `custom` folder. You can also add your custom [whitelist.txt](../03_For_Hosts/05_Whitelisting.md) file and your custom [config.ini.php](../03_For_Hosts/08_Custom_Configuration.md) to this folder.
3. Run `docker-compose up` to recreate service.
1. Create a folder in the location of your docker-compose.yml or your general docker working area (in this example it will be `/home/docker/rssbridge/config` ).
2. Copy your [custom bridges](../05_Bridge_API/01_How_to_create_a_new_bridge.md) to the `/home/docker/rssbridge/config` folder. You can also add your custom [whitelist.txt](../03_For_Hosts/05_Whitelisting.md) file and your custom [config.ini.php](../03_For_Hosts/08_Custom_Configuration.md) to this folder.
3. Map the folder to `/config` inside the container. To do that, replace the `</local/custom/path>` from the previous examples with `/home/docker/rssbridge/config`

View File

@ -0,0 +1,35 @@
Github Codespaces lets you develop RSS-Bridge right from within your browser in an online hosted environment without the need to install anything. Github Codespaces is free, check out [this](https://github.com/features/codespaces) for more info.
# How to get started
You must enable Codespaces for your account [here](https://github.com/features/codespaces) . After you are enabled to use Codespaces, you will get the additional functionality that you can see in the screenshots below.
# How to develop for RSS-Bridge
This will give you an example workflow of how to create a bridge for RSS-Bridge using codespaces
1. Fork the main RSS-Bridge repo
2. On your own repo, click the "code" icon on the top on your repo and select "Create codespace on master"
![create codespace](../images/codespaces_01.png)
3. A new window will open and show this screen. This means that your dev environment is being prepared
![creating](../images/codespaces_02.png)
4. When the window has loaded, give it some time to run all the preparation scripts. You will see that it is done when you see a "Listen for Xdebug (rss-bridge)" line in the bottom row
![done](../images/codespaces_03.png)
5. At this point, there is a running instance of RSS-Bridge active that you can open by clicking on the "PORTS" tab and then on the icon to open the website for port 3100
![ports](../images/codespaces_04.png)
6. Xdebug is already started so you can set breakpoints and check out the variables in the debug pane
![debug](../images/codespaces_05.png)
7. You can now create a new branch for your new bridge by clicking on the "master" entry in the bottom left and select "create new branch" from the menu.
8. You can commit straight from the IDE as your github credentials are already included in the Codespace.
9. To open a PR, either go back to the Github website and open it there or do it right from the Codespaces instance using the github integration (when you push a new branch, it will ask you if you want to open a new PR).
# How-Tos
This guide assumes that you already know the basics of php development, some basics in VScode and some basics in working with git. If you want to know more about any of these steps, check out these How-Tos
* Check [How to create a new Bridge](../05_Bridge_API/01_How_to_create_a_new_bridge.md) on how to do that.
* Check [This Youtube Tutorial](https://youtu.be/i_23KUAEtUM?t=54) for a quick introduction to using VSCode with Git (ignore the initial git setup, Codespaces does that for you)

View File

@ -0,0 +1,55 @@
These are examples of how to setup a local development environment to add bridges, improve the docs, etc.
## Docker
The following can serve as an example for using docker:
```
# create a new directory
mkdir rss-bridge-contribution
cd rss-bridge-contribution
# clone the project into a subfolder
git clone https://github.com/RSS-Bridge/rss-bridge
```
Then add a `docker-compose.yml` file:
```yml
version: '3'
services:
rss-bridge:
build:
context: ./rss-bridge
ports:
- 3000:80
volumes:
- ./config:/config
- ./rss-bridge/bridges:/app/bridges
```
You can then access RSS-Bridge at `localhost:3000` and [add your bridge](../05_Bridge_API/How_to_create_a_new_bridge) to the `rss-bridge/bridges` folder.
If you need to edit any other files, like from the `lib` folder add this to the `volumes` section: `./rss-bridge/lib:/app/lib`.
### Docs with Docker
If you want to edit the docs add this to your docker-compose.yml:
```yml
services:
[...]
daux:
image: daux/daux.io
ports:
- 8085:8085
working_dir: /build
volumes:
- ./rss-bridge/docs:/build/docs
network_mode: host
```
and run for example the `daux serve` command with `docker-compose run --rm daux daux serve`.
After that you can access the docs at `localhost:8085` and edit the files in `rss-bridge/docs`.

View File

@ -489,11 +489,11 @@ public function collectData()
Within the context of the current bridge, loads a value by key from cache. Optionally specifies the cache duration for the key. Returns `null` if the key doesn't exist or the value is expired.
```php
protected function loadCacheValue($key, $duration = 86400)
protected function loadCacheValue($key, $duration = null)
```
- `$key` - the name under which the value is stored in the cache.
- `$duration` - the maximum time in seconds after which the value expires. The default duration is 86400 (24 hours).
- `$duration` - the maximum time in seconds after which the value expires.
Usage example:

View File

@ -7,6 +7,35 @@ $this->getInput('your input name here');
`getInput` will either return the value for your parameter or `null` if the parameter is unknown or not specified.
# getKey
The `getKey` function is used to receive the key name to a selected list value given the name of the list, specified in `const PARAMETERS`
Is able to work with multidimensional list arrays.
```PHP
// Given a multidimensional array like this
const PARAMETERS = [[
'country' => [
'name' => 'Country',
'type' => 'list',
'values' => [
'North America' => [
'Mexico' => 'mx',
'United States' => 'us'
],
'South America' => [
'Uruguay' => 'uy',
'Venezuela' => 've'
],
]
]
]],
// Provide the list name to the function
$this->getKey('country');
// if the selected value was "ve", this function will return "Venezuela"
```
`getKey` will either return the key name for your parameter or `null` if the parameter is unknown or not specified.
# getContents
The `getContents` function uses [cURL](https://secure.php.net/manual/en/book.curl.php) to acquire data from the specified URI while respecting the various settings defined at a global level by RSS-Bridge (i.e., proxy host, user agent, etc.). This function accepts a few parameters:
@ -24,7 +53,7 @@ $html = getContents($url, $header, $opts);
```
# getSimpleHTMLDOM
The `getSimpleHTMLDOM` function is a wrapper for the [simple_html_dom](http://simplehtmldom.sourceforge.net/) [file_get_html](http://simplehtmldom.sourceforge.net/manual_api.htm#api) function in order to provide context by design.
The `getSimpleHTMLDOM` function is a wrapper for the [simple_html_dom](https://simplehtmldom.sourceforge.io/) [file_get_html](https://simplehtmldom.sourceforge.io/docs/1.9/api/file_get_html/) function in order to provide context by design.
```PHP
$html = getSimpleHTMLDOM('your URI');

View File

@ -12,7 +12,7 @@ Configuration
- I will not detail exactly how to do this, as the specific process will likely change over time. You should easily be able to find guides using your search engine of choice.
- A basic free developer account grants Essential access to the Twitter API v2, which should be sufficient for this bridge.
- Note: as of April 2023, the "Free" access level no longer allows read access. The cheapest access level with read access is called "Basic".
2. Create a Twitter Project and App, get Bearer Token
@ -34,4 +34,4 @@ Configuration
[TwitterV2Bridge]
twitterv2apitoken = %Bearer Token from step 2%
```
- If you don't have a **config.ini.php**, create one by making a copy of **config.default.ini.php**
- If you don't have a **config.ini.php**, create one by making a copy of **config.default.ini.php**

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

30
formats/IcsFormat.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* ICS
* Returns $this->events in iCalendar format.
*/
class ICSFormat extends FormatAbstract {
const MIME_TYPE = 'text/plain';
public function stringify(){
$events = $this->getEvents();
$calendar = new \Calendar($events);
return $calendar->getString();
$toReturn = print_r($data, true);
// Remove invalid non-UTF8 characters
ini_set('mbstring.substitute_character', 'none');
$toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
return $toReturn;
}
public function display(){
$this
->setContentType(self::MIME_TYPE . '; charset=' . $this->getCharset())
->callContentType();
return parent::display();
}
}

69
formats/SfeedFormat.php Normal file
View File

@ -0,0 +1,69 @@
<?PHP
class SfeedFormat extends FormatAbstract
{
const MIME_TYPE = 'text/plain';
private function escape(string $str)
{
$str = str_replace('\\', '\\\\', $str);
$str = str_replace("\n", '\\n', $str);
return str_replace("\t", '\\t', $str);
}
private function getFirstEnclosure(array $enclosures)
{
if (count($enclosures) >= 1) {
return $enclosures[0];
}
return '';
}
private function getCategories(array $cats)
{
$toReturn = '';
$i = 1;
foreach ($cats as $cat) {
$toReturn .= trim($cat);
if (count($cats) > $i++) {
$toReturn .= '|';
}
}
return $toReturn;
}
public function stringify()
{
$items = $this->getItems();
$toReturn = '';
foreach ($items as $item) {
$toReturn .= sprintf(
"%s\t%s\t%s\t%s\thtml\t\t%s\t%s\t%s\n",
$item->toArray()['timestamp'],
preg_replace('/\s/', ' ', $item->toArray()['title']),
$item->toArray()['uri'],
$this->escape($item->toArray()['content']),
$item->toArray()['author'],
$this->getFirstEnclosure(
$item->toArray()['enclosures']
),
$this->escape(
$this->getCategories(
$item->toArray()['categories']
)
)
);
}
// Remove invalid non-UTF8 characters
ini_set('mbstring.substitute_character', 'none');
$toReturn = mb_convert_encoding(
$toReturn,
$this->getCharset(),
'UTF-8'
);
return $toReturn;
}
}
// vi: expandtab

View File

@ -14,6 +14,13 @@
final class AuthenticationMiddleware
{
public function __construct()
{
if (Configuration::getConfig('authentication', 'password') === '') {
throw new \Exception('The authentication password cannot be the empty string');
}
}
public function __invoke(): void
{
$user = $_SERVER['PHP_AUTH_USER'] ?? null;

View File

@ -106,6 +106,17 @@ abstract class BridgeAbstract implements BridgeInterface
*/
protected array $inputs = [];
/**
* Holds the list of events collected by the bridge
*
* Events must be collected by {@see BridgeInterface::collectData()}
*
* Use {@see BridgeAbstract::getEvents()} to access events.
*
* @var array
*/
protected $events = [];
/**
* Holds the name of the queried context
*
@ -119,6 +130,11 @@ abstract class BridgeAbstract implements BridgeInterface
return $this->items;
}
/** {@inheritdoc} */
public function getEvents() {
return $this->events;
}
/**
* Sets the input values for a given context.
*
@ -294,6 +310,40 @@ abstract class BridgeAbstract implements BridgeInterface
return $this->inputs[$this->queriedContext][$input]['value'];
}
/**
* Get the key name of a given input
* Can process multilevel arrays with two levels, the max level a list can have
*
* @param string $input The input name
* @return string|null The accompaning key to a given input or null if the input is not defined
*/
public function getKey($input)
{
if (!isset($this->inputs[$this->queriedContext][$input]['value'])) {
return null;
}
if (array_key_exists('global', static::PARAMETERS)) {
if (array_key_exists($input, static::PARAMETERS['global'])) {
$context = 'global';
}
}
if (!isset($context)) {
$context = $this->queriedContext;
}
$needle = $this->inputs[$this->queriedContext][$input]['value'];
foreach (static::PARAMETERS[$context][$input]['values'] as $first_level_key => $first_level_value) {
if ($needle === (string)$first_level_value) {
return $first_level_key;
} elseif (is_array($first_level_value)) {
foreach ($first_level_value as $second_level_key => $second_level_value) {
if ($needle === (string)$second_level_value) {
return $second_level_key;
}
}
}
}
}
/**
* Get bridge configuration value
*/
@ -375,10 +425,10 @@ abstract class BridgeAbstract implements BridgeInterface
* Loads a cached value for the specified key
*
* @param string $key Key name
* @param int $duration Cache duration (optional, default: 24 hours)
* @param int $duration Cache duration (optional)
* @return mixed Cached value or null if the key doesn't exist or has expired
*/
protected function loadCacheValue($key, int $duration = 86400)
protected function loadCacheValue($key, $duration = null)
{
$cacheFactory = new CacheFactory();
@ -387,7 +437,7 @@ abstract class BridgeAbstract implements BridgeInterface
$scope = $this->getShortName();
$cache->setScope($scope);
$cache->setKey($key);
if ($cache->getTime() < time() - $duration) {
if ($duration && $cache->getTime() < time() - $duration) {
return null;
}
return $cache->loadData();

View File

@ -87,6 +87,13 @@ interface BridgeInterface
*/
public function getItems();
/**
* Returns an array of collected events
*
* @return array Associative array of events
*/
public function getEvents();
/**
* Returns the bridge maintainer
*

View File

@ -24,19 +24,34 @@ class CacheFactory
if (preg_match('/(.+)(?:Cache)$/i', $name, $matches)) {
$name = $matches[1];
}
if (in_array(strtolower($name), array_map('strtolower', $cacheNames))) {
$index = array_search(strtolower($name), array_map('strtolower', $cacheNames));
$name = $cacheNames[$index];
} else {
$index = array_search(strtolower($name), array_map('strtolower', $cacheNames));
if ($index === false) {
throw new \InvalidArgumentException(sprintf('Invalid cache name: "%s"', $name));
}
if (! preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) {
throw new \InvalidArgumentException(sprintf('Invalid cache name: "%s"', $name));
$className = $cacheNames[$index] . 'Cache';
if (!preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $className)) {
throw new \InvalidArgumentException(sprintf('Invalid cache classname: "%s"', $className));
}
$className = $name . 'Cache';
if (!file_exists(PATH_LIB_CACHES . $className . '.php')) {
throw new \Exception('Unable to find the cache file');
switch ($className) {
case NullCache::class:
return new NullCache();
case FileCache::class:
return new FileCache([
// Intentionally checking for "truthy" value
'path' => Configuration::getConfig('FileCache', 'path') ?: PATH_CACHE,
'enable_purge' => Configuration::getConfig('FileCache', 'enable_purge'),
]);
case SQLiteCache::class:
return new SQLiteCache();
case MemcachedCache::class:
return new MemcachedCache();
default:
if (!file_exists(PATH_LIB_CACHES . $className . '.php')) {
throw new \Exception('Unable to find the cache file');
}
return new $className();
}
return new $className();
}
}

130
lib/Calendar.php Normal file
View File

@ -0,0 +1,130 @@
<?php
class Calendar {
/** @var array $calendar the calendar array */
protected $calendar = array();
/** @var string $header the header of the calendar */
protected $header =
"BEGIN:VCALENDAR\r\n" .
"PRODID:-//php/ics\r\n" .
"VERSION:2.0\r\n" .
"METHOD:PUBLISH\r\n";
/** @var string $footer the footer of the calendar */
protected $footer = 'END:VCALENDAR';
/** @var string $string ics string */
protected $string = '';
/**
* __construct
* @param array $calendar the calendar as an array
*/
public function __construct(array $calendar) {
$this->calendar = $calendar;
$this->string = $this->header;
foreach($this->calendar as $event) {
$this->string .= self::generateEventString($event);
}
}
/**
* getString
* @return string ics string
*/
public function getString() {
return $this->string . $this->footer;
}
public function addEvent(array $event) {
$calendar[] = $event;
$this->string .= self::generateEventString($event);
}
public function toArray() {
return $this->calendar;
}
/**
*
* generateEventString
* @param array $event
* @return string event as ics string
*/
public static function generateEventString(Event $event) {
$ret = "BEGIN:VEVENT\r\n";
$eventParts = array();
// set uid
if($event->uid != null) {
$eventParts['UID'] = md5($event->uid . "@" . $_SERVER['SERVER_NAME']);
}
// set creation date
if($event->stamp != null) {
$eventParts['DTSTAMP'] = gmstrftime("%Y%m%dT%H%M00Z", $event->stamp);
}
elseif($event->start != null) {
$eventParts['DTSTAMP'] = gmstrftime("%Y%m%dT%H%M00Z", $event->start);
}
$format = $event->fill ? "%Y%m%d" : "%Y%m%dT%H%M00Z";
// set start time of the event
if($event->start != null) {
$eventParts['DTSTART'] = gmstrftime($format, $event->start);
}
// set end time of the event
if($event->end != null) {
$eventParts['DTEND'] = gmstrftime($format, $event->end);
}
// set summary
if($event->summary != null) {
$eventParts['SUMMARY'] = self::cleanString($event->summary);
}
// set description
if($event->desc != null) {
$eventParts['DESCRIPTION'] = self::cleanString($event->desc);
}
// set location
if($event->location != null) {
$eventParts['LOCATION'] = self::cleanString($event->location);
}
// check if all needed values are set if not throw exception
if(!isset($eventParts['UID']) ||
!isset($eventParts['DTSTAMP']) ||
!isset($eventParts['DTSTART']) ||
!isset($eventParts['DTEND']) ||
!isset($eventParts['SUMMARY']))
{
throw new Exception(implode(', ', $eventParts));
}
// add event parts to return string
foreach($eventParts as $strKey => $strValue) {
$ret .= $strKey . ":" . $strValue . "\r\n";
}
// add end to return string
$ret .= "END:VEVENT" . "\r\n";
// return event string
return($ret);
}
/**
* cleanString
* @param string $strDirtyString the dirty input string
* @return string cleaned string
*/
public static function cleanString($dirty) {
$bad = array('<br />', '<br/>', '<br>', "\r\n", "\r", "\n", "\t", '"');
$good = array('\n', '\n', '\n', '', '', '', ' ', '\"');
return(trim(str_replace($bad, $good, strip_tags($dirty, '<br>'))));
}
}

View File

@ -19,7 +19,7 @@
*/
final class Configuration
{
private const VERSION = 'dev.2022-06-14';
private const VERSION = 'dev.2023-03-22';
private static $config = [];

314
lib/Event.php Normal file
View File

@ -0,0 +1,314 @@
<?php
class Event {
protected $uid = null;
protected $stamp = null;
protected $start = null;
protected $end = null;
protected $summary = null;
protected $desc = null;
protected $location = null;
protected $fill = null;
protected $timezone = "GMT";
protected function correctTimezone($timestamp) {
$offset = (new DateTime())
->setTimezone(new DateTimeZone($this->timezone))
->setTimestamp($timestamp)
->getOffset();
return $timestamp - $offset;
}
public function setTimezone($timezone) {
$this->timezone = $timezone;
}
/**
* Get unique id
*
* Use {@see FeedItem::setUid()} to set the unique id.
*
* @param string The unique id.
*/
public function getUid() {
return $this->uid;
}
/**
* Set unique id.
*
* Use {@see FeedItem::getUid()} to get the unique id.
*
* @param string $uid A string that uniquely identifies the current item
* @return self
*/
public function setUid($uid) {
$this->uid = null; // Clear previous data
if(!is_string($uid)) {
Debug::log('Unique id must be a string!');
} elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) {
// keep id if it already is a SHA-1 hash
$this->uid = $uid;
} else {
$this->uid = sha1($uid);
}
return $this;
}
/**
* Get current stamp.
*
* Use {@see FeedItem::setStamp()} to set the stamp.
*
* @return string|null The current stamp or null if it hasn't been set.
*/
public function getStamp() {
return $this->correctTimezone($this->stamp);
}
/**
* Set stamp.
*
* Use {@see FeedItem::getStamp()} to get the stamp.
*
* @param int $stamp The stamp
* @return self
*/
public function setStamp($stamp) {
$this->stamp = self::validStamp($stamp);
return $this;
}
/**
* Get current from.
*
* Use {@see FeedItem::setStart()} to set the from.
*
* @return int|null The current from or null if it hasn't been set.
*/
public function getStart() {
return $this->correctTimezone($this->start);
}
/**
* Set from of first release.
*
* _Note_: The from should represent the number of seconds since
* January 1 1970 00:00:00 GMT (Unix time).
*
* _Remarks_: If the provided from is a string (not numeric), this
* function automatically attempts to parse the string using
* [strtotime](http://php.net/manual/en/function.strtotime.php)
*
* @link http://php.net/manual/en/function.strtotime.php strtotime (PHP)
* @link https://en.wikipedia.org/wiki/Unix_time Unix time (Wikipedia)
*
* @param string|int $start A from of when the item was first released
* @return self
*/
public function setStart($start) {
$this->start = self::validStamp($start); // Clear previous data
return $this;
}
/**
* Get the current end name.
*
* Use {@see FeedItem::setEnd()} to set the end.
*
* @return string|null The end or null if it hasn't been set.
*/
public function getEnd() {
return $this->correctTimezone($this->end);
}
/**
* Set the end name.
*
* Use {@see FeedItem::getEnd()} to get the end.
*
* @param string $end The end name.
* @return self
*/
public function setEnd($end) {
$this->end = self::validStamp($end);
return $this;
}
/**
* Get item summary.
*
* Use {@see FeedItem::setSummary()} to set the item summary.
*
* @return string|null The item summary or null if it hasn't been set.
*/
public function getSummary() {
return $this->summary;
}
/**
* Set item summary.
*
* Note: This function casts objects of type simple_html_dom and
* simple_html_dom_node to string.
*
* Use {@see FeedItem::getSummary()} to get the current item summary.
*
* @param string|object $summary The item summary as text or simple_html_dom
* object.
* @return self
*/
public function setSummary($summary) {
$this->summary = null; // Clear previous data
if(!is_string($summary)) {
Debug::log('Summary must be a string!');
} else {
$this->summary = trim($summary);
}
return $this;
}
/**
* Get item desc.
*
* Use {@see FeedItem::setDesc()} to set feed desc.
*
* @return array Desc as array of enclosure Uids.
*/
public function getDesc() {
return $this->desc;
}
/**
* Set item desc.
*
* Use {@see FeedItem::getDesc()} to get the current item desc.
*
* @param array $desc Array of desc, where each element links to
* one enclosure.
* @return self
*/
public function setDesc($desc) {
$this->desc = array(); // Clear previous data
if(!is_string($desc)) {
Debug::log('Desc must be a string!');
} else {
$this->desc = trim($desc);
}
return $this;
}
/**
* Get item location.
*
* Use {@see FeedItem::setLocation()} to set item location.
*
* @param array The item location.
*/
public function getLocation() {
return $this->location;
}
/**
* Set item location.
*
* Use {@see FeedItem::getLocation()} to get the current item location.
*
* @param array $location Array of location, where each element defines
* a single category name.
* @return self
*/
public function setLocation($location) {
$this->location = array(); // Clear previous data
if(!is_string($location)) {
Debug::log('Location must be a string!');
} else {
$this->location = trim($location);
}
return $this;
}
public function getFill() {
return $this->fill;
}
public function setFill($fill) {
$this->fill = false; // Clear previous data
if(!is_bool($fill)) {
Debug::log('Fill must be a bool!');
} else {
$this->fill = $fill;
}
return $this;
}
/**
* Transform current object to array
*
* @return array
*/
public function toArray() {
return array(
'uid' => $this->uid,
'stamp' => $this->stamp,
'start' => $this->start,
'end' => $this->end,
'summary' => $this->summary,
'desc' => $this->desc,
'location' => $this->location,
'fill' => $this->fill,
'timezone' => $this->timezone,
);
}
function __set($name, $value) {
switch($name) {
case 'uid': $this->setUid($value); break;
case 'stamp': $this->setStamp($value); break;
case 'start': $this->setStart($value); break;
case 'end': $this->setEnd($value); break;
case 'summary': $this->setSummary($value); break;
case 'desc': $this->setDesc($value); break;
case 'location': $this->setLocation($value); break;
case 'fill': $this->setFill($value); break;
case 'timezone': $this->setTimezone($value); break;
}
}
function __get($name) {
switch($name) {
case 'uid': return $this->getUid();
case 'stamp': return $this->getStamp();
case 'start': return $this->getStart();
case 'end': return $this->getEnd();
case 'summary': return $this->getSummary();
case 'desc': return $this->getDesc();
case 'location': return $this->getLocation();
case 'fill': return $this->getFill();
default:
return null;
}
}
public static function validStamp($stamp) {
if(!is_numeric($stamp)
&& !$stamp = strtotime($stamp)) {
Debug::log('Unable to parse from!');
}
if($stamp <= 0) {
Debug::log('Start must be greater than zero!');
} else {
return $stamp;
}
return null;
}
}

View File

@ -102,7 +102,7 @@ abstract class FeedExpander extends BridgeAbstract
$httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)];
$content = getContents($url, $httpHeaders);
if ($content === '') {
throw new \Exception(sprintf('Unable to parse xml from `%s` because we got the empty string', $url));
throw new \Exception(sprintf('Unable to parse xml from `%s` because we got the empty string', $url), 10);
}
// Maybe move this call earlier up the stack frames
// Disable triggering of the php error-handler and handle errors manually instead
@ -121,7 +121,7 @@ abstract class FeedExpander extends BridgeAbstract
// Render only the first error into exception message
$firstXmlErrorMessage = $xmlErrors[0]->message;
}
throw new \Exception(sprintf('Unable to parse xml from `%s` %s', $url, $firstXmlErrorMessage ?? ''));
throw new \Exception(sprintf('Unable to parse xml from `%s` %s', $url, $firstXmlErrorMessage ?? ''), 11);
}
// Restore previous behaviour in case other code relies on it being off
libxml_use_internal_errors(false);

View File

@ -40,6 +40,9 @@ abstract class FormatAbstract implements FormatInterface
/** @var array $extraInfos The extra infos */
protected $extraInfos;
/** @var array $events The events */
protected $events;
/** {@inheritdoc} */
public function getMimeType()
{
@ -129,4 +132,64 @@ abstract class FormatAbstract implements FormatInterface
return $this->extraInfos;
}
/**
* {@inheritdoc}
*
* @param array $events {@inheritdoc}
*/
public function setEvents(array $events){
$this->events = $events;
return $this;
}
/** {@inheritdoc} */
public function getEvents(){
if(!is_array($this->events))
throw new \LogicException('Feed the ' . get_class($this) . ' with "setEvents" method before !');
return $this->events;
}
/**
* Sanitize HTML while leaving it functional.
*
* Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and
* potentially dangerous things.
*
* @param string $html The HTML content
* @return string The sanitized HTML content
*
* @todo This belongs into `html.php`
* @todo Maybe switch to http://htmlpurifier.org/
* @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
*/
protected function sanitizeHtml($html)
{
$html = str_replace('<script', '<&zwnj;script', $html); // Disable scripts, but leave them visible.
$html = str_replace('<iframe', '<&zwnj;iframe', $html);
$html = str_replace('<link', '<&zwnj;link', $html);
// We leave alone object and embed so that videos can play in RSS readers.
return $html;
}
/**
* Trim each element of an array
*
* This function applies `trim()` to all elements in the array, if the element
* is a valid string.
*
* @param array $elements The array to trim
* @return array The trimmed array
*
* @todo This is a utility function that doesn't belong here, find a new home.
*/
protected function array_trim($elements){
foreach($elements as $key => $value) {
if(is_string($value))
$elements[$key] = trim($value);
}
return $elements;
}
}

View File

@ -34,8 +34,8 @@ final class Logger
unset($context['e']);
$context['type'] = get_class($e);
$context['code'] = $e->getCode();
$context['message'] = $e->getMessage();
$context['file'] = trim_path_prefix($e->getFile());
$context['message'] = sanitize_root($e->getMessage());
$context['file'] = sanitize_root($e->getFile());
$context['line'] = $e->getLine();
$context['url'] = get_current_url();
$context['trace'] = trace_to_call_points(trace_from_exception($e));
@ -58,6 +58,7 @@ final class Logger
}
}
}
// Intentionally not sanitizing $message
$text = sprintf(
"[%s] rssbridge.%s %s %s\n",
now()->format('Y-m-d H:i:s'),
@ -65,6 +66,7 @@ final class Logger
$message,
$context ? Json::encode($context) : ''
);
// Log to stderr/stdout whatever that is
error_log($text);
}
}

View File

@ -34,8 +34,12 @@ final class RssBridge
if ((error_reporting() & $code) === 0) {
return false;
}
$text = sprintf('%s at %s line %s', $message, trim_path_prefix($file), $line);
// Drop the current frame
$text = sprintf(
'%s at %s line %s',
sanitize_root($message),
sanitize_root($file),
$line
);
Logger::warning($text);
if (Debug::isEnabled()) {
print sprintf("<pre>%s</pre>\n", e($text));
@ -49,8 +53,8 @@ final class RssBridge
$message = sprintf(
'Fatal Error %s: %s in %s line %s',
$error['type'],
$error['message'],
trim_path_prefix($error['file']),
sanitize_root($error['message']),
sanitize_root($error['file']),
$error['line']
);
Logger::error($message);
@ -63,8 +67,8 @@ final class RssBridge
// Consider: ini_set('error_reporting', E_ALL & ~E_DEPRECATED);
date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
$authenticationMiddleware = new AuthenticationMiddleware();
if (Configuration::getConfig('authentication', 'enable')) {
$authenticationMiddleware = new AuthenticationMiddleware();
$authenticationMiddleware();
}

View File

@ -178,29 +178,28 @@ function getContents(
$response['content'] = $cache->loadData();
break;
default:
if (Debug::isEnabled()) {
// Include a part of the response body in the exception message
throw new HttpException(
sprintf(
'%s resulted in `%s %s: %s`',
$url,
$result['code'],
Response::STATUS_CODES[$result['code']] ?? '',
mb_substr($result['body'], 0, 500),
),
$result['code']
);
} else {
throw new HttpException(
sprintf(
'%s resulted in `%s %s`',
$url,
$result['code'],
Response::STATUS_CODES[$result['code']] ?? '',
),
$result['code']
);
$exceptionMessage = sprintf(
'%s resulted in %s %s %s',
$url,
$result['code'],
Response::STATUS_CODES[$result['code']] ?? '',
// If debug, include a part of the response body in the exception message
Debug::isEnabled() ? mb_substr($result['body'], 0, 500) : '',
);
// The following code must be extracted if it grows too much
$cloudflareTitles = [
'<title>Just a moment...',
'<title>Please Wait...',
'<title>Attention Required!'
];
foreach ($cloudflareTitles as $cloudflareTitle) {
if (str_contains($result['body'], $cloudflareTitle)) {
throw new CloudFlareException($exceptionMessage, $result['code']);
}
}
throw new HttpException($exceptionMessage, $result['code']);
}
if ($returnFull === true) {
return $response;

View File

@ -1,6 +1,10 @@
<?php
final class HttpException extends \Exception
class HttpException extends \Exception
{
}
final class CloudFlareException extends HttpException
{
}
@ -50,8 +54,8 @@ function create_sane_exception_message(\Throwable $e): string
return sprintf(
'%s: %s in %s line %s',
get_class($e),
$e->getMessage(),
trim_path_prefix($e->getFile()),
sanitize_root($e->getMessage()),
sanitize_root($e->getFile()),
$e->getLine()
);
}
@ -74,7 +78,7 @@ function trace_from_exception(\Throwable $e): array
$trace = [];
foreach ($frames as $frame) {
$trace[] = [
'file' => trim_path_prefix($frame['file'] ?? ''),
'file' => sanitize_root($frame['file'] ?? ''),
'line' => $frame['line'] ?? null,
'class' => $frame['class'] ?? null,
'type' => $frame['type'] ?? null,
@ -121,9 +125,17 @@ function frame_to_call_point(array $frame): string
*
* Example: "/home/davidsf/rss-bridge/index.php" => "index.php"
*/
function trim_path_prefix(string $filePath): string
function sanitize_root(string $filePath): string
{
return mb_substr($filePath, mb_strlen(dirname(__DIR__)) + 1);
// Root folder of the project e.g. /home/satoshi/repos/rss-bridge
$root = dirname(__DIR__);
return _sanitize_path_name($filePath, $root);
}
function _sanitize_path_name(string $s, string $pathName): string
{
// Remove all occurrences of $pathName in the string
return str_replace(["$pathName/", $pathName], '', $s);
}
/**
@ -218,3 +230,8 @@ function now(): \DateTimeImmutable
{
return new \DateTimeImmutable();
}
function create_random_string(int $bytes = 16): string
{
return bin2hex(openssl_random_pseudo_bytes($bytes));
}

View File

@ -66,7 +66,6 @@ p {
/* Header */
header {
margin-top: 40px;
padding: 15px;
color: #1182DB;
text-align: center;
@ -133,7 +132,7 @@ input:focus:-ms-input-placeholder { opacity: 0; }
.container {
width: 60%;
margin: 30px auto;
margin: 0 auto 30px auto;
}
/* Section */

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="RSS-Bridge" />
<title><?= e($_title ?? 'RSS-Bridge') ?></title>
<link href="static/style.css" rel="stylesheet">
<link href="static/style.css?2023-03-24" rel="stylesheet">
<link rel="icon" type="image/png" href="static/favicon.png">
</head>

View File

@ -1,8 +1,48 @@
<div class="error">
<h1>Application Error</h1>
<?php if ($e instanceof HttpException): ?>
<?php if ($e instanceof CloudFlareException): ?>
<h2>The website is protected by CloudFlare</h2>
<p>
RSS-Bridge tried to fetch a website.
The fetching was blocked by CloudFlare.
CloudFlare is anti-bot software.
Its purpose is to block non-humans.
</p>
<?php endif; ?>
<p>The application could not run because of the following error:</p>
<?php if ($e->getCode() === 404): ?>
<h2>The website was not found</h2>
<p>
RSS-Bridge tried to fetch a page on a website.
But it doesn't exists.
</p>
<?php endif; ?>
<?php if ($e->getCode() === 429): ?>
<h2>Try again later</h2>
<p>
RSS-Bridge tried to fetch a website.
They told us to try again later.
</p>
<?php endif; ?>
<?php else: ?>
<?php if ($e->getCode() === 10): ?>
<h2>The rss feed is completely empty</h2>
<p>
RSS-Bridge tried parse the empty string as xml.
The fetched url is not pointing to real xml.
</p>
<?php endif; ?>
<?php if ($e->getCode() === 11): ?>
<h2>There is something wrong with the rss feed</h2>
<p>
RSS-Bridge tried parse xml. It failed. The xml is probably broken.
</p>
<?php endif; ?>
<?php endif; ?>
<h2>Details</h2>
@ -16,11 +56,11 @@
</div>
<div>
<strong>Message:</strong> <?= e($e->getMessage()) ?>
<strong>Message:</strong> <?= e(sanitize_root($e->getMessage())) ?>
</div>
<div>
<strong>File:</strong> <?= e(trim_path_prefix($e->getFile())) ?>
<strong>File:</strong> <?= e(sanitize_root($e->getFile())) ?>
</div>
<div>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/ >
<meta name="description" content="RSS-Bridge" />
<title><?= e($title) ?></title>
<link href="static/style.css" rel="stylesheet">
<link href="static/style.css?2023-03-24" rel="stylesheet">
<link rel="icon" type="image/png" href="static/favicon.png">
<?php foreach ($linkTags as $link): ?>
@ -24,12 +24,6 @@
<div class="container">
<header>
<a href="./">
<img width="400" src="static/logo_600px.png">
</a>
</header>
<h1 class="pagetitle">
<a href="<?= e($uri) ?>" target="_blank"><?= e($title) ?></a>
</h1>

31
tests/CacheTest.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace RssBridge\Tests;
use PHPUnit\Framework\TestCase;
class CacheTest extends TestCase
{
public function testFileCache()
{
$temporaryFolder = sprintf('%s/rss_bridge_%s/', sys_get_temp_dir(), create_random_string());
mkdir($temporaryFolder);
$sut = new \FileCache([
'path' => $temporaryFolder,
'enable_purge' => true,
]);
$sut->setScope('scope');
$sut->purgeCache(-1);
$sut->setKey(['key']);
$this->assertNull($sut->loadData());
$sut->saveData('data');
$this->assertSame('data', $sut->loadData());
$this->assertIsNumeric($sut->getTime());
$sut->purgeCache(-1);
// Intentionally not deleting the temp folder
}
}

View File

@ -26,25 +26,24 @@ final class UtilsTest extends TestCase
$this->assertSame('1 TB', format_bytes(1024 ** 4));
}
public function testFileCache()
public function testSanitizePathName()
{
$sut = new \FileCache();
$sut->setScope('scope');
$sut->purgeCache(-1);
$sut->setKey(['key']);
$this->assertNull($sut->loadData());
$sut->saveData('data');
$this->assertSame('data', $sut->loadData());
$this->assertIsNumeric($sut->getTime());
$sut->purgeCache(-1);
$this->assertSame('index.php', _sanitize_path_name('/home/satoshi/rss-bridge/index.php', '/home/satoshi/rss-bridge'));
$this->assertSame('tests/UtilsTest.php', _sanitize_path_name('/home/satoshi/rss-bridge/tests/UtilsTest.php', '/home/satoshi/rss-bridge'));
$this->assertSame('bug in lib/kek.php', _sanitize_path_name('bug in /home/satoshi/rss-bridge/lib/kek.php', '/home/satoshi/rss-bridge'));
}
public function testTrimFilePath()
public function testSanitizePathNameInErrorMessage()
{
$this->assertSame('', trim_path_prefix(dirname(__DIR__)));
$this->assertSame('tests', trim_path_prefix(__DIR__));
$this->assertSame('tests/UtilsTest.php', trim_path_prefix(__DIR__ . '/UtilsTest.php'));
$raw = 'Error: Argument 1 passed to foo() must be an instance of kk, string given, called in /home/satoshi/rss-bridge/bridges/RumbleBridge.php';
$sanitized = 'Error: Argument 1 passed to foo() must be an instance of kk, string given, called in bridges/RumbleBridge.php';
$this->assertSame($sanitized, _sanitize_path_name($raw, '/home/satoshi/rss-bridge'));
}
public function testCreateRandomString()
{
$this->assertSame(2, strlen(create_random_string(1)));
$this->assertSame(4, strlen(create_random_string(2)));
$this->assertSame(6, strlen(create_random_string(3)));
}
}

View File

@ -110,6 +110,14 @@ function str_get_html(
$defaultSpanText
);
// The following two if statements are rss-bridge patch
if (empty($str)) {
throw new \Exception('Refusing to parse empty string input');
}
if (strlen($str) > MAX_FILE_SIZE) {
throw new \Exception('Refusing to parse too big input');
}
if (empty($str) || strlen($str) > MAX_FILE_SIZE) {
$dom->clear();
return false;