2019-04-30 18:32:33 -04:00
< ? php
2020-02-08 11:16:42 -05:00
/**
2021-03-29 02:40:20 -04:00
* @ copyright Copyright ( C ) 2010 - 2021 , the Friendica project
2020-02-08 11:16:42 -05:00
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
*/
2019-04-30 18:32:33 -04:00
namespace Friendica\App ;
use FastRoute\DataGenerator\GroupCountBased ;
use FastRoute\Dispatcher ;
use FastRoute\RouteCollector ;
use FastRoute\RouteParser\Std ;
2020-07-27 01:57:44 -04:00
use Friendica\Core\Cache\Duration ;
use Friendica\Core\Cache\ICache ;
2019-09-26 15:18:01 -04:00
use Friendica\Core\Hook ;
2020-01-19 16:38:33 -05:00
use Friendica\Core\L10n ;
2021-07-25 00:31:48 -04:00
use Friendica\Core\Lock\ILock ;
2019-10-11 11:55:02 -04:00
use Friendica\Network\HTTPException ;
2019-04-30 18:32:33 -04:00
/**
* 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
{
2021-06-09 03:27:42 -04:00
const DELETE = 'DELETE' ;
const GET = 'GET' ;
const PATCH = 'PATCH' ;
const POST = 'POST' ;
const PUT = 'PUT' ;
const OPTIONS = 'OPTIONS' ;
2019-09-26 15:18:01 -04:00
const ALLOWED_METHODS = [
2020-11-26 02:02:31 -05:00
self :: DELETE ,
2019-09-26 15:18:01 -04:00
self :: GET ,
2020-11-26 02:02:31 -05:00
self :: PATCH ,
self :: POST ,
self :: PUT ,
2021-06-09 03:27:42 -04:00
self :: OPTIONS
2019-09-26 15:18:01 -04:00
];
2019-04-30 18:32:33 -04:00
/** @var RouteCollector */
protected $routeCollector ;
/**
2019-09-26 15:18:01 -04:00
* @ var string The HTTP method
2019-04-30 18:32:33 -04:00
*/
2019-09-26 15:18:01 -04:00
private $httpMethod ;
2019-05-02 16:03:27 -04:00
2019-11-05 00:03:05 -05:00
/**
* @ var array Module parameters
*/
private $parameters = [];
2020-01-19 16:38:33 -05:00
/** @var L10n */
private $l10n ;
2020-07-27 01:57:44 -04:00
/** @var ICache */
private $cache ;
2021-07-25 00:31:48 -04:00
/** @var ILock */
private $lock ;
2020-07-27 01:57:44 -04:00
/** @var string */
private $baseRoutesFilepath ;
2019-09-26 15:18:01 -04:00
/**
2020-07-27 01:57:44 -04:00
* @ param array $server The $_SERVER variable
* @ param string $baseRoutesFilepath The path to a base routes file to leverage cache , can be empty
* @ param L10n $l10n
* @ param ICache $cache
* @ param RouteCollector | null $routeCollector
2019-09-26 15:18:01 -04:00
*/
2021-07-25 00:31:48 -04:00
public function __construct ( array $server , string $baseRoutesFilepath , L10n $l10n , ICache $cache , ILock $lock , RouteCollector $routeCollector = null )
2019-09-26 15:18:01 -04:00
{
2020-07-27 01:57:44 -04:00
$this -> baseRoutesFilepath = $baseRoutesFilepath ;
2020-01-19 16:38:33 -05:00
$this -> l10n = $l10n ;
2020-07-27 01:57:44 -04:00
$this -> cache = $cache ;
2021-07-25 00:31:48 -04:00
$this -> lock = $lock ;
2020-01-19 16:38:33 -05:00
2019-09-26 15:18:01 -04:00
$httpMethod = $server [ 'REQUEST_METHOD' ] ? ? self :: GET ;
$this -> httpMethod = in_array ( $httpMethod , self :: ALLOWED_METHODS ) ? $httpMethod : self :: GET ;
2019-05-02 16:03:27 -04:00
2019-09-26 15:18:01 -04:00
$this -> routeCollector = isset ( $routeCollector ) ?
$routeCollector :
new RouteCollector ( new Std (), new GroupCountBased ());
2020-10-15 21:45:51 -04:00
if ( $this -> baseRoutesFilepath && ! file_exists ( $this -> baseRoutesFilepath )) {
throw new HTTPException\InternalServerErrorException ( 'Routes file path does\'n exist.' );
}
2019-09-26 15:18:01 -04:00
}
2019-05-02 16:03:27 -04:00
2019-09-26 15:18:01 -04:00
/**
2020-07-27 01:57:44 -04:00
* This will be called either automatically if a base routes file path was submitted ,
* or can be called manually with a custom route array .
*
2019-09-26 15:18:01 -04:00
* @ param array $routes The routes to add to the Router
*
* @ return self The router instance with the loaded routes
*
2019-10-11 11:55:02 -04:00
* @ throws HTTPException\InternalServerErrorException In case of invalid configs
2019-09-26 15:18:01 -04:00
*/
2019-11-06 22:34:38 -05:00
public function loadRoutes ( array $routes )
2019-09-26 15:18:01 -04:00
{
$routeCollector = ( isset ( $this -> routeCollector ) ?
$this -> routeCollector :
new RouteCollector ( new Std (), new GroupCountBased ()));
2019-11-06 22:34:38 -05:00
$this -> addRoutes ( $routeCollector , $routes );
$this -> routeCollector = $routeCollector ;
2020-07-27 01:57:44 -04:00
// Add routes from addons
Hook :: callAll ( 'route_collection' , $this -> routeCollector );
2019-11-06 22:34:38 -05:00
return $this ;
}
private function addRoutes ( RouteCollector $routeCollector , array $routes )
{
2019-09-26 15:18:01 -04:00
foreach ( $routes as $route => $config ) {
if ( $this -> isGroup ( $config )) {
$this -> addGroup ( $route , $config , $routeCollector );
} elseif ( $this -> isRoute ( $config )) {
$routeCollector -> addRoute ( $config [ 1 ], $route , $config [ 0 ]);
} else {
2019-10-11 11:55:02 -04:00
throw new HTTPException\InternalServerErrorException ( " Wrong route config for route ' " . print_r ( $route , true ) . " ' " );
2019-09-26 15:18:01 -04:00
}
}
}
2019-05-01 15:29:04 -04:00
2019-09-26 15:18:01 -04:00
/**
* Adds a group of routes to a given group
*
* @ param string $groupRoute The route of the group
* @ param array $routes The routes of the group
* @ param RouteCollector $routeCollector The route collector to add this group
*/
private function addGroup ( string $groupRoute , array $routes , RouteCollector $routeCollector )
{
$routeCollector -> addGroup ( $groupRoute , function ( RouteCollector $routeCollector ) use ( $routes ) {
2019-11-06 22:34:38 -05:00
$this -> addRoutes ( $routeCollector , $routes );
2019-05-01 15:29:04 -04:00
});
2019-09-26 15:18:01 -04:00
}
2019-05-13 01:38:15 -04:00
2019-09-26 15:18:01 -04:00
/**
* Returns true in case the config is a group config
*
* @ param array $config
*
* @ return bool
*/
private function isGroup ( array $config )
{
return
is_array ( $config ) &&
is_string ( array_keys ( $config )[ 0 ]) &&
// This entry should NOT be a BaseModule
( substr ( array_keys ( $config )[ 0 ], 0 , strlen ( 'Friendica\Module' )) !== 'Friendica\Module' ) &&
// The second argument is an array (another routes)
is_array ( array_values ( $config )[ 0 ]);
2019-04-30 18:32:33 -04:00
}
2019-09-26 15:18:01 -04:00
/**
* Returns true in case the config is a route config
*
* @ param array $config
*
* @ return bool
*/
private function isRoute ( array $config )
2019-04-30 18:32:33 -04:00
{
2019-09-26 15:18:01 -04:00
return
// The config array should at least have one entry
! empty ( $config [ 0 ]) &&
// This entry should be a BaseModule
( substr ( $config [ 0 ], 0 , strlen ( 'Friendica\Module' )) === 'Friendica\Module' ) &&
// Either there is no other argument
( empty ( $config [ 1 ]) ||
// Or the second argument is an array (HTTP-Methods)
is_array ( $config [ 1 ]));
2019-04-30 18:32:33 -04:00
}
2019-09-26 15:18:01 -04:00
/**
* The current route collector
*
* @ return RouteCollector | null
*/
2019-04-30 18:32:33 -04:00
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
2019-09-26 15:18:01 -04:00
*
2019-10-11 11:55:02 -04:00
* @ return string A Friendica\BaseModule - extending class name if a route rule matched
*
* @ throws HTTPException\InternalServerErrorException
* @ throws HTTPException\MethodNotAllowedException If a rule matched but the method didn ' t
* @ throws HTTPException\NotFoundException If no rule matched
2019-04-30 18:32:33 -04:00
*/
public function getModuleClass ( $cmd )
{
$cmd = '/' . ltrim ( $cmd , '/' );
2020-07-27 01:57:44 -04:00
$dispatcher = new Dispatcher\GroupCountBased ( $this -> getCachedDispatchData ());
2019-04-30 18:32:33 -04:00
$moduleClass = null ;
2019-11-05 00:03:05 -05:00
$this -> parameters = [];
2019-04-30 18:32:33 -04:00
2019-09-26 15:18:01 -04:00
$routeInfo = $dispatcher -> dispatch ( $this -> httpMethod , $cmd );
2019-04-30 18:32:33 -04:00
if ( $routeInfo [ 0 ] === Dispatcher :: FOUND ) {
$moduleClass = $routeInfo [ 1 ];
2019-11-05 00:03:05 -05:00
$this -> parameters = $routeInfo [ 2 ];
2019-10-11 11:55:02 -04:00
} elseif ( $routeInfo [ 0 ] === Dispatcher :: METHOD_NOT_ALLOWED ) {
2020-01-19 16:38:33 -05:00
throw new HTTPException\MethodNotAllowedException ( $this -> l10n -> t ( 'Method not allowed for this module. Allowed method(s): %s' , implode ( ', ' , $routeInfo [ 1 ])));
2019-10-11 11:55:02 -04:00
} else {
2020-01-19 16:38:33 -05:00
throw new HTTPException\NotFoundException ( $this -> l10n -> t ( 'Page not found.' ));
2019-04-30 18:32:33 -04:00
}
return $moduleClass ;
}
2019-11-05 00:03:05 -05:00
/**
* Returns the module parameters .
*
* @ return array parameters
*/
public function getModuleParameters ()
{
return $this -> parameters ;
}
2020-07-27 01:57:44 -04:00
/**
* If a base routes file path has been provided , we can load routes from it if the cache misses .
*
* @ return array
* @ throws HTTPException\InternalServerErrorException
*/
private function getDispatchData ()
{
$dispatchData = [];
2020-10-15 21:45:51 -04:00
if ( $this -> baseRoutesFilepath ) {
2020-07-27 01:57:44 -04:00
$dispatchData = require $this -> baseRoutesFilepath ;
if ( ! is_array ( $dispatchData )) {
throw new HTTPException\InternalServerErrorException ( 'Invalid base routes file' );
}
}
$this -> loadRoutes ( $dispatchData );
return $this -> routeCollector -> getData ();
}
/**
* We cache the dispatch data for speed , as computing the current routes ( version 2020.09 )
* takes about 850 ms for each requests .
*
* The cached " routerDispatchData " lasts for a day , and must be cleared manually when there
* is any changes in the enabled addons list .
*
2020-10-15 11:45:15 -04:00
* Additionally , we check for the base routes file last modification time to automatically
* trigger re - computing the dispatch data .
*
2020-07-27 01:57:44 -04:00
* @ return array | mixed
* @ throws HTTPException\InternalServerErrorException
*/
private function getCachedDispatchData ()
{
$routerDispatchData = $this -> cache -> get ( 'routerDispatchData' );
2020-10-15 11:45:15 -04:00
$lastRoutesFileModifiedTime = $this -> cache -> get ( 'lastRoutesFileModifiedTime' );
$forceRecompute = false ;
2020-07-27 01:57:44 -04:00
2020-10-15 21:45:51 -04:00
if ( $this -> baseRoutesFilepath ) {
2020-10-15 11:45:15 -04:00
$routesFileModifiedTime = filemtime ( $this -> baseRoutesFilepath );
$forceRecompute = $lastRoutesFileModifiedTime != $routesFileModifiedTime ;
}
if ( ! $forceRecompute && $routerDispatchData ) {
2020-07-27 01:57:44 -04:00
return $routerDispatchData ;
}
2021-07-25 00:31:48 -04:00
if ( ! $this -> lock -> acquire ( 'getCachedDispatchData' , 0 )) {
// Immediately return uncached data when we can't aquire a lock
return $this -> getDispatchData ();
}
2020-07-27 01:57:44 -04:00
$routerDispatchData = $this -> getDispatchData ();
$this -> cache -> set ( 'routerDispatchData' , $routerDispatchData , Duration :: DAY );
2020-10-15 11:45:15 -04:00
if ( ! empty ( $routesFileModifiedTime )) {
2021-07-25 00:31:48 -04:00
$this -> cache -> set ( 'lastRoutesFileModifiedTime' , $routesFileModifiedTime , Duration :: MONTH );
}
if ( $this -> lock -> isLocked ( 'getCachedDispatchData' )) {
$this -> lock -> release ( 'getCachedDispatchData' );
2020-10-15 11:45:15 -04:00
}
2020-07-27 01:57:44 -04:00
return $routerDispatchData ;
}
2019-04-30 18:32:33 -04:00
}