Introduce Arguments / Module class

- move from App
- add tests for Arguments/Module class
This commit is contained in:
Philipp Holzer 2019-08-11 14:24:05 +02:00
parent 3480891277
commit 2c5ba7fc15
No known key found for this signature in database
GPG key ID: D8365C3D36B77D90
12 changed files with 1080 additions and 358 deletions

View file

@ -18,4 +18,4 @@ $dice = (new Dice())->addRules(include __DIR__ . '/static/dependencies.config.ph
$a = \Friendica\BaseObject::getApp(); $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));

View file

@ -8,9 +8,11 @@ use Detection\MobileDetect;
use DOMDocument; use DOMDocument;
use DOMXPath; use DOMXPath;
use Exception; use Exception;
use Friendica\App\Arguments;
use Friendica\App\Module;
use Friendica\Core\Config\Cache\ConfigCache; use Friendica\Core\Config\Cache\ConfigCache;
use Friendica\Core\Config\Configuration; use Friendica\Core\Config\Configuration;
use Friendica\Core\Hook; use Friendica\Core\Config\PConfiguration;
use Friendica\Core\L10n\L10n; use Friendica\Core\L10n\L10n;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Core\Theme; use Friendica\Core\Theme;
@ -18,6 +20,7 @@ use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Module\Login; use Friendica\Module\Login;
use Friendica\Module\Special\HTTPException as ModuleHTTPException;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\BaseURL; use Friendica\Util\BaseURL;
use Friendica\Util\ConfigFileLoader; use Friendica\Util\ConfigFileLoader;
@ -41,7 +44,7 @@ use Psr\Log\LoggerInterface;
*/ */
class App class App
{ {
public $module_class = null; /** @deprecated 2019.09 - use App\Arguments->getQueryString() */
public $query_string = ''; public $query_string = '';
public $page = []; public $page = [];
public $profile; public $profile;
@ -53,9 +56,13 @@ class App
public $page_contact; public $page_contact;
public $content; public $content;
public $data = []; public $data = [];
/** @deprecated 2019.09 - use App\Arguments->getCommand() */
public $cmd = ''; public $cmd = '';
/** @deprecated 2019.09 - use App\Arguments->getArgv() or Arguments->get() */
public $argv; public $argv;
/** @deprecated 2019.09 - use App\Arguments->getArgc() */
public $argc; public $argc;
/** @deprecated 2019.09 - Use App\Module->getName() instead */
public $module; public $module;
public $timezone; public $timezone;
public $interactive = true; public $interactive = true;
@ -93,6 +100,8 @@ class App
/** /**
* @var bool true, if the call is from an backend node (f.e. worker) * @var bool true, if the call is from an backend node (f.e. worker)
*
* @deprecated 2019.09 - use App\Module->isBackend() instead
*/ */
private $isBackend; private $isBackend;
@ -136,6 +145,16 @@ class App
*/ */
private $l10n; private $l10n;
/**
* @var App\Arguments
*/
private $args;
/**
* @var App\Module
*/
private $moduleClass;
/** /**
* Returns the current config cache of this node * Returns the current config cache of this node
* *
@ -197,6 +216,16 @@ class App
return $this->mode; 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. * Register a stylesheet file path to be included in the <head> tag of every page.
* Inclusion is done in App->initHead(). * Inclusion is done in App->initHead().
@ -247,7 +276,7 @@ class App
* *
* @throws Exception if the Basepath is not usable * @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->database = $database;
$this->config = $config; $this->config = $config;
@ -257,6 +286,8 @@ class App
$this->profiler = $profiler; $this->profiler = $profiler;
$this->logger = $logger; $this->logger = $logger;
$this->l10n = $l10n; $this->l10n = $l10n;
$this->args = $args;
$this->isBackend = $module->isBackend();
$this->profiler->reset(); $this->profiler->reset();
@ -273,59 +304,10 @@ class App
. $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
. $this->getBasePath()); . $this->getBasePath());
if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) { $this->cmd = $args->getCommand();
$this->query_string = substr($_SERVER['QUERY_STRING'], 9); $this->argv = $args->getArgv();
} elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) { $this->argc = $args->getArgc();
$this->query_string = substr($_SERVER['QUERY_STRING'], 2); $this->query_string = $args->getQueryString();
}
// 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);
// Detect mobile devices // Detect mobile devices
$mobile_detect = new MobileDetect(); $mobile_detect = new MobileDetect();
@ -346,10 +328,6 @@ class App
*/ */
public function reload() public function reload()
{ {
$this->isBackend = basename($_SERVER['PHP_SELF'], '.php') !== 'index';
$this->getMode()->determine($this->getBasePath());
if ($this->getMode()->has(App\Mode::DBAVAILABLE)) { if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
$this->profiler->update($this->config); $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 * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN
* *
* @return string Friendica server base URL * @return string Friendica server base URL
*
* @deprecated 2019.09 - use BaseUrl->get($ssl) instead
*/ */
public function getBaseURL($ssl = false) public function getBaseURL($ssl = false)
{ {
@ -453,9 +433,9 @@ class App
* - Infinite scroll data * - Infinite scroll data
* - head.tpl template * - 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 the update is 'deactivated' set it to the highest integer number (~24 days)
if ($interval < 0) { if ($interval < 0) {
@ -467,8 +447,8 @@ class App
} }
// Default title: current module called // Default title: current module called
if (empty($this->page['title']) && $this->module) { if (empty($this->page['title']) && $module->getName()) {
$this->page['title'] = ucfirst($this->module); $this->page['title'] = ucfirst($module->getName());
} }
// Prepend the sitename to the page title // Prepend the sitename to the page title
@ -520,7 +500,7 @@ class App
* - Registered footer scripts (through App->registerFooterScript()) * - Registered footer scripts (through App->registerFooterScript())
* - footer.tpl template * - footer.tpl template
*/ */
public function initFooter() private function initFooter()
{ {
// If you're just visiting, let javascript take you home // If you're just visiting, let javascript take you home
if (!empty($_SESSION['visitor_home'])) { if (!empty($_SESSION['visitor_home'])) {
@ -595,58 +575,6 @@ class App
$this->getBaseURL(); $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 * @brief Checks if the maximum number of database processes is reached
* *
@ -740,7 +668,7 @@ class App
*/ */
public function isMaxLoadReached() public function isMaxLoadReached()
{ {
if ($this->isBackend()) { if ($this->isBackend) {
$process = 'backend'; $process = 'backend';
$maxsysload = intval($this->config->get('system', 'maxloadavg')); $maxsysload = intval($this->config->get('system', 'maxloadavg'));
if ($maxsysload < 1) { if ($maxsysload < 1) {
@ -930,21 +858,13 @@ class App
} }
/** /**
* Returns the value of a argv key * @deprecated use Arguments->get() instead
* 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 * @see App\Arguments
* @param mixed $default the default value if not found
*
* @return mixed returns the value of the argument
*/ */
public function getArgumentValue($position, $default = '') public function getArgumentValue($position, $default = '')
{ {
if (array_key_exists($position, $this->argv)) { return $this->args->get($position, $default);
return $this->argv[$position];
}
return $default;
} }
/** /**
@ -973,9 +893,13 @@ class App
* request and a representation of the response. * request and a representation of the response.
* *
* This probably should change to limit the size of this monster method. * 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 { try {
// Missing DB connection: ERROR // Missing DB connection: ERROR
if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) { if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
@ -985,7 +909,7 @@ class App
// Max Load Average reached: ERROR // Max Load Average reached: ERROR
if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) { if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
header('Retry-After: 120'); 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.'); throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.');
} }
@ -993,7 +917,7 @@ class App
if (!$this->getMode()->isInstall()) { if (!$this->getMode()->isInstall()) {
// Force SSL redirection // Force SSL redirection
if ($this->baseURL->checkRedirectHttps()) { if ($this->baseURL->checkRedirectHttps()) {
System::externalRedirect($this->getBaseURL() . '/' . $this->query_string); System::externalRedirect($this->baseURL->get() . '/' . $this->args->getQueryString());
} }
Core\Session::init(); Core\Session::init();
@ -1001,7 +925,7 @@ class App
} }
// Exclude the backend processes from the session management // Exclude the backend processes from the session management
if (!$this->isBackend()) { if (!$module->isBackend()) {
$stamp1 = microtime(true); $stamp1 = microtime(true);
session_start(); session_start();
$this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack()); $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
@ -1021,7 +945,6 @@ class App
// ZRL // ZRL
if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) { if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
$this->query_string = Model\Profile::stripZrls($this->query_string);
if (!local_user()) { if (!local_user()) {
// Only continue when the given profile link seems valid // Only continue when the given profile link seems valid
// Valid profile links contain a path with "/profile/" and no query parameters // 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()) { if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
$token = $_GET['owt']; $token = $_GET['owt'];
$this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
Model\Profile::openWebAuthInit($token); Model\Profile::openWebAuthInit($token);
} }
Module\Login::sessionAuth(); Login::sessionAuth();
if (empty($_SESSION['authenticated'])) { if (empty($_SESSION['authenticated'])) {
header('X-Account-Management-Status: none'); header('X-Account-Management-Status: none');
@ -1066,9 +988,9 @@ class App
// in install mode, any url loads install module // in install mode, any url loads install module
// but we need "view" module for stylesheet // but we need "view" module for stylesheet
if ($this->getMode()->isInstall() && $this->module !== 'install') { if ($this->getMode()->isInstall() && $moduleName !== 'install') {
$this->internalRedirect('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'); $this->internalRedirect('maintenance');
} else { } else {
$this->checkURL(); $this->checkURL();
@ -1091,152 +1013,65 @@ class App
]; ];
// Compatibility with the Android Diaspora client // Compatibility with the Android Diaspora client
if ($this->module == 'stream') { if ($moduleName == 'stream') {
$this->internalRedirect('network?order=post'); $this->internalRedirect('network?order=post');
} }
if ($this->module == 'conversations') { if ($moduleName == 'conversations') {
$this->internalRedirect('message'); $this->internalRedirect('message');
} }
if ($this->module == 'commented') { if ($moduleName == 'commented') {
$this->internalRedirect('network?order=comment'); $this->internalRedirect('network?order=comment');
} }
if ($this->module == 'liked') { if ($moduleName == 'liked') {
$this->internalRedirect('network?order=comment'); $this->internalRedirect('network?order=comment');
} }
if ($this->module == 'activity') { if ($moduleName == 'activity') {
$this->internalRedirect('network?conv=1'); $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'); $this->internalRedirect('bookmarklet');
} }
if (($this->module == 'user') && ($this->cmd == 'user/edit')) { if (($moduleName == 'user') && ($this->args->getCommand() == 'user/edit')) {
$this->internalRedirect('settings'); $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'); $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 // 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) { } catch(HTTPException $e) {
Module\Special\HTTPException::rawContent($e); ModuleHTTPException::rawContent($e);
} }
$content = ''; $content = '';
try { try {
$moduleClass = $module->getClassName();
$arr = ['content' => $content]; $arr = ['content' => $content];
Core\Hook::callAll($this->module . '_mod_content', $arr); Core\Hook::callAll($moduleClass . '_mod_content', $arr);
$content = $arr['content']; $content = $arr['content'];
$arr = ['content' => call_user_func([$this->module_class, 'content'])]; $arr = ['content' => call_user_func([$moduleClass, 'content'])];
Core\Hook::callAll($this->module . '_mod_aftercontent', $arr); Core\Hook::callAll($moduleClass . '_mod_aftercontent', $arr);
$content .= $arr['content']; $content .= $arr['content'];
} catch(HTTPException $e) { } catch(HTTPException $e) {
$content = Module\Special\HTTPException::content($e); $content = ModuleHTTPException::content($e);
} }
// initialise content region // initialise content region
@ -1253,7 +1088,7 @@ class App
* all the module functions have executed so that all * all the module functions have executed so that all
* theme choices made by the modules can take effect. * 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 /* Build the page ending -- this is stuff that goes right before
* the closing </body> tag * the closing </body> tag
@ -1265,7 +1100,7 @@ class App
} }
// Add the navigation (menu) template // 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['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
$this->page['nav'] = Content\Nav::build($this); $this->page['nav'] = Content\Nav::build($this);
} }

194
src/App/Arguments.php Normal file
View file

@ -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);
}
}

View file

@ -24,27 +24,9 @@ class Mode
*/ */
private $mode; private $mode;
/** public function __construct(int $mode = 0)
* @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)
{ {
$this->basepath = $basepath->getPath(); $this->mode = $mode;
$this->database = $database;
$this->configCache = $configCache;
$this->mode = 0;
} }
/** /**
@ -54,50 +36,46 @@ class Mode
* - App::MODE_MAINTENANCE: The maintenance mode has been set * - App::MODE_MAINTENANCE: The maintenance mode has been set
* - App::MODE_NORMAL : Normal run with all features enabled * - App::MODE_NORMAL : Normal run with all features enabled
* *
* @param string $basePath the Basepath of the Application * @return Mode returns the determined mode
*
* @return Mode returns itself
* *
* @throws \Exception * @throws \Exception
*/ */
public function determine($basePath = null) public function determine(BasePath $basepath, Database $database, ConfigCache $configCache)
{ {
if (!empty($basePath)) { $mode = 0;
$this->basepath = $basePath;
$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') if (!$database->connected()) {
&& !file_exists($this->basepath . '/config/local.ini.php') return new Mode($mode);
&& !file_exists($this->basepath . '/.htconfig.php')) {
return $this;
} }
$this->mode |= Mode::LOCALCONFIGPRESENT; $mode |= Mode::DBAVAILABLE;
if (!$this->database->connected()) { if ($database->fetchFirst("SHOW TABLES LIKE 'config'") === false) {
return $this; return new Mode($mode);
} }
$this->mode |= Mode::DBAVAILABLE; $mode |= Mode::DBCONFIGAVAILABLE;
if ($this->database->fetchFirst("SHOW TABLES LIKE 'config'") === false) { if (!empty($configCache->get('system', 'maintenance')) ||
return $this;
}
$this->mode |= Mode::DBCONFIGAVAILABLE;
if (!empty($this->configCache->get('system', 'maintenance')) ||
// Don't use Config or Configuration here because we're possibly BEFORE initializing the Configuration, // Don't use Config or Configuration here because we're possibly BEFORE initializing the Configuration,
// so this could lead to a dependency circle // so this could lead to a dependency circle
!empty($this->database->selectFirst('config', ['v'], ['cat' => 'system', 'k' => 'maintenance'])['v'])) { !empty($database->selectFirst('config', ['v'], ['cat' => 'system', 'k' => 'maintenance'])['v'])) {
return $this; return new Mode($mode);
} }
$this->mode |= Mode::MAINTENANCEDISABLED; $mode |= Mode::MAINTENANCEDISABLED;
return $this; return new Mode($mode);
} }
/** /**

280
src/App/Module.php Normal file
View file

@ -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);
}
}

View file

@ -11,7 +11,6 @@ use Friendica\Database\Database;
use Friendica\Database\DBStructure; use Friendica\Database\DBStructure;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Util\Network; use Friendica\Util\Network;
use Friendica\Util\Profiler;
use Friendica\Util\Strings; use Friendica\Util\Strings;
/** /**
@ -591,8 +590,7 @@ class Installer
/** /**
* Checking the Database connection and if it is available for the current installation * Checking the Database connection and if it is available for the current installation
* *
* @param ConfigCache $configCache The configuration cache * @param Database $dba
* @param Profiler $profiler The profiler of this app
* *
* @return bool true if the check was successful, otherwise false * @return bool true if the check was successful, otherwise false
* @throws Exception * @throws Exception

View file

@ -1224,29 +1224,6 @@ class Profile
return $uid; 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 * search for Profiles
* *

View file

@ -111,7 +111,7 @@ class Install extends BaseModule
self::checkSetting($configCache, $_POST, 'database', 'database', ''); self::checkSetting($configCache, $_POST, 'database', 'database', '');
// If we cannot connect to the database, return to the previous step // 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; self::$currentWizardStep = self::DATABASE_CONFIG;
} }
@ -135,7 +135,7 @@ class Install extends BaseModule
self::checkSetting($configCache, $_POST, 'config', 'admin_email', ''); self::checkSetting($configCache, $_POST, 'config', 'admin_email', '');
// If we cannot connect to the database, return to the Database config wizard // 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; self::$currentWizardStep = self::DATABASE_CONFIG;
return; return;
} }

View file

@ -27,14 +27,14 @@ use Psr\Log\LoggerInterface;
* *
*/ */
return [ return [
'*' => [ '*' => [
// marks all class result as shared for other creations, so there's just // marks all class result as shared for other creations, so there's just
// one instance for the whole execution // one instance for the whole execution
'shared' => true, 'shared' => true,
], ],
'$basepath' => [ '$basepath' => [
'instanceOf' => Util\BasePath::class, 'instanceOf' => Util\BasePath::class,
'call' => [ 'call' => [
['getPath', [], Dice::CHAIN_CALL], ['getPath', [], Dice::CHAIN_CALL],
], ],
'constructParams' => [ 'constructParams' => [
@ -42,14 +42,14 @@ return [
$_SERVER $_SERVER
] ]
], ],
Util\BasePath::class => [ Util\BasePath::class => [
'constructParams' => [ 'constructParams' => [
dirname(__FILE__, 2), dirname(__FILE__, 2),
$_SERVER $_SERVER
] ]
], ],
Util\ConfigFileLoader::class => [ Util\ConfigFileLoader::class => [
'shared' => true, 'shared' => true,
'constructParams' => [ 'constructParams' => [
[Dice::INSTANCE => '$basepath'], [Dice::INSTANCE => '$basepath'],
], ],
@ -60,24 +60,24 @@ return [
['createCache', [], Dice::CHAIN_CALL], ['createCache', [], Dice::CHAIN_CALL],
], ],
], ],
App\Mode::class => [ App\Mode::class => [
'call' => [ 'call' => [
['determine', [], Dice::CHAIN_CALL], ['determine', [], Dice::CHAIN_CALL],
], ],
], ],
Config\Configuration::class => [ Config\Configuration::class => [
'instanceOf' => Factory\ConfigFactory::class, 'instanceOf' => Factory\ConfigFactory::class,
'call' => [ 'call' => [
['createConfig', [], Dice::CHAIN_CALL], ['createConfig', [], Dice::CHAIN_CALL],
], ],
], ],
Config\PConfiguration::class => [ Config\PConfiguration::class => [
'instanceOf' => Factory\ConfigFactory::class, 'instanceOf' => Factory\ConfigFactory::class,
'call' => [ 'call' => [
['createPConfig', [], Dice::CHAIN_CALL], ['createPConfig', [], Dice::CHAIN_CALL],
] ]
], ],
Database::class => [ Database::class => [
'constructParams' => [ 'constructParams' => [
[DICE::INSTANCE => \Psr\Log\NullLogger::class], [DICE::INSTANCE => \Psr\Log\NullLogger::class],
$_SERVER, $_SERVER,
@ -89,7 +89,7 @@ return [
* Same as: * Same as:
* $baseURL = new Util\BaseURL($configuration, $_SERVER); * $baseURL = new Util\BaseURL($configuration, $_SERVER);
*/ */
Util\BaseURL::class => [ Util\BaseURL::class => [
'constructParams' => [ 'constructParams' => [
$_SERVER, $_SERVER,
], ],
@ -106,34 +106,46 @@ return [
* $app = $dice->create(App::class, [], ['$channel' => 'index']); * $app = $dice->create(App::class, [], ['$channel' => 'index']);
* and is automatically passed as an argument with the same name * and is automatically passed as an argument with the same name
*/ */
LoggerInterface::class => [ LoggerInterface::class => [
'instanceOf' => Factory\LoggerFactory::class, 'instanceOf' => Factory\LoggerFactory::class,
'call' => [ 'call' => [
['create', [], Dice::CHAIN_CALL], ['create', [], Dice::CHAIN_CALL],
], ],
], ],
'$devLogger' => [ '$devLogger' => [
'instanceOf' => Factory\LoggerFactory::class, 'instanceOf' => Factory\LoggerFactory::class,
'call' => [ 'call' => [
['createDev', [], Dice::CHAIN_CALL], ['createDev', [], Dice::CHAIN_CALL],
] ]
], ],
Cache\ICache::class => [ Cache\ICache::class => [
'instanceOf' => Factory\CacheFactory::class, 'instanceOf' => Factory\CacheFactory::class,
'call' => [ 'call' => [
['create', [], Dice::CHAIN_CALL], ['create', [], Dice::CHAIN_CALL],
], ],
], ],
Cache\IMemoryCache::class => [ Cache\IMemoryCache::class => [
'instanceOf' => Factory\CacheFactory::class, 'instanceOf' => Factory\CacheFactory::class,
'call' => [ 'call' => [
['create', [], Dice::CHAIN_CALL], ['create', [], Dice::CHAIN_CALL],
], ],
], ],
ILock::class => [ ILock::class => [
'instanceOf' => Factory\LockFactory::class, 'instanceOf' => Factory\LockFactory::class,
'call' => [ 'call' => [
['create', [], Dice::CHAIN_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],
],
],
]; ];

View file

@ -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);
}
}

View file

@ -38,22 +38,20 @@ class ModeTest extends MockedTest
$this->setUpVfsDir(); $this->setUpVfsDir();
$this->basePathMock = \Mockery::mock(BasePath::class); $this->basePathMock = \Mockery::mock(BasePath::class);
$this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once();
$this->databaseMock = \Mockery::mock(Database::class); $this->databaseMock = \Mockery::mock(Database::class);
$this->configCacheMock = \Mockery::mock(Config\Cache\ConfigCache::class); $this->configCacheMock = \Mockery::mock(Config\Cache\ConfigCache::class);
} }
public function testItEmpty() public function testItEmpty()
{ {
$mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); $mode = new Mode();
$this->assertTrue($mode->isInstall()); $this->assertTrue($mode->isInstall());
$this->assertFalse($mode->isNormal()); $this->assertFalse($mode->isNormal());
} }
public function testWithoutConfig() 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')); $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')); $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->assertTrue($mode->isInstall());
$this->assertFalse($mode->isNormal()); $this->assertFalse($mode->isNormal());
@ -71,10 +69,11 @@ class ModeTest extends MockedTest
public function testWithoutDatabase() public function testWithoutDatabase()
{ {
$this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once();
$this->databaseMock->shouldReceive('connected')->andReturn(false)->once(); $this->databaseMock->shouldReceive('connected')->andReturn(false)->once();
$mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock);
$mode->determine();
$this->assertFalse($mode->isNormal()); $this->assertFalse($mode->isNormal());
$this->assertTrue($mode->isInstall()); $this->assertTrue($mode->isInstall());
@ -85,12 +84,13 @@ class ModeTest extends MockedTest
public function testWithoutDatabaseSetup() public function testWithoutDatabaseSetup()
{ {
$this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once();
$this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('connected')->andReturn(true)->once();
$this->databaseMock->shouldReceive('fetchFirst') $this->databaseMock->shouldReceive('fetchFirst')
->with('SHOW TABLES LIKE \'config\'')->andReturn(false)->once(); ->with('SHOW TABLES LIKE \'config\'')->andReturn(false)->once();
$mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock);
$mode->determine();
$this->assertFalse($mode->isNormal()); $this->assertFalse($mode->isNormal());
$this->assertTrue($mode->isInstall()); $this->assertTrue($mode->isInstall());
@ -100,14 +100,15 @@ class ModeTest extends MockedTest
public function testWithMaintenanceMode() public function testWithMaintenanceMode()
{ {
$this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once();
$this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('connected')->andReturn(true)->once();
$this->databaseMock->shouldReceive('fetchFirst') $this->databaseMock->shouldReceive('fetchFirst')
->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once(); ->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once();
$this->configCacheMock->shouldReceive('get')->with('system', 'maintenance') $this->configCacheMock->shouldReceive('get')->with('system', 'maintenance')
->andReturn(true)->once(); ->andReturn(true)->once();
$mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock);
$mode->determine();
$this->assertFalse($mode->isNormal()); $this->assertFalse($mode->isNormal());
$this->assertFalse($mode->isInstall()); $this->assertFalse($mode->isInstall());
@ -118,6 +119,8 @@ class ModeTest extends MockedTest
public function testNormalMode() public function testNormalMode()
{ {
$this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once();
$this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('connected')->andReturn(true)->once();
$this->databaseMock->shouldReceive('fetchFirst') $this->databaseMock->shouldReceive('fetchFirst')
->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once(); ->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once();
@ -127,8 +130,7 @@ class ModeTest extends MockedTest
->with('config', ['v'], ['cat' => 'system', 'k' => 'maintenance']) ->with('config', ['v'], ['cat' => 'system', 'k' => 'maintenance'])
->andReturn(['v' => null])->once(); ->andReturn(['v' => null])->once();
$mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock);
$mode->determine();
$this->assertTrue($mode->isNormal()); $this->assertTrue($mode->isNormal());
$this->assertFalse($mode->isInstall()); $this->assertFalse($mode->isInstall());
@ -142,6 +144,8 @@ class ModeTest extends MockedTest
*/ */
public function testDisabledMaintenance() public function testDisabledMaintenance()
{ {
$this->basePathMock->shouldReceive('getPath')->andReturn($this->root->url())->once();
$this->databaseMock->shouldReceive('connected')->andReturn(true)->once(); $this->databaseMock->shouldReceive('connected')->andReturn(true)->once();
$this->databaseMock->shouldReceive('fetchFirst') $this->databaseMock->shouldReceive('fetchFirst')
->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once(); ->with('SHOW TABLES LIKE \'config\'')->andReturn(true)->once();
@ -151,8 +155,7 @@ class ModeTest extends MockedTest
->with('config', ['v'], ['cat' => 'system', 'k' => 'maintenance']) ->with('config', ['v'], ['cat' => 'system', 'k' => 'maintenance'])
->andReturn(['v' => '0'])->once(); ->andReturn(['v' => '0'])->once();
$mode = new Mode($this->basePathMock, $this->databaseMock, $this->configCacheMock); $mode = (new Mode())->determine($this->basePathMock, $this->databaseMock, $this->configCacheMock);
$mode->determine();
$this->assertTrue($mode->isNormal()); $this->assertTrue($mode->isNormal());
$this->assertFalse($mode->isInstall()); $this->assertFalse($mode->isInstall());
@ -160,4 +163,18 @@ class ModeTest extends MockedTest
$this->assertTrue($mode->has(Mode::DBCONFIGAVAILABLE)); $this->assertTrue($mode->has(Mode::DBCONFIGAVAILABLE));
$this->assertTrue($mode->has(Mode::MAINTENANCEDISABLED)); $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);
}
} }

View file

@ -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);
}
}