1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-24 09:02:59 +02:00

Compare commits

..

42 Commits

Author SHA1 Message Date
logmanoriginal
ae2c35c18a [Configuration] Bump version to 2019-03-17 2019-03-17 20:28:55 +01:00
logmanoriginal
5b80bcaa04 [README] Update list of contributors
The list acutally didn't change, but it's sorted properly now
(thanks to em92 for the suggestion)
2019-03-17 20:28:15 +01:00
fulmeek
5ea985164e [OneFortuneADayBridge] use date in UTC for seed (#1059) 2019-03-14 19:44:36 +01:00
fulmeek
696afa96d3 [BakaUpdatesMangaReleasesBridge] filter title and groups (#1058)
Baka-Updates Manga uses an asterisk (*) to denote series information have
been updated within the last 24 hours. This is not helpful in a feed.
2019-03-14 19:43:00 +01:00
Roliga
326a707739 [SoundcloudBridge] Update API key (#1062) 2019-03-12 13:29:11 +01:00
LogMANOriginal
1ac66b3fdc [README] Add sqlite3 as requirement for SQLiteCache 2019-03-02 19:42:40 +01:00
logmanoriginal
f450b2e118 [SQLiteCache] Check sqlite3 extension in __construct
Checks if the sqlite3 extension is loaded and throws an error
if it's missing.
2019-03-02 19:33:44 +01:00
sysadminstory
688c950916 [DealabsBridge] Patch unparsable Deal date (#1053)
In case of a unparsable date, the text to DateTime object failed, and
this resulted to a Fatal error while using this DateTime object . To
prvent this fatal error, if the date parsing failse, then a DateTime
object is created with the actual date.
2019-03-02 19:10:57 +01:00
fulmeek
9d85b951f7 [BakaUpdatesMangaReleasesBridge] rework to parse new layout (#1052)
* rework to parse new layout
* skip incomplete rows

The last row could have fewer columns if there are less rows than the items limit. This usually should not happen, though.

* use constant for skipping
2019-03-02 19:09:16 +01:00
somini
dac685b887 [ComboiosDePortugalBridge] Add new bridge (#1049) 2019-03-02 19:05:23 +01:00
ORelio
d37f0c14a0 [LeMondeInformatique] Handle special articles (#1039)
Fix content extraction for special article compiling previous articles
2019-03-02 19:03:29 +01:00
Ryan Liptak
b96c25a3af [BandcampBridge] Update to use newer POST API (#1045)
Bandcamp tags pages have a new layout and now use a POST API endpoint to view each page of releases.

Output of this bridge should be almost the same as before, with a few small improvements:
- Small album image in 'content', larger album image in 'enclosures'
- RSS item titles/authors are appended with the releaser in parentheses if the artist name and the releaser are different (i.e. Record Label's Bandcamp releases an album called Bar by the band named Foo, it would get the title 'Foo - Bar (Record Label)' and the author 'Foo (Record Label)')
2019-02-24 12:08:34 +01:00
fulmeek
dc1b1b13cc [SQLiteCache] Implement cache based on SQLite 3 (#1035) 2019-02-24 12:04:27 +01:00
logmanoriginal
e3588f62bd [Cache] Fix cache types ending on 'cache' are not detected correctly
References #1000
2019-02-24 11:56:43 +01:00
fulmeek
958ba815c7 [OneFortuneADayBridge] Add lucky number feature (#1038) 2019-02-24 11:49:17 +01:00
somini
3d24596a52 [AsahiShimbunAJW] Add new Bridge (#1036) 2019-02-24 11:47:29 +01:00
Lyra
f9ed934c8c Update contributors and bump version 2019-02-19 22:05:06 +01:00
Nono
777c204838 [VMwareSecurityBridge] New Bridge (#1041)
* Create VMwareSecurityBridge.php
2019-02-19 21:53:20 +01:00
Nono
ae40f7b388 [MozillaSecurityBridge] Make the URI unique by adding timestamp (#1005)
* added unique UID + URI 

if UID is mandatory for RSS-Bridge, the unicity of the URI is also mandatory for some reader (like kriss feed).
2019-02-19 21:50:00 +01:00
Lyra
473a62ed44 [RoadAndTrackBridge] Added new bridge 2019-02-12 15:12:04 +01:00
Klimplant
4c58768d4d [CachetBridge] Add new bridge (#1034)
* Fix issue with CachetAPI Pagination

Fixing issue that only the oldest 20 entries were shown.

_Background:_

_Cachet has a, lets call it odd, system of pagination. On the first page you see the incidents first created, so they are not what you want to see. But on the last page you can have 1 or 20 of the newest incidents. So you have to take the incidents from the last page (call it Pmax) and combine them with the incidents from  Pmax - 1._
2019-02-11 21:07:46 +01:00
ORelio
ca9c2abb60 [FeedExpander] Fix item href being used as feed uri (#1033) 2019-02-11 19:07:03 +01:00
logmanoriginal
556a417dd6 core: Add support for custom cache types via config.ini.php
This commit adds support for a new parameter which specifies the type
of cache to use for caching. It is specified in config.ini.php:

 [cache]

 type = "..."

Currently only one type of cache is supported (see /caches). All uses
of 'FileCache' were replaced by this configuration option.

Note: Caching currently depends on files and folders (due to FileCache).
Experience may vary depending on the selected cache type. For now always
check if FileCache is working before testing alternative types.

References #1000
2019-02-06 18:52:44 +01:00
LogMANOriginal
51ee541d5a core: Implement action factory (#1002) 2019-02-06 18:34:51 +01:00
Nova
69cb65c1af [GlowficBridge] Add new bridge (#1031) 2019-02-06 18:20:25 +01:00
David Pedersen
29b187fc12 [AppleMusicBridge] Add new bridge (#1026) 2019-02-06 17:43:20 +01:00
fulmeek
80f6a8b3d4 [MrssFormat] Rework to make it valid RSS 2.0 + Media RSS (#996) 2019-02-06 17:18:33 +01:00
logmanoriginal
32d4da8b76 [Bridge] Fix failed to open stream when reading non-existing whitelist 2019-02-04 17:35:40 +01:00
fulmeek
0063d2c376 [HtmlFormat] minor typographical fix-ups (#1009) 2019-02-04 15:33:13 +01:00
fulmeek
11a39af35c [FormatImplementationTest] Add unit tests for format implementations (#1008) 2019-02-04 14:59:09 +01:00
fulmeek
f65a4076ba [CacheImplementationTest] Add unit tests for cache implementations (#1007) 2019-02-04 14:58:11 +01:00
triatic
25593d9c18 [TwitterBridge] Append username of retweeter to author (#1016)
Append username of retweeter to author. Useful when viewing all unread tweets in an RSS reader which are not sorted within username folders.
2019-02-04 14:56:07 +01:00
LogMANOriginal
394149b114 core: Add item uid (#1017)
'uid' represents the unique id for a feed item. This item is null by
default and can be set to any string value. The provided string value
is always hashed to sha1 to make it the same length in all cases.

References #977, #1005
2019-02-03 20:56:41 +01:00
logmanoriginal
a29512deee [BridgeCard] Don't warn about the 'required' attribute if it is set to false 2019-01-22 19:12:37 +01:00
logmanoriginal
e0db349a57 bridges: Fix bridges that don't pass the unit test 2019-01-22 18:24:32 +01:00
logmanoriginal
d532d0e0c4 [BridgeImplementationTest] Add test for "required" attribute on lists and checkboxes
Lists and checkboxes don't support the "required" flag and should not
define it. Note that the "required" flag can be set to false if so
desired.
2019-01-22 18:22:49 +01:00
logmanoriginal
434c12672f lib: Ignore required attribute on lists an checkboxes
References #1014
2019-01-22 18:11:52 +01:00
fulmeek
ab2e566ee1 [AtomFormat] Update to comply with RFC 4287 (#995)
https://tools.ietf.org/html/rfc4287
2019-01-21 17:22:30 +01:00
fulmeek
493e76e4b9 [BakaUpdatesMangaReleasesBridge] Add new bridge (#999) 2019-01-15 16:36:42 +01:00
logmanoriginal
37d882a8d5 [GlassdoorBridge] Fix incorrect CSS selector 2019-01-13 22:04:21 +01:00
logmanoriginal
bcd7bccc46 vendor: Update PHP Simple HTML DOM Parser to 1.8.1
https://sourceforge.net/projects/simplehtmldom/files/simplehtmldom/1.8.1/

Note: Some bridges may need fixes in their CSS queries if they don't follow
the specification.
2019-01-13 22:02:59 +01:00
logmanoriginal
2def7a04a3 Bump version to dev.2019-01-13 2019-01-13 19:23:59 +01:00
84 changed files with 3739 additions and 1167 deletions

View File

@@ -66,6 +66,7 @@ RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.php)
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
@@ -111,41 +112,16 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
-->
* [16mhz](https://github.com/16mhz)
* [adamchainz](https://github.com/adamchainz)
* [Ahiles3005](https://github.com/Ahiles3005)
* [Albirew](https://github.com/Albirew)
* [aledeg](https://github.com/aledeg)
* [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [Daiyousei](https://github.com/Daiyousei)
* [Djuuu](https://github.com/Djuuu)
* [Draeli](https://github.com/Draeli)
* [EtienneM](https://github.com/EtienneM)
* [Frenzie](https://github.com/Frenzie)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [GregThib](https://github.com/GregThib)
* [Grummfy](https://github.com/Grummfy)
* [JackNUMBER](https://github.com/JackNUMBER)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ORelio](https://github.com/ORelio)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [Piranhaplant](https://github.com/Piranhaplant)
* [Riduidel](https://github.com/Riduidel)
* [Roliga](https://github.com/Roliga)
* [Strubbl](https://github.com/Strubbl)
* [TheRadialActive](https://github.com/TheRadialActive)
* [TwizzyDizzy](https://github.com/TwizzyDizzy)
* [WalterBarrett](https://github.com/WalterBarrett)
* [ZeNairolf](https://github.com/ZeNairolf)
* [adamchainz](https://github.com/adamchainz)
* [aledeg](https://github.com/aledeg)
* [alexAubin](https://github.com/alexAubin)
* [az5he6ch](https://github.com/az5he6ch)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
@@ -156,21 +132,37 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
* [disk0x](https://github.com/disk0x)
* [eMerzh](https://github.com/eMerzh)
* [Djuuu](https://github.com/Djuuu)
* [Draeli](https://github.com/Draeli)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy)
* [hunhejj](https://github.com/hunhejj)
* [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER)
* [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [klimplant](https://github.com/klimplant)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n)
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
* [m0zes](https://github.com/m0zes)
* [matthewseal](https://github.com/matthewseal)
@@ -180,28 +172,40 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [metaMMA](https://github.com/metaMMA)
* [mickael-bertrand](https://github.com/mickael-bertrand)
* [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ORelio](https://github.com/ORelio)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon)
* [Piranhaplant](https://github.com/Piranhaplant)
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [pitchoule](https://github.com/pitchoule)
* [pmaziere](https://github.com/pmaziere)
* [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95)
* [qwertygc](https://github.com/qwertygc)
* [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga)
* [sebsauvage](https://github.com/sebsauvage)
* [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502)
* [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz)
* [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [TheRadialActive](https://github.com/TheRadialActive)
* [triatic](https://github.com/triatic)
* [WalterBarrett](https://github.com/WalterBarrett)
* [wtuuju](https://github.com/wtuuju)
* [yardenac](https://github.com/yardenac)
* [ZeNairolf](https://github.com/ZeNairolf)
Licenses
===

50
actions/DetectAction.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DetectAction extends ActionAbstract {
public function execute() {
$targetURL = $this->userData['url']
or returnClientError('You must specify a url!');
$format = $this->userData['format']
or returnClientError('You must specify a format!');
foreach(Bridge::getBridgeNames() as $bridgeName) {
if(!Bridge::isWhitelisted($bridgeName)) {
continue;
}
$bridge = Bridge::create($bridgeName);
if($bridge === false) {
continue;
}
$bridgeParams = $bridge->detectParameters($targetURL);
if(is_null($bridgeParams)) {
continue;
}
$bridgeParams['bridge'] = $bridgeName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
}
}

234
actions/DisplayAction.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DisplayAction extends ActionAbstract {
public function execute() {
$bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
$format = $this->userData['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
$format = substr($format, 0, $pos);
}
// whitelist control
if(!Bridge::isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
// Data retrieval
$bridge = Bridge::create($bridge);
$noproxy = array_key_exists('_noproxy', $this->userData)
&& filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
// Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $this->userData)) {
if(!CUSTOM_CACHE_TIMEOUT) {
unset($this->userData['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData);
header('Location: ' . $uri, true, 301);
die();
}
$cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
} else {
$cache_timeout = $bridge->getCacheTimeout();
}
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Initialize cache
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE);
$cache->purgeCache(86400); // 24 hours
$cache->setParameters($cache_params);
$items = array();
$infos = array();
$mtime = $cache->getTime();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
$cached = $cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
foreach($cached['items'] as $item) {
$items[] = new \FeedItem($item);
}
$infos = $cached['extraInfos'];
}
} else { // Collect new data
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = $bridge->getItems();
// Transform "legacy" items to FeedItems if necessary.
// Remove this code when support for "legacy" items ends!
if(isset($items[0]) && is_array($items[0])) {
$feedItems = array();
foreach($items as $item) {
$feedItems[] = new \FeedItem($item);
}
$items = $feedItems;
}
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item->setTitle(
'Bridge encountered an unexpected situation! ('
. $this->userData['_error_time']
. ')'
);
} else {
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
}
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} catch(Exception $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
}
// Store data in cache
$cache->saveData(array(
'items' => array_map(function($i){ return $i->toArray(); }, $items),
'extraInfos' => $infos
));
}
// Data transformation
try {
$format = Format::create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
}
}

53
actions/ListAction.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class ListAction extends ActionAbstract {
public function execute() {
$list = new StdClass();
$list->bridges = array();
$list->total = 0;
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridge = Bridge::create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
$list->bridges[$bridgeName] = array(
'status' => 'inactive'
);
continue;
}
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
'uri' => $bridge->getURI(),
'name' => $bridge->getName(),
'icon' => $bridge->getIcon(),
'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription()
);
}
$list->total = count($list->bridges);
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
}
}

View File

@@ -10,7 +10,6 @@ class AllocineFRBridge extends BridgeAbstract {
'category' => array(
'name' => 'category',
'type' => 'list',
'required' => true,
'exampleValue' => 'Faux Raccord',
'title' => 'Select your category',
'values' => array(

View File

@@ -16,7 +16,6 @@ class AmazonBridge extends BridgeAbstract {
'sort' => array(
'name' => 'Sort by',
'type' => 'list',
'required' => false,
'values' => array(
'Relevance' => 'relevanceblender',
'Price: Low to High' => 'price-asc-rank',
@@ -29,7 +28,6 @@ class AmazonBridge extends BridgeAbstract {
'tld' => array(
'name' => 'Country',
'type' => 'list',
'required' => true,
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',

View File

@@ -19,7 +19,6 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
'tld' => array(
'name' => 'Country',
'type' => 'list',
'required' => true,
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',

View File

@@ -0,0 +1,62 @@
<?php
class AppleMusicBridge extends BridgeAbstract {
const NAME = 'Apple Music';
const URI = 'https://www.apple.com';
const DESCRIPTION = 'Fetches the latest releases from an artist';
const MAINTAINER = 'Limero';
const PARAMETERS = [[
'url' => [
'name' => 'Artist URL',
'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274',
'required' => true,
],
'imgSize' => [
'name' => 'Image size for thumbnails (in px)',
'type' => 'number',
'defaultValue' => 512,
'required' => true,
]
]];
const CACHE_TIMEOUT = 21600; // 6 hours
public function collectData() {
$url = $this->getInput('url');
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request: ' . $url);
$imgSize = $this->getInput('imgSize');
// Grab the json data from the page
$html = $html->find('script[id=shoebox-ember-data-store]', 0);
$html = strstr($html, '{');
$html = substr($html, 0, -9);
$json = json_decode($html);
// Loop through each object
foreach ($json->included as $obj) {
if ($obj->type === 'lockup/album') {
$this->items[] = [
'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name,
'uri' => $obj->attributes->url,
'timestamp' => $obj->attributes->releaseDate,
'enclosures' => $obj->relationships->artwork->data->id,
];
} elseif ($obj->type === 'image') {
$images[$obj->id] = $obj->attributes->url;
}
}
// Add the images to each item
foreach ($this->items as &$item) {
$item['enclosures'] = [
str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]),
];
}
// Sort the order to put the latest albums first
usort($this->items, function($a, $b){
return $a['timestamp'] < $b['timestamp'];
});
}
}

View File

@@ -0,0 +1,72 @@
<?php
class AsahiShimbunAJWBridge extends BridgeAbstract {
const NAME = 'Asahi Shimbun AJW';
const BASE_URI = 'http://www.asahi.com';
const URI = self::BASE_URI . '/ajw/';
const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'section' => array(
'type' => 'list',
'name' => 'Section',
'values' => array(
'Japan » Social Affairs' => 'japan/social',
'Japan » People' => 'japan/people',
'Japan » 3/11 Disaster' => 'japan/0311disaster',
'Japan » Sci & Tech' => 'japan/sci_tech',
'Politics' => 'politics',
'Business' => 'business',
'Culture » Style' => 'culture/style',
'Culture » Movies' => 'culture/movies',
'Culture » Manga & Anime' => 'culture/manga_anime',
'Asia » China' => 'asia/china',
'Asia » Korean Peninsula' => 'asia/korean_peninsula',
'Asia » Around Asia' => 'asia/around_asia',
'Opinion » Editorial' => 'opinion/editorial',
'Opinion » Vox Populi' => 'opinion/vox',
),
'defaultValue' => 'Politics',
)
)
);
private function getSectionURI($section) {
return self::getURI() . $section . '/';
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')))
or returnServerError('Could not load content');
foreach($html->find('#MainInner li a') as $element) {
if ($element->parent()->class == 'HeadlineTopImage-S') {
Debug::log('Skip Headline, it is repeated below');
continue;
}
$item = array();
$item['uri'] = self::BASE_URI . $element->href;
$e_lead = $element->find('span.Lead', 0);
if ($e_lead) {
$item['content'] = $e_lead->innertext;
$e_lead->outertext = '';
} else {
$item['content'] = $element->innertext;
}
$e_date = $element->find('span.EnDate', 0);
if ($e_date) {
$item['timestamp'] = strtotime($e_date->innertext);
$e_date->outertext = '';
}
$e_video = $element->find('span.EnVideo', 0);
if ($e_video) {
$e_video->outertext = '';
$element->innertext = "VIDEO: $element->innertext";
}
$item['title'] = $element->innertext;
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
const NAME = 'Baka Updates Manga Releases';
const URI = 'https://www.mangaupdates.com/';
const DESCRIPTION = 'Get the latest series releases';
const MAINTAINER = 'fulmeek';
const PARAMETERS = array(array(
'series_id' => array(
'name' => 'Series ID',
'type' => 'number',
'required' => true,
'exampleValue' => '12345'
)
));
const LIMIT_COLS = 5;
const LIMIT_ITEMS = 10;
private $feedName = '';
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Series not found');
// content is an unstructured pile of divs, ugly to parse
$cols = $html->find('div#main_content div.row > div.text');
if (!$cols)
returnServerError('No releases');
$rows = array_slice(
array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS
);
if (isset($rows[0][1])) {
$this->feedName = $this->filterHTML($rows[0][1]->plaintext);
}
foreach($rows as $cols) {
if (count($cols) < self::LIMIT_COLS) continue;
$item = array();
$title = array();
$item['content'] = '';
$objDate = $cols[0];
if ($objDate)
$item['timestamp'] = strtotime($objDate->plaintext);
$objTitle = $cols[1];
if ($objTitle) {
$title[] = $this->filterHTML($objTitle->plaintext);
$item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
}
$objVolume = $cols[2];
if ($objVolume && !empty($objVolume->plaintext))
$title[] = 'Vol.' . $objVolume->plaintext;
$objChapter = $cols[3];
if ($objChapter && !empty($objChapter->plaintext))
$title[] = 'Chp.' . $objChapter->plaintext;
$objAuthor = $cols[4];
if ($objAuthor && !empty($objAuthor->plaintext)) {
$item['author'] = $this->filterHTML($objAuthor->plaintext);
$item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
}
$item['title'] = implode(' ', $title);
$item['uri'] = $this->getURI();
$item['uid'] = hash('sha1', $item['title']);
$this->items[] = $item;
}
}
public function getURI(){
$series_id = $this->getInput('series_id');
if (!empty($series_id)) {
return self::URI . 'releases.html?search=' . $series_id . '&stype=series';
}
return self::URI;
}
public function getName(){
if(!empty($this->feedName)) {
return $this->feedName . ' - ' . self::NAME;
}
return parent::getName();
}
private function filterText($text) {
return rtrim($text, '*');
}
private function filterHTML($text) {
return $this->filterText(html_entity_decode($text));
}
}

View File

@@ -13,48 +13,72 @@ class BandcampBridge extends BridgeAbstract {
'required' => true
)
));
const IMGURI = 'https://f4.bcbits.com/';
const IMGSIZE_300PX = 23;
const IMGSIZE_700PX = 16;
public function getIcon() {
return 'https://s4.bcbits.com/img/bc_favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('No results for this query.');
$url = self::URI . 'api/hub/1/dig_deeper';
$data = $this->buildRequestJson();
$header = array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data)
);
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $data
);
$content = getContents($url, $header, $opts)
or returnServerError('Could not complete request to: ' . $url);
foreach($html->find('li.item') as $release) {
$script = $release->find('div.art', 0)->getAttribute('onclick');
$uri = ltrim($script, "return 'url(");
$uri = rtrim($uri, "')");
$json = json_decode($content);
$item = array();
$item['author'] = $release->find('div.itemsubtext', 0)->plaintext
. ' - '
. $release->find('div.itemtext', 0)->plaintext;
if ($json->ok !== true) {
returnServerError('Invalid response');
}
$item['title'] = $release->find('div.itemsubtext', 0)->plaintext
. ' - '
. $release->find('div.itemtext', 0)->plaintext;
foreach ($json->items as $entry) {
$url = $entry->tralbum_url;
$artist = $entry->artist;
$title = $entry->title;
// e.g. record label is the releaser, but not the artist
$releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null;
$item['content'] = '<img src="'
. $uri
. '"/><br/>'
. $release->find('div.itemsubtext', 0)->plaintext
. ' - '
. $release->find('div.itemtext', 0)->plaintext;
$full_title = $artist . ' - ' . $title;
$full_artist = $artist;
if (isset($releaser)) {
$full_title .= ' (' . $releaser . ')';
$full_artist .= ' (' . $releaser . ')';
}
$small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX);
$img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX);
$item['id'] = $release->find('a', 0)->getAttribute('href');
$item['uri'] = $release->find('a', 0)->getAttribute('href');
$item = array(
'uri' => $url,
'author' => $full_artist,
'title' => $full_title
);
$item['content'] = "<img src='$small_img' /><br/>$full_title";
$item['enclosures'] = array($img);
$this->items[] = $item;
}
}
public function getURI(){
if(!is_null($this->getInput('tag'))) {
return self::URI . 'tag/' . urlencode($this->getInput('tag')) . '?sort_field=date';
}
private function buildRequestJson(){
$requestJson = array(
'tag' => $this->getInput('tag'),
'page' => 1,
'sort' => 'date'
);
return json_encode($requestJson);
}
return parent::getURI();
private function getImageUrl($id, $size){
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
}
public function getName(){

View File

@@ -17,7 +17,6 @@ class BundesbankBridge extends BridgeAbstract {
self::PARAM_LANG => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'defaultValue' => self::LANG_DE,
'values' => array(
'English' => self::LANG_EN,

134
bridges/CachetBridge.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
class CachetBridge extends BridgeAbstract {
const NAME = 'Cachet Bridge';
const URI = 'https://cachethq.io/';
const DESCRIPTION = 'Returns status updates from any Cachet installation';
const MAINTAINER = 'klimplant';
const PARAMETERS = array(
array(
'host' => array(
'name' => 'Cachet installation',
'type' => 'text',
'required' => true,
'title' => 'The URL of the Cachet installation',
'exampleValue' => 'https://demo.cachethq.io/',
), 'additional_info' => array(
'name' => 'Additional Timestamps',
'type' => 'checkbox',
'title' => 'Whether to include the given timestamps'
)
)
);
const CACHE_TIMEOUT = 300;
private $componentCache = [];
public function getURI() {
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
}
/**
* Validates the ping request to the cache API
*
* @param string $ping
* @return boolean
*/
private function validatePing($ping) {
$ping = json_decode($ping);
if ($ping === null) {
return false;
}
return $ping->data === 'Pong!';
}
/**
* Returns the component name of a cachat component
*
* @param integer $id
* @return string
*/
private function getComponentName($id) {
if ($id === 0) {
return '';
}
if (array_key_exists($id, $this->componentCache)) {
return $this->componentCache[$id];
}
$component = getContents($this->getURI() . '/api/v1/components/' . $id);
$component = json_decode($component);
if ($component === null) {
return '';
}
return $component->data->name;
}
public function collectData() {
$ping = getContents(urljoin($this->getURI(), '/api/v1/ping'));
if (!$this->validatePing($ping)) {
returnClientError('Provided URI is invalid!');
}
$url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc');
$incidents = getContents($url);
$incidents = json_decode($incidents);
if ($incidents === null) {
returnClientError('/api/v1/incidents returned no valid json');
}
usort($incidents->data, function ($a, $b) {
$timeA = strtotime($a->updated_at);
$timeB = strtotime($b->updated_at);
return $timeA > $timeB ? -1 : 1;
});
foreach ($incidents->data as $incident) {
if (isset($incident->permalink)) {
$permalink = $incident->permalink;
} else {
$permalink = urljoin($this->getURI(), '/incident/' . $incident->id);
}
$title = $incident->human_status . ': ' . $incident->name;
$message = '';
if ($this->getInput('additional_info')) {
if (isset($incident->occurred_at)) {
$message .= 'Occurred at: ' . $incident->occurred_at . "\r\n";
}
if (isset($incident->scheduled_at)) {
$message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n";
}
if (isset($incident->created_at)) {
$message .= 'Created at: ' . $incident->created_at . "\r\n";
}
if (isset($incident->updated_at)) {
$message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n";
}
}
$message .= $incident->message;
$content = nl2br($message);
$componentName = $this->getComponentName($incident->component_id);
$uidOrig = $permalink . $incident->created_at;
$uid = hash('sha512', $uidOrig);
$timestamp = strtotime($incident->created_at);
$categories = [];
$categories[] = $incident->human_status;
if ($componentName !== '') {
$categories[] = $componentName;
}
$item = [];
$item['uri'] = $permalink;
$item['title'] = $title;
$item['timestamp'] = $timestamp;
$item['content'] = $content;
$item['uid'] = $uid;
$item['categories'] = $categories;
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
class ComboiosDePortugalBridge extends BridgeAbstract {
const NAME = 'CP | Avisos';
const BASE_URI = 'https://www.cp.pt';
const URI = self::BASE_URI . '/passageiros/pt';
const DESCRIPTION = 'Comboios de Portugal | Avisos';
const MAINTAINER = 'somini';
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos')
or returnServerError('Could not load content');
foreach($html->find('.warnings-table a') as $element) {
$item = array();
$item['title'] = $element->innertext;
$item['uri'] = self::BASE_URI . $element->href;
$this->items[] = $item;
}
}
}

View File

@@ -15,7 +15,6 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
'channel' => [
'name' => 'Release Channel',
'type' => 'list',
'required' => true,
'defaultValue' => self::STABLE,
'values' => [
'Stable' => self::STABLE,

View File

@@ -15,13 +15,11 @@ class DealabsBridge extends PepperBridgeAbstract {
'hide_expired' => array(
'name' => 'Masquer les éléments expirés',
'type' => 'checkbox',
'required' => true
),
'hide_local' => array(
'name' => 'Masquer les deals locaux',
'type' => 'checkbox',
'title' => 'Masquer les deals en magasins physiques',
'required' => true
),
'priceFrom' => array(
'name' => 'Prix minimum',
@@ -41,7 +39,6 @@ class DealabsBridge extends PepperBridgeAbstract {
'group' => array(
'name' => 'Groupe',
'type' => 'list',
'required' => true,
'title' => 'Groupe dont il faut afficher les deals',
'values' => array(
'Abonnements internet' => 'abonnements-internet',
@@ -957,7 +954,6 @@ class DealabsBridge extends PepperBridgeAbstract {
'order' => array(
'name' => 'Trier par',
'type' => 'list',
'required' => true,
'title' => 'Ordre de tri des deals',
'values' => array(
'Du deal le plus Hot au moins Hot' => '',
@@ -1380,8 +1376,11 @@ class PepperBridgeAbstract extends BridgeAbstract {
// Add the Hour and minutes
$date_str .= ' 00:00';
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
// In some case, the date is not recognized : as a workaround the actual date is taken
if($date === false) {
$date = new DateTime();
}
return $date->getTimestamp();
}

View File

@@ -15,7 +15,6 @@ class DesoutterBridge extends BridgeAbstract {
'news_lang' => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'title' => 'Select your language',
'defaultValue' => 'Corporate',
'values' => array(
@@ -66,7 +65,6 @@ class DesoutterBridge extends BridgeAbstract {
'industry_lang' => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'title' => 'Select your language',
'defaultValue' => 'Corporate',
'values' => array(
@@ -117,7 +115,6 @@ class DesoutterBridge extends BridgeAbstract {
'full' => array(
'name' => 'Load full articles',
'type' => 'checkbox',
'required' => false,
'title' => 'Enable to load the full article for each item'
)
)

View File

@@ -120,7 +120,7 @@ class ElloBridge extends BridgeAbstract {
}
private function getAPIKey() {
$cache = Cache::create('FileCache');
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE);
$cache->setParameters(['key']);
$key = $cache->loadData();

View File

@@ -15,7 +15,6 @@ class ExtremeDownloadBridge extends BridgeAbstract {
'filter' => array(
'name' => 'Type de contenu',
'type' => 'list',
'required' => true,
'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux',
'values' => array(
'Streaming et Téléchargement' => 'both',

View File

@@ -11,7 +11,6 @@ class FDroidBridge extends BridgeAbstract {
'u' => array(
'name' => 'Widget selection',
'type' => 'list',
'required' => true,
'values' => array(
'Latest added apps' => 'added',
'Latest updated apps' => 'updated'

View File

@@ -11,7 +11,6 @@ class FeedExpanderExampleBridge extends FeedExpander {
'version' => array(
'name' => 'Version',
'type' => 'list',
'required' => true,
'title' => 'Select your feed format/version',
'defaultValue' => 'RSS 2.0',
'values' => array(

View File

@@ -10,7 +10,6 @@ class GBAtempBridge extends BridgeAbstract {
'type' => array(
'name' => 'Type',
'type' => 'list',
'required' => true,
'values' => array(
'News' => 'N',
'Reviews' => 'R',

View File

@@ -117,7 +117,7 @@ class GlassdoorBridge extends BridgeAbstract {
$item['title'] = $post->find('header', 0)->plaintext;
$item['content'] = $post->find('div[class="excerpt-content"]', 0)->plaintext;
$item['enclosures'] = array(
$this->getFullSizeImageURI($post->find('div[class="post-thumb"]', 0)->{'data-original'})
$this->getFullSizeImageURI($post->find('div[class*="post-thumb"]', 0)->{'data-original'})
);
// optionally load full articles

88
bridges/GlowficBridge.php Normal file
View File

@@ -0,0 +1,88 @@
<?php
class GlowficBridge extends BridgeAbstract {
const MAINTAINER = 'l1n';
const NAME = 'Glowfic Bridge';
const URI = 'https://www.glowfic.com';
const CACHE_TIMEOUT = 3600; // 1 hour
const DESCRIPTION = 'Returns the latest replies on a glowfic post.';
const PARAMETERS = array(
'global' => array(),
'Thread' => array(
'post_id' => array(
'name' => 'Post ID',
'title' => 'https://www.glowfic.com/posts/<POST ID>',
'type' => 'number'
),
'start_page' => array(
'name' => 'Start Page',
'title' => 'To start from an offset page',
'type' => 'number'
)
)
);
public function collectData() {
$url = $this->getAPIURI();
$metadata = get_headers( $url . '/replies', true ) or returnClientError('Post did not return reply headers.');
$metadata['Last-Page'] = ceil( $metadata['Total'] / $metadata['Per-Page'] );
if(!is_null($this->getInput('start_page')) &&
$this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0) {
$first_page = $metadata['Last-Page'] - $this->getInput('start_page');
} else if(!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) {
$first_page = $this->getInput('start_page');
} else {
$first_page = 1;
}
for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) {
$jsonContents = getContents($url . '/replies?page=' . $page_offset ) or
returnClientError('Could not retrieve replies for page ' . $page_offset . '.');
$replies = json_decode($jsonContents);
foreach ($replies as $reply) {
$item = array();
$item['content'] = $reply->{'content'};
$item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'};
if ($reply->{'icon'}) {
$item['enclosures'] = array($reply->{'icon'}->{'url'});
}
$item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')';
$item['timestamp'] = date('r', strtotime($reply->{'created_at'}));
$item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'};
$this->items[] = $item;
}
}
}
private function getAPIURI() {
$url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id');
return $url;
}
public function getURI() {
$url = parent::getURI() . '/posts/' . $this->getInput('post_id');
return $url;
}
private function getPost() {
$url = $this->getAPIURI();
$jsonPost = getContents( $url ) or returnClientError('Could not retrieve post metadata.');
$post = json_decode($jsonPost);
return $post;
}
public function getName(){
if(!is_null($this->getInput('post_id'))) {
$post = $this->getPost();
return $post->{'subject'} . ' - ' . parent::getName();
}
return parent::getName();
}
public function getDescription(){
if(!is_null($this->getInput('post_id'))) {
$post = $this->getPost();
return $post->{'content'};
}
return parent::getName();
}
}

View File

@@ -17,13 +17,11 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
'hide_expired' => array(
'name' => 'Hide expired deals',
'type' => 'checkbox',
'required' => true
),
'hide_local' => array(
'name' => 'Hide local deals',
'type' => 'checkbox',
'title' => 'Hide deals in physical store',
'required' => true
),
'priceFrom' => array(
'name' => 'Minimal Price',
@@ -43,7 +41,6 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
'group' => array(
'name' => 'Group',
'type' => 'list',
'required' => true,
'title' => 'Group whose deals must be displayed',
'values' => array(
'2DS' => '2ds',
@@ -1317,7 +1314,6 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
'order' => array(
'name' => 'Order by',
'type' => 'list',
'required' => true,
'title' => 'Sort order of deals',
'values' => array(
'From the most to the least hot deal' => '-hot',

View File

@@ -21,7 +21,6 @@ class InstructablesBridge extends BridgeAbstract {
'category' => array(
'name' => 'Category',
'type' => 'list',
'required' => true,
'values' => array(
'Play' => array(
'All' => '/play/',
@@ -240,7 +239,6 @@ class InstructablesBridge extends BridgeAbstract {
'filter' => array(
'name' => 'Filter',
'type' => 'list',
'required' => true,
'values' => array(
'Featured' => ' ',
'Recent' => 'recent/',

View File

@@ -34,7 +34,6 @@ class JustETFBridge extends BridgeAbstract {
'global' => array(
'lang' => array(
'name' => 'Language',
'required' => true,
'type' => 'list',
'values' => array(
'Englisch' => 'en',

View File

@@ -11,7 +11,6 @@ class KununuBridge extends BridgeAbstract {
'site' => array(
'name' => 'Site',
'type' => 'list',
'required' => true,
'title' => 'Select your site',
'values' => array(
'Austria' => 'at',
@@ -23,7 +22,6 @@ class KununuBridge extends BridgeAbstract {
'full' => array(
'name' => 'Load full article',
'type' => 'checkbox',
'required' => false,
'exampleValue' => 'checked',
'title' => 'Activate to load full article'
)

View File

@@ -20,12 +20,13 @@ class LeMondeInformatiqueBridge extends FeedExpander {
str_replace(
'/grande/',
'/petite/',
$article_html->find('.article-image', 0)->find('img', 0)->src
$article_html->find('.article-image > img, figure > img', 0)->src
)
);
//No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
$item['content'] = utf8_encode($this->cleanArticle($article_html->find('div.col-primary', 0)->innertext));
$content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
$item['content'] = utf8_encode($this->cleanArticle($content_node->innertext));
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext);
return $item;

View File

@@ -13,7 +13,6 @@ class MangareaderBridge extends BridgeAbstract {
'category' => array(
'name' => 'Category',
'type' => 'list',
'required' => true,
'values' => array(
'All' => 'all',
'Action' => 'action',

View File

@@ -21,7 +21,8 @@ class MozillaSecurityBridge extends BridgeAbstract {
$item['title'] = $element->innertext;
$item['timestamp'] = strtotime($element->innertext);
$item['content'] = $element->next_sibling()->innertext;
$item['uri'] = self::URI;
$item['uri'] = self::URI . '?' . $item['timestamp'];
$item['uid'] = self::URI . '?' . $item['timestamp'];
$this->items[] = $item;
}
}

View File

@@ -17,13 +17,11 @@ class MydealsBridge extends PepperBridgeAbstract {
'hide_expired' => array(
'name' => 'Abgelaufenes ausblenden',
'type' => 'checkbox',
'required' => true
),
'hide_local' => array(
'name' => 'Lokales ausblenden',
'type' => 'checkbox',
'title' => 'Deals im physischen Geschäft ausblenden',
'required' => true
),
'priceFrom' => array(
'name' => 'Minimaler Preis',
@@ -43,7 +41,6 @@ class MydealsBridge extends PepperBridgeAbstract {
'group' => array(
'name' => 'Gruppen',
'type' => 'list',
'required' => true,
'title' => 'Gruppe, deren Deals angezeigt werden müssen',
'values' => array(
'Elektronik' => 'elektronik',
@@ -66,7 +63,6 @@ class MydealsBridge extends PepperBridgeAbstract {
'order' => array(
'name' => 'sortieren nach',
'type' => 'list',
'required' => true,
'title' => 'Sortierung der deals',
'values' => array(
'Vom heißesten zum kältesten Deal' => '',

View File

@@ -11,7 +11,6 @@ class NineGagBridge extends BridgeAbstract {
'd' => array(
'name' => 'Section',
'type' => 'list',
'required' => true,
'values' => array(
'Hot' => 'hot',
'Trending' => 'trending',
@@ -28,7 +27,6 @@ class NineGagBridge extends BridgeAbstract {
'g' => array(
'name' => 'Section',
'type' => 'list',
'required' => true,
'values' => array(
'Animals' => 'cute',
'Anime & Manga' => 'anime-manga',
@@ -88,7 +86,6 @@ class NineGagBridge extends BridgeAbstract {
't' => array(
'name' => 'Type',
'type' => 'list',
'required' => true,
'values' => array(
'Hot' => 'hot',
'Fresh' => 'fresh',

View File

@@ -21,8 +21,7 @@ class NotAlwaysBridge extends BridgeAbstract {
'Friendly' => 'friendly',
'Hopeless' => 'hopeless',
'Unfiltered' => 'unfiltered'
),
'required' => true
)
)
));

View File

@@ -9,7 +9,6 @@ class OnVaSortirBridge extends FeedExpander {
'city' => array(
'name' => 'City',
'type' => 'list',
'required' => true,
'values' => array(
'Agen' => 'Agen',
'Ajaccio' => 'Ajaccio',

View File

@@ -35,25 +35,33 @@ class OneFortuneADayBridge extends BridgeAbstract {
'23:00' => 23,
),
'defaultValue' => 5
),
'lucky' => array(
'name' => 'Lucky number (optional)',
'type' => 'text'
)
));
const LIMIT_ITEMS = 7;
const DAY_SECS = 86400;
public function getDescription(){
return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand();
}
public function collectData() {
$time = gmmktime((int)$this->getInput('time'), 0, 0);
if ($time > time())
$time -= self::DAY_SECS;
for ($i = self::LIMIT_ITEMS; $i > 0; --$i) {
$seed = date('Ymd', $time);
$seed = gmdate('Ymd', $time) . $this->getInput('lucky');
$quote = $this->getQuote($seed);
$item['title'] = strftime('%A, %x', $time);
$item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = $time;
$item['uri'] = 'urn:sha1:' . hash('sha1', $seed);
$item['uid'] = hash('sha1', $seed);
$this->items[] = $item;

View File

@@ -11,7 +11,6 @@ class OpenClassroomsBridge extends BridgeAbstract {
'u' => array(
'name' => 'Catégorie',
'type' => 'list',
'required' => true,
'values' => array(
'Arts & Culture' => 'arts',
'Code' => 'code',

View File

@@ -0,0 +1,106 @@
<?php
class RoadAndTrackBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Road And Track Bridge';
const URI = 'https://www.roadandtrack.com/';
const CACHE_TIMEOUT = 86400; // 24h
const DESCRIPTION = 'Returns the latest news from Road & Track.';
const PARAMETERS = array(
array(
'new-cars' => array(
'name' => 'New Cars',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load New Cars articles'
),
'motorsports' => array(
'name' => 'Motorsports',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load Motorsports articles'
),
'car-culture' => array(
'name' => 'Car Culture',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load Car Culture articles'
),
'car-shows' => array(
'name' => 'Car shows',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Activate to load Car shows articles'
)
)
);
const SIG_URL = 'https://cloud.mazdigital.com/feeds/production/comboapp/204/api/v3/';
public function collectData() {
//Magic
$signVal = '?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9jbG91ZC5tYXpkaWd';
$signVal .= 'pdGFsLmNvbS9mZWVkcy9wcm9kdWN0aW9uL2NvbWJvYXBwLzIwNC8qIiwiQ29uZGl0aW9uIj';
$signVal .= 'p7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNTUyNTU5MDUzfSwiSXBBZGRyZ';
$signVal .= 'XNzIjp7IkFXUzpTb3VyY2VJcCI6IjAuMC4wLjAvMCJ9fX1dfQ__&Signature=jgS~Jccjs';
$signVal .= 'lXMMywWesmwDpUbHvEmrADRP7iBRzT~OiP-O~zI-8TtQzqTP7GUrpB9~v69CvhO7-JVtw94';
$signVal .= 'VC3N6lQrwsxTTIhpS57YGeV~MbZx~P653yUV7jb3jpJE2yUawfXnEkD-XzOIn8-caMo~14i';
$signVal .= 'KuWV9KNDkTJaRgOMy0rrVpWqiuBjCu5s5B8Ylt2qwcpOvHjXSqG9IY5c7GUIXKsk8yXzGFi';
$signVal .= 'yzy8hfuGgdx0n7fgl7c4-EoDgQaz~U76g0epejPxV5Csj16rCCfAqBU5kZJnACZ1vvOvRcV';
$signVal .= 'Wiu8KUuUuCS04SPmJ73Y5XoY8~uXRScxZG1kAFTIAhT4nYVlg__&Key-Pair-Id=APKAIZB';
$signVal .= 'QNNSW4WGIFP4Q';
$newsElements = array();
if($this->getInput('new-cars')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7591/item_feed' . $signVal))
);
}
if($this->getInput('motorsports')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7590/item_feed' . $signVal))
);
}
if($this->getInput('car-culture')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7588/item_feed' . $signVal))
);
}
if($this->getInput('car-shows')) {
$newsElements = array_merge($newsElements,
json_decode(getContents(self::SIG_URL . '7589/item_feed' . $signVal))
);
}
usort($newsElements, function($a, $b) {
return $b->published - $a->published;
});
$limit = 19;
foreach($newsElements as $element) {
$item = array();
$item['uri'] = $element->sourceUrl;
$item['timestamp'] = $element->published;
$item['enclosures'] = array($element->cover->url);
$item['title'] = $element->title;
$item['content'] = $this->getArticleContent($element);
$this->items[] = $item;
if($limit > 0) {
$limit--;
} else {
break;
}
}
}
private function getArticleContent($article) {
return getContents($article->contentUrl);
}
}

View File

@@ -18,7 +18,6 @@ class SkimfeedBridge extends BridgeAbstract {
'box_channel' => array(
'name' => 'Channel',
'type' => 'list',
'required' => true,
'title' => 'Select your channel',
'values' => array(
'Hacker News' => '/news/hacker-news.html',
@@ -68,7 +67,6 @@ class SkimfeedBridge extends BridgeAbstract {
'tech_channel' => array(
'name' => 'Tech channel',
'type' => 'list',
'required' => true,
'title' => 'Select your tech channel',
'values' => array(
'Agg' => array(

View File

@@ -14,7 +14,7 @@ class SoundCloudBridge extends BridgeAbstract {
)
));
const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
const CLIENT_ID = 'W0KEWWILAjDiRH89X0jpwzuq6rbSK08R';
public function collectData(){

View File

@@ -165,7 +165,7 @@ class TwitterBridge extends BridgeAbstract {
// Skip retweets?
if($this->getInput('noretweet')
&& $tweet->getAttribute('data-screen-name') !== $this->getInput('u')) {
&& strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) {
continue;
}
@@ -189,6 +189,9 @@ class TwitterBridge extends BridgeAbstract {
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
// get author
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
if(strcasecmp($tweet->getAttribute('data-screen-name'), $this->getInput('u'))) {
$item['author'] .= ' RT: @' . $this->getInput('u');
}
// get avatar link
$item['avatar'] = $tweet->find('img', 0)->src;
// get TweetID

View File

@@ -0,0 +1,31 @@
<?php
class VMwareSecurityBridge extends BridgeAbstract {
const MAINTAINER = 'm0le.net';
const NAME = 'VMware Security Advisories';
const URI = 'https://www.vmware.com/security/advisories.html';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'VMware Security Advisories';
const WEBROOT = 'https://www.vmware.com';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request VSA.');
$html = defaultLinkTo($html, self::WEBROOT);
$item = array();
$articles = $html->find('div[class="news_block"]');
foreach ($articles as $element) {
$item['uri'] = $element->find('a', 0)->getAttribute('href');
$title = $element->find('a', 0)->innertext;
$item['title'] = $title;
$item['timestamp'] = strtotime($element->find('p', 0)->innertext);
$item['content'] = $element->find('p', 1)->innertext;
$item['uid'] = $title;
$this->items[] = $item;
}
}
}

View File

@@ -9,7 +9,6 @@ class WikiLeaksBridge extends BridgeAbstract {
'category' => array(
'name' => 'Category',
'type' => 'list',
'required' => true,
'title' => 'Select your category',
'values' => array(
'News' => '-News-',
@@ -28,7 +27,6 @@ class WikiLeaksBridge extends BridgeAbstract {
'teaser' => array(
'name' => 'Show teaser',
'type' => 'checkbox',
'required' => false,
'title' => 'If checked feeds will display the teaser',
'defaultValue' => true
)

View File

@@ -13,7 +13,6 @@ class WikipediaBridge extends BridgeAbstract {
'language' => array(
'name' => 'Language',
'type' => 'list',
'required' => true,
'title' => 'Select your language',
'exampleValue' => 'English',
'values' => array(
@@ -27,7 +26,6 @@ class WikipediaBridge extends BridgeAbstract {
'subject' => array(
'name' => 'Subject',
'type' => 'list',
'required' => true,
'title' => 'What subject are you interested in?',
'exampleValue' => 'Today\'s featured article',
'values' => array(

View File

@@ -75,7 +75,7 @@ class WordPressPluginUpdateBridge extends BridgeAbstract {
private function getCachedDate($url){
Debug::log('getting pubdate from url ' . $url . '');
// Initialize cache
$cache = Cache::create('FileCache');
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE . 'pages/');
$params = [$url];
$cache->setParameters($params);

99
caches/SQLiteCache.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
/**
* Cache based on SQLite 3 <https://www.sqlite.org>
*/
class SQLiteCache implements CacheInterface {
protected $path;
protected $param;
private $db = null;
public function __construct() {
if (!extension_loaded('sqlite3'))
die('"sqlite3" extension not loaded. Please check "php.ini"');
$file = PATH_CACHE . 'cache.sqlite';
if (!is_file($file)) {
$this->db = new SQLite3($file);
$this->db->enableExceptions(true);
$this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
} else {
$this->db = new SQLite3($file);
$this->db->enableExceptions(true);
}
$this->db->busyTimeout(5000);
}
public function loadData(){
$Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
$Qselect->bindValue(':key', $this->getCacheKey());
$result = $Qselect->execute();
if ($result instanceof SQLite3Result) {
$data = $result->fetchArray(SQLITE3_ASSOC);
if (isset($data['value'])) {
return unserialize($data['value']);
}
}
return null;
}
public function saveData($datas){
$Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
$Qupdate->bindValue(':key', $this->getCacheKey());
$Qupdate->bindValue(':value', serialize($datas));
$Qupdate->bindValue(':updated', time());
$Qupdate->execute();
return $this;
}
public function getTime(){
$Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
$Qselect->bindValue(':key', $this->getCacheKey());
$result = $Qselect->execute();
if ($result instanceof SQLite3Result) {
$data = $result->fetchArray(SQLITE3_ASSOC);
if (isset($data['updated'])) {
return $data['updated'];
}
}
return false;
}
public function purgeCache($duration){
$Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
$Qdelete->bindValue(':expired', time() - $duration);
$Qdelete->execute();
}
/**
* Set cache path
* @return self
*/
public function setPath($path){
$this->path = $path;
return $this;
}
/**
* Set HTTP GET parameters
* @return self
*/
public function setParameters(array $param){
$this->param = array_map('strtolower', $param);
return $this;
}
////////////////////////////////////////////////////////////////////////////
protected function getCacheKey(){
if(is_null($this->param)) {
throw new \Exception('Call "setParameters" first!');
}
return hash('sha1', $this->path . http_build_query($this->param), true);
}
}

View File

@@ -6,6 +6,10 @@
[cache]
; Defines the cache type used by RSS-Bridge
; "file" = FileCache (default)
type = "file"
; Allow users to specify custom timeout for specific requests.
; true = enabled
; false = disabled (default)

View File

@@ -1,21 +1,30 @@
<?php
/**
* Atom
* Documentation Source http://en.wikipedia.org/wiki/Atom_%28standard%29 and
* http://tools.ietf.org/html/rfc4287
*/
* AtomFormat - RFC 4287: The Atom Syndication Format
* https://tools.ietf.org/html/rfc4287
*
* Validator:
* https://validator.w3.org/feed/
*/
class AtomFormat extends FormatAbstract{
public function stringify(){
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
const LIMIT_TITLE = 140;
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
public function stringify(){
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
$feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
$uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
// since we can't guarantee that all items have an author,
// a global feed author is mandatory
$feedAuthor = 'RSS-Bridge';
$uriparts = parse_url($uri);
if(!empty($extraInfos['icon'])) {
$icon = $extraInfos['icon'];
@@ -27,11 +36,40 @@ class AtomFormat extends FormatAbstract{
$entries = '';
foreach($this->getItems() as $item) {
$entryAuthor = $this->xml_encode($item->getAuthor());
$entryTimestamp = $item->getTimestamp();
$entryTitle = $this->xml_encode($item->getTitle());
$entryUri = $this->xml_encode($item->getURI());
$entryTimestamp = $this->xml_encode(date(DATE_ATOM, $item->getTimestamp()));
$entryContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
$entryContent = $item->getContent();
$entryUri = $item->getURI();
$entryID = '';
if (!empty($item->getUid()))
$entryID = 'urn:sha1:' . $item->getUid();
if (empty($entryID)) // Fallback to provided URI
$entryID = $this->xml_encode($entryUri);
if (empty($entryID)) // Fallback to title and content
$entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
if (empty($entryTimestamp))
$entryTimestamp = $this->lastModified;
if (empty($entryTitle)) {
$entryTitle = str_replace("\n", ' ', strip_tags($entryContent));
if (strlen($entryTitle) > self::LIMIT_TITLE) {
$wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n");
$entryTitle = substr($entryTitle, 0, $wrapPos) . '...';
}
}
if (empty($entryContent))
$entryContent = $entryTitle;
$entryAuthor = $this->xml_encode($item->getAuthor());
$entryTitle = $this->xml_encode($entryTitle);
$entryUri = $this->xml_encode($entryUri);
$entryTimestamp = $this->xml_encode(gmdate(DATE_ATOM, $entryTimestamp));
$entryContent = $this->xml_encode($this->sanitizeHtml($entryContent));
$entryEnclosures = '';
foreach($item->getEnclosures() as $enclosure) {
@@ -49,16 +87,28 @@ class AtomFormat extends FormatAbstract{
. PHP_EOL;
}
$entryLinkAlternate = '';
if (!empty($entryUri)) {
$entryLinkAlternate = '<link rel="alternate" type="text/html" href="'
. $entryUri
. '"/>';
}
if (!empty($entryAuthor)) {
$entryAuthor = '<author><name>'
. $entryAuthor
. '</name></author>';
}
$entries .= <<<EOD
<entry>
<author>
<name>{$entryAuthor}</name>
</author>
<title type="html">{$entryTitle}</title>
<link rel="alternate" type="text/html" href="{$entryUri}" />
<id>{$entryUri}</id>
<published>{$entryTimestamp}</published>
<updated>{$entryTimestamp}</updated>
<id>{$entryID}</id>
{$entryLinkAlternate}
{$entryAuthor}
<content type="html">{$entryContent}</content>
{$entryEnclosures}
{$entryCategories}
@@ -67,21 +117,24 @@ class AtomFormat extends FormatAbstract{
EOD;
}
$feedTimestamp = date(DATE_ATOM, time());
$charset = $this->getCharset();
$feedTimestamp = gmdate(DATE_ATOM, $this->lastModified);
$charset = $this->getCharset();
/* Data are prepared, now let's begin the "MAGIE !!!" */
$toReturn = <<<EOD
<?xml version="1.0" encoding="{$charset}"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0">
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">{$title}</title>
<id>http{$https}://{$httpHost}{$httpInfo}/</id>
<id>{$feedUrl}</id>
<icon>{$icon}</icon>
<logo>{$icon}</logo>
<updated>{$feedTimestamp}</updated>
<author>
<name>{$feedAuthor}</name>
</author>
<link rel="alternate" type="text/html" href="{$uri}" />
<link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
<link rel="self" type="application/atom+xml" href="{$feedUrl}" />
{$entries}
</feed>
EOD;

View File

@@ -16,21 +16,22 @@ class JsonFormat extends FormatAbstract {
'content',
'enclosures',
'categories',
'uid',
);
public function stringify(){
$urlScheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
$extraInfos = $this->getExtraInfos();
$data = array(
'version' => 'https://jsonfeed.org/version/1',
'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
'feed_url' => $urlScheme . $urlHost . $urlRequest
'version' => 'https://jsonfeed.org/version/1',
'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost,
'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY,
'feed_url' => $urlPrefix . $urlHost . $urlRequest
);
if (!empty($extraInfos['icon'])) {
@@ -42,20 +43,24 @@ class JsonFormat extends FormatAbstract {
foreach ($this->getItems() as $item) {
$entry = array();
$entryAuthor = $item->getAuthor();
$entryTitle = $item->getTitle();
$entryUri = $item->getURI();
$entryTimestamp = $item->getTimestamp();
$entryContent = $this->sanitizeHtml($item->getContent());
$entryEnclosures = $item->getEnclosures();
$entryCategories = $item->getCategories();
$entryAuthor = $item->getAuthor();
$entryTitle = $item->getTitle();
$entryUri = $item->getURI();
$entryTimestamp = $item->getTimestamp();
$entryContent = $this->sanitizeHtml($item->getContent());
$entryEnclosures = $item->getEnclosures();
$entryCategories = $item->getCategories();
$vendorFields = $item->toArray();
foreach (self::VENDOR_EXCLUDES as $key) {
unset($vendorFields[$key]);
}
$entry['id'] = $entryUri;
$entry['id'] = $item->getUid();
if (empty($entry['id'])) {
$entry['id'] = $entryUri;
}
if (!empty($entryTitle)) {
$entry['title'] = $entryTitle;
@@ -82,8 +87,8 @@ class JsonFormat extends FormatAbstract {
$entry['attachments'] = array();
foreach ($entryEnclosures as $enclosure) {
$entry['attachments'][] = array(
'url' => $enclosure,
'mime_type' => getMimeType($enclosure)
'url' => $enclosure,
'mime_type' => getMimeType($enclosure)
);
}
}

View File

@@ -1,18 +1,45 @@
<?php
/**
* Mrss
* Documentation Source http://www.rssboard.org/media-rss
*/
* MrssFormat - RSS 2.0 + Media RSS
* http://www.rssboard.org/rss-specification
* http://www.rssboard.org/media-rss
*
* Validators:
* https://validator.w3.org/feed/
* http://www.rssboard.org/rss-validator/
*
* Notes about the implementation:
*
* - The item author is not supported as it needs to be an e-mail address to be
* valid.
* - The RSS specification does not explicitly allow to have more than one
* enclosure as every item is meant to provide one "story", thus having
* multiple enclosures per item may lead to unexpected behavior.
* On top of that, it requires to have a length specified, which RSS-Bridge
* can't provide.
* - The Media RSS extension comes in handy, since it allows to have multiple
* enclosures, even though they recommend to have only one enclosure because
* of the one-story-per-item reason. It only requires to specify the URL,
* everything else is optional.
* - Since the Media RSS extension has its own namespace, the output is a valid
* RSS 2.0 feed that works with feed readers that don't support the extension.
*/
class MrssFormat extends FormatAbstract {
public function stringify(){
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 's' : '';
$httpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
$httpInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
const ALLOWED_IMAGE_EXT = array(
'.gif', '.jpg', '.png'
);
$serverRequestUri = isset($_SERVER['REQUEST_URI']) ? $this->xml_encode($_SERVER['REQUEST_URI']) : '';
public function stringify(){
$urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
$urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
$urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
$urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
$feedUrl = $this->xml_encode($urlPrefix . $urlHost . $urlRequest);
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
$icon = $extraInfos['icon'];
if(!empty($extraInfos['uri'])) {
$uri = $this->xml_encode($extraInfos['uri']);
@@ -20,34 +47,48 @@ class MrssFormat extends FormatAbstract {
$uri = REPOSITORY;
}
$uriparts = parse_url($uri);
$icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico');
$items = '';
foreach($this->getItems() as $item) {
$itemAuthor = $this->xml_encode($item->getAuthor());
$itemTimestamp = $item->getTimestamp();
$itemTitle = $this->xml_encode($item->getTitle());
$itemUri = $this->xml_encode($item->getURI());
$itemTimestamp = $this->xml_encode(date(DATE_RFC2822, $item->getTimestamp()));
$itemContent = $this->xml_encode($this->sanitizeHtml($item->getContent()));
$entryID = $item->getUid();
$isPermaLink = 'false';
if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI
$entryID = $itemUri;
$isPermaLink = 'true';
}
if (empty($entryID)) // Fallback to title and content
$entryID = hash('sha1', $itemTitle . $itemContent);
$entryTitle = '';
if (!empty($itemTitle))
$entryTitle = '<title>' . $itemTitle . '</title>';
$entryLink = '';
if (!empty($itemUri))
$entryLink = '<link>' . $itemUri . '</link>';
$entryPublished = '';
if (!empty($itemTimestamp)) {
$entryPublished = '<pubDate>'
. $this->xml_encode(gmdate(DATE_RFC2822, $itemTimestamp))
. '</pubDate>';
}
$entryDescription = '';
if (!empty($itemContent))
$entryDescription = '<description>' . $itemContent . '</description>';
$entryEnclosuresWarning = '';
$entryEnclosures = '';
if(!empty($item->getEnclosures())) {
$entryEnclosures .= '<enclosure url="'
. $this->xml_encode($item->getEnclosures()[0])
. '" type="' . getMimeType($item->getEnclosures()[0]) . '" />';
if(count($item->getEnclosures()) > 1) {
$entryEnclosures .= PHP_EOL;
$entryEnclosuresWarning = '&lt;br&gt;Warning:
Some media files might not be shown to you. Consider using the ATOM format instead!';
foreach($item->getEnclosures() as $enclosure) {
$entryEnclosures .= '<atom:link rel="enclosure" href="'
. $enclosure . '" type="' . getMimeType($enclosure) . '" />'
. PHP_EOL;
}
}
foreach($item->getEnclosures() as $enclosure) {
$entryEnclosures .= '<media:content url="'
. $this->xml_encode($enclosure)
. '" type="' . getMimeType($enclosure) . '"/>'
. PHP_EOL;
}
$entryCategories = '';
@@ -60,12 +101,11 @@ Some media files might not be shown to you. Consider using the ATOM format inste
$items .= <<<EOD
<item>
<title>{$itemTitle}</title>
<link>{$itemUri}</link>
<guid isPermaLink="true">{$itemUri}</guid>
<pubDate>{$itemTimestamp}</pubDate>
<description>{$itemContent}{$entryEnclosuresWarning}</description>
<author>{$itemAuthor}</author>
{$entryTitle}
{$entryLink}
<guid isPermaLink="{$isPermaLink}">{$entryID}</guid>
{$entryPublished}
{$entryDescription}
{$entryEnclosures}
{$entryCategories}
</item>
@@ -75,22 +115,28 @@ EOD;
$charset = $this->getCharset();
/* xml attributes need to have certain characters escaped to be w3c compliant */
$imageTitle = htmlspecialchars($title, ENT_COMPAT);
$feedImage = '';
if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) {
$feedImage .= <<<EOD
<image>
<url>{$icon}</url>
<title>{$title}</title>
<link>{$uri}</link>
</image>
EOD;
}
/* Data are prepared, now let's begin the "MAGIE !!!" */
$toReturn = <<<EOD
<?xml version="1.0" encoding="{$charset}"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:media="http://search.yahoo.com/mrss/"
xmlns:atom="http://www.w3.org/2005/Atom">
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{$title}</title>
<link>http{$https}://{$httpHost}{$httpInfo}/</link>
<link>{$uri}</link>
<description>{$title}</description>
<image url="{$icon}" title="{$imageTitle}" link="{$uri}"/>
<atom:link rel="alternate" type="text/html" href="{$uri}" />
<atom:link rel="self" href="http{$https}://{$httpHost}{$serverRequestUri}" />
{$feedImage}
<atom:link rel="alternate" type="text/html" href="{$uri}"/>
<atom:link rel="self" href="{$feedUrl}" type="application/atom+xml"/>
{$items}
</channel>
</rss>

286
index.php
View File

@@ -51,287 +51,15 @@ $whitelist_default = array(
try {
Bridge::setWhitelist($whitelist_default);
$actionFac = new \ActionFactory();
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
$action = array_key_exists('action', $params) ? $params['action'] : null;
$bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
// Return list of bridges as JSON formatted text
if($action === 'list') {
$list = new StdClass();
$list->bridges = array();
$list->total = 0;
foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridge = Bridge::create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
$list->bridges[$bridgeName] = array(
'status' => 'inactive'
);
continue;
}
$status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
'uri' => $bridge->getURI(),
'name' => $bridge->getName(),
'icon' => $bridge->getIcon(),
'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription()
);
}
$list->total = count($list->bridges);
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
} elseif($action === 'detect') {
$targetURL = $params['url']
or returnClientError('You must specify a url!');
$format = $params['format']
or returnClientError('You must specify a format!');
foreach(Bridge::getBridgeNames() as $bridgeName) {
if(!Bridge::isWhitelisted($bridgeName)) {
continue;
}
$bridge = Bridge::create($bridgeName);
if($bridge === false) {
continue;
}
$bridgeParams = $bridge->detectParameters($targetURL);
if(is_null($bridgeParams)) {
continue;
}
$bridgeParams['bridge'] = $bridgeName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
} elseif($action === 'display' && !empty($bridge)) {
$format = $params['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
if(($pos = strpos($format, 'Format')) === (strlen($format) - strlen('Format'))) {
$format = substr($format, 0, $pos);
}
// whitelist control
if(!Bridge::isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
// Data retrieval
$bridge = Bridge::create($bridge);
$noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
// Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $params)) {
if(!CUSTOM_CACHE_TIMEOUT) {
unset($params['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
header('Location: ' . $uri, true, 301);
die();
}
$cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
} else {
$cache_timeout = $bridge->getCacheTimeout();
}
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$params,
array_fill_keys(
array(
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$params,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(PATH_CACHE);
$cache->purgeCache(86400); // 24 hours
$cache->setParameters($cache_params);
$items = array();
$infos = array();
$mtime = $cache->getTime();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
$cached = $cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
foreach($cached['items'] as $item) {
$items[] = new \FeedItem($item);
}
$infos = $cached['extraInfos'];
}
} else { // Collect new data
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = $bridge->getItems();
// Transform "legacy" items to FeedItems if necessary.
// Remove this code when support for "legacy" items ends!
if(isset($items[0]) && is_array($items[0])) {
$feedItems = array();
foreach($items as $item) {
$feedItems[] = new \FeedItem($item);
}
$items = $feedItems;
}
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item->setTitle('Bridge encountered an unexpected situation! (' . $params['_error_time'] . ')');
} else {
$item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
}
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($params)
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} catch(Exception $e) {
error_log($e);
$item = new \FeedItem();
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($params)
);
$item->setTitle('Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')');
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
}
// Store data in cache
$cache->saveData(array(
'items' => array_map(function($i){ return $i->toArray(); }, $items),
'extraInfos' => $infos
));
}
// Data transformation
try {
$format = Format::create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
if(array_key_exists('action', $params)) {
$action = $actionFac->create($params['action']);
$action->setUserData($params);
$action->execute();
} else {
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
echo BridgeList::create($showInactive);
}
} catch(\Exception $e) {

33
lib/ActionAbstract.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* An abstract class for action objects
*/
abstract class ActionAbstract implements ActionInterface {
/**
* Holds the user data.
*
* @var array
*/
protected $userData = null;
/**
* {@inheritdoc}
*
* @param array $userData {@inheritdoc}
*/
public function setUserData($userData) {
$this->userData = $userData;
}
}

65
lib/ActionFactory.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Factory for action objects.
*/
class ActionFactory extends FactoryAbstract {
/**
* {@inheritdoc}
*
* @param string $name {@inheritdoc}
*/
public function create($name) {
$filePath = $this->buildFilePath($name);
if(!file_exists($filePath)) {
throw new \Exception('File ' . $filePath . ' does not exist!');
}
require_once $filePath;
$class = $this->buildClassName($name);
if((new \ReflectionClass($class))->isInstantiable()) {
return new $class();
}
return false;
}
/**
* Build class name from action name
*
* The class name consists of the action name with prefix "Action". The first
* character of the class name must be uppercase.
*
* Example: 'display' => 'DisplayAction'
*
* @param string $name The action name.
* @return string The class name.
*/
protected function buildClassName($name) {
return ucfirst(strtolower($name)) . 'Action';
}
/**
* Build file path to the action class.
*
* @param string $name The action name.
* @return string Path to the action class.
*/
protected function buildFilePath($name) {
return $this->getWorkingDir() . $this->buildClassName($name) . '.php';
}
}

34
lib/ActionInterface.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Interface for action objects.
*/
interface ActionInterface {
/**
* Set user data for the action to consume.
*
* @param array $userData An associative array of user data.
* @return void
*/
function setUserData($userData);
/**
* Execute the action.
*
* Note: This function directly outputs data to the user.
*
* @return void
*/
function execute();
}

View File

@@ -213,7 +213,7 @@ class Bridge {
// Create initial whitelist or load from disk
if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
file_put_contents(WHITELIST, implode("\n", self::$whitelist));
} else {
} elseif(file_exists(WHITELIST)) {
$contents = trim(file_get_contents(WHITELIST));

View File

@@ -207,6 +207,11 @@ This bridge is not fetching its content through a secure connection</div>';
* @return string The list input field
*/
private static function getListInput($entry, $id, $name) {
if(isset($entry['required']) && $entry['required'] === true) {
Debug::log('The "required" attribute is not supported for lists.');
unset($entry['required']);
}
$list = '<select '
. self::getInputAttributes($entry)
. ' id="'
@@ -267,6 +272,11 @@ This bridge is not fetching its content through a secure connection</div>';
* @return string The checkbox input field
*/
private static function getCheckboxInput($entry, $id, $name) {
if(isset($entry['required']) && $entry['required'] === true) {
Debug::log('The "required" attribute is not supported for checkboxes.');
unset($entry['required']);
}
return '<input '
. self::getInputAttributes($entry)
. ' id="'

View File

@@ -64,6 +64,8 @@ class Cache {
* @return object|bool The cache object or false if the class is not instantiable.
*/
public static function create($name){
$name = self::sanitizeCacheName($name) . 'Cache';
if(!self::isCacheName($name)) {
throw new \InvalidArgumentException('Cache name invalid!');
}
@@ -137,4 +139,75 @@ class Cache {
public static function isCacheName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
/**
* Returns a list of cache names from the working directory.
*
* The list is cached internally to allow for successive calls.
*
* @return array List of cache names
*/
public static function getCacheNames(){
static $cacheNames = array(); // Initialized on first call
if(empty($cacheNames)) {
$files = scandir(self::getWorkingDir());
if($files !== false) {
foreach($files as $file) {
if(preg_match('/^([^.]+)Cache\.php$/U', $file, $out)) {
$cacheNames[] = $out[1];
}
}
}
}
return $cacheNames;
}
/**
* Returns the sanitized cache name.
*
* The cache name can be specified in various ways:
* * The PHP file name (i.e. `FileCache.php`)
* * The PHP file name without file extension (i.e. `FileCache`)
* * The cache name (i.e. `file`)
*
* Casing is ignored (i.e. `FILE` and `fIlE` are the same).
*
* A cache file matching the given cache name must exist in the working
* directory!
*
* @param string $name The cache name
* @return string|null The sanitized cache name if the provided name is
* valid, null otherwise.
*/
protected static function sanitizeCacheName($name) {
if(is_string($name)) {
// Trim trailing '.php' if exists
if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
$name = $matches[1];
}
// Trim trailing 'Cache' if exists
if(preg_match('/(.+)(?:Cache)$/i', $name, $matches)) {
$name = $matches[1];
}
// The name is valid if a corresponding file is found on disk
if(in_array(strtolower($name), array_map('strtolower', self::getCacheNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', self::getCacheNames()));
return self::getCacheNames()[$index];
}
Debug::log('Invalid cache name specified: "' . $name . '"!');
}
return null; // Bad parameter
}
}

View File

@@ -28,7 +28,7 @@ final class Configuration {
*
* @todo Replace this property by a constant.
*/
public static $VERSION = '2019-01-13';
public static $VERSION = '2019-03-17';
/**
* Holds the configuration data.
@@ -179,6 +179,9 @@ final class Configuration {
/** Name of the proxy server */
define('PROXY_NAME', self::getConfig('proxy', 'name'));
if(!is_string(self::getConfig('cache', 'type')))
die('Parameter [cache] => "type" is not a valid string! Please check "config.ini.php"!');
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');

70
lib/FactoryAbstract.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Abstract class for factories.
*/
abstract class FactoryAbstract {
/**
* Holds the working directory
*
* @var string
*/
private $workingDir = null;
/**
* Set the working directory.
*
* @param string $dir The working directory.
* @return void
*/
public function setWorkingDir($dir) {
$this->workingDir = null;
if(!is_string($dir)) {
throw new \InvalidArgumentException('Working directory must be a string!');
}
if(!file_exists($dir)) {
throw new \Exception('Working directory does not exist!');
}
if(!is_dir($dir)) {
throw new \InvalidArgumentException($dir . ' is not a directory!');
}
$this->workingDir = realpath($dir) . '/';
}
/**
* Get the working directory
*
* @return string The working directory.
*/
public function getWorkingDir() {
if(is_null($this->workingDir)) {
throw new \LogicException('Working directory is not set!');
}
return $this->workingDir;
}
/**
* Creates a new instance for the object specified by name.
*
* @param string $name The name of the object to create.
* @return object The object instance
*/
abstract public function create($name);
}

View File

@@ -265,7 +265,7 @@ abstract class FeedExpander extends BridgeAbstract {
//When "link" field is present, URL is more reliable than "id" field
if (count($feedItem->link) === 1) {
$this->uri = (string)$feedItem->link[0]['href'];
$item['uri'] = (string)$feedItem->link[0]['href'];
} else {
foreach($feedItem->link as $link) {
if(strtolower($link['rel']) === 'alternate') {

View File

@@ -55,6 +55,9 @@ class FeedItem {
/** @var array List of category names or tags */
protected $categories = array();
/** @var string Unique ID for the current item */
protected $uid = null;
/** @var array Associative list of additional parameters */
protected $misc = array(); // Custom parameters
@@ -391,6 +394,37 @@ class FeedItem {
return $this;
}
/**
* 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!');
} else {
$this->uid = sha1($uid);
}
return $this;
}
/**
* Add miscellaneous elements to the item.
*
@@ -426,6 +460,7 @@ class FeedItem {
'content' => $this->content,
'enclosures' => $this->enclosures,
'categories' => $this->categories,
'uid' => $this->uid,
), $this->misc
);
}
@@ -454,6 +489,7 @@ class FeedItem {
case 'content': $this->setContent($value); break;
case 'enclosures': $this->setEnclosures($value); break;
case 'categories': $this->setCategories($value); break;
case 'uid': $this->setUid($value); break;
default: $this->addMisc($name, $value);
}
}
@@ -476,6 +512,7 @@ class FeedItem {
case 'content': return $this->getContent();
case 'enclosures': return $this->getEnclosures();
case 'categories': return $this->getCategories();
case 'uid': return $this->getUid();
default:
if(array_key_exists($name, $this->misc))
return $this->misc[$name];

View File

@@ -196,7 +196,10 @@ class ParameterValidator {
if(isset($data[$id]) && !empty($data[$id])) {
$queriedContexts[$context] = true;
} elseif(isset($properties['required'])
&& $properties['required'] === true) {
&& $properties['required'] === true
&& isset($properties['type'])
&& $properties['type'] !== 'checkbox'
&& $properties['type'] !== 'list') {
$queriedContexts[$context] = false;
break;
}

View File

@@ -45,7 +45,7 @@ function getContents($url, $header = array(), $opts = array()){
Debug::log('Reading contents from "' . $url . '"');
// Initialize cache
$cache = Cache::create('FileCache');
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE . 'server/');
$cache->purgeCache(86400); // 24 hours (forced)
@@ -268,7 +268,7 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
Debug::log('Caching url ' . $url . ', duration ' . $duration);
// Initialize cache
$cache = Cache::create('FileCache');
$cache = Cache::create(Configuration::getConfig('cache', 'type'));
$cache->setPath(PATH_CACHE . 'pages/');
$cache->purgeCache(86400); // 24 hours (forced)

View File

@@ -29,6 +29,9 @@ define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
/** Path to the caches library */
define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
/** Path to the actions library */
define('PATH_LIB_ACTIONS', __DIR__ . '/../actions/');
/** Path to the cache folder */
define('PATH_CACHE', __DIR__ . '/../cache/');
@@ -39,11 +42,13 @@ define('WHITELIST', __DIR__ . '/../whitelist.txt');
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
// Interfaces
require_once PATH_LIB . 'ActionInterface.php';
require_once PATH_LIB . 'BridgeInterface.php';
require_once PATH_LIB . 'CacheInterface.php';
require_once PATH_LIB . 'FormatInterface.php';
// Classes
require_once PATH_LIB . 'FactoryAbstract.php';
require_once PATH_LIB . 'FeedItem.php';
require_once PATH_LIB . 'Debug.php';
require_once PATH_LIB . 'Exceptions.php';
@@ -58,6 +63,8 @@ require_once PATH_LIB . 'Configuration.php';
require_once PATH_LIB . 'BridgeCard.php';
require_once PATH_LIB . 'BridgeList.php';
require_once PATH_LIB . 'ParameterValidator.php';
require_once PATH_LIB . 'ActionFactory.php';
require_once PATH_LIB . 'ActionAbstract.php';
// Functions
require_once PATH_LIB . 'html.php';
@@ -65,6 +72,7 @@ require_once PATH_LIB . 'error.php';
require_once PATH_LIB . 'contents.php';
// Vendor
define('MAX_FILE_SIZE', 10000000); /* Allow larger files for simple_html_dom */
require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';

View File

@@ -14,6 +14,9 @@
<testsuite name="formats">
<directory suffix="FormatTest.php">tests</directory>
</testsuite>
<testsuite name="actions">
<directory suffix="ActionTest.php">tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -32,7 +32,6 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
width: 60%;
margin: 30px auto;
padding: 15px 15px;
text-align: center;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.09);
border-radius: 4px;
}
@@ -59,6 +58,18 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
}
section > div.content, section > div.attachments {
padding: 10px;
}
section h1, section h2, section h3, section b, section strong {
font-weight: bold;
}
section i, section em {
font-style: italic;
}
section p:not(:last-child) {
margin-bottom: 1em;
}
section li {
margin-left: 1em;
}
section > div.attachments > li.enclosure {
list-style-type: circle;
@@ -84,4 +95,4 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
}
button:hover {
background: #49afff;
}
}

View File

@@ -0,0 +1,59 @@
<?php
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class ActionImplementationTest extends TestCase {
private $class;
private $obj;
/**
* @dataProvider dataActionsProvider
*/
public function testClassName($path) {
$this->setAction($path);
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
$this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"');
}
/**
* @dataProvider dataActionsProvider
*/
public function testClassType($path) {
$this->setAction($path);
$this->assertInstanceOf(ActionInterface::class, $this->obj);
}
/**
* @dataProvider dataActionsProvider
*/
public function testVisibleMethods($path) {
$allowedActionAbstract = get_class_methods(ActionAbstract::class);
sort($allowedActionAbstract);
$this->setAction($path);
$methods = get_class_methods($this->obj);
sort($methods);
$this->assertEquals($allowedActionAbstract, $methods);
}
////////////////////////////////////////////////////////////////////////////
public function dataActionsProvider() {
$actions = array();
foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) {
$actions[basename($path, '.php')] = array($path);
}
return $actions;
}
private function setAction($path) {
require_once $path;
$this->class = basename($path, '.php');
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
$this->obj = new $this->class();
}
}

89
tests/AtomFormatTest.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
/**
* AtomFormat - RFC 4287: The Atom Syndication Format
* https://tools.ietf.org/html/rfc4287
*/
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class AtomFormatTest extends TestCase {
const PATH_SAMPLES = __DIR__ . '/samples/';
const PATH_EXPECTED = __DIR__ . '/samples/expectedAtomFormat/';
private $sample;
private $format;
private $data;
/**
* @dataProvider sampleProvider
* @runInSeparateProcess
* @requires function xdebug_get_headers
*/
public function testHeaders($path) {
$this->setSample($path);
$this->initFormat();
$this->assertContains(
'Content-Type: application/atom+xml; charset=' . $this->format->getCharset(),
xdebug_get_headers()
);
}
/**
* @dataProvider sampleProvider
* @runInSeparateProcess
*/
public function testOutput($path) {
$this->setSample($path);
$this->initFormat();
$this->assertXmlStringEqualsXmlFile($this->sample->expected, $this->data);
}
////////////////////////////////////////////////////////////////////////////
public function sampleProvider() {
$samples = array();
foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
$samples[basename($path, '.json')] = array($path);
}
return $samples;
}
private function setSample($path) {
$data = json_decode(file_get_contents($path), true);
if (isset($data['meta']) && isset($data['items'])) {
if (!empty($data['server']))
$this->setServerVars($data['server']);
$items = array();
foreach($data['items'] as $item) {
$items[] = new \FeedItem($item);
}
$this->sample = (object)array(
'meta' => $data['meta'],
'items' => $items,
'expected' => self::PATH_EXPECTED . basename($path, '.json') . '.xml'
);
} else {
$this->fail('invalid test sample: ' . basename($path, '.json'));
}
}
private function setServerVars($list) {
$_SERVER = array_merge($_SERVER, $list);
}
private function initFormat() {
$this->format = \Format::create('Atom');
$this->format->setItems($this->sample->items);
$this->format->setExtraInfos($this->sample->meta);
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
$this->data = $this->getActualOutput($this->format->display());
$this->assertNotFalse(simplexml_load_string($this->data));
ob_clean();
}
}

View File

@@ -98,6 +98,19 @@ class BridgeImplementationTest extends TestCase {
if (isset($options['required'])) {
$this->assertInternalType('bool', $options['required'], $field . ': invalid required');
if($options['required'] === true && isset($options['type'])) {
switch($options['type']) {
case 'list':
case 'checkbox':
$this->assertArrayNotHasKey(
'required',
$options,
$field . ': "required" attribute not supported for ' . $options['type']
);
break;
}
}
}
if (isset($options['title'])) {

View File

@@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class CacheImplementationTest extends TestCase {
private $class;
/**
* @dataProvider dataCachesProvider
*/
public function testClassName($path) {
$this->setCache($path);
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
$this->assertStringEndsWith('Cache', $this->class, 'class name must end with "Cache"');
}
/**
* @dataProvider dataCachesProvider
*/
public function testClassType($path) {
$this->setCache($path);
$this->assertTrue(is_subclass_of($this->class, CacheInterface::class), 'class must be subclass of CacheInterface');
}
////////////////////////////////////////////////////////////////////////////
public function dataCachesProvider() {
$caches = array();
foreach (glob(PATH_LIB_CACHES . '*.php') as $path) {
$caches[basename($path, '.php')] = array($path);
}
return $caches;
}
private function setCache($path) {
require_once $path;
$this->class = basename($path, '.php');
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
}
}

View File

@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class FormatImplementationTest extends TestCase {
private $class;
private $obj;
/**
* @dataProvider dataFormatsProvider
*/
public function testClassName($path) {
$this->setFormat($path);
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
$this->assertStringEndsWith('Format', $this->class, 'class name must end with "Format"');
}
/**
* @dataProvider dataFormatsProvider
*/
public function testClassType($path) {
$this->setFormat($path);
$this->assertInstanceOf(FormatInterface::class, $this->obj);
}
////////////////////////////////////////////////////////////////////////////
public function dataFormatsProvider() {
$formats = array();
foreach (glob(PATH_LIB_FORMATS . '*.php') as $path) {
$formats[basename($path, '.php')] = array($path);
}
return $formats;
}
private function setFormat($path) {
require_once $path;
$this->class = basename($path, '.php');
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
$this->obj = new $this->class();
}
}

90
tests/ListActionTest.php Normal file
View File

@@ -0,0 +1,90 @@
<?php
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class ListActionTest extends TestCase {
private $action;
private $data;
/**
* @runInSeparateProcess
* @requires function xdebug_get_headers
*/
public function testHeaders() {
$this->initAction();
$this->assertContains(
'Content-Type: application/json',
xdebug_get_headers()
);
}
/**
* @runInSeparateProcess
*/
public function testOutput() {
$this->initAction();
$items = json_decode($this->data, true);
$this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
$this->assertArrayHasKey('total', $items, 'Missing "total" parameter');
$this->assertInternalType('int', $items['total'], 'Invalid type');
$this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array');
$this->assertEquals(
$items['total'],
count($items['bridges']),
'Item count doesn\'t match'
);
$this->assertEquals(
count(Bridge::getBridgeNames()),
count($items['bridges']),
'Number of bridges doesn\'t match'
);
$expectedKeys = array(
'status',
'uri',
'name',
'icon',
'parameters',
'maintainer',
'description'
);
$allowedStatus = array(
'active',
'inactive'
);
foreach($items['bridges'] as $bridge) {
foreach($expectedKeys as $key) {
$this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"');
}
$this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
}
}
////////////////////////////////////////////////////////////////////////////
private function initAction() {
$actionFac = new ActionFactory();
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
$this->action = $actionFac->create('list');
$this->action->setUserData(array()); /* no user data required */
ob_start();
$this->action->execute();
$this->data = ob_get_contents();
ob_clean();
ob_end_flush();
}
}

90
tests/MrssFormatTest.php Normal file
View File

@@ -0,0 +1,90 @@
<?php
/**
* MrssFormat - RSS 2.0 + Media RSS
* http://www.rssboard.org/rss-specification
* http://www.rssboard.org/media-rss
*/
require_once __DIR__ . '/../lib/rssbridge.php';
use PHPUnit\Framework\TestCase;
class MrssFormatTest extends TestCase {
const PATH_SAMPLES = __DIR__ . '/samples/';
const PATH_EXPECTED = __DIR__ . '/samples/expectedMrssFormat/';
private $sample;
private $format;
private $data;
/**
* @dataProvider sampleProvider
* @runInSeparateProcess
* @requires function xdebug_get_headers
*/
public function testHeaders($path) {
$this->setSample($path);
$this->initFormat();
$this->assertContains(
'Content-Type: application/rss+xml; charset=' . $this->format->getCharset(),
xdebug_get_headers()
);
}
/**
* @dataProvider sampleProvider
* @runInSeparateProcess
*/
public function testOutput($path) {
$this->setSample($path);
$this->initFormat();
$this->assertXmlStringEqualsXmlFile($this->sample->expected, $this->data);
}
////////////////////////////////////////////////////////////////////////////
public function sampleProvider() {
$samples = array();
foreach (glob(self::PATH_SAMPLES . '*.json') as $path) {
$samples[basename($path, '.json')] = array($path);
}
return $samples;
}
private function setSample($path) {
$data = json_decode(file_get_contents($path), true);
if (isset($data['meta']) && isset($data['items'])) {
if (!empty($data['server']))
$this->setServerVars($data['server']);
$items = array();
foreach($data['items'] as $item) {
$items[] = new \FeedItem($item);
}
$this->sample = (object)array(
'meta' => $data['meta'],
'items' => $items,
'expected' => self::PATH_EXPECTED . basename($path, '.json') . '.xml'
);
} else {
$this->fail('invalid test sample: ' . basename($path, '.json'));
}
}
private function setServerVars($list) {
$_SERVER = array_merge($_SERVER, $list);
}
private function initFormat() {
$this->format = \Format::create('Mrss');
$this->format->setItems($this->sample->items);
$this->format->setExtraInfos($this->sample->meta);
$this->format->setLastModified(strtotime('2000-01-01 12:00:00 UTC'));
$this->data = $this->getActualOutput($this->format->display());
$this->assertNotFalse(simplexml_load_string($this->data));
ob_clean();
}
}

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">Sample feed with common data</title>
<id>https://example.com/feed?type=common&amp;items=4</id>
<icon>https://example.com/logo.png</icon>
<logo>https://example.com/logo.png</logo>
<updated>2000-01-01T12:00:00+00:00</updated>
<author>
<name>RSS-Bridge</name>
</author>
<link href="https://example.com/blog/" rel="alternate" type="text/html"/>
<link href="https://example.com/feed?type=common&amp;items=4" rel="self" type="application/atom+xml"/>
<entry>
<title type="html">Test Entry</title>
<published>2018-12-01T12:00:00+00:00</published>
<updated>2018-12-01T12:00:00+00:00</updated>
<id>http://example.com/blog/test-entry</id>
<link href="http://example.com/blog/test-entry" rel="alternate" type="text/html"/>
<author>
<name>fulmeek</name>
</author>
<content type="html">Hello world, this is a test entry.</content>
<category term="test"/>
<category term="Hello World"/>
<category term="example"/>
</entry>
<entry>
<title type="html">Announcing JSON Feed</title>
<published>2017-05-17T13:02:12+00:00</published>
<updated>2017-05-17T13:02:12+00:00</updated>
<id>https://jsonfeed.org/2017/05/17/announcing_json_feed</id>
<link href="https://jsonfeed.org/2017/05/17/announcing_json_feed" rel="alternate" type="text/html"/>
<author>
<name>Brent Simmons and Manton Reece</name>
</author>
<content type="html">&lt;p&gt;We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and its less prone to bugs.&lt;/p&gt;
&lt;p&gt;So we developed JSON Feed, a format similar to &lt;a href="http://cyber.harvard.edu/rss/rss.html"&gt;RSS&lt;/a&gt; and &lt;a href="https://tools.ietf.org/html/rfc4287"&gt;Atom&lt;/a&gt; but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jsonfeed.org/version/1"&gt;See the spec&lt;/a&gt;. Its at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;We have a &lt;a href="https://github.com/manton/jsonfeed-wp"&gt;WordPress plugin&lt;/a&gt; and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, well update the &lt;a href="https://jsonfeed.org/code"&gt;code&lt;/a&gt; page.&lt;/p&gt;
&lt;p&gt;See &lt;a href="https://jsonfeed.org/mappingrssandatom"&gt;Mapping RSS and Atom to JSON Feed&lt;/a&gt; for more on the similarities between the formats.&lt;/p&gt;
&lt;p&gt;This website — the Markdown files and supporting resources — &lt;a href="https://github.com/brentsimmons/JSONFeed"&gt;is up on GitHub&lt;/a&gt;, and youre welcome to comment there.&lt;/p&gt;
&lt;p&gt;This website is also a blog, and you can subscribe to the &lt;a href="https://jsonfeed.org/xml/rss.xml"&gt;RSS feed&lt;/a&gt; or the &lt;a href="https://jsonfeed.org/feed.json"&gt;JSON feed&lt;/a&gt; (if your reader supports it).&lt;/p&gt;
&lt;p&gt;We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the &lt;a href="https://jsonfeed.org/version/1"&gt;spec&lt;/a&gt;. But — most importantly — &lt;a href="http://furbo.org/"&gt;Craig Hockenberry&lt;/a&gt; spent a little time making it look pretty. :)&lt;/p&gt;</content>
</entry>
<entry>
<title type="html">Atom draft-07 snapshot</title>
<published>2005-07-31T12:29:29+00:00</published>
<updated>2005-07-31T12:29:29+00:00</updated>
<id>urn:sha1:dd6b6c920d3b340ab9e07faf6682f2a7c4f70134</id>
<link href="http://example.org/2005/04/02/atom" rel="alternate" type="text/html"/>
<author>
<name>Mark Pilgrim</name>
</author>
<content type="html">&lt;p&gt;&lt;i&gt;[Update: The Atom draft is finished.]&lt;/i&gt;&lt;/p&gt;</content>
<link rel="enclosure" type="audio/mpeg" href="http://example.org/audio/ph34r_my_podcast.mp3"/>
</entry>
<entry>
<title type="html">Star City</title>
<published>2003-06-03T09:39:21+00:00</published>
<updated>2003-06-03T09:39:21+00:00</updated>
<id>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</id>
<link href="http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp" rel="alternate" type="text/html"/>
<content type="html">How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</content>
</entry>
</feed>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">Sample feed with minimum data</title>
<id>https://example.com/feed</id>
<icon>https://github.com/favicon.ico</icon>
<logo>https://github.com/favicon.ico</logo>
<updated>2000-01-01T12:00:00+00:00</updated>
<author>
<name>RSS-Bridge</name>
</author>
<link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
</feed>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">Sample feed with minimum data</title>
<id>https://example.com/feed</id>
<icon>https://github.com/favicon.ico</icon>
<logo>https://github.com/favicon.ico</logo>
<updated>2000-01-01T12:00:00+00:00</updated>
<author>
<name>RSS-Bridge</name>
</author>
<link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
<entry>
<title type="html">Sample Item #1</title>
<published>2000-01-01T12:00:00+00:00</published>
<updated>2000-01-01T12:00:00+00:00</updated>
<id>urn:sha1:29f59918d266c56a935da13e4122b524298e5a39</id>
<content type="html">Sample Item #1</content>
</entry>
<entry>
<title type="html">Sample Item #2</title>
<published>2000-01-01T12:00:00+00:00</published>
<updated>2000-01-01T12:00:00+00:00</updated>
<id>urn:sha1:edf358cad1a7ae255d6bc97640dd9d27738f1b7b</id>
<content type="html">Sample Item #2</content>
</entry>
</feed>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">Sample microblog feed</title>
<id>https://example.com/feed</id>
<icon>https://example.com/logo.png</icon>
<logo>https://example.com/logo.png</logo>
<updated>2000-01-01T12:00:00+00:00</updated>
<author>
<name>RSS-Bridge</name>
</author>
<link href="https://example.com/blog/" rel="alternate" type="text/html"/>
<link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
<entry>
<title type="html">Oh 😲 I found three monkeys 🙈🙉🙊</title>
<published>2018-10-07T16:53:03+00:00</published>
<updated>2018-10-07T16:53:03+00:00</updated>
<id>urn:sha1:1918f084648b82057c1dd3faa3d091da82a6fac2</id>
<content type="html">Oh 😲 I found three monkeys 🙈🙉🙊</content>
</entry>
<entry>
<title type="html">Something happened</title>
<published>2018-10-07T16:38:17+00:00</published>
<updated>2018-10-07T16:38:17+00:00</updated>
<id>urn:sha1:e62189168a06dfa74f61c621c79c33c4c8517e1f</id>
<content type="html">Something happened</content>
</entry>
</feed>

View File

@@ -26,7 +26,7 @@
},
"content_html": "<p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and its less prone to bugs.</p>\n\n<p>So we developed JSON Feed, a format similar to <a href=\"http://cyber.harvard.edu/rss/rss.html\">RSS</a> and <a href=\"https://tools.ietf.org/html/rfc4287\">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>\n\n<p><a href=\"https://jsonfeed.org/version/1\">See the spec</a>. Its at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>\n\n<h4>Notes</h4>\n\n<p>We have a <a href=\"https://github.com/manton/jsonfeed-wp\">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, well update the <a href=\"https://jsonfeed.org/code\">code</a> page.</p>\n\n<p>See <a href=\"https://jsonfeed.org/mappingrssandatom\">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>\n\n<p>This website — the Markdown files and supporting resources — <a href=\"https://github.com/brentsimmons/JSONFeed\">is up on GitHub</a>, and youre welcome to comment there.</p>\n\n<p>This website is also a blog, and you can subscribe to the <a href=\"https://jsonfeed.org/xml/rss.xml\">RSS feed</a> or the <a href=\"https://jsonfeed.org/feed.json\">JSON feed</a> (if your reader supports it).</p>\n\n<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href=\"https://jsonfeed.org/version/1\">spec</a>. But — most importantly — <a href=\"http://furbo.org/\">Craig Hockenberry</a> spent a little time making it look pretty. :)</p>"
},{
"id": "http://example.org/2005/04/02/atom",
"id": "dd6b6c920d3b340ab9e07faf6682f2a7c4f70134",
"url": "http://example.org/2005/04/02/atom",
"title": "Atom draft-07 snapshot",
"date_modified": "2005-07-31T12:29:29+00:00",

View File

@@ -0,0 +1,64 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>Sample feed with common data</title>
<link>https://example.com/blog/</link>
<description>Sample feed with common data</description>
<image>
<url>https://example.com/logo.png</url>
<title>Sample feed with common data</title>
<link>https://example.com/blog/</link>
</image>
<atom:link href="https://example.com/blog/" rel="alternate" type="text/html"/>
<atom:link href="https://example.com/feed?type=common&amp;items=4" rel="self" type="application/atom+xml"/>
<item>
<title>Test Entry</title>
<link>http://example.com/blog/test-entry</link>
<guid isPermaLink="true">http://example.com/blog/test-entry</guid>
<pubDate>Sat, 01 Dec 2018 12:00:00 +0000</pubDate>
<description>Hello world, this is a test entry.</description>
<category>test</category>
<category>Hello World</category>
<category>example</category>
</item>
<item>
<title>Announcing JSON Feed</title>
<link>https://jsonfeed.org/2017/05/17/announcing_json_feed</link>
<guid isPermaLink="true">https://jsonfeed.org/2017/05/17/announcing_json_feed</guid>
<pubDate>Wed, 17 May 2017 13:02:12 +0000</pubDate>
<description>&lt;p&gt;We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and its less prone to bugs.&lt;/p&gt;
&lt;p&gt;So we developed JSON Feed, a format similar to &lt;a href="http://cyber.harvard.edu/rss/rss.html"&gt;RSS&lt;/a&gt; and &lt;a href="https://tools.ietf.org/html/rfc4287"&gt;Atom&lt;/a&gt; but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jsonfeed.org/version/1"&gt;See the spec&lt;/a&gt;. Its at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;We have a &lt;a href="https://github.com/manton/jsonfeed-wp"&gt;WordPress plugin&lt;/a&gt; and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, well update the &lt;a href="https://jsonfeed.org/code"&gt;code&lt;/a&gt; page.&lt;/p&gt;
&lt;p&gt;See &lt;a href="https://jsonfeed.org/mappingrssandatom"&gt;Mapping RSS and Atom to JSON Feed&lt;/a&gt; for more on the similarities between the formats.&lt;/p&gt;
&lt;p&gt;This website — the Markdown files and supporting resources — &lt;a href="https://github.com/brentsimmons/JSONFeed"&gt;is up on GitHub&lt;/a&gt;, and youre welcome to comment there.&lt;/p&gt;
&lt;p&gt;This website is also a blog, and you can subscribe to the &lt;a href="https://jsonfeed.org/xml/rss.xml"&gt;RSS feed&lt;/a&gt; or the &lt;a href="https://jsonfeed.org/feed.json"&gt;JSON feed&lt;/a&gt; (if your reader supports it).&lt;/p&gt;
&lt;p&gt;We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the &lt;a href="https://jsonfeed.org/version/1"&gt;spec&lt;/a&gt;. But — most importantly — &lt;a href="http://furbo.org/"&gt;Craig Hockenberry&lt;/a&gt; spent a little time making it look pretty. :)&lt;/p&gt;</description>
</item>
<item>
<title>Atom draft-07 snapshot</title>
<link>http://example.org/2005/04/02/atom</link>
<guid isPermaLink="false">dd6b6c920d3b340ab9e07faf6682f2a7c4f70134</guid>
<pubDate>Sun, 31 Jul 2005 12:29:29 +0000</pubDate>
<description>&lt;p&gt;&lt;i&gt;[Update: The Atom draft is finished.]&lt;/i&gt;&lt;/p&gt;</description>
<media:content url="http://example.org/audio/ph34r_my_podcast.mp3" type="audio/mpeg"/>
</item>
<item>
<title>Star City</title>
<link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
<guid isPermaLink="true">http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</guid>
<pubDate>Tue, 03 Jun 2003 09:39:21 +0000</pubDate>
<description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</description>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>Sample feed with minimum data</title>
<link>https://github.com/RSS-Bridge/rss-bridge/</link>
<description>Sample feed with minimum data</description>
<atom:link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
<atom:link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
</channel>
</rss>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>Sample feed with minimum data</title>
<link>https://github.com/RSS-Bridge/rss-bridge/</link>
<description>Sample feed with minimum data</description>
<atom:link href="https://github.com/RSS-Bridge/rss-bridge/" rel="alternate" type="text/html"/>
<atom:link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
<item>
<title>Sample Item #1</title>
<guid isPermaLink="false">29f59918d266c56a935da13e4122b524298e5a39</guid>
</item>
<item>
<title>Sample Item #2</title>
<guid isPermaLink="false">edf358cad1a7ae255d6bc97640dd9d27738f1b7b</guid>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>Sample microblog feed</title>
<link>https://example.com/blog/</link>
<description>Sample microblog feed</description>
<image>
<url>https://example.com/logo.png</url>
<title>Sample microblog feed</title>
<link>https://example.com/blog/</link>
</image>
<atom:link href="https://example.com/blog/" rel="alternate" type="text/html"/>
<atom:link href="https://example.com/feed" rel="self" type="application/atom+xml"/>
<item>
<guid isPermaLink="false">1918f084648b82057c1dd3faa3d091da82a6fac2</guid>
<pubDate>Sun, 07 Oct 2018 16:53:03 +0000</pubDate>
<description>Oh 😲 I found three monkeys 🙈🙉🙊</description>
</item>
<item>
<guid isPermaLink="false">e62189168a06dfa74f61c621c79c33c4c8517e1f</guid>
<pubDate>Sun, 07 Oct 2018 16:38:17 +0000</pubDate>
<description>Something happened</description>
</item>
</channel>
</rss>

View File

@@ -25,6 +25,7 @@
"content": "<p>We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and its less prone to bugs.</p>\n\n<p>So we developed JSON Feed, a format similar to <a href=\"http://cyber.harvard.edu/rss/rss.html\">RSS</a> and <a href=\"https://tools.ietf.org/html/rfc4287\">Atom</a> but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.</p>\n\n<p><a href=\"https://jsonfeed.org/version/1\">See the spec</a>. Its at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.</p>\n\n<h4>Notes</h4>\n\n<p>We have a <a href=\"https://github.com/manton/jsonfeed-wp\">WordPress plugin</a> and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, well update the <a href=\"https://jsonfeed.org/code\">code</a> page.</p>\n\n<p>See <a href=\"https://jsonfeed.org/mappingrssandatom\">Mapping RSS and Atom to JSON Feed</a> for more on the similarities between the formats.</p>\n\n<p>This website — the Markdown files and supporting resources — <a href=\"https://github.com/brentsimmons/JSONFeed\">is up on GitHub</a>, and youre welcome to comment there.</p>\n\n<p>This website is also a blog, and you can subscribe to the <a href=\"https://jsonfeed.org/xml/rss.xml\">RSS feed</a> or the <a href=\"https://jsonfeed.org/feed.json\">JSON feed</a> (if your reader supports it).</p>\n\n<p>We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the <a href=\"https://jsonfeed.org/version/1\">spec</a>. But — most importantly — <a href=\"http://furbo.org/\">Craig Hockenberry</a> spent a little time making it look pretty. :)</p>"
},{
"uri": "http://example.org/2005/04/02/atom",
"uid": "tag:example.org,2003:3.2397",
"title": "Atom draft-07 snapshot",
"timestamp": 1122812969,
"author": "Mark Pilgrim",

File diff suppressed because it is too large Load Diff