From 4e4b6c1ec238610577231dfb700c0967d6b8fb02 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Fri, 19 Jan 2024 17:45:27 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B06935=20-=20Migrate=20"object.xxx"=20oper?= =?UTF-8?q?ations=20/=20routes=20to=20Symfony=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/UI.php | 164 ++++++++++++++++++++++++++++++++---------- pages/ajax.render.php | 76 +++++++++++++++++++- 2 files changed, 202 insertions(+), 38 deletions(-) diff --git a/pages/UI.php b/pages/UI.php index 2ef41873d..877667592 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -6,14 +6,12 @@ use Combodo\iTop\Application\Helper\Session; use Combodo\iTop\Application\TwigBase\Twig\TwigHelper; -use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Form\Form; use Combodo\iTop\Application\UI\Base\Component\GlobalSearch\GlobalSearchHelper; use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; -use Combodo\iTop\Application\UI\Base\Component\Title\Title; use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory; use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory; @@ -23,7 +21,9 @@ use Combodo\iTop\Application\WebPage\ErrorPage; use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\WebPage; use Combodo\iTop\Controller\Base\Layout\ObjectController; +use Combodo\iTop\Kernel; use Combodo\iTop\Service\Router\Router; +use Symfony\Component\HttpFoundation\Request; /** * Displays a popup welcome message, once per session at maximum @@ -319,6 +319,7 @@ try $oKPI->ComputeAndReport('User login'); // First check if we can redirect the route to a dedicated controller + // IMPORTANT: Even though iTop router is deprecated, we must ensure old URLs keep working (for a while) as they can be used in received notifications, bookmarks, extensions, ... $sRoute = utils::ReadParam('route', '', false, utils::ENUM_SANITIZATION_FILTER_ROUTE); $oRouter = Router::GetInstance(); if ($oRouter->CanDispatchRoute($sRoute)) { @@ -341,7 +342,131 @@ try $oP = new iTopWebPage(Dict::S('UI:WelcomeToITop'), $bPrintable); $oP->SetMessage($sLoginMessage); - // All the following actions use advanced forms that require more javascript to be loaded + //----------------------------------------------------------------------------------------------------------------- + // + // Mind that the following backward compatibility pattern is also present in the `pages/ajax.render.php` endpoint + // + //----------------------------------------------------------------------------------------------------------------- + // + // Kept for backward compatibility with iTop 3.1.x notification URLs, don't remove + // Non-dispatchable "routes" (iTop 3.1) are mapped to their old corresponding "operation" (iTop 3.0 and older), in order to be later redirected to the proper new URLs (iTop 3.2 and newer) + switch ($sRoute) { + case 'object.new': + case 'object.apply_new': + case 'object.modify': + case 'object.apply_modify': + $operation = str_ireplace("object.", "", $sRoute); + break; + } + + if (utils::IsNotNullOrEmptyString($sRoute)) { + DeprecatedCallsLog::Debug("Calling UI.php with a deprecated \"route\" parameter that was migrated to Symfony router, redirecting to fallback \"operation\" parameter. If URI comes from a custom code, migrate it. If URI comes from an old notification, mind that it may not work in future versions.", LogChannels::ROUTER, [ + "request_uri" => $_SERVER["REQUEST_URI"], + "route" => $sRoute, + "fallback_operation" => $operation, + ]); + } + + // Kept for backward compatibility with iTop 3.0 and older notification URLs, don't remove + // These operations have been migrated to Symfony router, we need to redirect to the proper new URLs + switch ($operation) { + /** @deprecated 3.1.0 Use the "b_object_new" Symfony route instead */ + case 'new': + /** @deprecated 3.1.0 Use the "b_object_apply_new" Symfony route instead */ + case 'apply_new': + /** @deprecated 3.1.0 Use the "b_object_modify" Symfony route instead */ + case 'modify': + /** @deprecated 3.1.0 Use the "b_object_apply_new" Symfony route instead */ + case 'apply_modify': + // Start a Symfony kernel and redirect to the corresponding route / controller + $oKernel = new Kernel("prod", false); + $oRequest = Request::createFromGlobals(); + + /** @var string $sRouteName Route name to redirect to */ + $sRouteName = ''; + /** @var string[] $aPath Parts of the Symfony route path (eg. for "/object/{sCLass}/{sId}", ["sClass" => "UserRequest", "sId" => "123"]) */ + $aPath = []; + /** @var string[] $aQuery Query parameters of the request -parameters after the "?" of the URI- (eg. for "/object/UserRequest/123?a=1&b=2", ["a" => 1, "b" => 2]) */ + $aQuery = $oRequest->query->all(); + + // Find controller corresponding to the old "operation" + switch ($operation) { + case 'new': + $sRouteName = 'b_object_new'; + $aPath['_controller'] = ObjectController::class . "::OperationNew"; + $aPath['sClass'] = utils::ReadParam('class', null, false, utils::ENUM_SANITIZATION_FILTER_CLASS); + break; + + case 'apply_new': + $sRouteName = 'b_object_apply_new'; + $aPath['_controller'] = ObjectController::class . "::OperationApplyNew"; + break; + + case 'modify': + $sRouteName = 'b_object_modify'; + $aPath['_controller'] = ObjectController::class . "::OperationModify"; + $aPath['sClass'] = utils::ReadParam('class', '', false, utils::ENUM_SANITIZATION_FILTER_CLASS); + $aPath['sId'] = utils::ReadParam('id', ''); + break; + + case 'apply_modify': + $sRouteName = 'b_object_apply_modify'; + $aPath['_controller'] = ObjectController::class . "::OperationApplyModify"; + break; + } + + // Redirect + ///** @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface $oUrlGenerator */ + //$oResponse = $oKernel->handle($oRequest); + //$oUrlGenerator = $oKernel->getContainer()->get('router'); + + // - Solution 1: Simplement redirection + // Pros: + // - Redirects to the new URL + // - Keeps query parameters + // - Uses the real Symfony kernel as it will be a complete new process + // Cons: + // - Loose posted parameters if any (but could be ok as we essentially do this for notifications / bookmarks); extensions will be migrated + // - Makes an HTTP redirection, could be an issue in some infras? I don't think so, we already do it in several places + /*$sRedirectUrl = $oUrlGenerator->generate($sRouteName, $aPath + $aQuery); + // Generated URL is based on the script the Kernel is instantiated in, so we have to correct it to point to the new unique endpoint + $sRedirectUrl = str_ireplace("pages/UI.php", "app.php", $sRedirectUrl); + header("location: $sRedirectUrl"); + die();*/ + + // - Solution 2: Symfony redirection + // Pros: + // - Redirects to the new URL + // - Keeps query parameters + // - Uses the real Symfony kernel as it will be a complete new process + // - Keeps redirection working even though there are deprecated messages printed before headers are send (unlike with solution 1) + // Cons: + // - Loose posted parameters if any (but could be ok as we essentially do this for notifications / bookmarks); extensions will be migrated + // - Makes an HTTP redirection, could be an issue in some infras? I don't think so, we already do it in several places + /*$sRedirectUrl = $oUrlGenerator->generate($sRouteName, $aPath + $aQuery); + // Generated URL is based on the script the Kernel is instantiated in, so we have to correct it to point to the new unique endpoint + $sRedirectUrl = str_ireplace("pages/UI.php", "app.php", $sRedirectUrl); + $oResponse = new RedirectResponse($sRedirectUrl); + $oResponse->send(); + $oKernel->terminate($oRequest, $oResponse); + die();*/ + + + // - Solution 3: Internal redirection + // Pros: + // - Keeps query parameters + // - Keeps posted parameters (essential for submission URLs) + // Cons: + // - Still displays the old URL + // - Uses a not so well-formed Symfony kernel? + $oSubRequest = $oRequest->duplicate($aQuery, null, $aPath); + $oResponse = $oKernel->handle($oSubRequest, \Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST); + $oResponse->send(); + $oKernel->terminate($oSubRequest, $oResponse); + die(); + } + + // All the following operations use advanced forms that require more javascript to be loaded switch($operation) { case 'new': // Form to create a new object @@ -678,15 +803,6 @@ try /////////////////////////////////////////////////////////////////////////////////////////// - /** @deprecated 3.1.0 Use the "object.modify" route instead */ - // Kept for backward compatibility with notification URLs, don't remove - case 'modify': // Legacy operation - $oController = new ObjectController(); - $oP = $oController->OperationModify(); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - case 'select_for_modify_all': // Select the list of objects to be modified (bulk modify) UI::OperationSelectForModifyAll($oP); break; @@ -704,22 +820,6 @@ try /////////////////////////////////////////////////////////////////////////////////////////// - /** @deprecated 3.1.0 Use the "object.new" route instead */ - // Kept for backward compatibility - case 'new': // Form to create a new object - $oController = new ObjectController(); - $oP = $oController->OperationNew(); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - - case 'apply_modify': // Applying the modifications to an existing object - $oController = new ObjectController(); - $oP = $oController->OperationApplyModify(); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - case 'select_for_deletion': // Select multiple objects for deletion $oP->DisableBreadCrumb(); $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); @@ -813,14 +913,6 @@ try /////////////////////////////////////////////////////////////////////////////////////////// - case 'apply_new': // Creation of a new object - - $oController = new ObjectController(); - $oP = $oController->OperationApplyNew(); - break; - - /////////////////////////////////////////////////////////////////////////////////////////// - case 'select_bulk_stimulus': // Form displayed when applying a stimulus to many objects $oP->DisableBreadCrumb(); $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 6393e21b3..d11157ef9 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -21,8 +21,10 @@ use Combodo\iTop\Controller\Base\Layout\ObjectController; use Combodo\iTop\Controller\PreferencesController; use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer; use Combodo\iTop\Renderer\Console\ConsoleFormRenderer; +use Combodo\iTop\Kernel; use Combodo\iTop\Service\Router\Router; use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager; +use Symfony\Component\HttpFoundation\Request; require_once('../approot.inc.php'); @@ -44,7 +46,6 @@ try $oKPI = new ExecutionKPI(); require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - $sRoute = utils::ReadParam('route', '', false, utils::ENUM_SANITIZATION_FILTER_ROUTE); $operation = utils::ReadParam('operation', '', false, utils::ENUM_SANITIZATION_FILTER_OPERATION); // Only allow export functions to portal users @@ -78,6 +79,7 @@ try } // First check if we can redirect the route to a dedicated controller + // IMPORTANT: Even though iTop router is deprecated, we must ensure old URLs keep working (for a while) as they can be used in received notifications, bookmarks, extensions, ... $sRoute = utils::ReadParam('route', '', false, utils::ENUM_SANITIZATION_FILTER_ROUTE); $oRouter = Router::GetInstance(); if ($oRouter->CanDispatchRoute($sRoute)) { @@ -97,12 +99,82 @@ try // Default for most operations, but can be switch to a JsonPage, DownloadPage or else if the operation requires it $oPage = new AjaxPage(""); + //--------------------------------------------------------------------------------------------------------- + // + // Mind that the following backward compatibility pattern is also present in the `pages/UI.php` endpoint + // + //--------------------------------------------------------------------------------------------------------- + // + // Kept for backward compatibility with iTop 3.1.x notification URLs, don't remove + // Non-dispatchable "routes" (iTop 3.1) are mapped to their old corresponding "operation" (iTop 3.0 and older), in order to be later redirected to the proper new URLs (iTop 3.2 and newer) + switch ($sRoute) { + case 'object.new': + case 'object.modify': + $operation = str_ireplace("object.", "", $sRoute); + break; + } + + if (utils::IsNotNullOrEmptyString($sRoute)) { + DeprecatedCallsLog::Debug("Calling UI.php with a deprecated \"route\" parameter that was migrated to Symfony router, redirecting to fallback \"operation\" parameter. If URI comes from a custom code, migrate it. If URI comes from an old notification, mind that it may not work in future versions.", LogChannels::ROUTER, [ + "request_uri" => $_SERVER["REQUEST_URI"], + "route" => $sRoute, + "fallback_operation" => $operation, + ]); + } + + // Kept for backward compatibility with iTop 3.0 and older notification URLs, don't remove + // These operations have been migrated to Symfony router, we need to redirect to the proper new URLs + switch ($operation) { + /** @deprecated 3.1.0 Use the "b_object_new" Symfony route instead */ + case 'new': + /** @deprecated 3.1.0 Use the "b_object_modify" Symfony route instead */ + case 'modify': + // Start a Symfony kernel and redirect to the corresponding route / controller + $oKernel = new Kernel("prod", false); + $oRequest = Request::createFromGlobals(); + + /** @var string $sRouteName Route name to redirect to */ + $sRouteName = ''; + /** @var string[] $aPath Parts of the Symfony route path (eg. for "/object/{sCLass}/{sId}", ["sClass" => "UserRequest", "sId" => "123"]) */ + $aPath = []; + /** @var string[] $aQuery Query parameters of the request -parameters after the "?" of the URI- (eg. for "/object/UserRequest/123?a=1&b=2", ["a" => 1, "b" => 2]) */ + $aQuery = $oRequest->query->all(); + + // Find controller corresponding to the old "operation" + switch ($operation) { + case 'new': + $sRouteName = 'b_object_new'; + $aPath['_controller'] = ObjectController::class . "::OperationNew"; + $aPath['sClass'] = utils::ReadParam('class', null, false, utils::ENUM_SANITIZATION_FILTER_CLASS); + break; + + case 'modify': + $sRouteName = 'b_object_modify'; + $aPath['_controller'] = ObjectController::class . "::OperationModify"; + $aPath['sClass'] = utils::ReadParam('class', '', false, utils::ENUM_SANITIZATION_FILTER_CLASS); + $aPath['sId'] = utils::ReadParam('id', ''); + break; + } + + // Internal Symfony redirection + // Pros: + // - Keeps query parameters + // - Keeps posted parameters (essential for submission URLs) + // Cons: + // - Still displays the old URL + // - Uses a not so well-formed Symfony kernel? + $oSubRequest = $oRequest->duplicate($aQuery, null, $aPath); + $oResponse = $oKernel->handle($oSubRequest, \Symfony\Component\HttpKernel\HttpKernelInterface::SUB_REQUEST); + $oResponse->send(); + $oKernel->terminate($oSubRequest, $oResponse); + die(); + } + $sFilter = utils::ReadParam('filter', '', false, 'raw_data'); $sEncoding = utils::ReadParam('encoding', 'serialize'); $sClass = utils::ReadParam('class', 'MissingAjaxParam', false, 'class'); $sStyle = utils::ReadParam('style', 'list'); - $oAjaxRenderController = new AjaxRenderController(); switch ($operation) {