diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbea7a9d5..a06b0c6d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,22 +41,24 @@ The following is an overview of the source code directory structure: settings for the database, SSO setup (if any), e-mail, hyperlinks, branding, etc. + controller_config.php + Controller configuration. Here we map request targets + onto controllers. + engine_config.php Model configuration. This maps the model interfaces - onto the concrete implementations. + onto concrete implementations. ui_config.php - Controller and navigation configuration. Controller - configuration maps request targets onto controllers; - navigation configuration defines menu items, access - controls, and implementations. + User interface configuration. This defines menu items, + access controls, and implementations. controllers/ Controllers are responsible for processing requests that are received by the application. Controllers are instantiated and invoked by the Dispatcher, whose operation is specified via metadata in - config/ui_config.php. + config/controller_config.php. css/ CSS assets. These files are automatically whitespace diff --git a/config/controller_config.php b/config/controller_config.php new file mode 100644 index 000000000..e7ae66ee4 --- /dev/null +++ b/config/controller_config.php @@ -0,0 +1,19 @@ + ZK\UI\UIController::class, + 'addexp' => ZK\Controllers\ExportAdd::class, + 'export' => ZK\Controllers\ExportPlaylist::class, + 'afile' => ZK\Controllers\ExportAfile::class, + 'opensearch' => ZK\Controllers\OpenSearch::class, + 'daily' => ZK\Controllers\RunDaily::class, + 'print' => ZK\Controllers\PrintTags::class, + 'rss' => ZK\Controllers\RSS::class, + 'api' => ZK\Controllers\API::class, + 'sso' => ZK\Controllers\SSOLogin::class, + 'push' => ZK\Controllers\PushServer::class, +]; diff --git a/config/ui_config.php b/config/ui_config.php index 403780361..a9c99b932 100644 --- a/config/ui_config.php +++ b/config/ui_config.php @@ -1,10 +1,9 @@ ZK\UI\Main::class, - 'addexp' => ZK\Controllers\ExportAdd::class, - 'export' => ZK\Controllers\ExportPlaylist::class, - 'afile' => ZK\Controllers\ExportAfile::class, - 'opensearch' => ZK\Controllers\OpenSearch::class, - 'daily' => ZK\Controllers\RunDaily::class, - 'print' => ZK\Controllers\PrintTags::class, - 'rss' => ZK\Controllers\RSS::class, - 'api' => ZK\Controllers\API::class, - 'sso' => ZK\Controllers\SSOLogin::class, - 'push' => ZK\Controllers\PushServer::class, -]; diff --git a/controllers/API.php b/controllers/API.php index f7a5ea0b9..eb09c0aec 100644 --- a/controllers/API.php +++ b/controllers/API.php @@ -360,7 +360,7 @@ class API extends CommandTarget implements IController { private $limit; private $serializer; - public function processRequest($dispatcher) { + public function processRequest() { $wantXml = $_REQUEST["xml"] || substr($_SERVER["HTTP_ACCEPT"], 0, 8) == "text/xml"; $this->serializer = $wantXml?new XMLSerializer():new JSONSerializer(); diff --git a/controllers/Dispatcher.php b/controllers/Dispatcher.php index 966f7da6e..c413ace96 100644 --- a/controllers/Dispatcher.php +++ b/controllers/Dispatcher.php @@ -38,79 +38,16 @@ use ZK\Engine\Engine; class Dispatcher { - private $menu; private $controllers; public function __construct() { - // UI configuration file - $this->menu = new Config('ui_config', 'menu'); - $customMenu = Engine::param('custom_menu'); - if($customMenu) - $this->menu->merge($customMenu); - // Controllers - $this->controllers = new Config('ui_config', 'controllers'); + $this->controllers = new Config('controller_config', 'controllers'); $customControllers = Engine::param('custom_controllers'); if($customControllers) $this->controllers->merge($customControllers); } - /** - * return menu entry that matches the specified action - */ - public function match($action) { - return $this->menu->iterate(function($entry) use($action) { - if($entry[1] == $action || substr($entry[1], -1) == '%' && - substr($entry[1], 0, -1) == substr($action, 0, strlen($entry[1])-1)) - return $entry; - }); - } - - /** - * dispatch action to the appropriate menu item/command target - */ - public function dispatch($action, $subaction, $session) { - $entry = $this->match($action); - - // If no action was selected or if action is unauthorized, - // default to the first one - if(!$entry || !$session->isAuth($entry[0])) - $entry = $this->menu->default(); - - $handler = new $entry[3](); - if($handler instanceof CommandTarget) - $handler->process($action, $subaction, $session); - } - - /** - * indicate whether the specified action is authorized for the session - */ - public function isActionAuth($action, $session) { - $entry = $this->match($action); - return !$entry || $session->isAuth($entry[0]); - } - - /** - * compose the menu for the specified session - */ - public function composeMenu($action, $session) { - $result = []; - $this->menu->iterate(function($entry) use(&$result, $action, $session) { - if($entry[2] && $session->isAuth($entry[0])) { - $baseAction = substr($entry[1], -1) == '%'? - substr($entry[1], 0, -1):$entry[1]; - $selected = $entry[1] == $action || - substr($entry[1], -1) == '%' && - substr($entry[1], 0, -1) == - substr($action, 0, strlen($entry[1]) - 1); - $result[] = [ 'action' => $baseAction, - 'label' => $entry[2], - 'selected' => $selected ]; - } - }); - return $result; - } - /** * dispatch request to the specified controller */ @@ -120,6 +57,6 @@ public function processRequest($controller="") { $this->controllers->default():$p; $impl = new $implClass(); if($impl instanceof IController) - $impl->processRequest($this); + $impl->processRequest(); } } diff --git a/controllers/ExportAdd.php b/controllers/ExportAdd.php index d3d641046..707a68694 100644 --- a/controllers/ExportAdd.php +++ b/controllers/ExportAdd.php @@ -28,7 +28,7 @@ use ZK\Engine\IChart; class ExportAdd implements IController { - public function processRequest($dispatcher) { + public function processRequest() { // Ensure there's a date $date = $_REQUEST["date"]; if(strlen($date) != 10 || diff --git a/controllers/ExportAfile.php b/controllers/ExportAfile.php index 1e75ab713..164dc5306 100644 --- a/controllers/ExportAfile.php +++ b/controllers/ExportAfile.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2019 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -29,7 +29,7 @@ use ZK\UI\AddManager; class ExportAfile implements IController { - public function processRequest($dispatcher) { + public function processRequest() { $userAgent = $_SERVER["HTTP_USER_AGENT"]; ?> diff --git a/controllers/ExportPlaylist.php b/controllers/ExportPlaylist.php index 786dcff6b..2d7ae0301 100644 --- a/controllers/ExportPlaylist.php +++ b/controllers/ExportPlaylist.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2018 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -48,7 +48,7 @@ class ExportPlaylist extends CommandTarget implements IController { private $time; private $records; - public function processRequest($dispatcher) { + public function processRequest() { // Ensure user has selected a playlist $playlist = intval($_REQUEST["playlist"]); if($playlist == 0) { diff --git a/controllers/IController.php b/controllers/IController.php index 707e805a9..4170e0d61 100644 --- a/controllers/IController.php +++ b/controllers/IController.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2018 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -25,5 +25,5 @@ namespace ZK\Controllers; interface IController { - public function processRequest($dispatcher); + public function processRequest(); } diff --git a/controllers/OpenSearch.php b/controllers/OpenSearch.php index f9bac3e1c..2503eb72a 100644 --- a/controllers/OpenSearch.php +++ b/controllers/OpenSearch.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2018 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -29,7 +29,7 @@ use ZK\UI\UICommon as UI; class OpenSearch implements IController { - public function processRequest($dispatcher) { + public function processRequest() { $baseURL = UI::getBaseURL(); $favicon = Engine::param('favicon', 'favicon.ico'); $banner = Engine::param("station")." ".Engine::param("application"); diff --git a/controllers/PrintTags.php b/controllers/PrintTags.php index 0755bdc96..89bde44c1 100644 --- a/controllers/PrintTags.php +++ b/controllers/PrintTags.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2018 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -48,7 +48,7 @@ class PrintTags implements IController { ], ]; - public function processRequest($dispatcher) { + public function processRequest() { header("Content-type: application/pdf"); $form = empty($_REQUEST["form"])?self::LABEL_FORM:$_REQUEST["form"]; diff --git a/controllers/PushServer.php b/controllers/PushServer.php index 2812f8ee1..8f8776ec2 100644 --- a/controllers/PushServer.php +++ b/controllers/PushServer.php @@ -218,7 +218,7 @@ public static function sendAsyncNotification($show = null, $spin = null) { socket_close($socket); } - public function processRequest($dispatcher) { + public function processRequest() { if(php_sapi_name() != "cli") { http_response_code(400); return; diff --git a/controllers/RSS.php b/controllers/RSS.php index 781cc5d51..7b67dda14 100644 --- a/controllers/RSS.php +++ b/controllers/RSS.php @@ -57,7 +57,7 @@ private static function htmlnumericentities($str) { // $str); } - public function processRequest($dispatcher) { + public function processRequest() { $this->session = Engine::session(); header("Content-type: text/xml"); diff --git a/controllers/RunDaily.php b/controllers/RunDaily.php index 057d7a9f3..1cefa6204 100644 --- a/controllers/RunDaily.php +++ b/controllers/RunDaily.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2018 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -34,7 +34,7 @@ class RunDaily implements IController { private $catCodes; - public function processRequest($dispatcher) { + public function processRequest() { if(php_sapi_name() != "cli") { http_response_code(400); return; diff --git a/controllers/SSOLogin.php b/controllers/SSOLogin.php index aae30d461..f1e9f8ead 100644 --- a/controllers/SSOLogin.php +++ b/controllers/SSOLogin.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2020 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -33,7 +33,7 @@ class SSOLogin implements IController { private $action; private $ssoOptions; - public function processRequest($dispatcher) { + public function processRequest() { $params = SSOCommon::zkQSParams(); $state = $params["state"]; if($state) { diff --git a/custom/KzsuUIController.php b/custom/KzsuUIController.php index ffe110112..21277ec33 100644 --- a/custom/KzsuUIController.php +++ b/custom/KzsuUIController.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2020 Jim Mason + * @copyright Copyright (C) 1997-2021 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -36,8 +36,8 @@ * The file is provided as an example of a custom UI controller; * it is not part of the basic functionality of Zookeeper Online. */ -class KzsuUIController extends Main { - protected function emitBodyHeader($dispatcher) { +class KzsuUIController extends UIController { + protected function emitBodyHeader() { $urls = Engine::param('urls'); $station_full = Engine::param('station_full'); ?> diff --git a/ui/Main.php b/ui/UIController.php similarity index 84% rename from ui/Main.php rename to ui/UIController.php index 9e2c3694b..d779ad112 100644 --- a/ui/Main.php +++ b/ui/UIController.php @@ -26,6 +26,7 @@ use ZK\Controllers\IController; use ZK\Controllers\SSOCommon; +use ZK\Engine\Config; use ZK\Engine\Engine; use ZK\Engine\IUser; use ZK\Engine\Session; @@ -34,31 +35,114 @@ use JSMin\JSMin; -class Main implements IController { +class MenuEntry { + private const FIELDS = [ 'access', 'action', 'label', 'implementation' ]; + + private $entry; + + public function __construct($entry) { + $this->entry = $entry; + } + + public function __get($var) { + $index = array_search($var, self::FIELDS); + return $index !== false?$this->entry[$index]:null; + } +} + +class UIController implements IController { protected $ssoUser; protected $dn; protected $session; + protected $menu; + + /** + * return menu item that matches the specified action + */ + protected function match($action) { + return $this->menu->iterate(function($entry) use($action) { + $item = new MenuEntry($entry); + $itemAction = $item->action; + if($itemAction == $action || substr($itemAction, -1) == '%' && + substr($itemAction, 0, -1) == substr($action, 0, strlen($itemAction)-1)) + return $item; + }); + } + + /** + * dispatch action to the appropriate menu item/command target + */ + public function dispatch($action, $subaction) { + $item = $this->match($action); - public function processRequest($dispatcher) { + // If no action was selected or if action is unauthorized, + // default to the first one + if(!$item || !$this->session->isAuth($item->access)) + $item = new MenuEntry($this->menu->default()); + $implClass = $item->implementation; + $menuItem = new $implClass(); + if($menuItem instanceof MenuItem) + $menuItem->process($action, $subaction, $this->session); + } + + /** + * indicate whether the specified action is authorized for the session + */ + public function isActionAuth($action) { + $item = $this->match($action); + return !$item || $this->session->isAuth($item->access); + } + + /** + * compose the menu for the current session + */ + public function composeMenu($action) { + $result = []; + $this->menu->iterate(function($entry) use(&$result, $action) { + $item = new MenuEntry($entry); + $itemAction = $item->action; + if($item->label && $this->session->isAuth($item->access)) { + $baseAction = substr($itemAction, -1) == '%'? + substr($itemAction, 0, -1):$itemAction; + $selected = $itemAction == $action || + substr($itemAction, -1) == '%' && + substr($itemAction, 0, -1) == + substr($action, 0, strlen($itemAction) - 1); + $result[] = [ 'action' => $baseAction, + 'label' => $item->label, + 'selected' => $selected ]; + } + }); + return $result; + } + + public function processRequest() { $this->session = Engine::session(); - $this->preProcessRequest($dispatcher); + + // UI configuration file + $this->menu = new Config('ui_config', 'menu'); + $customMenu = Engine::param('custom_menu'); + if($customMenu) + $this->menu->merge($customMenu); + + $this->preProcessRequest(); $isJson = substr($_SERVER["HTTP_ACCEPT"], 0, 16) === 'application/json'; if ($isJson) { $action = $_REQUEST["action"]; $subaction = $_REQUEST["subaction"]; - $dispatcher->dispatch($action, $subaction, $this->session); + $this->dispatch($action, $subaction); } else { $this->emitResponseHeader(); - $this->emitBody($dispatcher); + $this->emitBody(); } } - protected function preProcessRequest($dispatcher) { + protected function preProcessRequest() { // Validate the requested action is authorized - if(!empty($_REQUEST["session"]) && !empty($_REQUEST["action"]) && - !$dispatcher->isActionAuth($_REQUEST["action"], $this->session) && + if(!empty($_REQUEST["action"]) && + !$this->isActionAuth($_REQUEST["action"]) && $_REQUEST["action"] != "loginValidate" && $_REQUEST["action"] != "logout") { $_REQUEST["action"] = "invalidAction"; @@ -137,10 +221,10 @@ protected function emitResponseHeader() { ".Engine::param('application')."

\n"; echo " \n"; - $menu = $dispatcher->composeMenu($action, $this->session); + $menu = $this->composeMenu($action); foreach($menu as $item) { echo " " . "
\n"; } - protected function emitMain($dispatcher, $action, $subaction) { + protected function emitMain($action, $subaction) { switch($action) { case "login": $this->emitLogin(); @@ -197,12 +281,12 @@ protected function emitMain($dispatcher, $action, $subaction) { // fall through... default: // dispatch action - $dispatcher->dispatch($action, $subaction, $this->session); + $this->dispatch($action, $subaction); break; } } - protected function emitBodyHeader($dispatcher) { + protected function emitBodyHeader() { $urls = Engine::param('urls'); $station_full = Engine::param('station_full'); ?> @@ -217,19 +301,19 @@ protected function emitBodyHeader($dispatcher) {
emitBodyHeader($dispatcher); + $this->emitBodyHeader(); echo "
\n"; echo "
\n"; - $this->emitNavBar($dispatcher, $_REQUEST["action"]); + $this->emitNavBar($_REQUEST["action"]); echo "
\n"; echo "
\n"; - $this->emitMain($dispatcher, $_REQUEST["action"], $_REQUEST["subaction"]); + $this->emitMain($_REQUEST["action"], $_REQUEST["subaction"]); ?>