mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2025-08-24 09:02:59 +02:00
Compare commits
42 Commits
2019-01-13
...
2019-03-17
Author | SHA1 | Date | |
---|---|---|---|
|
ae2c35c18a | ||
|
5b80bcaa04 | ||
|
5ea985164e | ||
|
696afa96d3 | ||
|
326a707739 | ||
|
1ac66b3fdc | ||
|
f450b2e118 | ||
|
688c950916 | ||
|
9d85b951f7 | ||
|
dac685b887 | ||
|
d37f0c14a0 | ||
|
b96c25a3af | ||
|
dc1b1b13cc | ||
|
e3588f62bd | ||
|
958ba815c7 | ||
|
3d24596a52 | ||
|
f9ed934c8c | ||
|
777c204838 | ||
|
ae40f7b388 | ||
|
473a62ed44 | ||
|
4c58768d4d | ||
|
ca9c2abb60 | ||
|
556a417dd6 | ||
|
51ee541d5a | ||
|
69cb65c1af | ||
|
29b187fc12 | ||
|
80f6a8b3d4 | ||
|
32d4da8b76 | ||
|
0063d2c376 | ||
|
11a39af35c | ||
|
f65a4076ba | ||
|
25593d9c18 | ||
|
394149b114 | ||
|
a29512deee | ||
|
e0db349a57 | ||
|
d532d0e0c4 | ||
|
434c12672f | ||
|
ab2e566ee1 | ||
|
493e76e4b9 | ||
|
37d882a8d5 | ||
|
bcd7bccc46 | ||
|
2def7a04a3 |
64
README.md
64
README.md
@@ -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
50
actions/DetectAction.php
Normal 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
234
actions/DisplayAction.php
Normal 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
53
actions/ListAction.php
Normal 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);
|
||||
}
|
||||
}
|
@@ -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(
|
||||
|
@@ -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',
|
||||
|
@@ -19,7 +19,6 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
|
||||
'tld' => array(
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'Australia' => 'com.au',
|
||||
'Brazil' => 'com.br',
|
||||
|
62
bridges/AppleMusicBridge.php
Normal file
62
bridges/AppleMusicBridge.php
Normal 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'];
|
||||
});
|
||||
}
|
||||
}
|
72
bridges/AsahiShimbunAJWBridge.php
Normal file
72
bridges/AsahiShimbunAJWBridge.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
99
bridges/BakaUpdatesMangaReleasesBridge.php
Normal file
99
bridges/BakaUpdatesMangaReleasesBridge.php
Normal 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));
|
||||
}
|
||||
}
|
@@ -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(){
|
||||
|
@@ -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
134
bridges/CachetBridge.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
22
bridges/ComboiosDePortugalBridge.php
Normal file
22
bridges/ComboiosDePortugalBridge.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,7 +15,6 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
|
||||
'channel' => [
|
||||
'name' => 'Release Channel',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'defaultValue' => self::STABLE,
|
||||
'values' => [
|
||||
'Stable' => self::STABLE,
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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'
|
||||
)
|
||||
)
|
||||
|
@@ -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();
|
||||
|
@@ -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',
|
||||
|
@@ -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'
|
||||
|
@@ -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(
|
||||
|
@@ -10,7 +10,6 @@ class GBAtempBridge extends BridgeAbstract {
|
||||
'type' => array(
|
||||
'name' => 'Type',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'News' => 'N',
|
||||
'Reviews' => 'R',
|
||||
|
@@ -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
88
bridges/GlowficBridge.php
Normal 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();
|
||||
}
|
||||
}
|
@@ -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',
|
||||
|
@@ -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/',
|
||||
|
@@ -34,7 +34,6 @@ class JustETFBridge extends BridgeAbstract {
|
||||
'global' => array(
|
||||
'lang' => array(
|
||||
'name' => 'Language',
|
||||
'required' => true,
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Englisch' => 'en',
|
||||
|
@@ -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'
|
||||
)
|
||||
|
@@ -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;
|
||||
|
@@ -13,7 +13,6 @@ class MangareaderBridge extends BridgeAbstract {
|
||||
'category' => array(
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'All' => 'all',
|
||||
'Action' => 'action',
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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' => '',
|
||||
|
@@ -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',
|
||||
|
@@ -21,8 +21,7 @@ class NotAlwaysBridge extends BridgeAbstract {
|
||||
'Friendly' => 'friendly',
|
||||
'Hopeless' => 'hopeless',
|
||||
'Unfiltered' => 'unfiltered'
|
||||
),
|
||||
'required' => true
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
|
@@ -9,7 +9,6 @@ class OnVaSortirBridge extends FeedExpander {
|
||||
'city' => array(
|
||||
'name' => 'City',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'Agen' => 'Agen',
|
||||
'Ajaccio' => 'Ajaccio',
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -11,7 +11,6 @@ class OpenClassroomsBridge extends BridgeAbstract {
|
||||
'u' => array(
|
||||
'name' => 'Catégorie',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'Arts & Culture' => 'arts',
|
||||
'Code' => 'code',
|
||||
|
106
bridges/RoadAndTrackBridge.php
Normal file
106
bridges/RoadAndTrackBridge.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
@@ -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(
|
||||
|
@@ -14,7 +14,7 @@ class SoundCloudBridge extends BridgeAbstract {
|
||||
)
|
||||
));
|
||||
|
||||
const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
|
||||
const CLIENT_ID = 'W0KEWWILAjDiRH89X0jpwzuq6rbSK08R';
|
||||
|
||||
public function collectData(){
|
||||
|
||||
|
@@ -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
|
||||
|
31
bridges/VMwareSecurityBridge.php
Normal file
31
bridges/VMwareSecurityBridge.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
|
@@ -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(
|
||||
|
@@ -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
99
caches/SQLiteCache.php
Normal 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);
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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 = '<br>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
286
index.php
@@ -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
33
lib/ActionAbstract.php
Normal 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
65
lib/ActionFactory.php
Normal 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
34
lib/ActionInterface.php
Normal 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();
|
||||
}
|
@@ -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));
|
||||
|
||||
|
@@ -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="'
|
||||
|
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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
70
lib/FactoryAbstract.php
Normal 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);
|
||||
}
|
@@ -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') {
|
||||
|
@@ -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];
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
59
tests/ActionImplementationTest.php
Normal file
59
tests/ActionImplementationTest.php
Normal 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
89
tests/AtomFormatTest.php
Normal 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();
|
||||
}
|
||||
}
|
@@ -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'])) {
|
||||
|
42
tests/CacheImplementationTest.php
Normal file
42
tests/CacheImplementationTest.php
Normal 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');
|
||||
}
|
||||
}
|
44
tests/FormatImplementationTest.php
Normal file
44
tests/FormatImplementationTest.php
Normal 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
90
tests/ListActionTest.php
Normal 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
90
tests/MrssFormatTest.php
Normal 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();
|
||||
}
|
||||
}
|
77
tests/samples/expectedAtomFormat/feed.common.xml
Normal file
77
tests/samples/expectedAtomFormat/feed.common.xml
Normal 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&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&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"><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 it’s less prone to bugs.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<p><a href="https://jsonfeed.org/version/1">See the spec</a>. It’s 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>
|
||||
|
||||
<h4>Notes</h4>
|
||||
|
||||
<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, we’ll update the <a href="https://jsonfeed.org/code">code</a> page.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<p>This website — the Markdown files and supporting resources — <a href="https://github.com/brentsimmons/JSONFeed">is up on GitHub</a>, and you’re welcome to comment there.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<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></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"><p><i>[Update: The Atom draft is finished.]</i></p></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 <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</content>
|
||||
</entry>
|
||||
|
||||
</feed>
|
15
tests/samples/expectedAtomFormat/feed.empty.xml
Normal file
15
tests/samples/expectedAtomFormat/feed.empty.xml
Normal 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>
|
30
tests/samples/expectedAtomFormat/feed.emptyItems.xml
Normal file
30
tests/samples/expectedAtomFormat/feed.emptyItems.xml
Normal 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>
|
30
tests/samples/expectedAtomFormat/feed.microblog.xml
Normal file
30
tests/samples/expectedAtomFormat/feed.microblog.xml
Normal 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>
|
@@ -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 it’s 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>. It’s 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, we’ll 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 you’re 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",
|
||||
|
64
tests/samples/expectedMrssFormat/feed.common.xml
Normal file
64
tests/samples/expectedMrssFormat/feed.common.xml
Normal 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&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><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 it’s less prone to bugs.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<p><a href="https://jsonfeed.org/version/1">See the spec</a>. It’s 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>
|
||||
|
||||
<h4>Notes</h4>
|
||||
|
||||
<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, we’ll update the <a href="https://jsonfeed.org/code">code</a> page.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<p>This website — the Markdown files and supporting resources — <a href="https://github.com/brentsimmons/JSONFeed">is up on GitHub</a>, and you’re welcome to comment there.</p>
|
||||
|
||||
<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>
|
||||
|
||||
<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></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><p><i>[Update: The Atom draft is finished.]</i></p></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 <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
10
tests/samples/expectedMrssFormat/feed.empty.xml
Normal file
10
tests/samples/expectedMrssFormat/feed.empty.xml
Normal 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>
|
19
tests/samples/expectedMrssFormat/feed.emptyItems.xml
Normal file
19
tests/samples/expectedMrssFormat/feed.emptyItems.xml
Normal 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>
|
26
tests/samples/expectedMrssFormat/feed.microblog.xml
Normal file
26
tests/samples/expectedMrssFormat/feed.microblog.xml
Normal 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>
|
@@ -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 it’s 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>. It’s 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, we’ll 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 you’re 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",
|
||||
|
2000
vendor/simplehtmldom/simple_html_dom.php
vendored
2000
vendor/simplehtmldom/simple_html_dom.php
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user