diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 5304623c9..706bd800c 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -24,6 +24,7 @@ use Combodo\iTop\Application\UI\DisplayBlock\BlockChartAjaxBars\BlockChartAjaxBa use Combodo\iTop\Application\UI\DisplayBlock\BlockChartAjaxPie\BlockChartAjaxPie; use Combodo\iTop\Application\UI\DisplayBlock\BlockCsv\BlockCsv; use Combodo\iTop\Application\UI\DisplayBlock\BlockList\BlockList; +use Combodo\iTop\Router\Router; require_once(APPROOT.'/application/utils.inc.php'); @@ -1722,6 +1723,7 @@ class MenuBlock extends DisplayBlock */ public function GetRenderContent(WebPage $oPage, array $aExtraParams, string $sId) { + $oRouter = Router::GetInstance(); $oRenderBlock = new UIContentBlock(); if ($this->m_sStyle == 'popup') // popup is a synonym of 'list' for backward compatibility @@ -1893,7 +1895,7 @@ class MenuBlock extends DisplayBlock if ($bIsModifyAllowed) { $aRegularActions['UI:Menu:Modify'] = array( 'label' => Dict::S('UI:Menu:Modify'), - 'url' => "{$sRootUrl}pages/$sUIPage?route=object.modify&class=$sClass&id=$id{$sContext}#", + 'url' => $oRouter->GenerateUrl('object.modify', ['class' => $sClass, 'id' => $id]) . "{$sContext}#", ) + $aActionParams; } if ($bIsDeleteAllowed) { diff --git a/core/log.class.inc.php b/core/log.class.inc.php index abec75f72..4895c95c9 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -605,6 +605,12 @@ class LogChannels public const NOTIFICATIONS = 'notifications'; public const PORTAL = 'portal'; + + /** + * @var string + * @since 3.1.0 + */ + public const ROUTER = 'router'; } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index ca9641b8d..ccc6e1a99 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -438,6 +438,8 @@ return array( 'Combodo\\iTop\\Renderer\\FieldRenderer' => $baseDir . '/sources/Renderer/FieldRenderer.php', 'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php', + 'Combodo\\iTop\\Router\\Exception\\RouteNotFoundException' => $baseDir . '/sources/Router/Exception/RouteNotFoundException.php', + 'Combodo\\iTop\\Router\\Exception\\RouterException' => $baseDir . '/sources/Router/Exception/RouterException.php', 'Combodo\\iTop\\Router\\Router' => $baseDir . '/sources/Router/Router.php', 'Combodo\\iTop\\Service\\Base\\ObjectRepository' => $baseDir . '/sources/Service/Base/ObjectRepository.php', 'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => $baseDir . '/sources/Service/Base/iDataPostProcessor.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 3a8756e97..b905ac6c2 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -803,6 +803,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Renderer\\FieldRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FieldRenderer.php', 'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php', + 'Combodo\\iTop\\Router\\Exception\\RouteNotFoundException' => __DIR__ . '/../..' . '/sources/Router/Exception/RouteNotFoundException.php', + 'Combodo\\iTop\\Router\\Exception\\RouterException' => __DIR__ . '/../..' . '/sources/Router/Exception/RouterException.php', 'Combodo\\iTop\\Router\\Router' => __DIR__ . '/../..' . '/sources/Router/Router.php', 'Combodo\\iTop\\Service\\Base\\ObjectRepository' => __DIR__ . '/../..' . '/sources/Service/Base/ObjectRepository.php', 'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Base/iDataPostProcessor.php', diff --git a/sources/Application/UI/Base/Layout/Object/ObjectSummary.php b/sources/Application/UI/Base/Layout/Object/ObjectSummary.php index c264bbb7f..61c272464 100644 --- a/sources/Application/UI/Base/Layout/Object/ObjectSummary.php +++ b/sources/Application/UI/Base/Layout/Object/ObjectSummary.php @@ -11,6 +11,7 @@ use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\Popov use Combodo\iTop\Application\UI\Base\tUIContentAreas; use Combodo\iTop\Application\UI\Base\UIBlock; use Combodo\iTop\Core\MetaModel\FriendlyNameType; +use Combodo\iTop\Router\Router; use DBObject; use Dict; use MetaModel; @@ -99,6 +100,7 @@ class ObjectSummary extends ObjectDetails */ private function ComputeActions() { + $oRouter = Router::GetInstance(); $oDetailsButton = null; if(UserRights::IsActionAllowed($this->sClassName, UR_ACTION_MODIFY)) { $sRootUrl = utils::GetAbsoluteUrlAppRoot(); @@ -111,7 +113,7 @@ class ObjectSummary extends ObjectDetails '_blank' ); $oModifyButton = ButtonUIBlockFactory::MakeLinkNeutral( - $sRootUrl.'pages/UI.php?route=object.modify&class='.$this->sClassName.'&id='.$this->sObjectId, + $oRouter->GenerateUrl('object.modify', ['class' => $this->sClassName, 'id' => $this->sObjectId]), Dict::S('UI:Menu:Modify'), 'fas fa-external-link-alt', '_blank', diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php index 118ea4a69..c4ee43ec7 100644 --- a/sources/Controller/Base/Layout/ObjectController.php +++ b/sources/Controller/Base/Layout/ObjectController.php @@ -18,6 +18,7 @@ use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper; use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectSummary; use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory; use Combodo\iTop\Controller\AbstractController; +use Combodo\iTop\Router\Router; use Combodo\iTop\Service\Base\ObjectRepository; use CoreCannotSaveObjectException; use DeleteException; @@ -62,6 +63,7 @@ class ObjectController extends AbstractController $sStateCode = utils::ReadParam('state', ''); $bCheckSubClass = utils::ReadParam('checkSubclass', true); $oAppContext = new ApplicationContext(); + $oRouter = Router::GetInstance(); if ($this->IsHandlingXmlHttpRequest()) { $oPage = new AjaxPage(''); @@ -181,7 +183,7 @@ JS; } else { if ($this->IsHandlingXmlHttpRequest()) { $oClassForm = cmdbAbstractObject::DisplayFormBlockSelectClassToCreate($sClass, MetaModel::GetName($sClass), $oAppContext, $aPossibleClasses, ['state' => $sStateCode]); - $sCurrentUrl = utils::GetAbsoluteUrlAppRoot().'/pages/UI.php?route=object.new'; + $sCurrentUrl = $oRouter->GenerateUrl('object.new'); $oClassForm->SetOnSubmitJsCode( <<IsHandlingXmlHttpRequest()) { throw new CoreException('LinksetController can only be called in ajax.'); } - + + $oRouter = Router::GetInstance(); $oPage = new AjaxPage(''); $sProposedRealClass = utils::ReadParam('class', '', false, 'class'); @@ -231,7 +233,7 @@ JS 'att_code' => $sAttCode, 'host_class' => $sClass, 'host_id' => $sId]); - $sCurrentUrl = utils::GetAbsoluteUrlAppRoot().'/pages/UI.php?route=linkset.create_linked_object'; + $sCurrentUrl = $oRouter->GenerateUrl('linkset.create_linked_object'); $oClassForm->SetOnSubmitJsCode( << + * @package Combodo\iTop\Router\Exception + * @since 3.1.0 + * @internal + */ +class RouteNotFoundException extends RouterException +{ + +} \ No newline at end of file diff --git a/sources/Router/Exception/RouterException.php b/sources/Router/Exception/RouterException.php new file mode 100644 index 000000000..72a2a2406 --- /dev/null +++ b/sources/Router/Exception/RouterException.php @@ -0,0 +1,24 @@ + + * @package Combodo\iTop\Router\Exception + * @since 3.1.0 + * @internal + */ +class RouterException extends Exception +{ + +} \ No newline at end of file diff --git a/sources/Router/Router.php b/sources/Router/Router.php index 46cc15944..b3c791698 100644 --- a/sources/Router/Router.php +++ b/sources/Router/Router.php @@ -6,6 +6,7 @@ namespace Combodo\iTop\Router; +use Combodo\iTop\Router\Exception\RouteNotFoundException; use ReflectionClass; use ReflectionMethod; use utils; @@ -89,6 +90,37 @@ class Router // Don't do anything, we don't want to be initialized } + /** + * @param string $sRoute Code of the route to generate the URL for (eg. "object.modify" => "https://itop/pages/UI.php?route=object.modify") + * @param array $aParams Parameters to add to the URL query string, they will be URL-encoded automatically. + * Note that only scalars and arrays are supported. + * (eg. ["foo" => "bar", "some_array" => [1, 2, 3]] will be append to the URL as "&foo=bar&some_array[]=1&some_array[]=2&some_array[]=3") + * @param bool $bAbsoluteUrl Whether the URL should be absolute (include the app root URL) or not + * + * @return string Absolute or relative URL to access $sRoute + * @throws \Exception + */ + public function GenerateUrl(string $sRoute, array $aParams = [], bool $bAbsoluteUrl = true): string + { + // Stop if route cannot be found, it will ease DX and troubleshooting + if (false === $this->CanDispatchRoute($sRoute)) { + throw new RouteNotFoundException('Could not find route "'.$sRoute.'"'); + } + + // Prepare base URL + $sUrl = $bAbsoluteUrl ? utils::GetAbsoluteUrlAppRoot() : ''; + + // Add route URL + $sUrl .= 'pages/UI.php?route=' . $sRoute; + + // Add parameters and url encode them + if (count($aParams) > 0) { + $sUrl .= '&' . http_build_query($aParams); + } + + return $sUrl; + } + /** * @param string $sRoute * diff --git a/sources/Service/SummaryCard/SummaryCardService.php b/sources/Service/SummaryCard/SummaryCardService.php index 5cb65c325..5a76be5dc 100644 --- a/sources/Service/SummaryCard/SummaryCardService.php +++ b/sources/Service/SummaryCard/SummaryCardService.php @@ -6,6 +6,7 @@ use Combodo\iTop\Controller\AbstractController; use Combodo\iTop\Core\MetaModel\FriendlyNameType; +use Combodo\iTop\Router\Router; /** * Class SummaryCardService @@ -25,7 +26,8 @@ class SummaryCardService { */ public static function GetHyperlinkMarkup(string $sObjClass, $sObjKey): string { - $sRoute = utils::GetAbsoluteUrlAppRoot()."/pages/ajax.render.php?route=object.summary&obj_key=$sObjKey&obj_class=$sObjClass"; + $oRouter = Router::GetInstance(); + $sRoute = $oRouter->GenerateUrl("object.summary", ["obj_class" => $sObjClass, "obj_key" => $sObjKey]); return << placeholder that will be replaced with the real app root url at run time + * @param bool $bValid + * @param string $sRoute + * @param array $aParams + * @param bool $bAbsoluteUrl + * + * @return void + * @throws \Exception + */ + public function testGenerateUrl(string $sExpectedUrl, bool $bValid, string $sRoute, array $aParams, bool $bAbsoluteUrl = true): void + { + $oRouter = Router::GetInstance(); + + if (false === $bValid) { + $this->expectException(RouteNotFoundException::class); + } + $sTestedUrl = $oRouter->GenerateUrl($sRoute, $aParams, $bAbsoluteUrl); + $sExpectedUrl = str_ireplace('', utils::GetAbsoluteUrlAppRoot(), $sExpectedUrl); + + $this->assertEquals($sTestedUrl, $sExpectedUrl, 'Generated URL does not match'); + } + + public function GenerateUrlProvider(): array + { + return [ + 'invalid route' => [ + '', + false, + 'foo.bar', + [], + true, + ], + 'relative route with no params' => [ + 'pages/UI.php?route=object.modify', + true, + 'object.modify', + [], + false, + ], + 'absolute route with no params' => [ + 'pages/UI.php?route=object.modify', + true, + 'object.modify', + [], + true, + ], + 'absolute route with scalar params' => [ + 'pages/UI.php?route=object.modify&class=Person&id=123', + true, + 'object.modify', + [ + 'class' => 'Person', + 'id' => 123 + ], + true, + ], + 'absolute route with 1 dimension array params' => [ + 'pages/UI.php?route=object.modify&class=Person&id=123&default%5Bname%5D=Castor&default%5Bfirst_name%5D=P%C3%A8re', + true, + 'object.modify', + [ + 'class' => 'Person', + 'id' => 123, + 'default' => [ + 'name' => 'Castor', + 'first_name' => 'Père', + ], + ], + true, + ], + 'absolute route with 2 dimensions array params' => [ + 'pages/UI.php?route=object.modify&class=Person&id=123&default%5Bname%5D=Castor&default%5Bfirst_name%5D=P%C3%A8re&foo%5Bfirst%5D%5B0%5D=10&foo%5Bfirst%5D%5B1%5D=20&foo%5Bsecond%5D%5B0%5D=30&foo%5Bsecond%5D%5B1%5D=40', + true, + 'object.modify', + [ + 'class' => 'Person', + 'id' => 123, + 'default' => [ + 'name' => 'Castor', + 'first_name' => 'Père', + ], + 'foo' => [ + 'first' => ['10', '20'], + 'second' => ['30', '40'], + ], + ], + true, + ], + ]; + } + /** * @dataProvider CanDispatchRouteProvider * @covers \Combodo\iTop\Router\Router::CanDispatchRoute