]> git.mxchange.org Git - friendica.git/commitdiff
Introduce Arguments / Module class
authorPhilipp Holzer <admin+github@philipp.info>
Sun, 11 Aug 2019 12:24:05 +0000 (14:24 +0200)
committerPhilipp Holzer <admin+github@philipp.info>
Sun, 11 Aug 2019 12:24:05 +0000 (14:24 +0200)
- move from App
- add tests for Arguments/Module class

12 files changed:
index.php
src/App.php
src/App/Arguments.php [new file with mode: 0644]
src/App/Mode.php
src/App/Module.php [new file with mode: 0644]
src/Core/Installer.php
src/Model/Profile.php
src/Module/Install.php
static/dependencies.config.php
tests/src/App/ArgumentsTest.php [new file with mode: 0644]
tests/src/App/ModeTest.php
tests/src/App/ModuleTest.php [new file with mode: 0644]

index 50e553bcd892f71e967662a5cb1990066a301ce9..e8733a82091df1a484f4375b0dd3beb18e76011b 100644 (file)
--- a/index.php
+++ b/index.php
@@ -18,4 +18,4 @@ $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));
index bf9e1296caac6d0e60366e15dfbf3f3ea8a44b06..98605da2ac342d86e812a7c5558b00065b963c1d 100644 (file)
@@ -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 <head> 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
-        *
-        * @param int $position the position of the argument
-        * @param mixed $default the default value if not found
+        * @deprecated use Arguments->get() instead
         *
-        * @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;
-
-                       $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']);
+                       $this->page['page_title'] = $moduleName;
 
-                       // 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;
-                       }
+                       // 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);
 
-                       if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
-                               $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
-                               $func($this);
-                       }
+                       // Let the module run it's internal process (init, get, post, ...)
+                       $module->run($this->l10n, $this, $this->logger, $this->getCurrentTheme(), $_SERVER, $_POST);
 
-                       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 </body> 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 (file)
index 0000000..8047186
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+
+namespace Friendica\App;
+
+/**
+ * Determine all arguments of the current call, including
+ * - The whole querystring (except the pagename/q parameter)
+ * - The command
+ * - The arguments (C-Style based)
+ * - The count of arguments
+ */
+class Arguments
+{
+       /**
+        * @var string The complete query string
+        */
+       private $queryString;
+       /**
+        * @var string The current Friendica command
+        */
+       private $command;
+       /**
+        * @var array The arguments of the current execution
+        */
+       private $argv;
+       /**
+        * @var int The count of arguments
+        */
+       private $argc;
+
+       public function __construct(string $queryString = '', string $command = '', array $argv = [Module::DEFAULT], int $argc = 1)
+       {
+               $this->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
index 6cb79b6edd4532ac25d4ffeb9791162b82b4ee33..166d47a1740f13e1d5185cd0a79e322ea6053060 100644 (file)
@@ -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;
 
-               $this->mode = 0;
+               $basepathName = $basepath->getPath();
 
-               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 (!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 |= Mode::LOCALCONFIGPRESENT;
+               $mode |= Mode::LOCALCONFIGPRESENT;
 
-               if (!$this->database->connected()) {
-                       return $this;
+               if (!$database->connected()) {
+                       return new Mode($mode);
                }
 
-               $this->mode |= Mode::DBAVAILABLE;
+               $mode |= Mode::DBAVAILABLE;
 
-               if ($this->database->fetchFirst("SHOW TABLES LIKE 'config'") === false) {
-                       return $this;
+               if ($database->fetchFirst("SHOW TABLES LIKE 'config'") === false) {
+                       return new Mode($mode);
                }
 
-               $this->mode |= Mode::DBCONFIGAVAILABLE;
+               $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 (file)
index 0000000..cf2c453
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+
+namespace Friendica\App;
+
+use Friendica\App;
+use Friendica\BaseObject;
+use Friendica\Core;
+use Friendica\LegacyModule;
+use Friendica\Module\Home;
+use Friendica\Module\PageNotFound;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Holds the common context of the current, loaded module
+ */
+class Module
+{
+       const DEFAULT = 'home';
+       const DEFAULT_CLASS = Home::class;
+
+       /**
+        * @var string The module name
+        */
+       private $module;
+
+       /**
+        * @var BaseObject The module class
+        */
+       private $module_class;
+
+       /**
+        * @var bool true, if the module is a backend module
+        */
+       private $isBackend;
+
+       /**
+        * @var bool true, if the loaded addon is private, so we have to print out not allowed
+        */
+       private $printNotAllowedAddon;
+
+       /**
+        * A list of modules, which are backend methods
+        *
+        * @var array
+        */
+       const BACKEND_MODULES = [
+               '_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',
+       ];
+
+       /**
+        * @return string
+        */
+       public function getName()
+       {
+               return $this->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);
+       }
+}
index 8dcf776a7022a23c18ea4854fb54661a46e71ed3..16d7bf0aad681f19d9f81c3ac87a9200dcb4c1ce 100644 (file)
@@ -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
index f215af1af54c54f3f627ccfc3136b1328c8e583a..b82d47d3133966b781d8c5d7ad34884dba26581c 100644 (file)
@@ -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
         *
index a7427ead0dbb226de0b1d0c851a5684ecfcde127..3b8ebb471c5e6cccd202b57fe6af861d829bfe44 100644 (file)
@@ -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;
                                }
index 1d0908f32507252b0fdb6881d8442fab9f2401e3..5d0b0b97b38c827556ef8d275b7e86da79a89a62 100644 (file)
@@ -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 (file)
index 0000000..ee04199
--- /dev/null
@@ -0,0 +1,242 @@
+<?php
+
+namespace Friendica\Test\src\App;
+
+use Friendica\App;
+use PHPUnit\Framework\TestCase;
+
+class ArgumentsTest extends TestCase
+{
+       private function assertArguments(array $assert, App\Arguments $arguments)
+       {
+               $this->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);
+       }
+}
index 06aad106614f578c435490baad5c4b34a36020f5..30d0dc531ba3872ace4929f6fad2d0438dab5529 100644 (file)
@@ -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 (file)
index 0000000..309e94b
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+
+namespace Friendica\Test\src\App;
+
+use Friendica\App;
+use Friendica\Core\Config\Configuration;
+use Friendica\LegacyModule;
+use Friendica\Module\PageNotFound;
+use Friendica\Module\WellKnown\HostMeta;
+use Friendica\Test\DatabaseTest;
+
+class ModuleTest extends DatabaseTest
+{
+       private function assertModule(array $assert, App\Module $module)
+       {
+               $this->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);
+       }
+}