From 0af9747c6c94fd456460d7716fd1867cd887a44a Mon Sep 17 00:00:00 2001 From: Philipp Holzer Date: Mon, 12 Aug 2019 18:13:58 +0200 Subject: [PATCH] Add Arguments & Modules class --- index.php | 6 +- src/App.php | 331 ++++++++------------------------ src/App/Arguments.php | 194 +++++++++++++++++++ src/App/Mode.php | 70 +++---- src/App/Module.php | 279 +++++++++++++++++++++++++++ src/Core/Installer.php | 4 +- src/Model/Profile.php | 23 --- src/Module/Install.php | 4 +- static/dependencies.config.php | 52 +++-- tests/src/App/ArgumentsTest.php | 242 +++++++++++++++++++++++ tests/src/App/ModeTest.php | 47 +++-- tests/src/App/ModuleTest.php | 189 ++++++++++++++++++ 12 files changed, 1083 insertions(+), 358 deletions(-) create mode 100644 src/App/Arguments.php create mode 100644 src/App/Module.php create mode 100644 tests/src/App/ArgumentsTest.php create mode 100644 tests/src/App/ModuleTest.php diff --git a/index.php b/index.php index 50e553bcd8..1e6439e038 100644 --- a/index.php +++ b/index.php @@ -18,4 +18,8 @@ $dice = (new Dice())->addRules(include __DIR__ . '/static/dependencies.config.ph $a = \Friendica\BaseObject::getApp(); -$a->runFrontend(); +$a->runFrontend( + $dice->create(\Friendica\App\Module::class), + $dice->create(\Friendica\App\Router::class), + $dice->create(\Friendica\Core\Config\PConfiguration::class) +); diff --git a/src/App.php b/src/App.php index bf9e1296ca..98605da2ac 100644 --- a/src/App.php +++ b/src/App.php @@ -8,9 +8,11 @@ use Detection\MobileDetect; use DOMDocument; use DOMXPath; use Exception; +use Friendica\App\Arguments; +use Friendica\App\Module; use Friendica\Core\Config\Cache\ConfigCache; use Friendica\Core\Config\Configuration; -use Friendica\Core\Hook; +use Friendica\Core\Config\PConfiguration; use Friendica\Core\L10n\L10n; use Friendica\Core\System; use Friendica\Core\Theme; @@ -18,6 +20,7 @@ use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Model\Profile; use Friendica\Module\Login; +use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Network\HTTPException; use Friendica\Util\BaseURL; use Friendica\Util\ConfigFileLoader; @@ -41,7 +44,7 @@ use Psr\Log\LoggerInterface; */ class App { - public $module_class = null; + /** @deprecated 2019.09 - use App\Arguments->getQueryString() */ public $query_string = ''; public $page = []; public $profile; @@ -53,9 +56,13 @@ class App public $page_contact; public $content; public $data = []; + /** @deprecated 2019.09 - use App\Arguments->getCommand() */ public $cmd = ''; + /** @deprecated 2019.09 - use App\Arguments->getArgv() or Arguments->get() */ public $argv; + /** @deprecated 2019.09 - use App\Arguments->getArgc() */ public $argc; + /** @deprecated 2019.09 - Use App\Module->getName() instead */ public $module; public $timezone; public $interactive = true; @@ -93,6 +100,8 @@ class App /** * @var bool true, if the call is from an backend node (f.e. worker) + * + * @deprecated 2019.09 - use App\Module->isBackend() instead */ private $isBackend; @@ -136,6 +145,16 @@ class App */ private $l10n; + /** + * @var App\Arguments + */ + private $args; + + /** + * @var App\Module + */ + private $moduleClass; + /** * Returns the current config cache of this node * @@ -197,6 +216,16 @@ class App return $this->mode; } + /** + * Returns the Database of the Application + * + * @return Database + */ + public function getDBA() + { + return $this->database; + } + /** * Register a stylesheet file path to be included in the tag of every page. * Inclusion is done in App->initHead(). @@ -247,7 +276,7 @@ class App * * @throws Exception if the Basepath is not usable */ - public function __construct(Database $database, Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n) + public function __construct(Database $database, Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args, Module $module) { $this->database = $database; $this->config = $config; @@ -257,6 +286,8 @@ class App $this->profiler = $profiler; $this->logger = $logger; $this->l10n = $l10n; + $this->args = $args; + $this->isBackend = $module->isBackend(); $this->profiler->reset(); @@ -273,59 +304,10 @@ class App . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR . $this->getBasePath()); - if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) { - $this->query_string = substr($_SERVER['QUERY_STRING'], 9); - } elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) { - $this->query_string = substr($_SERVER['QUERY_STRING'], 2); - } - - // removing trailing / - maybe a nginx problem - $this->query_string = ltrim($this->query_string, '/'); - - if (!empty($_GET['pagename'])) { - $this->cmd = trim($_GET['pagename'], '/\\'); - } elseif (!empty($_GET['q'])) { - $this->cmd = trim($_GET['q'], '/\\'); - } - - // fix query_string - $this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string); - - // unix style "homedir" - if (substr($this->cmd, 0, 1) === '~') { - $this->cmd = 'profile/' . substr($this->cmd, 1); - } - - // Diaspora style profile url - if (substr($this->cmd, 0, 2) === 'u/') { - $this->cmd = 'profile/' . substr($this->cmd, 2); - } - - /* - * Break the URL path into C style argc/argv style arguments for our - * modules. Given "http://example.com/module/arg1/arg2", $this->argc - * will be 3 (integer) and $this->argv will contain: - * [0] => 'module' - * [1] => 'arg1' - * [2] => 'arg2' - * - * - * There will always be one argument. If provided a naked domain - * URL, $this->argv[0] is set to "home". - */ - - $this->argv = explode('/', $this->cmd); - $this->argc = count($this->argv); - if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) { - $this->module = str_replace('.', '_', $this->argv[0]); - $this->module = str_replace('-', '_', $this->module); - } else { - $this->argc = 1; - $this->argv = ['home']; - $this->module = 'home'; - } - - $this->isBackend = $this->isBackend || $this->checkBackend($this->module); + $this->cmd = $args->getCommand(); + $this->argv = $args->getArgv(); + $this->argc = $args->getArgc(); + $this->query_string = $args->getQueryString(); // Detect mobile devices $mobile_detect = new MobileDetect(); @@ -346,10 +328,6 @@ class App */ public function reload() { - $this->isBackend = basename($_SERVER['PHP_SELF'], '.php') !== 'index'; - - $this->getMode()->determine($this->getBasePath()); - if ($this->getMode()->has(App\Mode::DBAVAILABLE)) { $this->profiler->update($this->config); @@ -399,6 +377,8 @@ class App * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN * * @return string Friendica server base URL + * + * @deprecated 2019.09 - use BaseUrl->get($ssl) instead */ public function getBaseURL($ssl = false) { @@ -453,9 +433,9 @@ class App * - Infinite scroll data * - head.tpl template */ - public function initHead() + private function initHead(App\Module $module, PConfiguration $pconfig) { - $interval = ((local_user()) ? Core\PConfig::get(local_user(), 'system', 'update_interval') : 40000); + $interval = ((local_user()) ? $pconfig->get(local_user(), 'system', 'update_interval') : 40000); // If the update is 'deactivated' set it to the highest integer number (~24 days) if ($interval < 0) { @@ -467,8 +447,8 @@ class App } // Default title: current module called - if (empty($this->page['title']) && $this->module) { - $this->page['title'] = ucfirst($this->module); + if (empty($this->page['title']) && $module->getName()) { + $this->page['title'] = ucfirst($module->getName()); } // Prepend the sitename to the page title @@ -520,7 +500,7 @@ class App * - Registered footer scripts (through App->registerFooterScript()) * - footer.tpl template */ - public function initFooter() + private function initFooter() { // If you're just visiting, let javascript take you home if (!empty($_SESSION['visitor_home'])) { @@ -595,58 +575,6 @@ class App $this->getBaseURL(); } - /** - * @brief Checks if the site is called via a backend process - * - * This isn't a perfect solution. But we need this check very early. - * So we cannot wait until the modules are loaded. - * - * @param string $module - * @return bool - */ - private function checkBackend($module) { - static $backends = [ - '_well_known', - 'api', - 'dfrn_notify', - 'feed', - 'fetch', - 'followers', - 'following', - 'hcard', - 'hostxrd', - 'inbox', - 'manifest', - 'nodeinfo', - 'noscrape', - 'objects', - 'outbox', - 'poco', - 'post', - 'proxy', - 'pubsub', - 'pubsubhubbub', - 'receive', - 'rsd_xml', - 'salmon', - 'statistics_json', - 'xrd', - ]; - - // Check if current module is in backend or backend flag is set - return in_array($module, $backends); - } - - /** - * Returns true, if the call is from a backend node (f.e. from a worker) - * - * @return bool Is it a known backend? - */ - public function isBackend() - { - return $this->isBackend; - } - /** * @brief Checks if the maximum number of database processes is reached * @@ -740,7 +668,7 @@ class App */ public function isMaxLoadReached() { - if ($this->isBackend()) { + if ($this->isBackend) { $process = 'backend'; $maxsysload = intval($this->config->get('system', 'maxloadavg')); if ($maxsysload < 1) { @@ -930,21 +858,13 @@ class App } /** - * Returns the value of a argv key - * TODO there are a lot of $a->argv usages in combination with defaults() which can be replaced with this method + * @deprecated use Arguments->get() instead * - * @param int $position the position of the argument - * @param mixed $default the default value if not found - * - * @return mixed returns the value of the argument + * @see App\Arguments */ public function getArgumentValue($position, $default = '') { - if (array_key_exists($position, $this->argv)) { - return $this->argv[$position]; - } - - return $default; + return $this->args->get($position, $default); } /** @@ -973,9 +893,13 @@ class App * request and a representation of the response. * * This probably should change to limit the size of this monster method. + * + * @param App\Module $module The determined module */ - public function runFrontend() + public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig) { + $moduleName = $module->getName(); + try { // Missing DB connection: ERROR if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) { @@ -985,7 +909,7 @@ class App // Max Load Average reached: ERROR if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) { header('Retry-After: 120'); - header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string); + header('Refresh: 120; url=' . $this->baseURL->get() . "/" . $this->args->getQueryString()); throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.'); } @@ -993,7 +917,7 @@ class App if (!$this->getMode()->isInstall()) { // Force SSL redirection if ($this->baseURL->checkRedirectHttps()) { - System::externalRedirect($this->getBaseURL() . '/' . $this->query_string); + System::externalRedirect($this->baseURL->get() . '/' . $this->args->getQueryString()); } Core\Session::init(); @@ -1001,7 +925,7 @@ class App } // Exclude the backend processes from the session management - if (!$this->isBackend()) { + if (!$module->isBackend()) { $stamp1 = microtime(true); session_start(); $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack()); @@ -1021,7 +945,6 @@ class App // ZRL if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) { - $this->query_string = Model\Profile::stripZrls($this->query_string); if (!local_user()) { // Only continue when the given profile link seems valid // Valid profile links contain a path with "/profile/" and no query parameters @@ -1044,11 +967,10 @@ class App if (!empty($_GET['owt']) && $this->getMode()->isNormal()) { $token = $_GET['owt']; - $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt'); Model\Profile::openWebAuthInit($token); } - Module\Login::sessionAuth(); + Login::sessionAuth(); if (empty($_SESSION['authenticated'])) { header('X-Account-Management-Status: none'); @@ -1066,9 +988,9 @@ class App // in install mode, any url loads install module // but we need "view" module for stylesheet - if ($this->getMode()->isInstall() && $this->module !== 'install') { + if ($this->getMode()->isInstall() && $moduleName !== 'install') { $this->internalRedirect('install'); - } elseif (!$this->getMode()->isInstall() && !$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module !== 'maintenance') { + } elseif (!$this->getMode()->isInstall() && !$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $moduleName !== 'maintenance') { $this->internalRedirect('maintenance'); } else { $this->checkURL(); @@ -1091,152 +1013,65 @@ class App ]; // Compatibility with the Android Diaspora client - if ($this->module == 'stream') { + if ($moduleName == 'stream') { $this->internalRedirect('network?order=post'); } - if ($this->module == 'conversations') { + if ($moduleName == 'conversations') { $this->internalRedirect('message'); } - if ($this->module == 'commented') { + if ($moduleName == 'commented') { $this->internalRedirect('network?order=comment'); } - if ($this->module == 'liked') { + if ($moduleName == 'liked') { $this->internalRedirect('network?order=comment'); } - if ($this->module == 'activity') { + if ($moduleName == 'activity') { $this->internalRedirect('network?conv=1'); } - if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) { + if (($moduleName == 'status_messages') && ($this->args->getCommand() == 'status_messages/new')) { $this->internalRedirect('bookmarklet'); } - if (($this->module == 'user') && ($this->cmd == 'user/edit')) { + if (($moduleName == 'user') && ($this->args->getCommand() == 'user/edit')) { $this->internalRedirect('settings'); } - if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) { + if (($moduleName == 'tag_followings') && ($this->args->getCommand() == 'tag_followings/manage')) { $this->internalRedirect('search'); } - // Compatibility with the Firefox App - if (($this->module == "users") && ($this->cmd == "users/sign_in")) { - $this->module = "login"; - } - - /* - * ROUTING - * - * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the - * post() and/or content() static methods can be respectively called to produce a data change or an output. - */ - - // First we try explicit routes defined in App\Router - $this->router->collectRoutes(); - - $data = $this->router->getRouteCollector(); - Hook::callAll('route_collection', $data); - - $this->module_class = $this->router->getModuleClass($this->cmd); - - // Then we try addon-provided modules that we wrap in the LegacyModule class - if (!$this->module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) { - //Check if module is an app and if public access to apps is allowed or not - $privateapps = $this->config->get('config', 'private_addons', false); - if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) { - info($this->l10n->t("You must be logged in to use addons. ")); - } else { - include_once "addon/{$this->module}/{$this->module}.php"; - if (function_exists($this->module . '_module')) { - LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php"); - $this->module_class = LegacyModule::class; - } - } - } - - /* Finally, we look for a 'standard' program module in the 'mod' directory - * We emulate a Module class through the LegacyModule class - */ - if (!$this->module_class && file_exists("mod/{$this->module}.php")) { - LegacyModule::setModuleFile("mod/{$this->module}.php"); - $this->module_class = LegacyModule::class; - } - - /* The URL provided does not resolve to a valid module. - * - * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'. - * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic - - * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page - * this will often succeed and eventually do the right thing. - * - * Otherwise we are going to emit a 404 not found. - */ - if (!$this->module_class) { - // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit. - if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) { - exit(); - } - - if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) { - Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']); - $this->internalRedirect($_SERVER['REQUEST_URI']); - } - - Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG); - - $this->module_class = Module\PageNotFound::class; - } - // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid - $this->page['page_title'] = $this->module; + $this->page['page_title'] = $moduleName; - $placeholder = ''; + // determine the module class and save it to the module instance + // @todo there's an implicit dependency due SESSION::start(), so it has to be called here (yet) + $module = $module->determineClass($this->args, $router, $this->config); - Core\Hook::callAll($this->module . '_mod_init', $placeholder); + // Let the module run it's internal process (init, get, post, ...) + $module->run($this->l10n, $this, $this->logger, $this->getCurrentTheme(), $_SERVER, $_POST); - call_user_func([$this->module_class, 'init']); - - // "rawContent" is especially meant for technical endpoints. - // This endpoint doesn't need any theme initialization or other comparable stuff. - call_user_func([$this->module_class, 'rawContent']); - - // Load current theme info after module has been initialized as theme could have been set in module - $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php'; - if (file_exists($theme_info_file)) { - require_once $theme_info_file; - } - - if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) { - $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init'; - $func($this); - } - - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - Core\Hook::callAll($this->module . '_mod_post', $_POST); - call_user_func([$this->module_class, 'post']); - } - - Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder); - call_user_func([$this->module_class, 'afterpost']); } catch(HTTPException $e) { - Module\Special\HTTPException::rawContent($e); + ModuleHTTPException::rawContent($e); } $content = ''; try { + $moduleClass = $module->getClassName(); + $arr = ['content' => $content]; - Core\Hook::callAll($this->module . '_mod_content', $arr); + Core\Hook::callAll($moduleClass . '_mod_content', $arr); $content = $arr['content']; - $arr = ['content' => call_user_func([$this->module_class, 'content'])]; - Core\Hook::callAll($this->module . '_mod_aftercontent', $arr); + $arr = ['content' => call_user_func([$moduleClass, 'content'])]; + Core\Hook::callAll($moduleClass . '_mod_aftercontent', $arr); $content .= $arr['content']; } catch(HTTPException $e) { - $content = Module\Special\HTTPException::content($e); + $content = ModuleHTTPException::content($e); } // initialise content region @@ -1253,7 +1088,7 @@ class App * all the module functions have executed so that all * theme choices made by the modules can take effect. */ - $this->initHead(); + $this->initHead($module, $pconfig); /* Build the page ending -- this is stuff that goes right before * the closing tag @@ -1265,7 +1100,7 @@ class App } // Add the navigation (menu) template - if ($this->module != 'install' && $this->module != 'maintenance') { + if ($moduleName != 'install' && $moduleName != 'maintenance') { $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []); $this->page['nav'] = Content\Nav::build($this); } diff --git a/src/App/Arguments.php b/src/App/Arguments.php new file mode 100644 index 0000000000..8047186a07 --- /dev/null +++ b/src/App/Arguments.php @@ -0,0 +1,194 @@ +queryString = $queryString; + $this->command = $command; + $this->argv = $argv; + $this->argc = $argc; + } + + /** + * @return string The whole query string of this call + */ + public function getQueryString() + { + return $this->queryString; + } + + /** + * @return string The whole command of this call + */ + public function getCommand() + { + return $this->command; + } + + /** + * @return array All arguments of this call + */ + public function getArgv() + { + return $this->argv; + } + + /** + * @return int The count of arguments of this call + */ + public function getArgc() + { + return $this->argc; + } + + /** + * Returns the value of a argv key + * @todo there are a lot of $a->argv usages in combination with defaults() which can be replaced with this method + * + * @param int $position the position of the argument + * @param mixed $default the default value if not found + * + * @return mixed returns the value of the argument + */ + public function get(int $position, $default = '') + { + return $this->has($position) ? $this->argv[$position] : $default; + } + + /** + * @param int $position + * + * @return bool if the argument position exists + */ + public function has(int $position) + { + return array_key_exists($position, $this->argv); + } + + /** + * Determine the arguments of the current call + * + * @param array $server The $_SERVER variable + * @param array $get The $_GET variable + * + * @return Arguments The determined arguments + */ + public function determine(array $server, array $get) + { + $queryString = ''; + + if (!empty($server['QUERY_STRING']) && strpos($server['QUERY_STRING'], 'pagename=') === 0) { + $queryString = substr($server['QUERY_STRING'], 9); + } elseif (!empty($server['QUERY_STRING']) && strpos($server['QUERY_STRING'], 'q=') === 0) { + $queryString = substr($server['QUERY_STRING'], 2); + } + + // eventually strip ZRL + $queryString = $this->stripZRLs($queryString); + + // eventually strip OWT + $queryString = $this->stripQueryParam($queryString, 'owt'); + + // removing trailing / - maybe a nginx problem + $queryString = ltrim($queryString, '/'); + + if (!empty($get['pagename'])) { + $command = trim($get['pagename'], '/\\'); + } elseif (!empty($get['q'])) { + $command = trim($get['q'], '/\\'); + } else { + $command = Module::DEFAULT; + } + + + // fix query_string + if (!empty($command)) { + $queryString = str_replace( + $command . '&', + $command . '?', + $queryString + ); + } + + // unix style "homedir" + if (substr($command, 0, 1) === '~') { + $command = 'profile/' . substr($command, 1); + } + + // Diaspora style profile url + if (substr($command, 0, 2) === 'u/') { + $command = 'profile/' . substr($command, 2); + } + + /* + * Break the URL path into C style argc/argv style arguments for our + * modules. Given "http://example.com/module/arg1/arg2", $this->argc + * will be 3 (integer) and $this->argv will contain: + * [0] => 'module' + * [1] => 'arg1' + * [2] => 'arg2' + * + * + * There will always be one argument. If provided a naked domain + * URL, $this->argv[0] is set to "home". + */ + + $argv = explode('/', $command); + $argc = count($argv); + + + return new Arguments($queryString, $command, $argv, $argc); + } + + /** + * Strip zrl parameter from a string. + * + * @param string $queryString The input string. + * + * @return string The zrl. + */ + public function stripZRLs(string $queryString) + { + return preg_replace('/[?&]zrl=(.*?)(&|$)/ism', '$2', $queryString); + } + + /** + * Strip query parameter from a string. + * + * @param string $queryString The input string. + * @param string $param + * + * @return string The query parameter. + */ + public function stripQueryParam(string $queryString, string $param) + { + return preg_replace('/[?&]' . $param . '=(.*?)(&|$)/ism', '$2', $queryString); + } +} \ No newline at end of file diff --git a/src/App/Mode.php b/src/App/Mode.php index 6cb79b6edd..166d47a174 100644 --- a/src/App/Mode.php +++ b/src/App/Mode.php @@ -24,27 +24,9 @@ class Mode */ private $mode; - /** - * @var string the basepath of the application - */ - private $basepath; - - /** - * @var Database - */ - private $database; - - /** - * @var ConfigCache - */ - private $configCache; - - public function __construct(BasePath $basepath, Database $database, ConfigCache $configCache) + public function __construct(int $mode = 0) { - $this->basepath = $basepath->getPath(); - $this->database = $database; - $this->configCache = $configCache; - $this->mode = 0; + $this->mode = $mode; } /** @@ -54,50 +36,46 @@ class Mode * - App::MODE_MAINTENANCE: The maintenance mode has been set * - App::MODE_NORMAL : Normal run with all features enabled * - * @param string $basePath the Basepath of the Application - * - * @return Mode returns itself + * @return Mode returns the determined mode * * @throws \Exception */ - public function determine($basePath = null) + public function determine(BasePath $basepath, Database $database, ConfigCache $configCache) { - if (!empty($basePath)) { - $this->basepath = $basePath; + $mode = 0; + + $basepathName = $basepath->getPath(); + + if (!file_exists($basepathName . '/config/local.config.php') + && !file_exists($basepathName . '/config/local.ini.php') + && !file_exists($basepathName . '/.htconfig.php')) { + return new Mode($mode); } - $this->mode = 0; + $mode |= Mode::LOCALCONFIGPRESENT; - if (!file_exists($this->basepath . '/config/local.config.php') - && !file_exists($this->basepath . '/config/local.ini.php') - && !file_exists($this->basepath . '/.htconfig.php')) { - return $this; + if (!$database->connected()) { + return new Mode($mode); } - $this->mode |= Mode::LOCALCONFIGPRESENT; + $mode |= Mode::DBAVAILABLE; - if (!$this->database->connected()) { - return $this; + if ($database->fetchFirst("SHOW TABLES LIKE 'config'") === false) { + return new Mode($mode); } - $this->mode |= Mode::DBAVAILABLE; + $mode |= Mode::DBCONFIGAVAILABLE; - if ($this->database->fetchFirst("SHOW TABLES LIKE 'config'") === false) { - return $this; - } - - $this->mode |= Mode::DBCONFIGAVAILABLE; - - if (!empty($this->configCache->get('system', 'maintenance')) || + if (!empty($configCache->get('system', 'maintenance')) || // Don't use Config or Configuration here because we're possibly BEFORE initializing the Configuration, // so this could lead to a dependency circle - !empty($this->database->selectFirst('config', ['v'], ['cat' => 'system', 'k' => 'maintenance'])['v'])) { - return $this; + !empty($database->selectFirst('config', ['v'], ['cat' => 'system', 'k' => 'maintenance'])['v'])) { + return new Mode($mode); } - $this->mode |= Mode::MAINTENANCEDISABLED; + $mode |= Mode::MAINTENANCEDISABLED; - return $this; + return new Mode($mode); } /** diff --git a/src/App/Module.php b/src/App/Module.php new file mode 100644 index 0000000000..63be2f651d --- /dev/null +++ b/src/App/Module.php @@ -0,0 +1,279 @@ +module; + } + + /** + * @return string The base class name + */ + public function getClassName() + { + return $this->module_class; + } + + /** + * @return bool + */ + public function isBackend() + { + return $this->isBackend; + } + + public function __construct(string $module = self::DEFAULT, string $moduleClass = self::DEFAULT_CLASS, bool $isBackend = false, bool $printNotAllowedAddon = false) + { + $this->module = $module; + $this->module_class = $moduleClass; + $this->isBackend = $isBackend; + $this->printNotAllowedAddon = $printNotAllowedAddon; + } + + /** + * Determines the current module based on the App arguments and the server variable + * + * @param Arguments $args The Friendica arguments + * @param array $server The $_SERVER variable + * + * @return Module The module with the determined module + */ + public function determineModule(Arguments $args, array $server) + { + if ($args->getArgc() > 0) { + $module = str_replace('.', '_', $args->get(0)); + $module = str_replace('-', '_', $module); + } else { + $module = self::DEFAULT; + } + + // Compatibility with the Firefox App + if (($module == "users") && ($args->getCommand() == "users/sign_in")) { + $module = "login"; + } + + $isBackend = $this->checkBackend($module, $server); + + return new Module($module, $this->module_class, $isBackend, $this->printNotAllowedAddon); + } + + /** + * Determine the class of the current module + * + * @param Arguments $args The Friendica execution arguments + * @param Router $router The Friendica routing instance + * @param Core\Config\Configuration $config The Friendica Configuration + * + * @return Module The determined module of this call + * + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function determineClass(Arguments $args, Router $router, Core\Config\Configuration $config) + { + $printNotAllowedAddon = false; + + /** + * ROUTING + * + * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the + * post() and/or content() static methods can be respectively called to produce a data change or an output. + **/ + + // First we try explicit routes defined in App\Router + $router->collectRoutes(); + + $data = $router->getRouteCollector(); + Core\Hook::callAll('route_collection', $data); + + $module_class = $router->getModuleClass($args->getCommand()); + + // Then we try addon-provided modules that we wrap in the LegacyModule class + if (!$module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) { + //Check if module is an app and if public access to apps is allowed or not + $privateapps = $config->get('config', 'private_addons', false); + if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) { + $printNotAllowedAddon = true; + } else { + include_once "addon/{$this->module}/{$this->module}.php"; + if (function_exists($this->module . '_module')) { + LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php"); + $module_class = LegacyModule::class; + } + } + } + + /* Finally, we look for a 'standard' program module in the 'mod' directory + * We emulate a Module class through the LegacyModule class + */ + if (!$module_class && file_exists("mod/{$this->module}.php")) { + LegacyModule::setModuleFile("mod/{$this->module}.php"); + $module_class = LegacyModule::class; + } + + $module_class = !isset($module_class) ? PageNotFound::class : $module_class; + + return new Module($this->module, $module_class, $this->isBackend, $printNotAllowedAddon); + } + + /** + * Run the determined module class and calls all hooks applied to + * + * @param Core\L10n\L10n $l10n The L10n instance + * @param App $app The whole Friendica app (for method arguments) + * @param LoggerInterface $logger The Friendica logger + * @param string $currentTheme The chosen theme + * @param array $server The $_SERVER variable + * @param array $post The $_POST variables + * + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function run(Core\L10n\L10n $l10n, App $app, LoggerInterface $logger, string $currentTheme, array $server, array $post) + { + if ($this->printNotAllowedAddon) { + info($l10n->t("You must be logged in to use addons. ")); + } + + /* The URL provided does not resolve to a valid module. + * + * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'. + * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic - + * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page + * this will often succeed and eventually do the right thing. + * + * Otherwise we are going to emit a 404 not found. + */ + if ($this->module_class === PageNotFound::class) { + $queryString = $server['QUERY_STRING']; + // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit. + if (!empty($queryString) && preg_match('/{[0-9]}/', $queryString) !== 0) { + exit(); + } + + if (!empty($queryString) && ($queryString === 'q=internal_error.html') && isset($dreamhost_error_hack)) { + $logger->info('index.php: dreamhost_error_hack invoked.', ['Original URI' => $server['REQUEST_URI']]); + $app->internalRedirect($server['REQUEST_URI']); + } + + $logger->debug('index.php: page not found.', ['request_uri' => $server['REQUEST_URI'], 'address' => $server['REMOTE_ADDR'], 'query' => $server['QUERY_STRING']]); + } + + $placeholder = ''; + + Core\Hook::callAll($this->module . '_mod_init', $placeholder); + + call_user_func([$this->module_class, 'init']); + + // "rawContent" is especially meant for technical endpoints. + // This endpoint doesn't need any theme initialization or other comparable stuff. + call_user_func([$this->module_class, 'rawContent']); + + // Load current theme info after module has been initialized as theme could have been set in module + $theme_info_file = 'view/theme/' . $currentTheme . '/theme.php'; + if (file_exists($theme_info_file)) { + require_once $theme_info_file; + } + + if (function_exists(str_replace('-', '_', $currentTheme) . '_init')) { + $func = str_replace('-', '_', $currentTheme) . '_init'; + $func($app); + } + + if ($server['REQUEST_METHOD'] === 'POST') { + Core\Hook::callAll($this->module . '_mod_post', $post); + call_user_func([$this->module_class, 'post']); + } + + Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder); + call_user_func([$this->module_class, 'afterpost']); + } + + /** + * @brief Checks if the site is called via a backend process + * + * This isn't a perfect solution. But we need this check very early. + * So we cannot wait until the modules are loaded. + * + * @param string $module The determined module + * @param array $server The $_SERVER variable + * + * @return bool True, if the current module is called at backend + */ + private function checkBackend($module, array $server) + { + // Check if current module is in backend or backend flag is set + return basename(($server['PHP_SELF'] ?? ''), '.php') !== 'index' && + in_array($module, Module::BACKEND_MODULES); + } +} diff --git a/src/Core/Installer.php b/src/Core/Installer.php index 8dcf776a70..16d7bf0aad 100644 --- a/src/Core/Installer.php +++ b/src/Core/Installer.php @@ -11,7 +11,6 @@ use Friendica\Database\Database; use Friendica\Database\DBStructure; use Friendica\Object\Image; use Friendica\Util\Network; -use Friendica\Util\Profiler; use Friendica\Util\Strings; /** @@ -591,8 +590,7 @@ class Installer /** * Checking the Database connection and if it is available for the current installation * - * @param ConfigCache $configCache The configuration cache - * @param Profiler $profiler The profiler of this app + * @param Database $dba * * @return bool true if the check was successful, otherwise false * @throws Exception diff --git a/src/Model/Profile.php b/src/Model/Profile.php index f215af1af5..b82d47d313 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -1224,29 +1224,6 @@ class Profile return $uid; } - /** - * Strip zrl parameter from a string. - * - * @param string $s The input string. - * @return string The zrl. - */ - public static function stripZrls($s) - { - return preg_replace('/[\?&]zrl=(.*?)([\?&]|$)/is', '', $s); - } - - /** - * Strip query parameter from a string. - * - * @param string $s The input string. - * @param $param - * @return string The query parameter. - */ - public static function stripQueryParam($s, $param) - { - return preg_replace('/[\?&]' . $param . '=(.*?)(&|$)/ism', '$2', $s); - } - /** * search for Profiles * diff --git a/src/Module/Install.php b/src/Module/Install.php index a7427ead0d..3b8ebb471c 100644 --- a/src/Module/Install.php +++ b/src/Module/Install.php @@ -111,7 +111,7 @@ class Install extends BaseModule self::checkSetting($configCache, $_POST, 'database', 'database', ''); // If we cannot connect to the database, return to the previous step - if (!self::$installer->checkDB($configCache, $a->getProfiler())) { + if (!self::$installer->checkDB($a->getDBA())) { self::$currentWizardStep = self::DATABASE_CONFIG; } @@ -135,7 +135,7 @@ class Install extends BaseModule self::checkSetting($configCache, $_POST, 'config', 'admin_email', ''); // If we cannot connect to the database, return to the Database config wizard - if (!self::$installer->checkDB($configCache, $a->getProfiler())) { + if (!self::$installer->checkDB($a->getDBA())) { self::$currentWizardStep = self::DATABASE_CONFIG; return; } diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 1d0908f325..5d0b0b97b3 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -27,14 +27,14 @@ use Psr\Log\LoggerInterface; * */ return [ - '*' => [ + '*' => [ // marks all class result as shared for other creations, so there's just // one instance for the whole execution 'shared' => true, ], - '$basepath' => [ - 'instanceOf' => Util\BasePath::class, - 'call' => [ + '$basepath' => [ + 'instanceOf' => Util\BasePath::class, + 'call' => [ ['getPath', [], Dice::CHAIN_CALL], ], 'constructParams' => [ @@ -42,14 +42,14 @@ return [ $_SERVER ] ], - Util\BasePath::class => [ + Util\BasePath::class => [ 'constructParams' => [ dirname(__FILE__, 2), $_SERVER ] ], - Util\ConfigFileLoader::class => [ - 'shared' => true, + Util\ConfigFileLoader::class => [ + 'shared' => true, 'constructParams' => [ [Dice::INSTANCE => '$basepath'], ], @@ -60,24 +60,24 @@ return [ ['createCache', [], Dice::CHAIN_CALL], ], ], - App\Mode::class => [ - 'call' => [ + App\Mode::class => [ + 'call' => [ ['determine', [], Dice::CHAIN_CALL], ], ], - Config\Configuration::class => [ + Config\Configuration::class => [ 'instanceOf' => Factory\ConfigFactory::class, - 'call' => [ + 'call' => [ ['createConfig', [], Dice::CHAIN_CALL], ], ], - Config\PConfiguration::class => [ + Config\PConfiguration::class => [ 'instanceOf' => Factory\ConfigFactory::class, - 'call' => [ + 'call' => [ ['createPConfig', [], Dice::CHAIN_CALL], ] ], - Database::class => [ + Database::class => [ 'constructParams' => [ [DICE::INSTANCE => \Psr\Log\NullLogger::class], $_SERVER, @@ -89,7 +89,7 @@ return [ * Same as: * $baseURL = new Util\BaseURL($configuration, $_SERVER); */ - Util\BaseURL::class => [ + Util\BaseURL::class => [ 'constructParams' => [ $_SERVER, ], @@ -106,34 +106,46 @@ return [ * $app = $dice->create(App::class, [], ['$channel' => 'index']); * and is automatically passed as an argument with the same name */ - LoggerInterface::class => [ + LoggerInterface::class => [ 'instanceOf' => Factory\LoggerFactory::class, 'call' => [ ['create', [], Dice::CHAIN_CALL], ], ], - '$devLogger' => [ + '$devLogger' => [ 'instanceOf' => Factory\LoggerFactory::class, 'call' => [ ['createDev', [], Dice::CHAIN_CALL], ] ], - Cache\ICache::class => [ + Cache\ICache::class => [ 'instanceOf' => Factory\CacheFactory::class, 'call' => [ ['create', [], Dice::CHAIN_CALL], ], ], - Cache\IMemoryCache::class => [ + Cache\IMemoryCache::class => [ 'instanceOf' => Factory\CacheFactory::class, 'call' => [ ['create', [], Dice::CHAIN_CALL], ], ], - ILock::class => [ + ILock::class => [ 'instanceOf' => Factory\LockFactory::class, 'call' => [ ['create', [], Dice::CHAIN_CALL], ], ], + App\Arguments::class => [ + 'instanceOf' => App\Arguments::class, + 'call' => [ + ['determine', [$_SERVER, $_GET], Dice::CHAIN_CALL], + ], + ], + App\Module::class => [ + 'instanceOf' => App\Module::class, + 'call' => [ + ['determineModule', [$_SERVER], Dice::CHAIN_CALL], + ], + ], ]; diff --git a/tests/src/App/ArgumentsTest.php b/tests/src/App/ArgumentsTest.php new file mode 100644 index 0000000000..ee0419955d --- /dev/null +++ b/tests/src/App/ArgumentsTest.php @@ -0,0 +1,242 @@ +assertEquals($assert['queryString'], $arguments->getQueryString()); + $this->assertEquals($assert['command'], $arguments->getCommand()); + $this->assertEquals($assert['argv'], $arguments->getArgv()); + $this->assertEquals($assert['argc'], $arguments->getArgc()); + $this->assertCount($assert['argc'], $arguments->getArgv()); + } + + /** + * Test the default argument without any determinations + */ + public function testDefault() + { + $arguments = new App\Arguments(); + + $this->assertArguments([ + 'queryString' => '', + 'command' => '', + 'argv' => ['home'], + 'argc' => 1, + ], + $arguments); + } + + public function dataArguments() + { + return [ + 'withPagename' => [ + 'assert' => [ + 'queryString' => 'profile/test/it?arg1=value1&arg2=value2', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'pagename=profile/test/it?arg1=value1&arg2=value2', + ], + 'get' => [ + 'pagename' => 'profile/test/it', + ], + ], + 'withQ' => [ + 'assert' => [ + 'queryString' => 'profile/test/it?arg1=value1&arg2=value2', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'q=profile/test/it?arg1=value1&arg2=value2', + ], + 'get' => [ + 'q' => 'profile/test/it', + ], + ], + 'withWrongDelimiter' => [ + 'assert' => [ + 'queryString' => 'profile/test/it?arg1=value1&arg2=value2', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'pagename=profile/test/it&arg1=value1&arg2=value2', + ], + 'get' => [ + 'pagename' => 'profile/test/it', + ], + ], + 'withUnixHomeDir' => [ + 'assert' => [ + 'queryString' => '~test/it?arg1=value1&arg2=value2', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'pagename=~test/it?arg1=value1&arg2=value2', + ], + 'get' => [ + 'pagename' => '~test/it', + ], + ], + 'withDiasporaHomeDir' => [ + 'assert' => [ + 'queryString' => 'u/test/it?arg1=value1&arg2=value2', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'pagename=u/test/it?arg1=value1&arg2=value2', + ], + 'get' => [ + 'pagename' => 'u/test/it', + ], + ], + 'withTrailingSlash' => [ + 'assert' => [ + 'queryString' => 'profile/test/it?arg1=value1&arg2=value2/', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'pagename=profile/test/it?arg1=value1&arg2=value2/', + ], + 'get' => [ + 'pagename' => 'profile/test/it', + ], + ], + 'withWrongQueryString' => [ + 'assert' => [ + // empty query string?! + 'queryString' => '', + 'command' => 'profile/test/it', + 'argv' => ['profile', 'test', 'it'], + 'argc' => 3, + ], + 'server' => [ + 'QUERY_STRING' => 'wrong=profile/test/it?arg1=value1&arg2=value2/', + ], + 'get' => [ + 'pagename' => 'profile/test/it', + ], + ], + 'withMissingPageName' => [ + 'assert' => [ + 'queryString' => 'notvalid/it?arg1=value1&arg2=value2/', + 'command' => App\Module::DEFAULT, + 'argv' => [App\Module::DEFAULT], + 'argc' => 1, + ], + 'server' => [ + 'QUERY_STRING' => 'pagename=notvalid/it?arg1=value1&arg2=value2/', + ], + 'get' => [ + ], + ], + ]; + } + + /** + * Test all variants of argument determination + * + * @dataProvider dataArguments + */ + public function testDetermine(array $assert, array $server, array $get) + { + $arguments = (new App\Arguments()) + ->determine($server, $get); + + $this->assertArguments($assert, $arguments); + } + + /** + * Test if the get/has methods are working for the determined arguments + * + * @dataProvider dataArguments + */ + public function testGetHas(array $assert, array $server, array $get) + { + $arguments = (new App\Arguments()) + ->determine($server, $get); + + for ($i = 0; $i < $arguments->getArgc(); $i++) { + $this->assertTrue($arguments->has($i)); + $this->assertEquals($assert['argv'][$i], $arguments->get($i)); + } + + $this->assertFalse($arguments->has($arguments->getArgc())); + $this->assertEmpty($arguments->get($arguments->getArgc())); + $this->assertEquals('default', $arguments->get($arguments->getArgc(), 'default')); + } + + public function dataStripped() + { + return [ + 'strippedZRLFirst' => [ + 'assert' => '?arg1=value1', + 'input' => '?zrl=nope&arg1=value1', + ], + 'strippedZRLLast' => [ + 'assert' => '?arg1=value1', + 'input' => '?arg1=value1&zrl=nope', + ], + 'strippedZTLMiddle' => [ + 'assert' => '?arg1=value1&arg2=value2', + 'input' => '?arg1=value1&zrl=nope&arg2=value2', + ], + 'strippedOWTFirst' => [ + 'assert' => '?arg1=value1', + 'input' => '?owt=test&arg1=value1', + ], + 'strippedOWTLast' => [ + 'assert' => '?arg1=value1', + 'input' => '?arg1=value1&owt=test', + ], + 'strippedOWTMiddle' => [ + 'assert' => '?arg1=value1&arg2=value2', + 'input' => '?arg1=value1&owt=test&arg2=value2', + ], + ]; + } + + /** + * Test the ZRL and OWT stripping + * + * @dataProvider dataStripped + */ + public function testStrippedQueries(string $assert, string $input) + { + $command = 'test/it'; + + $arguments = (new App\Arguments()) + ->determine(['QUERY_STRING' => 'q=' . $command . $input,], ['pagename' => $command]); + + $this->assertEquals($command . $assert, $arguments->getQueryString()); + } + + /** + * Test that arguments are immutable + */ + public function testImmutable() + { + $argument = new App\Arguments(); + + $argNew = $argument->determine([], []); + + $this->assertNotSame($argument, $argNew); + } +} diff --git a/tests/src/App/ModeTest.php b/tests/src/App/ModeTest.php index 06aad10661..30d0dc531b 100644 --- a/tests/src/App/ModeTest.php +++ b/tests/src/App/ModeTest.php @@ -38,22 +38,20 @@ class ModeTest extends MockedTest $this->setUpVfsDir(); $this->basePathMock = \Mockery::mock(BasePath::class); - $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); - $this->databaseMock = \Mockery::mock(Database::class); $this->configCacheMock = \Mockery::mock(Config\Cache\ConfigCache::class); } public function testItEmpty() { - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); + $mode = new Mode(); $this->assertTrue($mode->isInstall()); $this->assertFalse($mode->isNormal()); } public function testWithoutConfig() { - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); + $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); $this->assertTrue($this->root->hasChild('config/local.config.php')); @@ -61,7 +59,7 @@ class ModeTest extends MockedTest $this->assertFalse($this->root->hasChild('config/local.config.php')); - $mode->determine(); + $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); $this->assertTrue($mode->isInstall()); $this->assertFalse($mode->isNormal()); @@ -71,10 +69,11 @@ class ModeTest extends MockedTest public function testWithoutDatabase() { + $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); + $this->databaseMock->shouldReceive('connected')->andReturn(false)->once(); - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); - $mode->determine(); + $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); $this->assertFalse($mode->isNormal()); $this->assertTrue($mode->isInstall()); @@ -85,12 +84,13 @@ class ModeTest extends MockedTest public function testWithoutDatabaseSetup() { + $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); + $this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('fetchFirst') ->with('SHOW TABLES LIKE \'config\'')->andReturn(false)->once(); - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); - $mode->determine(); + $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); $this->assertFalse($mode->isNormal()); $this->assertTrue($mode->isInstall()); @@ -100,14 +100,15 @@ class ModeTest extends MockedTest public function testWithMaintenanceMode() { + $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); + $this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('fetchFirst') ->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once(); $this->configCacheMock->shouldReceive('get')->with('system', 'maintenance') ->andReturn(true)->once(); - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); - $mode->determine(); + $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); $this->assertFalse($mode->isNormal()); $this->assertFalse($mode->isInstall()); @@ -118,6 +119,8 @@ class ModeTest extends MockedTest public function testNormalMode() { + $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); + $this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('fetchFirst') ->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once(); @@ -127,8 +130,7 @@ class ModeTest extends MockedTest ->with('config', ['v'], ['cat' => 'system', 'k' => 'maintenance']) ->andReturn(['v' => null])->once(); - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); - $mode->determine(); + $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); $this->assertTrue($mode->isNormal()); $this->assertFalse($mode->isInstall()); @@ -142,6 +144,8 @@ class ModeTest extends MockedTest */ public function testDisabledMaintenance() { + $this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once(); + $this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('fetchFirst') ->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once(); @@ -151,8 +155,7 @@ class ModeTest extends MockedTest ->with('config', ['v'], ['cat' => 'system', 'k' => 'maintenance']) ->andReturn(['v' => '0'])->once(); - $mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); - $mode->determine(); + $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); $this->assertTrue($mode->isNormal()); $this->assertFalse($mode->isInstall()); @@ -160,4 +163,18 @@ class ModeTest extends MockedTest $this->assertTrue($mode->has(Mode::DBCONFIGAVAILABLE)); $this->assertTrue($mode->has(Mode::MAINTENANCEDISABLED)); } + + /** + * Test that modes are immutable + */ + public function testImmutable() + { + $this->basePathMock->shouldReceive('getPath')->andReturn(null)->once(); + + $mode = new Mode(); + + $modeNew = $mode->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock); + + $this->assertNotSame($modeNew, $mode); + } } diff --git a/tests/src/App/ModuleTest.php b/tests/src/App/ModuleTest.php new file mode 100644 index 0000000000..309e94bba4 --- /dev/null +++ b/tests/src/App/ModuleTest.php @@ -0,0 +1,189 @@ +assertEquals($assert['isBackend'], $module->isBackend()); + $this->assertEquals($assert['name'], $module->getName()); + $this->assertEquals($assert['class'], $module->getClassName()); + } + + /** + * Test the default module mode + */ + public function testDefault() + { + $module = new App\Module(); + + $this->assertModule([ + 'isBackend' => false, + 'name' => App\Module::DEFAULT, + 'class' => App\Module::DEFAULT_CLASS, + ], $module); + } + + public function dataModuleName() + { + return [ + 'default' => [ + 'assert' => [ + 'isBackend' => false, + 'name' => 'network', + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments('network/data/in', + 'network/data/in', + ['network', 'data', 'in'], + 3), + 'server' => [], + ], + 'withStrikeAndPoint' => [ + 'assert' => [ + 'isBackend' => false, + 'name' => 'with_strike_and_point', + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments('with-strike.and-point/data/in', + 'with-strike.and-point/data/in', + ['with-strike.and-point', 'data', 'in'], + 3), + 'server' => [], + ], + 'withNothing' => [ + 'assert' => [ + 'isBackend' => false, + 'name' => App\Module::DEFAULT, + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments(), + 'server' => [] + ], + 'withIndex' => [ + 'assert' => [ + 'isBackend' => false, + 'name' => App\Module::DEFAULT, + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments(), + 'server' => ['PHP_SELF' => 'index.php'] + ], + 'withIndexButBackendMod' => [ + 'assert' => [ + 'isBackend' => false, + 'name' => App\Module::BACKEND_MODULES[0], + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments(App\Module::BACKEND_MODULES[0] . '/data/in', + App\Module::BACKEND_MODULES[0] . '/data/in', + [App\Module::BACKEND_MODULES[0], 'data', 'in'], + 3), + 'server' => ['PHP_SELF' => 'index.php'] + ], + 'withNotIndexAndBackendMod' => [ + 'assert' => [ + 'isBackend' => true, + 'name' => App\Module::BACKEND_MODULES[0], + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments(App\Module::BACKEND_MODULES[0] . '/data/in', + App\Module::BACKEND_MODULES[0] . '/data/in', + [App\Module::BACKEND_MODULES[0], 'data', 'in'], + 3), + 'server' => ['PHP_SELF' => 'daemon.php'] + ], + 'withFirefoxApp' => [ + 'assert' => [ + 'isBackend' => false, + 'name' => 'login', + 'class' => App\Module::DEFAULT_CLASS, + ], + 'args' => new App\Arguments('users/sign_in', + 'users/sign_in', + ['users', 'sign_in'], + 3), + 'server' => ['PHP_SELF' => 'index.php'], + ], + ]; + } + + /** + * Test the module name and backend determination + * + * @dataProvider dataModuleName + */ + public function testModuleName(array $assert, App\Arguments $args, array $server) + { + $module = (new App\Module())->determineModule($args, $server); + + $this->assertModule($assert, $module); + } + + public function dataModuleClass() + { + return [ + 'default' => [ + 'assert' => App\Module::DEFAULT_CLASS, + 'name' => App\Module::DEFAULT, + 'command' => App\Module::DEFAULT, + 'privAdd' => false, + ], + 'legacy' => [ + 'assert' => LegacyModule::class, + // API is one of the last modules to switch from legacy to new BaseModule + // so this should be a stable test case until we completely switch ;-) + 'name' => 'api', + 'command' => 'api/test/it', + 'privAdd' => false, + ], + 'new' => [ + 'assert' => HostMeta::class, + 'not_required', + 'command' => '.well-known/host-meta', + 'privAdd' => false, + ], + '404' => [ + 'assert' => PageNotFound::class, + 'name' => 'invalid', + 'command' => 'invalid', + 'privAdd' => false, + ] + ]; + } + + /** + * Test the determination of the module class + * + * @dataProvider dataModuleClass + */ + public function testModuleClass($assert, string $name, string $command, bool $privAdd) + { + $config = \Mockery::mock(Configuration::class); + $config->shouldReceive('get')->with('config', 'private_addons', false)->andReturn($privAdd)->atMost()->once(); + + $module = (new App\Module($name))->determineClass(new App\Arguments('', $command), new App\Router(), $config); + + $this->assertEquals($assert, $module->getClassName()); + } + + /** + * Test that modules are immutable + */ + public function testImmutable() + { + $module = new App\Module(); + + $moduleNew = $module->determineModule(new App\Arguments(), []); + + $this->assertNotSame($moduleNew, $module); + } +}