core: Implement action factory (#1002)
This commit is contained in:
parent
69cb65c1af
commit
51ee541d5a
12 changed files with 705 additions and 279 deletions
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('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
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
286
index.php
286
index.php
|
@ -51,287 +51,15 @@ $whitelist_default = array(
|
||||||
try {
|
try {
|
||||||
|
|
||||||
Bridge::setWhitelist($whitelist_default);
|
Bridge::setWhitelist($whitelist_default);
|
||||||
|
$actionFac = new \ActionFactory();
|
||||||
|
$actionFac->setWorkingDir(PATH_LIB_ACTIONS);
|
||||||
|
|
||||||
|
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);
|
$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));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo BridgeList::create($showInactive);
|
echo BridgeList::create($showInactive);
|
||||||
}
|
}
|
||||||
} catch(\Exception $e) {
|
} 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();
|
||||||
|
}
|
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);
|
||||||
|
}
|
|
@ -29,6 +29,9 @@ define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
|
||||||
/** Path to the caches library */
|
/** Path to the caches library */
|
||||||
define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
|
define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
|
||||||
|
|
||||||
|
/** Path to the actions library */
|
||||||
|
define('PATH_LIB_ACTIONS', __DIR__ . '/../actions/');
|
||||||
|
|
||||||
/** Path to the cache folder */
|
/** Path to the cache folder */
|
||||||
define('PATH_CACHE', __DIR__ . '/../cache/');
|
define('PATH_CACHE', __DIR__ . '/../cache/');
|
||||||
|
|
||||||
|
@ -39,11 +42,13 @@ define('WHITELIST', __DIR__ . '/../whitelist.txt');
|
||||||
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
|
define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
|
||||||
|
|
||||||
// Interfaces
|
// Interfaces
|
||||||
|
require_once PATH_LIB . 'ActionInterface.php';
|
||||||
require_once PATH_LIB . 'BridgeInterface.php';
|
require_once PATH_LIB . 'BridgeInterface.php';
|
||||||
require_once PATH_LIB . 'CacheInterface.php';
|
require_once PATH_LIB . 'CacheInterface.php';
|
||||||
require_once PATH_LIB . 'FormatInterface.php';
|
require_once PATH_LIB . 'FormatInterface.php';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
|
require_once PATH_LIB . 'FactoryAbstract.php';
|
||||||
require_once PATH_LIB . 'FeedItem.php';
|
require_once PATH_LIB . 'FeedItem.php';
|
||||||
require_once PATH_LIB . 'Debug.php';
|
require_once PATH_LIB . 'Debug.php';
|
||||||
require_once PATH_LIB . 'Exceptions.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 . 'BridgeCard.php';
|
||||||
require_once PATH_LIB . 'BridgeList.php';
|
require_once PATH_LIB . 'BridgeList.php';
|
||||||
require_once PATH_LIB . 'ParameterValidator.php';
|
require_once PATH_LIB . 'ParameterValidator.php';
|
||||||
|
require_once PATH_LIB . 'ActionFactory.php';
|
||||||
|
require_once PATH_LIB . 'ActionAbstract.php';
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
require_once PATH_LIB . 'html.php';
|
require_once PATH_LIB . 'html.php';
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
<testsuite name="formats">
|
<testsuite name="formats">
|
||||||
<directory suffix="FormatTest.php">tests</directory>
|
<directory suffix="FormatTest.php">tests</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
<testsuite name="actions">
|
||||||
|
<directory suffix="ActionTest.php">tests</directory>
|
||||||
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
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();
|
||||||
|
}
|
||||||
|
}
|
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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue