mirror of https://github.com/danoloan10/rss-bridge
Compare commits
54 Commits
75a426433f
...
7fc5182780
Author | SHA1 | Date |
---|---|---|
danoloan10 | 7fc5182780 | |
danoloan10 | 699556ca04 | |
danoloan10 | 03e5bd2304 | |
danoloan10 | b9a4892baf | |
danoloan10 | 5514a74b87 | |
mrnoname1000 | 887f4bbe15 | |
Paul Prechtel | 212c56fde5 | |
sysadminstory | 00e716d84d | |
July | f0c96008bc | |
Eugene Molotov | 343fd36671 | |
Paul Prechtel | a4a7473abb | |
Paul Prechtel | 4068668de9 | |
Eugene Molotov | 7c4591c550 | |
Paul Prechtel | 0718fdc829 | |
Dawid Wróbel | 7eca527160 | |
triatic | f1c54d5d55 | |
Korytov Pavel | 1ed7bdcddf | |
Paroleen | 8486c0f8ca | |
Eugene Molotov | 249133204e | |
Eugene Molotov | c8af9f9055 | |
Dag | 9bb04ba848 | |
realansgar | 307f5865c0 | |
DRogueRonin | 36e98e8481 | |
Dag | 347a0e9a3d | |
Dag | 4c3ebb312d | |
Dag | 9e9a697b8b | |
Miika Launiainen | 4e616c7092 | |
toineenzo | fbe7cc11ec | |
sysadminstory | 23fb5819cd | |
vdbhb59 | dc8ce20482 | |
mad-reyk | 224cce08a8 | |
mad-reyk | c1f446fd19 | |
Corentin Garcia | 19fc2dc100 | |
Dag | 2c94791bcd | |
Dag | 1ffb2df46d | |
Miika Launiainen | dc9530b405 | |
Bocki | 90bf5518cb | |
Bocki | 783160e715 | |
Bocki | 0a114c02c2 | |
Bocki | 2abdc7588a | |
Bocki | 84e0135959 | |
Korytov Pavel | f7200756c3 | |
sysadminstory | b8ad49c562 | |
Dag | 058e792b8f | |
Dag | 007f2b2d8a | |
Dag | a01c1f6ab0 | |
Bocki | f0e5ef0fc5 | |
Tone | b40714079f | |
Simon | 180c332406 | |
Joseph | 8c4dbb32de | |
Ololbu | 5ab949ca55 | |
Bocki | f3f98a117c | |
Bocki | f0d8cfd4d4 | |
Korytov Pavel | 4aed05c7b6 |
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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] . '}');
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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') {
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -23,6 +23,7 @@ class TldrTechBridge extends BridgeAbstract
|
|||
'values' => [
|
||||
'Tech' => 'tech',
|
||||
'Crypto' => 'crypto',
|
||||
'AI' => 'ai'
|
||||
],
|
||||
'defaultValue' => 'tech'
|
||||
]
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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`
|
|
@ -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)
|
|
@ -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`.
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 |
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>'))));
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
*/
|
||||
final class Configuration
|
||||
{
|
||||
private const VERSION = 'dev.2022-06-14';
|
||||
private const VERSION = 'dev.2023-03-22';
|
||||
|
||||
private static $config = [];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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', '<‌script', $html); // Disable scripts, but leave them visible.
|
||||
$html = str_replace('<iframe', '<‌iframe', $html);
|
||||
$html = str_replace('<link', '<‌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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue