Merge pull request #6969 from MrPetovan/task/router

Add rule-based router
This commit is contained in:
Philipp 2019-04-06 11:00:00 +02:00 committed by GitHub
commit f69d73b5ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 228 additions and 78 deletions

View file

@ -13,6 +13,7 @@ Version 2019.06 (UNRELEASED) (2019-06-?)
Added syslog and stream Logger [nupplaphil]
Added storage move cronjob [MrPetovan]
Added collapsible panel for connector permission fields [MrPetovan]
Added rule-based router [MrPetovan]
Closed Issues:
6303, 6478, 6319, 6921, 6903, 6943

View file

@ -35,6 +35,7 @@
"michelf/php-markdown": "^1.7",
"mobiledetect/mobiledetectlib": "2.8.*",
"monolog/monolog": "^1.24",
"nikic/fast-route": "^1.3",
"paragonie/random_compat": "^2.0",
"pear/text_languagedetect": "1.*",
"psr/container": "^1.0",

104
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8897c1f6912cc9b889534a8c59deead1",
"content-hash": "cf9846983bd7e4eb34f93ab6b493736b",
"packages": [
{
"name": "asika/simple-console",
@ -887,6 +887,52 @@
],
"time": "2018-11-05T09:00:11+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"type": "library",
"autoload": {
"psr-4": {
"FastRoute\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"description": "Fast request router for PHP",
"keywords": [
"router",
"routing"
],
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "npm-asset/cropperjs",
"version": "1.2.2",
@ -1083,22 +1129,6 @@
"require": {
"npm-asset/ev-emitter": ">=1.0.0,<2.0.0"
},
"require-dev": {
"npm-asset/chalk": ">=1.1.1,<2.0.0",
"npm-asset/cheerio": ">=0.19.0,<0.20.0",
"npm-asset/gulp": ">=3.9.0,<4.0.0",
"npm-asset/gulp-jshint": ">=1.11.2,<2.0.0",
"npm-asset/gulp-json-lint": ">=0.1.0,<0.2.0",
"npm-asset/gulp-rename": ">=1.2.2,<2.0.0",
"npm-asset/gulp-replace": ">=0.5.4,<0.6.0",
"npm-asset/gulp-requirejs-optimize": "dev-github:metafizzy/gulp-requirejs-optimize",
"npm-asset/gulp-uglify": ">=1.4.2,<2.0.0",
"npm-asset/gulp-util": ">=3.0.7,<4.0.0",
"npm-asset/highlight.js": ">=8.9.1,<9.0.0",
"npm-asset/marked": ">=0.3.5,<0.4.0",
"npm-asset/minimist": ">=1.2.0,<2.0.0",
"npm-asset/transfob": ">=1.0.0,<2.0.0"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1144,14 +1174,6 @@
"reference": null,
"shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3"
},
"require-dev": {
"npm-asset/grunt": "~0.4.2",
"npm-asset/grunt-contrib-cssmin": "~0.9.0",
"npm-asset/grunt-contrib-jshint": "~0.6.3",
"npm-asset/grunt-contrib-less": "~0.11.0",
"npm-asset/grunt-contrib-uglify": "~0.4.0",
"npm-asset/grunt-contrib-watch": "~0.6.1"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1185,32 +1207,6 @@
"reference": null,
"shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02"
},
"require-dev": {
"npm-asset/commitplease": "2.0.0",
"npm-asset/core-js": "0.9.17",
"npm-asset/grunt": "0.4.5",
"npm-asset/grunt-babel": "5.0.1",
"npm-asset/grunt-cli": "0.1.13",
"npm-asset/grunt-compare-size": "0.4.0",
"npm-asset/grunt-contrib-jshint": "0.11.2",
"npm-asset/grunt-contrib-uglify": "0.9.2",
"npm-asset/grunt-contrib-watch": "0.6.1",
"npm-asset/grunt-git-authors": "2.0.1",
"npm-asset/grunt-jscs": "2.1.0",
"npm-asset/grunt-jsonlint": "1.0.4",
"npm-asset/grunt-npmcopy": "0.1.0",
"npm-asset/gzip-js": "0.3.2",
"npm-asset/jsdom": "5.6.1",
"npm-asset/load-grunt-tasks": "1.0.0",
"npm-asset/qunit-assert-step": "1.0.3",
"npm-asset/qunitjs": "1.17.1",
"npm-asset/requirejs": "2.1.17",
"npm-asset/sinon": "1.10.3",
"npm-asset/sizzle": "2.2.1",
"npm-asset/strip-json-comments": "1.0.3",
"npm-asset/testswarm": "1.1.0",
"npm-asset/win-spawn": "2.0.0"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1361,12 +1357,6 @@
"reference": null,
"shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5"
},
"require-dev": {
"npm-asset/grunt": "~0.4.1",
"npm-asset/grunt-contrib-connect": "~0.5.0",
"npm-asset/grunt-contrib-jshint": "~0.7.1",
"npm-asset/grunt-contrib-uglify": "~0.2.7"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {

View file

@ -433,7 +433,11 @@ For `select`, **field** is:
- [3] (String): Additional help text; Optional, default is none.
- [4] (Array): Associative array of options. Item key is option value, item value is option label; Mandatory.
### route_collection
Called just before dispatching the router.
Hook data is a `\FastRoute\RouterCollector` object that should be used to add addon routes pointing to classes.
**Notice**: The class whose name is provided in the route handler must be reachable via auto-loader.
## Complete list of hook callbacks
@ -610,6 +614,7 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep-
Hook::callAll('load_config');
Hook::callAll('head');
Hook::callAll('footer');
Hook::callAll('route_collection');
### src/Model/Item.php

View file

@ -8,8 +8,10 @@ use Detection\MobileDetect;
use DOMDocument;
use DOMXPath;
use Exception;
use FastRoute\RouteCollector;
use Friendica\Core\Config\Cache\IConfigCache;
use Friendica\Core\Config\Configuration;
use Friendica\Core\Hook;
use Friendica\Core\Theme;
use Friendica\Database\DBA;
use Friendica\Model\Profile;
@ -35,7 +37,6 @@ use Psr\Log\LoggerInterface;
*/
class App
{
public $module_loaded = false;
public $module_class = null;
public $query_string = '';
public $page = [];
@ -77,6 +78,11 @@ class App
*/
private $mode;
/**
* @var App\Router
*/
private $router;
/**
* @var string The App URL path
*/
@ -172,6 +178,11 @@ class App
return $this->mode;
}
public function getRouter()
{
return $this->router;
}
/**
* Register a stylesheet file path to be included in the <head> tag of every page.
* Inclusion is done in App->initHead().
@ -217,20 +228,22 @@ class App
*
* @param Configuration $config The Configuration
* @param App\Mode $mode The mode of this Friendica app
* @param App\Router $router The router of this Friendica app
* @param LoggerInterface $logger The current app logger
* @param Profiler $profiler The profiler of this application
* @param bool $isBackend Whether it is used for backend or frontend (Default true=backend)
*
* @throws Exception if the Basepath is not usable
*/
public function __construct(Configuration $config, App\Mode $mode, LoggerInterface $logger, Profiler $profiler, $isBackend = true)
public function __construct(Configuration $config, App\Mode $mode, App\Router $router, LoggerInterface $logger, Profiler $profiler, $isBackend = true)
{
BaseObject::setApp($this);
$this->logger = $logger;
$this->config = $config;
$this->profiler = $profiler;
$this->mode = $mode;
$this->router = $router;
$this->profiler = $profiler;
$this->logger = $logger;
$this->checkBackend($isBackend);
$this->checkFriendicaApp();
@ -1247,9 +1260,24 @@ class App
$this->module = "login";
}
$privateapps = $this->config->get('config', 'private_addons', false);
if (Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
/*
* 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();
Hook::callAll('route_collection', $this->router->getRouteCollector());
$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(Core\L10n::t("You must be logged in to use addons. "));
} else {
@ -1257,24 +1285,21 @@ class App
if (function_exists($this->module . '_module')) {
LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
$this->module_class = 'Friendica\\LegacyModule';
$this->module_loaded = true;
}
}
}
// Controller class routing
if (! $this->module_loaded && class_exists('Friendica\\Module\\' . ucfirst($this->module))) {
// Then we try name-matching a Friendica\Module class
if (!$this->module_class && class_exists('Friendica\\Module\\' . ucfirst($this->module))) {
$this->module_class = 'Friendica\\Module\\' . ucfirst($this->module);
$this->module_loaded = true;
}
/* If not, next look for a 'standard' program module in the 'mod' directory
/* Finally, we look for a 'standard' program module in the 'mod' directory
* We emulate a Module class through the LegacyModule class
*/
if (! $this->module_loaded && file_exists("mod/{$this->module}.php")) {
if (!$this->module_class && file_exists("mod/{$this->module}.php")) {
LegacyModule::setModuleFile("mod/{$this->module}.php");
$this->module_class = 'Friendica\\LegacyModule';
$this->module_loaded = true;
}
/* The URL provided does not resolve to a valid module.
@ -1286,7 +1311,7 @@ class App
*
* Otherwise we are going to emit a 404 not found.
*/
if (! $this->module_loaded) {
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();
@ -1310,7 +1335,7 @@ class App
$content = '';
// Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
if ($this->module_loaded) {
if ($this->module_class) {
$this->page['page_title'] = $this->module;
$placeholder = '';
@ -1336,7 +1361,7 @@ class App
$func($this);
}
if ($this->module_loaded) {
if ($this->module_class) {
if (! $this->error && $_SERVER['REQUEST_METHOD'] === 'POST') {
Core\Hook::callAll($this->module . '_mod_post', $_POST);
call_user_func([$this->module_class, 'post']);

83
src/App/Router.php Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace Friendica\App;
use FastRoute\DataGenerator\GroupCountBased;
use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std;
use Friendica\Module;
/**
* Wrapper for FastRoute\Router
*
* This wrapper only makes use of a subset of the router features, mainly parses a route rule to return the relevant
* module class.
*
* Actual routes are defined in App->collectRoutes.
*
* @package Friendica\App
*/
class Router
{
/** @var RouteCollector */
protected $routeCollector;
/**
* Static declaration of Friendica routes.
*
* Supports:
* - Route groups
* - Variable parts
* Disregards:
* - HTTP method other than GET
* - Named parameters
*
* Handler must be the name of a class extending Friendica\BaseModule.
*
* @brief Static declaration of Friendica routes.
*/
public function collectRoutes()
{
$this->routeCollector->addRoute(['GET', 'POST'], '/itemsource[/{guid}]', Module\Itemsource::class);
}
public function __construct(RouteCollector $routeCollector = null)
{
if (!$routeCollector) {
$routeCollector = new RouteCollector(new Std(), new GroupCountBased());
}
$this->routeCollector = $routeCollector;
}
public function getRouteCollector()
{
return $this->routeCollector;
}
/**
* Returns the relevant module class name for the given page URI or NULL if no route rule matched.
*
* @param string $cmd The path component of the request URL without the query string
* @return string|null A Friendica\BaseModule-extending class name if a route rule matched
*/
public function getModuleClass($cmd)
{
$cmd = '/' . ltrim($cmd, '/');
$dispatcher = new \FastRoute\Dispatcher\GroupCountBased($this->routeCollector->getData());
$moduleClass = null;
// @TODO: Enable method-specific modules
$httpMethod = 'GET';
$routeInfo = $dispatcher->dispatch($httpMethod, $cmd);
if ($routeInfo[0] === Dispatcher::FOUND) {
$moduleClass = $routeInfo[1];
}
return $moduleClass;
}
}

View file

@ -24,6 +24,7 @@ class DependencyFactory
{
$basePath = BasePath::create($directory, $_SERVER);
$mode = new App\Mode($basePath);
$router = new App\Router();
$configLoader = new Config\ConfigFileLoader($basePath, $mode);
$configCache = Factory\ConfigFactory::createCache($configLoader);
$profiler = Factory\ProfilerFactory::create($configCache);
@ -34,6 +35,6 @@ class DependencyFactory
$logger = Factory\LoggerFactory::create($channel, $config, $profiler);
Factory\LoggerFactory::createDev($channel, $config, $profiler);
return new App($config, $mode, $logger, $profiler, $isBackend);
return new App($config, $mode, $router, $logger, $profiler, $isBackend);
}
}

View file

@ -37,7 +37,6 @@ class Itemsource extends \Friendica\BaseModule
$conversation = Model\Conversation::getByItemUri($item['uri']);
$guid = $item['guid'];
$item_id = $item['id'];
$item_uri = $item['uri'];
$source = $conversation['source'];

View file

@ -50,6 +50,7 @@ class ApiTest extends DatabaseTest
{
$basePath = BasePath::create(dirname(__DIR__) . '/../');
$mode = new App\Mode($basePath);
$router = new App\Router();
$configLoader = new ConfigFileLoader($basePath, $mode);
$configCache = Factory\ConfigFactory::createCache($configLoader);
$profiler = Factory\ProfilerFactory::create($configCache);
@ -57,7 +58,7 @@ class ApiTest extends DatabaseTest
$config = Factory\ConfigFactory::createConfig($configCache);
Factory\ConfigFactory::createPConfig($configCache);
$logger = Factory\LoggerFactory::create('test', $config, $profiler);
$this->app = new App($config, $mode, $logger, $profiler, false);
$this->app = new App($config, $mode, $router, $logger, $profiler, false);
parent::setUp();

View file

@ -0,0 +1,42 @@
<?php
namespace Friendica\Test\src\App;
use Friendica\App\Router;
use PHPUnit\Framework\TestCase;
class RouterTest extends TestCase
{
public function testGetModuleClass()
{
$router = new Router();
$routeCollector = $router->getRouteCollector();
$routeCollector->addRoute(['GET'], '/', 'IndexModuleClassName');
$routeCollector->addRoute(['GET'], '/test', 'TestModuleClassName');
$routeCollector->addRoute(['GET'], '/test/sub', 'TestSubModuleClassName');
$routeCollector->addRoute(['GET'], '/optional[/option]', 'OptionalModuleClassName');
$routeCollector->addRoute(['GET'], '/variable/{var}', 'VariableModuleClassName');
$routeCollector->addRoute(['GET'], '/optionalvariable[/{option}]', 'OptionalVariableModuleClassName');
$routeCollector->addRoute(['POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'], '/unsupported', 'UnsupportedMethodModuleClassName');
$this->assertEquals('IndexModuleClassName', $router->getModuleClass('/'));
$this->assertEquals('TestModuleClassName', $router->getModuleClass('/test'));
$this->assertNull($router->getModuleClass('/tes'));
$this->assertEquals('TestSubModuleClassName', $router->getModuleClass('/test/sub'));
$this->assertEquals('OptionalModuleClassName', $router->getModuleClass('/optional'));
$this->assertEquals('OptionalModuleClassName', $router->getModuleClass('/optional/option'));
$this->assertNull($router->getModuleClass('/optional/opt'));
$this->assertEquals('VariableModuleClassName', $router->getModuleClass('/variable/123abc'));
$this->assertNull($router->getModuleClass('/variable'));
$this->assertEquals('OptionalVariableModuleClassName', $router->getModuleClass('/optionalvariable'));
$this->assertEquals('OptionalVariableModuleClassName', $router->getModuleClass('/optionalvariable/123abc'));
$this->assertNull($router->getModuleClass('/unsupported'));
}
}

View file

@ -15,6 +15,7 @@ class DBATest extends DatabaseTest
{
$basePath = BasePath::create(dirname(__DIR__) . '/../../');
$mode = new App\Mode($basePath);
$router = new App\Router();
$configLoader = new ConfigFileLoader($basePath, $mode);
$configCache = Factory\ConfigFactory::createCache($configLoader);
$profiler = Factory\ProfilerFactory::create($configCache);
@ -22,7 +23,7 @@ class DBATest extends DatabaseTest
$config = Factory\ConfigFactory::createConfig($configCache);
Factory\ConfigFactory::createPConfig($configCache);
$logger = Factory\LoggerFactory::create('test', $config, $profiler);
$this->app = new App($config, $mode, $logger, $profiler, false);
$this->app = new App($config, $mode, $router, $logger, $profiler, false);
parent::setUp();

View file

@ -15,6 +15,7 @@ class DBStructureTest extends DatabaseTest
{
$basePath = BasePath::create(dirname(__DIR__) . '/../../');
$mode = new App\Mode($basePath);
$router = new App\Router();
$configLoader = new ConfigFileLoader($basePath, $mode);
$configCache = Factory\ConfigFactory::createCache($configLoader);
$profiler = Factory\ProfilerFactory::create($configCache);
@ -22,7 +23,7 @@ class DBStructureTest extends DatabaseTest
$config = Factory\ConfigFactory::createConfig($configCache);
Factory\ConfigFactory::createPConfig($configCache);
$logger = Factory\LoggerFactory::create('test', $config, $profiler);
$this->app = new App($config, $mode, $logger, $profiler, false);
$this->app = new App($config, $mode, $router, $logger, $profiler, false);
parent::setUp();
}