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 {
|
||||
|
||||
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();
|
||||
}
|
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 */
|
||||
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';
|
||||
|
|
|
@ -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>
|
||||
|
|
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