2019-04-30 22:32:33 +00:00
< ? php
2020-02-08 16:16:42 +00:00
/**
2022-01-02 07:27:47 +00:00
* @ copyright Copyright ( C ) 2010 - 2022 , the Friendica project
2020-02-08 16:16:42 +00: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 22:32:33 +00:00
namespace Friendica\App ;
2021-11-19 21:47:49 +00:00
use Dice\Dice ;
2019-04-30 22:32:33 +00:00
use FastRoute\DataGenerator\GroupCountBased ;
use FastRoute\Dispatcher ;
use FastRoute\RouteCollector ;
use FastRoute\RouteParser\Std ;
2021-11-19 21:47:49 +00:00
use Friendica\Capabilities\ICanHandleRequests ;
use Friendica\Core\Addon ;
2021-10-23 08:49:27 +00:00
use Friendica\Core\Cache\Enum\Duration ;
2021-10-26 19:44:29 +00:00
use Friendica\Core\Cache\Capability\ICanCache ;
2021-11-19 21:47:49 +00:00
use Friendica\Core\Config\Capability\IManageConfigValues ;
2019-09-26 19:18:01 +00:00
use Friendica\Core\Hook ;
2020-01-19 21:38:33 +00:00
use Friendica\Core\L10n ;
2021-10-26 19:44:29 +00:00
use Friendica\Core\Lock\Capability\ICanLock ;
2022-10-20 19:29:52 +00:00
use Friendica\Core\Session\Capability\IHandleUserSessions ;
2021-11-19 21:47:49 +00:00
use Friendica\LegacyModule ;
use Friendica\Module\HTTPException\MethodNotAllowed ;
use Friendica\Module\HTTPException\PageNotFound ;
2022-01-02 19:25:32 +00:00
use Friendica\Module\Special\Options ;
2019-10-11 15:55:02 +00:00
use Friendica\Network\HTTPException ;
2021-11-19 21:47:49 +00:00
use Friendica\Network\HTTPException\MethodNotAllowedException ;
use Friendica\Network\HTTPException\NotFoundException ;
2022-01-03 18:19:47 +00:00
use Friendica\Util\Router\FriendicaGroupCountBased ;
2021-12-10 20:15:15 +00:00
use Psr\Log\LoggerInterface ;
2019-04-30 22:32:33 +00: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 07:27:42 +00:00
const DELETE = 'DELETE' ;
const GET = 'GET' ;
const PATCH = 'PATCH' ;
const POST = 'POST' ;
const PUT = 'PUT' ;
const OPTIONS = 'OPTIONS' ;
2019-09-26 19:18:01 +00:00
const ALLOWED_METHODS = [
2020-11-26 07:02:31 +00:00
self :: DELETE ,
2019-09-26 19:18:01 +00:00
self :: GET ,
2020-11-26 07:02:31 +00:00
self :: PATCH ,
self :: POST ,
self :: PUT ,
2021-06-09 07:27:42 +00:00
self :: OPTIONS
2019-09-26 19:18:01 +00:00
];
2019-04-30 22:32:33 +00:00
/** @var RouteCollector */
protected $routeCollector ;
2019-11-05 05:03:05 +00:00
/**
* @ var array Module parameters
*/
private $parameters = [];
2020-01-19 21:38:33 +00:00
/** @var L10n */
private $l10n ;
2021-10-26 19:44:29 +00:00
/** @var ICanCache */
2020-07-27 05:57:44 +00:00
private $cache ;
2021-10-26 19:44:29 +00:00
/** @var ICanLock */
2021-07-25 04:31:48 +00:00
private $lock ;
2021-11-20 12:26:01 +00:00
/** @var Arguments */
2021-11-19 21:47:49 +00:00
private $args ;
/** @var IManageConfigValues */
private $config ;
2021-12-10 20:15:15 +00:00
/** @var LoggerInterface */
private $logger ;
2022-10-21 07:05:26 +00:00
/** @var bool */
private $isLocalUser ;
2022-10-20 19:29:52 +00:00
2021-12-10 20:15:15 +00:00
/** @var float */
private $dice_profiler_threshold ;
2021-11-19 21:47:49 +00:00
/** @var Dice */
private $dice ;
2020-07-27 05:57:44 +00:00
/** @var string */
private $baseRoutesFilepath ;
2021-11-20 14:38:03 +00:00
/** @var array */
private $server ;
2019-09-26 19:18:01 +00:00
/**
2020-07-27 05:57:44 +00: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
2021-10-26 19:44:29 +00:00
* @ param ICanCache $cache
2021-11-19 21:47:49 +00:00
* @ param ICanLock $lock
* @ param IManageConfigValues $config
* @ param Arguments $args
2021-12-10 20:15:15 +00:00
* @ param LoggerInterface $logger
2021-11-19 21:47:49 +00:00
* @ param Dice $dice
2022-10-20 19:29:52 +00:00
* @ param IHandleUserSessions $userSession
2020-07-27 05:57:44 +00:00
* @ param RouteCollector | null $routeCollector
2019-09-26 19:18:01 +00:00
*/
2022-10-20 19:29:52 +00:00
public function __construct ( array $server , string $baseRoutesFilepath , L10n $l10n , ICanCache $cache , ICanLock $lock , IManageConfigValues $config , Arguments $args , LoggerInterface $logger , Dice $dice , IHandleUserSessions $userSession , RouteCollector $routeCollector = null )
2019-09-26 19:18:01 +00:00
{
2022-01-03 18:19:47 +00:00
$this -> baseRoutesFilepath = $baseRoutesFilepath ;
$this -> l10n = $l10n ;
$this -> cache = $cache ;
$this -> lock = $lock ;
$this -> args = $args ;
$this -> config = $config ;
$this -> dice = $dice ;
$this -> server = $server ;
$this -> logger = $logger ;
2022-10-21 07:05:26 +00:00
$this -> isLocalUser = ! empty ( $userSession -> getLocalUserId ());
2021-12-10 20:15:15 +00:00
$this -> dice_profiler_threshold = $config -> get ( 'system' , 'dice_profiler_threshold' , 0 );
2021-11-20 14:38:03 +00:00
2022-01-03 18:19:47 +00:00
$this -> routeCollector = $routeCollector ? ? new RouteCollector ( new Std (), new GroupCountBased ());
2020-10-16 01:45:51 +00:00
if ( $this -> baseRoutesFilepath && ! file_exists ( $this -> baseRoutesFilepath )) {
throw new HTTPException\InternalServerErrorException ( 'Routes file path does\'n exist.' );
}
2019-09-26 19:18:01 +00:00
}
2019-05-02 20:03:27 +00:00
2019-09-26 19:18:01 +00:00
/**
2020-07-27 05:57:44 +00: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 19:18:01 +00:00
* @ param array $routes The routes to add to the Router
*
* @ return self The router instance with the loaded routes
*
2019-10-11 15:55:02 +00:00
* @ throws HTTPException\InternalServerErrorException In case of invalid configs
2019-09-26 19:18:01 +00:00
*/
2022-06-16 14:49:43 +00:00
public function loadRoutes ( array $routes ) : Router
2019-09-26 19:18:01 +00:00
{
2022-01-03 18:19:47 +00:00
$routeCollector = ( $this -> routeCollector ? ? new RouteCollector ( new Std (), new GroupCountBased ()));
2019-09-26 19:18:01 +00:00
2019-11-07 03:34:38 +00:00
$this -> addRoutes ( $routeCollector , $routes );
$this -> routeCollector = $routeCollector ;
2020-07-27 05:57:44 +00:00
// Add routes from addons
Hook :: callAll ( 'route_collection' , $this -> routeCollector );
2019-11-07 03:34:38 +00:00
return $this ;
}
2022-06-16 14:49:43 +00:00
/**
* Adds multiple routes to a route collector
*
* @ param RouteCollector $routeCollector Route collector instance
* @ param array $routes Multiple routes to be added
* @ throws HTTPException\InternalServerErrorException If route was wrong ( somehow )
*/
2019-11-07 03:34:38 +00:00
private function addRoutes ( RouteCollector $routeCollector , array $routes )
{
2019-09-26 19:18:01 +00:00
foreach ( $routes as $route => $config ) {
if ( $this -> isGroup ( $config )) {
$this -> addGroup ( $route , $config , $routeCollector );
} elseif ( $this -> isRoute ( $config )) {
2022-01-03 18:19:47 +00:00
// Always add the OPTIONS endpoint to a route
$httpMethods = ( array ) $config [ 1 ];
$httpMethods [] = Router :: OPTIONS ;
$routeCollector -> addRoute ( $httpMethods , $route , $config [ 0 ]);
2019-09-26 19:18:01 +00:00
} else {
2019-10-11 15:55:02 +00:00
throw new HTTPException\InternalServerErrorException ( " Wrong route config for route ' " . print_r ( $route , true ) . " ' " );
2019-09-26 19:18:01 +00:00
}
}
}
2019-05-01 19:29:04 +00:00
2019-09-26 19:18:01 +00: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-07 03:34:38 +00:00
$this -> addRoutes ( $routeCollector , $routes );
2019-05-01 19:29:04 +00:00
});
2019-09-26 19:18:01 +00:00
}
2019-05-13 05:38:15 +00:00
2019-09-26 19:18:01 +00: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 22:32:33 +00:00
}
2019-09-26 19:18:01 +00:00
/**
* Returns true in case the config is a route config
*
* @ param array $config
*
* @ return bool
*/
2022-06-16 14:49:43 +00:00
private function isRoute ( array $config ) : bool
2019-04-30 22:32:33 +00:00
{
2019-09-26 19:18:01 +00: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 22:32:33 +00:00
}
2019-09-26 19:18:01 +00:00
/**
* The current route collector
*
* @ return RouteCollector | null
*/
2019-04-30 22:32:33 +00: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 .
*
2019-10-11 15:55:02 +00: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 22:32:33 +00:00
*/
2022-06-16 14:49:43 +00:00
private function getModuleClass () : string
2019-04-30 22:32:33 +00:00
{
2021-11-19 21:47:49 +00:00
$cmd = $this -> args -> getCommand ();
2019-04-30 22:32:33 +00:00
$cmd = '/' . ltrim ( $cmd , '/' );
2022-01-03 18:19:47 +00:00
$dispatcher = new FriendicaGroupCountBased ( $this -> getCachedDispatchData ());
2019-04-30 22:32:33 +00:00
2019-11-05 05:03:05 +00:00
$this -> parameters = [];
2019-04-30 22:32:33 +00:00
2022-01-04 19:47:17 +00:00
// Check if the HTTP method is OPTIONS and return the special Options Module with the possible HTTP methods
2022-01-03 18:19:47 +00:00
if ( $this -> args -> getMethod () === static :: OPTIONS ) {
$moduleClass = Options :: class ;
2022-01-04 19:50:20 +00:00
$this -> parameters = [ 'allowedMethods' => $dispatcher -> getOptions ( $cmd )];
2022-01-03 18:19:47 +00:00
} else {
$routeInfo = $dispatcher -> dispatch ( $this -> args -> getMethod (), $cmd );
if ( $routeInfo [ 0 ] === Dispatcher :: FOUND ) {
$moduleClass = $routeInfo [ 1 ];
$this -> parameters = $routeInfo [ 2 ];
} elseif ( $routeInfo [ 0 ] === Dispatcher :: METHOD_NOT_ALLOWED ) {
2022-01-02 19:40:43 +00:00
throw new HTTPException\MethodNotAllowedException ( $this -> l10n -> t ( 'Method not allowed for this module. Allowed method(s): %s' , implode ( ', ' , $routeInfo [ 1 ])));
2022-01-03 18:19:47 +00:00
} else {
throw new HTTPException\NotFoundException ( $this -> l10n -> t ( 'Page not found.' ));
2022-01-02 19:40:43 +00:00
}
2019-04-30 22:32:33 +00:00
}
return $moduleClass ;
}
2019-11-05 05:03:05 +00:00
2021-11-20 14:38:03 +00:00
public function getModule ( ? string $module_class = null ) : ICanHandleRequests
2019-11-05 05:03:05 +00:00
{
2021-11-20 14:38:03 +00:00
$module_parameters = [ $this -> server ];
2021-11-19 21:47:49 +00:00
/**
* 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 .
**/
try {
2021-11-20 14:38:03 +00:00
$module_class = $module_class ? ? $this -> getModuleClass ();
2021-11-19 21:47:49 +00:00
$module_parameters [] = $this -> parameters ;
} catch ( MethodNotAllowedException $e ) {
$module_class = MethodNotAllowed :: class ;
} catch ( NotFoundException $e ) {
$moduleName = $this -> args -> getModuleName ();
// Then we try addon-provided modules that we wrap in the LegacyModule class
if ( Addon :: isEnabled ( $moduleName ) && file_exists ( " addon/ { $moduleName } / { $moduleName } .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 );
2022-10-21 07:05:26 +00:00
if ( ! $this -> isLocalUser && Hook :: isAddonApp ( $moduleName ) && $privateapps ) {
2021-11-19 21:47:49 +00:00
throw new MethodNotAllowedException ( $this -> l10n -> t ( " You must be logged in to use addons. " ));
} else {
include_once " addon/ { $moduleName } / { $moduleName } .php " ;
if ( function_exists ( $moduleName . '_module' )) {
$module_parameters [] = " addon/ { $moduleName } / { $moduleName } .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/ { $moduleName } .php " )) {
$module_parameters [] = " mod/ { $moduleName } .php " ;
$module_class = LegacyModule :: class ;
}
$module_class = $module_class ? : PageNotFound :: class ;
}
2021-12-10 20:15:15 +00:00
$stamp = microtime ( true );
try {
/** @var ICanHandleRequests $module */
return $this -> dice -> create ( $module_class , $module_parameters );
} finally {
if ( $this -> dice_profiler_threshold > 0 ) {
$dur = floatval ( microtime ( true ) - $stamp );
if ( $dur >= $this -> dice_profiler_threshold ) {
2022-10-06 21:50:20 +00:00
$this -> logger -> notice ( 'Dice module creation lasts too long.' , [ 'duration' => round ( $dur , 3 ), 'module' => $module_class , 'parameters' => $module_parameters ]);
2021-12-10 20:15:15 +00:00
}
}
}
2019-11-05 05:03:05 +00:00
}
2020-07-27 05:57:44 +00: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-16 01:45:51 +00:00
if ( $this -> baseRoutesFilepath ) {
2020-07-27 05:57:44 +00: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 15:45:15 +00:00
* Additionally , we check for the base routes file last modification time to automatically
* trigger re - computing the dispatch data .
*
2020-07-27 05:57:44 +00:00
* @ return array | mixed
* @ throws HTTPException\InternalServerErrorException
*/
private function getCachedDispatchData ()
{
2022-01-03 18:19:47 +00:00
$routerDispatchData = $this -> cache -> get ( 'routerDispatchData' );
2020-10-15 15:45:15 +00:00
$lastRoutesFileModifiedTime = $this -> cache -> get ( 'lastRoutesFileModifiedTime' );
2022-01-03 18:19:47 +00:00
$forceRecompute = false ;
2020-07-27 05:57:44 +00:00
2020-10-16 01:45:51 +00:00
if ( $this -> baseRoutesFilepath ) {
2020-10-15 15:45:15 +00:00
$routesFileModifiedTime = filemtime ( $this -> baseRoutesFilepath );
2022-01-03 18:19:47 +00:00
$forceRecompute = $lastRoutesFileModifiedTime != $routesFileModifiedTime ;
2020-10-15 15:45:15 +00:00
}
if ( ! $forceRecompute && $routerDispatchData ) {
2020-07-27 05:57:44 +00:00
return $routerDispatchData ;
}
2021-07-25 04:31:48 +00:00
if ( ! $this -> lock -> acquire ( 'getCachedDispatchData' , 0 )) {
// Immediately return uncached data when we can't aquire a lock
return $this -> getDispatchData ();
}
2020-07-27 05:57:44 +00:00
$routerDispatchData = $this -> getDispatchData ();
$this -> cache -> set ( 'routerDispatchData' , $routerDispatchData , Duration :: DAY );
2020-10-15 15:45:15 +00:00
if ( ! empty ( $routesFileModifiedTime )) {
2021-07-25 04:31:48 +00:00
$this -> cache -> set ( 'lastRoutesFileModifiedTime' , $routesFileModifiedTime , Duration :: MONTH );
}
if ( $this -> lock -> isLocked ( 'getCachedDispatchData' )) {
$this -> lock -> release ( 'getCachedDispatchData' );
2020-10-15 15:45:15 +00:00
}
2020-07-27 05:57:44 +00:00
return $routerDispatchData ;
}
2019-04-30 22:32:33 +00:00
}