diff --git a/actions/DetectAction.php b/actions/DetectAction.php
new file mode 100644
index 00000000..2ad79a27
--- /dev/null
+++ b/actions/DetectAction.php
@@ -0,0 +1,50 @@
+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);
+ }
+}
diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php
new file mode 100644
index 00000000..6b599329
--- /dev/null
+++ b/actions/DisplayAction.php
@@ -0,0 +1,234 @@
+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));
+ }
+ }
+}
diff --git a/actions/ListAction.php b/actions/ListAction.php
new file mode 100644
index 00000000..03e06119
--- /dev/null
+++ b/actions/ListAction.php
@@ -0,0 +1,53 @@
+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);
+ }
+}
diff --git a/index.php b/index.php
index a95302a0..819b5a52 100644
--- a/index.php
+++ b/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) {
diff --git a/lib/ActionAbstract.php b/lib/ActionAbstract.php
new file mode 100644
index 00000000..b925d609
--- /dev/null
+++ b/lib/ActionAbstract.php
@@ -0,0 +1,33 @@
+userData = $userData;
+ }
+}
diff --git a/lib/ActionFactory.php b/lib/ActionFactory.php
new file mode 100644
index 00000000..8146e542
--- /dev/null
+++ b/lib/ActionFactory.php
@@ -0,0 +1,65 @@
+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';
+ }
+}
diff --git a/lib/ActionInterface.php b/lib/ActionInterface.php
new file mode 100644
index 00000000..c38d057a
--- /dev/null
+++ b/lib/ActionInterface.php
@@ -0,0 +1,34 @@
+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);
+}
diff --git a/lib/rssbridge.php b/lib/rssbridge.php
index bc8c8d04..5a523588 100644
--- a/lib/rssbridge.php
+++ b/lib/rssbridge.php
@@ -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';
diff --git a/phpunit.xml b/phpunit.xml
index 4fe1ae0e..62937c47 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -14,6 +14,9 @@
tests
+
+ tests
+
diff --git a/tests/ActionImplementationTest.php b/tests/ActionImplementationTest.php
new file mode 100644
index 00000000..554432f3
--- /dev/null
+++ b/tests/ActionImplementationTest.php
@@ -0,0 +1,59 @@
+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();
+ }
+}
diff --git a/tests/ListActionTest.php b/tests/ListActionTest.php
new file mode 100644
index 00000000..7f625882
--- /dev/null
+++ b/tests/ListActionTest.php
@@ -0,0 +1,90 @@
+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();
+ }
+}