Merge remote-tracking branch 'origin/feature/5655-edit-object-in-modal' into develop

# Conflicts:
#	pages/ajax.render.php
This commit is contained in:
Molkobain
2022-11-23 15:57:55 +01:00
28 changed files with 4428 additions and 3860 deletions

View File

@@ -1838,7 +1838,7 @@ class MenuBlock extends DisplayBlock
if ($bIsModifyAllowed) {
$aRegularActions['UI:Menu:Modify'] = array(
'label' => Dict::S('UI:Menu:Modify'),
'url' => "{$sRootUrl}pages/$sUIPage?operation=modify&class=$sClass&id=$id{$sContext}#",
'url' => "{$sRootUrl}pages/$sUIPage?operation=object.modify&class=$sClass&id=$id{$sContext}#",
) + $aActionParams;
}
if ($bIsCreationAllowed) {

View File

@@ -69,6 +69,16 @@ class utils
* @since 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_CONTEXT_PARAM = 'context_param';
/**
* @var string To filter routes passed to back-end router before being redirected to corresponding controller / method
* @since 3.1.0
*/
public const ENUM_SANITIZATION_FILTER_ROUTE = 'route';
/**
* @var string To filter operation codes passed to back-end router before being redirected to corresponding controller (/ business logic in case of legacy operations)
* @since 3.1.0
*/
public const ENUM_SANITIZATION_FILTER_OPERATION = 'operation';
/**
* @var string
* @since 3.0.0
@@ -406,6 +416,8 @@ class utils
break;
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
case static::ENUM_SANITIZATION_FILTER_ROUTE:
case static::ENUM_SANITIZATION_FILTER_OPERATION:
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
@@ -427,27 +439,31 @@ class utils
switch ($sSanitizationFilter)
{
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
// same as parameter type but keep the dot character
// see N°1835 : when using file transaction_id on Windows you get *.tmp tokens
// it must be included at the regexp beginning otherwise you'll get an invalid character error
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[\. A-Za-z0-9_=-]*$/')));
// Same as parameter type but keep the dot character
// transaction_id, the dot is mostly for Windows servers when using file storage as the tokens are named *.tmp
// - See N°1835
// - Note: It must be included at the regexp beginning otherwise you'll get an invalid character error
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[\. A-Za-z0-9_=-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_ROUTE:
case static::ENUM_SANITIZATION_FILTER_OPERATION:
// - Routes should be of the "controller_namespace_code.controller_method_name" form
// - Operations should be allowed to be namespaced as well even though then don't have dedicated controller yet
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[\.A-Za-z0-9_-]*$/')));
break;
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[ A-Za-z0-9_=-]*$/'))); // the '=', '%3D, '%2B', '%2F'
// characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC)
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[ A-Za-z0-9_=-]*$/'))); // the '=', '%3D, '%2B', '%2F'
// Characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC)
break;
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name
break;
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[ A-Za-z0-9_=%:+-]*$/')));
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options" => array("regexp" => '/^[ A-Za-z0-9_=%:+-]*$/')));
break;
}

View File

@@ -100,6 +100,6 @@ $(document).ready(function()
};
}
CombodoPortalToolbox.OpenModal(oOptions);
CombodoModal.OpenModal(oOptions);
});
});

View File

@@ -397,7 +397,7 @@ $(function()
if(bRedirectInModal === true)
{
// Creating a new modal
CombodoPortalToolbox.OpenModal({
CombodoModal.OpenModal({
content: {
endpoint: sRedirectUrl,
data: {

View File

@@ -24,158 +24,22 @@
const CombodoPortalToolbox = {
/**
* Close all opened modals on the page
* @deprecated 3.1.0 Use CombodoModal.CloseAllModals() instead
*/
CloseAllModals: function()
{
$('.modal.in').modal('hide');
CloseAllModals: function() {
CombodoModal.CloseAllModals();
},
/**
* Open a standard modal and put the content of the URL in it.
*
* @param sTargetUrl
* @param bCloseOtherModals
* @deprecated 3.1.0 Use CombodoModal.OpenUrlInModal() instead
*/
OpenUrlInModal: function(sTargetUrl, bCloseOtherModals){
// Set default values
if(bCloseOtherModals === undefined)
{
bCloseOtherModals = false;
}
// Close other modals if necessary
if(bCloseOtherModals)
{
CombodoPortalToolbox.CloseAllModals();
}
// Opening modal
CombodoPortalToolbox.OpenModal({
content: {
endpoint: sTargetUrl,
}
});
OpenUrlInModal: function(sTargetUrl, bCloseOtherModals) {
CombodoModal.OpenUrlInModal(sTargetUrl, bCloseOtherModals);
},
/**
* Generic function to create and open a modal, used by high-level functions such as "CombodoPortalToolbox.OpenUrlInModal()".
* When developing extensions, you should use them instead.
*
* @param oOptions
* @returns object The jQuery object of the modal element
* @deprecated 3.1.0 Use CombodoModal.OpenModal() instead
*/
OpenModal: function(oOptions){
// Set default options
oOptions = $.extend(
true,
{
id: null, // ID of the created modal
attributes: {}, // HTML attributes
base_modal: {
usage: 'clone', // Either 'clone' or 'replace'
selector: '#modal-for-all' // Either a selector of the modal element used to base this one on or the modal element itself
},
content: undefined, // Either a string, an object containing the endpoint / data or undefined to keep base modal content as-is
size: 'lg', // Either 'xs' / 'sm' / 'md' / 'lg'
},
oOptions
);
// Compute modal selector
let oSelectorElem = null;
switch(typeof oOptions.base_modal.selector)
{
case 'string':
oSelectorElem = $(oOptions.base_modal.selector);
break;
case 'object':
oSelectorElem = oOptions.base_modal.selector;
break;
default:
if (window.console && window.console.warn)
{
console.warn('Could not open modal dialog as the select option was malformed: ', oOptions.content);
}
return false;
}
// Get modal element by either
let oModalElem = null;
// - Create a new modal from template
// Note : This could be better if we check for an existing modal first instead of always creating a new one
if (oOptions.base_modal.usage === 'clone')
{
oModalElem = oSelectorElem.clone();
// Force modal to have an HTML ID, otherwise it can lead to complications, especially with the portal_leave_handle.js
// See N°3469
var sModalID = (oOptions.id !== null) ? oOptions.id : 'modal-with-generated-id-'+Date.now();
oModalElem.attr('id', sModalID)
.appendTo('body');
}
// - Get an existing modal in the DOM
else
{
oModalElem = oSelectorElem;
}
// Set attributes
for(let sProp in oOptions.attributes)
{
oModalElem.attr(sProp, oOptions.attributes[sProp]);
}
// Resize to small modal
oModalElem.find('.modal-dialog')
.removeClass('modal-lg')
.addClass('modal-' + oOptions.size);
// Load content
switch (typeof oOptions.content)
{
case 'string':
oModalElem.find('.modal-content').html(oOptions.content);
//Manually triggers bootstrap event in order to keep listeners working
oModalElem.trigger('loaded.bs.modal');
break;
case 'object':
// Put loader while fetching content
oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
// Fetch content in background
oModalElem.find('.modal-content').load(
oOptions.content.endpoint,
oOptions.content.data || {},
function (sResponseText, sStatus)
{
// Hiding modal in case of error as the general AJAX error handler will display a message
if (sStatus === 'error')
{
oModalElem.modal('hide');
}
//Manually triggers bootstrap event in order to keep listeners working
oModalElem.trigger('loaded.bs.modal');
}
);
break;
case 'undefined':
// Do nothing, we keep the content as-is
break;
default:
if (window.console && window.console.warn)
{
console.warn('Could not open modal dialog as the content option was malformed: ', oOptions.content);
}
}
// Show modal
oModalElem.modal('show');
return oModalElem;
OpenModal: function(oOptions) {
return CombodoModal.OpenModal(oOptions);
},
/**
* Generic function to call a specific endpoint with callbacks
@@ -185,7 +49,7 @@ const CombodoPortalToolbox = {
* @param callbackOnSuccess
* @param callbackOnPending
*/
CallEndpoint: function(sEndpointUrl, oPostedData, callbackOnSuccess, callbackOnPending){
CallEndpoint: function(sEndpointUrl, oPostedData, callbackOnSuccess, callbackOnPending) {
// Call endpoint
$.post(sEndpointUrl, oPostedData, function(oResponse) {
// Call callback on success
@@ -216,3 +80,130 @@ const CombodoPortalToolbox = {
}
}
};
/**
* @override
* @inheritDoc
*/
CombodoModal.CloseAllModals = function() {
$('.modal.in').modal('hide');
};
/**
* @override
* @inheritDoc
*/
CombodoModal.OpenModal = function(oOptions) {
// Set default options
oOptions = $.extend(
true,
{
id: null, // ID of the created modal
attributes: {}, // HTML attributes
base_modal: {
usage: 'clone', // Either 'clone' or 'replace'
selector: '#modal-for-all' // Either a selector of the modal element used to base this one on or the modal element itself
},
content: undefined, // Either a string, an object containing the endpoint / data or undefined to keep base modal content as-is
size: 'lg', // Either 'xs' / 'sm' / 'md' / 'lg'
},
oOptions
);
// Compute modal selector
let oSelectorElem = null;
switch(typeof oOptions.base_modal.selector)
{
case 'string':
oSelectorElem = $(oOptions.base_modal.selector);
break;
case 'object':
oSelectorElem = oOptions.base_modal.selector;
break;
default:
if (window.console && window.console.warn)
{
console.warn('Could not open modal dialog as the select option was malformed: ', oOptions.content);
}
return false;
}
// Get modal element by either
let oModalElem = null;
// - Create a new modal from template
// Note : This could be better if we check for an existing modal first instead of always creating a new one
if (oOptions.base_modal.usage === 'clone')
{
oModalElem = oSelectorElem.clone();
// Force modal to have an HTML ID, otherwise it can lead to complications, especially with the portal_leave_handle.js
// See N°3469
var sModalID = (oOptions.id !== null) ? oOptions.id : 'modal-with-generated-id-'+Date.now();
oModalElem.attr('id', sModalID)
.appendTo('body');
}
// - Get an existing modal in the DOM
else
{
oModalElem = oSelectorElem;
}
// Set attributes
for(let sProp in oOptions.attributes)
{
oModalElem.attr(sProp, oOptions.attributes[sProp]);
}
// Resize to small modal
oModalElem.find('.modal-dialog')
.removeClass('modal-lg')
.addClass('modal-' + oOptions.size);
// Load content
switch (typeof oOptions.content)
{
case 'string':
oModalElem.find('.modal-content').html(oOptions.content);
//Manually triggers bootstrap event in order to keep listeners working
oModalElem.trigger('loaded.bs.modal');
break;
case 'object':
// Put loader while fetching content
oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
// Fetch content in background
oModalElem.find('.modal-content').load(
oOptions.content.endpoint,
oOptions.content.data || {},
function (sResponseText, sStatus)
{
// Hiding modal in case of error as the general AJAX error handler will display a message
if (sStatus === 'error')
{
oModalElem.modal('hide');
}
//Manually triggers bootstrap event in order to keep listeners working
oModalElem.trigger('loaded.bs.modal');
}
);
break;
case 'undefined':
// Do nothing, we keep the content as-is
break;
default:
if (window.console && window.console.warn)
{
console.warn('Could not open modal dialog as the content option was malformed: ', oOptions.content);
}
}
// Show modal
oModalElem.modal('show');
return oModalElem;
};

View File

@@ -208,7 +208,7 @@ class ObjectController extends BrickController
$oModifyButton = new JSButtonItem(
'modify_object',
Dict::S('UI:Menu:Modify'),
'CombodoPortalToolbox.OpenUrlInModal("'.$sModifyUrl.'", true);'
'CombodoModal.OpenUrlInModal("'.$sModifyUrl.'", true);'
);
// Putting this one first
$aData['form']['buttons']['actions'][] = $oModifyButton->GetMenuItem() + array('js_files' => $oModifyButton->GetLinkedScripts());

View File

@@ -25,7 +25,7 @@
sUrl = CombodoGlobalToolbox.AddParameterToUrl(sUrl, 'ar_token', '{{ ar_token }}');
// Creating a new modal
CombodoPortalToolbox.OpenModal({
CombodoModal.OpenModal({
base_modal: {
usage: 'replace',
selector: $(this).closest('.modal'),

View File

@@ -143,7 +143,7 @@
oEvent.stopPropagation();
// Creating a new modal
CombodoPortalToolbox.OpenModal({
CombodoModal.OpenModal({
content: {
endpoint: $(this).attr('href'),
},

View File

@@ -547,7 +547,7 @@ $(function()
$.post(
this.options.save_state_endpoint,
{
'operation': 'activity_panel_save_state',
'operation': 'activity_panel.save_state',
'object_class': this._GetHostObjectClass(),
'object_mode': this._GetHostObjectMode(),
'is_expanded': this.element.hasClass(this.css_classes.is_expanded),
@@ -936,7 +936,7 @@ $(function()
// Prepare parameters
let oParams = $.extend(oExtraInputs, {
operation: 'activity_panel_add_caselog_entries',
operation: 'activity_panel.add_caselog_entries',
object_class: this._GetHostObjectClass(),
object_id: this._GetHostObjectID(),
transaction_id: this.options.transaction_id,
@@ -1380,7 +1380,7 @@ $(function()
// Send XHR request
let oParams = {
operation: 'activity_panel_load_more_entries',
operation: 'activity_panel.load_more_entries',
object_class: this._GetHostObjectClass(),
object_id: this._GetHostObjectID(),
last_loaded_entries_ids: this.options.last_loaded_entries_ids,

View File

@@ -175,6 +175,23 @@ const CombodoBackofficeToolbox = {
}
};
/**
* @override
* @inheritDoc
*/
CombodoModal.CloseAllModals = function() {
// TODO: Implement
};
/**
* @override
* @inheritDoc
*/
CombodoModal.OpenModal = function(oOptions) {
// TODO: Implement
return null;
};
// Processing on each pages of the backoffice
$(document).ready(function(){
// Initialize global keyboard shortcuts

View File

@@ -1098,4 +1098,57 @@ const CombodoInlineImage = {
$(this).addClass('inline-image').attr('href', $(this).attr('src'));
}).magnificPopup({type: 'image', closeOnContentClick: true });
}
}
};
/**
* Abstract wrapper to manage modal dialogs in iTop.
* Implementations for the various GUIs may vary but APIs are the same.
*
* @since 3.1.0
*/
let CombodoModal = {
/**
* Close all opened modals on the page
*/
CloseAllModals: function() {
// Meant for overlaoding
CombodoJSConsole.Debug('CombodoModal.CloseAllModals not implemented');
},
/**
* Open a standard modal and put the content of the URL in it.
*
* @param sTargetUrl
* @param bCloseOtherModals
*/
OpenUrlInModal: function(sTargetUrl, bCloseOtherModals) {
// Set default values
if(bCloseOtherModals === undefined)
{
bCloseOtherModals = false;
}
// Close other modals if necessary
if(bCloseOtherModals)
{
CombodoModal.CloseAllModals();
}
// Opening modal
CombodoModal.OpenModal({
content: {
endpoint: sTargetUrl,
}
});
},
/**
* Generic function to create and open a modal, used by high-level functions such as "CombodoPortalToolbox.OpenUrlInModal()".
* When developing extensions, you should use them instead.
*
* @param oOptions
* @returns object The jQuery object of the modal element
*/
OpenModal: function(oOptions) {
// Meant for overlaoding
CombodoJSConsole.Debug('CombodoModal.OpenModal not implemented');
}
};

View File

@@ -347,10 +347,13 @@ return array(
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => $baseDir . '/sources/Application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => $baseDir . '/sources/Application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php',
'Combodo\\iTop\\Controller\\AbstractController' => $baseDir . '/sources/Controller/AbstractController.php',
'Combodo\\iTop\\Controller\\AjaxRenderController' => $baseDir . '/sources/Controller/AjaxRenderController.php',
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => $baseDir . '/sources/Controller/Base/Layout/ActivityPanelController.php',
'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController' => $baseDir . '/sources/Controller/Base/Layout/ObjectController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => $baseDir . '/sources/Controller/OAuth/OAuthLandingController.php',
'Combodo\\iTop\\Controller\\PreferencesController' => $baseDir . '/sources/Controller/PreferencesController.php',
'Combodo\\iTop\\Controller\\iController' => $baseDir . '/sources/Controller/iController.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => $baseDir . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => $baseDir . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
@@ -414,6 +417,7 @@ 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\\Router' => $baseDir . '/sources/Router/Router.php',
'Combodo\\iTop\\Service\\EventData' => $baseDir . '/sources/Application/Service/EventData.php',
'Combodo\\iTop\\Service\\EventHelper' => $baseDir . '/sources/Application/Service/EventHelper.php',
'Combodo\\iTop\\Service\\EventService' => $baseDir . '/sources/Application/Service/EventService.php',

View File

@@ -712,10 +712,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => __DIR__ . '/../..' . '/sources/Application/UI/Preferences/BlockShortcuts/BlockShortcuts.php',
'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => __DIR__ . '/../..' . '/sources/Application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php',
'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php',
'Combodo\\iTop\\Controller\\AbstractController' => __DIR__ . '/../..' . '/sources/Controller/AbstractController.php',
'Combodo\\iTop\\Controller\\AjaxRenderController' => __DIR__ . '/../..' . '/sources/Controller/AjaxRenderController.php',
'Combodo\\iTop\\Controller\\Base\\Layout\\ActivityPanelController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ActivityPanelController.php',
'Combodo\\iTop\\Controller\\Base\\Layout\\ObjectController' => __DIR__ . '/../..' . '/sources/Controller/Base/Layout/ObjectController.php',
'Combodo\\iTop\\Controller\\OAuth\\OAuthLandingController' => __DIR__ . '/../..' . '/sources/Controller/OAuth/OAuthLandingController.php',
'Combodo\\iTop\\Controller\\PreferencesController' => __DIR__ . '/../..' . '/sources/Controller/PreferencesController.php',
'Combodo\\iTop\\Controller\\iController' => __DIR__ . '/../..' . '/sources/Controller/iController.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\IOAuthClientProvider' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/IOAuthClientProvider.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAbstract' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAbstract.php',
'Combodo\\iTop\\Core\\Authentication\\Client\\OAuth\\OAuthClientProviderAzure' => __DIR__ . '/../..' . '/sources/Core/Authentication/Client/OAuth/OAuthClientProviderAzure.php',
@@ -779,6 +782,7 @@ 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\\Router' => __DIR__ . '/../..' . '/sources/Router/Router.php',
'Combodo\\iTop\\Service\\EventData' => __DIR__ . '/../..' . '/sources/Application/Service/EventData.php',
'Combodo\\iTop\\Service\\EventHelper' => __DIR__ . '/../..' . '/sources/Application/Service/EventHelper.php',
'Combodo\\iTop\\Service\\EventService' => __DIR__ . '/../..' . '/sources/Application/Service/EventService.php',

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -422,7 +422,7 @@ $('[data-role="ibo-preferences--user-preferences--picture-placeholder--image"]')
$.post(
GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
{
'operation': 'preferences_set_user_picture',
'operation': 'preferences.set_user_picture',
'image_filename': $(this).attr('data-image-name')
}
)

View File

@@ -165,7 +165,7 @@ abstract class Controller
$this->CheckAccess();
$this->m_sOperation = utils::ReadParam('operation', $this->m_sDefaultOperation);
$sMethodName = 'Operation'.$this->m_sOperation;
$sMethodName = 'Operation'.utils::ToCamelCase($this->m_sOperation);
$oKPI = new ExecutionKPI();
$oKPI->ComputeAndReport('Starting operation '.$this->m_sOperation);
if (method_exists($this, $sMethodName))
@@ -201,7 +201,7 @@ abstract class Controller
$this->CheckAccess();
$this->m_sOperation = utils::ReadParam('operation', $this->m_sDefaultOperation);
$sMethodName = 'Operation'.$this->m_sOperation;
$sMethodName = 'Operation'.utils::ToCamelCase($this->m_sOperation);
if (method_exists($this, $sMethodName))
{
$this->$sMethodName();

View File

@@ -529,9 +529,9 @@ JS
*/
public function SetContentLayout(PageContent $oLayout)
{
$oPrevContentLayout=$this->oContentLayout;
$oPrevContentLayout = $this->oContentLayout;
$this->oContentLayout = $oLayout;
foreach ($oPrevContentLayout->GetSubBlocks() as $oBlock){
foreach ($oPrevContentLayout->GetSubBlocks() as $oBlock) {
$this->AddUiBlock($oBlock);
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Controller;
/**
* Class AbstractController
*
* Abstract controller to centralize common features of business controllers which are still to be defined.
* Note that this can be extended by "TwigBase" controllers or standalone controllers.
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @package Combodo\iTop\Controller
* @since 3.1.0
*/
abstract class AbstractController implements iController
{
/**
* @inheritDoc
*/
public function IsHandlingXmlHttpRequest(): bool
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');
}
}

View File

@@ -8,6 +8,7 @@ namespace Combodo\iTop\Controller\Base\Layout;
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory;
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityPanelHelper;
use Combodo\iTop\Controller\AbstractController;
use Combodo\iTop\Renderer\BlockRenderer;
use Dict;
use Exception;
@@ -23,14 +24,14 @@ use utils;
* @since 3.0.0
* @package Combodo\iTop\Controller\Base\Layout
*/
class ActivityPanelController
class ActivityPanelController extends AbstractController
{
/**
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public static function SaveState(): void
public function SaveState(): void
{
$sObjectClass = utils::ReadPostedParam('object_class', '', utils::ENUM_SANITIZATION_FILTER_CLASS);
$sObjectMode = utils::ReadPostedParam('object_mode');
@@ -74,7 +75,7 @@ class ActivityPanelController
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public static function AddCaseLogsEntries(): array
public function AddCaseLogsEntries(): array
{
$sObjectClass = utils::ReadPostedParam('object_class', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$sObjectId = utils::ReadPostedParam('object_id', 0);
@@ -154,7 +155,7 @@ class ActivityPanelController
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public static function LoadMoreEntries(): array
public function LoadMoreEntries(): array
{
$sObjectClass = utils::ReadPostedParam('object_class', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$sObjectId = utils::ReadPostedParam('object_id', 0);

View File

@@ -0,0 +1,102 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Controller\Base\Layout;
use AjaxPage;
use ApplicationException;
use cmdbAbstractObject;
use CMDBObjectSet;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
use Combodo\iTop\Controller\AbstractController;
use Dict;
use iTopWebPage;
use MetaModel;
use SecurityException;
use utils;
use UserRights;
use WebPage;
/**
* Class ObjectController
*
* @internal
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @since 3.1.0
* @package Combodo\iTop\Controller\Base\Layout
*/
class ObjectController extends AbstractController
{
public const ROUTE_NAMESPACE = 'object';
/**
* @return \iTopWebPage|\AjaxPage Object edit form in its webpage
* @throws \ApplicationException
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \SecurityException
*/
public function OperationModify()
{
$bPrintable = utils::ReadParam('printable', '0') === '1';
$sClass = utils::ReadParam('class', '', false, 'class');
$sId = utils::ReadParam('id', '');
// Check parameters
if (utils::IsNullOrEmptyString($sClass) || utils::IsNullOrEmptyString($sId))
{
throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id'));
}
$oObj = MetaModel::GetObject($sClass, $sId, false);
// Check user permissions
// - Is allowed to view it?
if (is_null($oObj)) {
throw new ApplicationException(Dict::S('UI:ObjectDoesNotExist'));
}
// - Is allowed to edit it?
$oSet = CMDBObjectSet::FromObject($oObj);
if (UserRights::IsActionAllowed($sClass, UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_NO) {
throw new SecurityException('User not allowed to modify this object', array('class' => $sClass, 'id' => $sId));
}
// Prepare web page (should more likely be some kind of response object like for Symfony)
if ($this->IsHandlingXmlHttpRequest()) {
$oPage = new AjaxPage('');
} else {
$oPage = new iTopWebPage('', $bPrintable);
$oPage->DisableBreadCrumb();
$oPage->SetContentLayout(PageContentFactory::MakeForObjectDetails($oObj, cmdbAbstractObject::ENUM_DISPLAY_MODE_EDIT));
}
// - JS files
foreach (static::EnumRequiredForModificationJsFilesRelPaths() as $sJsFileRelPath) {
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().$sJsFileRelPath);
}
// Note: Code duplicated to the case 'apply_modify' in UI.php when a data integrity issue has been found
$oObj->DisplayModifyForm($oPage, array('wizard_container' => 1)); // wizard_container: Display the title above the form
return $oPage;
}
/**
* @return string[] Rel. paths (to iTop root folder) of required JS files for object modification (create, edit, stimulus, ...)
*/
public static function EnumRequiredForModificationJsFilesRelPaths(): array
{
return [
'js/json.js',
'js/forms-json-utils.js',
'js/wizardhelper.js',
'js/wizard.utils.js',
'js/linkswidget.js',
'js/linksdirectwidget.js',
'js/extkeywidget.js',
'js/jquery.blockUI.js',
];
}
}

View File

@@ -22,7 +22,7 @@ use utils;
* @since 3.0.0
* @package Combodo\iTop\Controller
*/
class PreferencesController
class PreferencesController extends AbstractController
{
/**
* @return string[]
@@ -31,7 +31,7 @@ class PreferencesController
* @throws \MySQLException
* @throws \Exception
*/
public static function SetUserPicture(): array
public function SetUserPicture(): array
{
$sImageFilename = utils::ReadPostedParam('image_filename', null, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Controller;
/**
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @since 3.1.0
* @package Combodo\iTop\Controller
*/
interface iController
{
/**
* @var string|null Meant for overlaoding. Route namespace, what will prefix the "route" parameter to define in which namespoce the operation is to be executed. If left to `null`, the controller will be ignored.
*/
public const ROUTE_NAMESPACE = null;
/**
* It works if your JavaScript library sets an X-Requested-With HTTP header.
* It is known to work with common JavaScript frameworks: {@link https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript}
*
* @see \Symfony\Component\HttpFoundation\Request::isXmlHttpRequest() Inspired by
*
* @return bool True if the current request is an XmlHttpRequest (eg. an AJAX request)
*/
public function IsHandlingXmlHttpRequest(): bool;
}

View File

@@ -249,7 +249,7 @@ EOF
oEvent.stopPropagation();
// Note : This could be better if we check for an existing modal first instead of always creating a new one
CombodoPortalToolbox.OpenModal({
CombodoModal.OpenModal({
content: {
endpoint: $(this).attr('href'),
},
@@ -527,7 +527,7 @@ EOF
'selector': '.modal[data-source-element="{$sButtonAddId}"]:first'
};
}
CombodoPortalToolbox.OpenModal(oOptions);
CombodoModal.OpenModal(oOptions);
});
JS
);

View File

@@ -381,7 +381,7 @@ EOF
<<<JS
$('#{$sHierarchicalButtonId}').off('click').on('click', function(){
// Creating a new modal
CombodoPortalToolbox.OpenModal({
CombodoModal.OpenModal({
attributes: {
'data-source-element': '{$sHierarchicalButtonId}',
},
@@ -441,7 +441,7 @@ JS
'selector': '.modal[data-source-element="{$sSearchButtonId}"]:first'
};
}
CombodoPortalToolbox.OpenModal(oOptions);
CombodoModal.OpenModal(oOptions);
});
JS
);

183
sources/Router/Router.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Router;
use utils;
/**
* Class Router
*
* Service to find the corresponding controller / method for a given "route" parameter
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @package Combodo\iTop\Router
* @since 3.1.0
* @internal
*/
class Router
{
/** @var \Combodo\iTop\Router\Router|null Singleton instance */
protected static ?Router $oSingleton = null;
/**
* @return $this The singleton instance of the router
*/
public static function GetInstance()
{
if (null === static::$oSingleton) {
static::$oSingleton = new static();
}
return static::$oSingleton;
}
/**********************/
/* Non-static methods */
/**********************/
/**
* Singleton pattern, can't use the constructor. Use {@see \Combodo\iTop\Router\Router::GetInstance()} instead.
*
* @return void
*/
private function __construct()
{
// Don't do anything, we don't want to be initialized
}
/**
* @param string $sRoute
*
* @return bool True if there is a matching handler for $sRoute
*/
public function CanDispatchRoute(string $sRoute): bool
{
return $this->GetDispatchSpecsForRoute($sRoute) !== null;
}
/**
* Dispatch the current request to the matching handler for $sRoute
*
* @param string $sRoute
*
* @return mixed Response from the route's handler, can be anything.
* Even though it can be anything, in most cases, response will either be:
* - A \WebPage for usual backoffice operations
* - null for TwigBase backoffice operations
*/
public function DispatchRoute(string $sRoute)
{
$aMethodSpecs = $this->GetDispatchSpecsForRoute($sRoute);
$mResponse = call_user_func_array([new $aMethodSpecs[0](), $aMethodSpecs[1]], []);
return $mResponse;
}
/**
* @param string $sRoute
*
* @return array{sControllerFQCN, sOperationMethodName}|null The FQCN controller and operation method matching $sRoute, null if no matching handler
*/
public function GetDispatchSpecsForRoute(string $sRoute)
{
$aRouteParts = $this->GetRouteParts($sRoute);
if (is_null($aRouteParts)) {
return null;
}
$sRouteNamespace = $aRouteParts['namespace'];
$sRouteOperation = $aRouteParts['operation'];
$sControllerFQCN = $this->FindControllerFromRouteNamespace($sRouteNamespace);
if (utils::IsNullOrEmptyString($sControllerFQCN)) {
return null;
}
$sOperationMethodName = $this->MakeOperationMethodNameFromOperation($sRouteOperation);
$aMethodSpecs = [$sControllerFQCN, $sOperationMethodName];
if (false === is_callable($aMethodSpecs)) {
return null;
}
return $aMethodSpecs;
}
/**
* @param string $sRoute
*
* @return array{namespace: string, operation: string}|null Route parts (namespace and operation) if route can be parsed, null otherwise
*/
public function GetRouteParts(string $sRoute)
{
if (utils::IsNullOrEmptyString($sRoute)) {
return null;
}
$sRouteNamespace = $this->GetRouteNamespace($sRoute);
$sRouteOperation = $this->GetRouteOperation($sRoute);
if (utils::IsNullOrEmptyString($sRouteNamespace) || utils::IsNullOrEmptyString($sRouteOperation)) {
return null;
}
return ['namespace' => $sRouteNamespace, 'operation' => $sRouteOperation];
}
/**
* @param string $sRoute
*
* @return string|null Namespace of the route (eg. "object" for "object.modify") if route can be parsed null otherwise
*/
public function GetRouteNamespace(string $sRoute): ?string
{
$mSeparatorPos = strripos($sRoute, '.', -1);
if (false === $mSeparatorPos) {
return null;
}
return substr($sRoute, 0, $mSeparatorPos);
}
/**
* @param string $sRoute
*
* @return string|null Operation of the route (eg. "modify" for "object.modify") if route can be parsed null otherwise
*/
public function GetRouteOperation(string $sRoute): ?string
{
$mSeparatorPos = strripos($sRoute, '.', -1);
if (false === $mSeparatorPos) {
return null;
}
return substr($sRoute, $mSeparatorPos + 1);
}
/**
* @param string $sRouteNamespace {@see static::$sRouteNamespace}
*
* @return string|null The FQCN of the controller matching the $sRouteNamespace, null if none matching.
*/
protected function FindControllerFromRouteNamespace(string $sRouteNamespace): ?string
{
foreach (utils::GetClassesForInterface('Combodo\iTop\Controller\iController', '', ['[\\\\/]lib[\\\\/]', '[\\\\/]node_modules[\\\\/]', '[\\\\/]test[\\\\/]']) as $sControllerFQCN) {
if ($sControllerFQCN::ROUTE_NAMESPACE === $sRouteNamespace) {
return $sControllerFQCN;
}
}
return null;
}
/**
* @param string $sOperation
*
* @return string The method name for the $sOperation regarding the convention
*/
protected function MakeOperationMethodNameFromOperation(string $sOperation): string
{
return 'Operation'.utils::ToCamelCase($sOperation);
}
}

View File

@@ -420,6 +420,55 @@ class UtilsTest extends ItopTestCase
];
}
/**
* @dataProvider ToCamelCaseProvider
* @covers utils::ToCamelCase
*
* @param string $sInput
* @param string $sExpectedOutput
*
* @return void
*/
public function testToCamelCase(string $sInput, string $sExpectedOutput)
{
$sTestedOutput = utils::ToCamelCase($sInput);
$this->assertEquals($sExpectedOutput, $sTestedOutput, "Camel case transformation for '$sInput' doesn't match. Got '$sTestedOutput', expected '$sExpectedOutput'.");
}
/**
* @since 3.1.0
* @return \string[][]
*/
public function ToCamelCaseProvider(): array
{
return [
'One word' => [
'hello',
'Hello',
],
'Two words separated with space' => [
'hello world',
'HelloWorld',
],
'Two words separated with underscore' => [
'hello_world',
'HelloWorld',
],
'Two words separated with dash' => [
'hello-world',
'HelloWorld',
],
'Two words separated with dot' => [
'hello.world',
'Hello.world',
],
'Three words separated with underscore and space' => [
'hello_there world',
'HelloThereWorld',
],
];
}
/**
* @dataProvider ToAcronymProvider
* @covers utils::ToAcronym
@@ -654,8 +703,8 @@ class UtilsTest extends ItopTestCase
public function sanitizerDataProvider()
{
return [
'good integer' => ['integer', '2565', '2565'],
'bad integer' => ['integer', 'a2656', '2656'],
'good integer' => [utils::ENUM_SANITIZATION_FILTER_INTEGER, '2565', '2565'],
'bad integer' => [utils::ENUM_SANITIZATION_FILTER_INTEGER, 'a2656', '2656'],
/**
* 'class' filter needs a loaded datamodel... and is only an indirection to \MetaModel::IsValidClass so might very important to test !
* If we switch this class to ItopDataTestCase then we are seeing :
@@ -665,20 +714,26 @@ class UtilsTest extends ItopTestCase
*/
// 'good class' => ['class', 'UserRequest', 'UserRequest'],
// 'bad class' => ['class', 'MyUserRequest',null],
'good string' => ['string', 'Is Peter smart and funny?', 'Is Peter smart and funny?'],
'bad string' => ['string', 'Is Peter <smart> & funny?', 'Is Peter &#60;smart&#62; &#38; funny?'],
'good transaction_id' => ['transaction_id', '8965.-dd', '8965.-dd'],
'bad transaction_id' => ['transaction_id', '8965.-dd+', null],
'good parameter' => ['parameter', 'JU8965-dd=_', 'JU8965-dd=_'],
'bad parameter' => ['parameter', '8965.-dd+', null],
'good field_name' => ['field_name', 'Name->bUzz38', 'Name->bUzz38'],
'bad field_name' => ['field_name', 'name-buzz', null],
'good context_param' => ['context_param', '%dssD25_=%:+-', '%dssD25_=%:+-'],
'bad context_param' => ['context_param', '%dssD,25_=%:+-', null],
'good element_identifier' => ['element_identifier', 'AD05nb', 'AD05nb'],
'bad element_identifier' => ['element_identifier', 'AD05nb+', 'AD05nb'],
'good url' => ['url', 'https://www.w3schools.com', 'https://www.w3schools.com'],
'bad url' => ['url', 'https://www.w3schoo<6F><6F>ls.co<63>m', 'https://www.w3schools.com'],
'good string' => [utils::ENUM_SANITIZATION_FILTER_STRING, 'Is Peter smart and funny?', 'Is Peter smart and funny?'],
'bad string' => [utils::ENUM_SANITIZATION_FILTER_STRING, 'Is Peter <smart> & funny?', 'Is Peter &#60;smart&#62; &#38; funny?'],
'good transaction_id' => [utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID, '8965.-dd', '8965.-dd'],
'bad transaction_id' => [utils::ENUM_SANITIZATION_FILTER_TRANSACTION_ID, '8965.-dd+', null],
'good route' => [utils::ENUM_SANITIZATION_FILTER_ROUTE, 'object.modify', 'object.modify'],
'good route with underscore' => [utils::ENUM_SANITIZATION_FILTER_ROUTE, 'object.apply_modify', 'object.apply_modify'],
'bad route with space' => [utils::ENUM_SANITIZATION_FILTER_ROUTE, 'object modify', null],
'good operation' => [utils::ENUM_SANITIZATION_FILTER_OPERATION, 'modify', 'modify'],
'good operation with underscore' => [utils::ENUM_SANITIZATION_FILTER_OPERATION, 'apply_modify', 'apply_modify'],
'bad operation with space' => [utils::ENUM_SANITIZATION_FILTER_OPERATION, 'apply modify', null],
'good parameter' => [utils::ENUM_SANITIZATION_FILTER_PARAMETER, 'JU8965-dd=_', 'JU8965-dd=_'],
'bad parameter' => [utils::ENUM_SANITIZATION_FILTER_PARAMETER, '8965.-dd+', null],
'good field_name' => [utils::ENUM_SANITIZATION_FILTER_FIELD_NAME, 'Name->bUzz38', 'Name->bUzz38'],
'bad field_name' => [utils::ENUM_SANITIZATION_FILTER_FIELD_NAME, 'name-buzz', null],
'good context_param' => [utils::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM, '%dssD25_=%:+-', '%dssD25_=%:+-'],
'bad context_param' => [utils::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM, '%dssD,25_=%:+-', null],
'good element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb', 'AD05nb'],
'bad element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb+', 'AD05nb'],
'good url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://www.w3schools.com', 'https://www.w3schools.com'],
'bad url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://www.w3schoo<6F><6F>ls.co<63>m', 'https://www.w3schools.com'],
'raw_data' => ['raw_data', '<Test>\s😃😃😃', '<Test>\s😃😃😃'],
];
}

View File

@@ -0,0 +1,205 @@
<?php
/*
* @copyright Copyright (C) 2010-2022 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Router\Router;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
/**
* Class RouterTest
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @since 3.1.0
* @covers \Combodo\iTop\Router\Router
*/
class RouterTest extends ItopTestCase
{
/**
* @dataProvider CanDispatchRouteProvider
* @covers \Combodo\iTop\Router\Router::CanDispatchRoute
*
* @param string $sRoute
* @param $bExpectedResult
*
* @return void
*/
// public function testCanDispatchRoute(string $sRoute, $bExpectedResult): void
// {
// $oRouter = Router::GetInstance();
// $bTestedResult = $oRouter->CanDispatchRoute($sRoute);
//
// $sRouteNamespace = $oRouter->GetRouteNamespace($sRoute);
// $sRouteOperation = $oRouter->GetRouteOperation($sRoute);
// $aRouteParts = $oRouter->GetRouteParts($sRoute);
// $sControllerFQCN = $this->InvokeNonPublicMethod(get_class($oRouter), 'FindControllerFromRouteNamespace', $oRouter, ['object']);
// $sMethodName = $this->InvokeNonPublicMethod(get_class($oRouter), 'MakeOperationMethodNameFromOperation', $oRouter, ['modify']);
// $aDispatchSpecs = $oRouter->GetDispatchSpecsForRoute($sRoute);
//
//$this->debug($sRoute);
//$this->debug($sRouteNamespace);
//$this->debug($sRouteOperation);
//$this->debug($aRouteParts);
//$this->debug($sControllerFQCN);
//$this->debug($sMethodName);
//$this->debug(is_callable([$sControllerFQCN, $sMethodName]) ? 'true' : 'false');
//$this->debug($aDispatchSpecs);
//$this->debug($bTestedResult);
// $this->assertEquals($bExpectedResult, $bTestedResult, "Dispatch capability for '$sRoute' was not the expected one. Got ".var_export($bTestedResult, true).", expected ".var_export($bExpectedResult, true));
// }
public function CanDispatchRouteProvider(): array
{
return [
'Existing handler' => [
'object.modify',
true,
],
'Existing controller but unknown operation' => [
'object.modify_me_please',
false,
],
'Unknown controller' => [
'foo.bar',
false,
],
];
}
/**
* @dataProvider GetRouteNamespaceProvider
* @covers \Combodo\iTop\Router\Router::GetRouteNamespace
*
* @param string $sRoute
* @param string|null $sExpectedNamespace
*
* @return void
*/
public function testGetRouteNamespace(string $sRoute, ?string $sExpectedNamespace): void
{
$oRouter = Router::GetInstance();
$sTestedNamespace = $oRouter->GetRouteNamespace($sRoute);
$this->assertEquals($sExpectedNamespace, $sTestedNamespace, "Namespace found for '$sRoute' was not the expected one. Got '$sTestedNamespace', expected '$sExpectedNamespace'.");
}
public function GetRouteNamespaceProvider(): array
{
return [
'Operation without namespace' => [
'some_operation',
null,
],
'Operation with namespace' => [
'some_namespace.some_operation',
'some_namespace',
],
'Operation with multi-levels namespace' => [
'some.deep.namespace.some_operation',
'some.deep.namespace',
],
];
}
/**
* @dataProvider GetRouteOperationProvider
* @covers \Combodo\iTop\Router\Router::GetRouteOperation
*
* @param string $sRoute
* @param string|null $sExpectedOperation
*
* @return void
*/
public function testGetRouteOperation(string $sRoute, ?string $sExpectedOperation): void
{
$oRouter = Router::GetInstance();
$sTestedOperation = $oRouter->GetRouteOperation($sRoute);
$this->assertEquals($sExpectedOperation, $sTestedOperation, "Operation found for '$sRoute' was not the expected one. Got '$sTestedOperation', expected '$sExpectedOperation'.");
}
public function GetRouteOperationProvider(): array
{
return [
'Operation without namespace' => [
'some_operation',
null,
],
'Operation with namespace' => [
'some_namespace.some_operation',
'some_operation',
],
'Operation with multi-levels namespace' => [
'some.deep.namespace.some_operation',
'some_operation',
],
];
}
/**
* @dataProvider FindControllerFromRouteNamespaceProvider
* @covers \Combodo\iTop\Router\Router::FindControllerFromRouteNamespace
*
* @param string $sRouteNamespace
* @param string $sExpectedControllerFQCN
*
* @return void
*/
public function testFindControllerFromRouteNamespace(string $sRoute, ?string $sExpectedControllerFQCN): void
{
$oRouter = Router::GetInstance();
$sRouteNamespace = $oRouter->GetRouteNamespace($sRoute);
$sTestedControllerFQCN = $this->InvokeNonPublicMethod(get_class($oRouter), 'FindControllerFromRouteNamespace', $oRouter, [$sRouteNamespace]);
$this->assertEquals($sExpectedControllerFQCN, $sTestedControllerFQCN, "Controller found for '$sRouteNamespace' was not the expected one. Got '$sTestedControllerFQCN', expected '$sExpectedControllerFQCN'.");
}
public function FindControllerFromRouteNamespaceProvider(): array
{
return [
'Object controller' => [
'object.modify',
'Combodo\iTop\Controller\Base\Layout\ObjectController',
],
'Unknown controller' => [
'something_that_should_not_exist_in_the_default_package.foo',
null,
],
];
}
/**
* @dataProvider GetOperationMethodNameFromRouteOperationProvider
* @covers \Combodo\iTop\Router\Router::MakeOperationMethodNameFromOperation
*
* @param string $sRoute
* @param string $sExpectedMethodName
*
* @return void
*/
public function testGetOperationMethodNameFromRouteOperation(string $sRoute, string $sExpectedMethodName): void
{
$oRouter = Router::GetInstance();
$aRouteParts = $oRouter->GetRouteParts($sRoute);
$sTestedMethodName = $this->InvokeNonPublicMethod(get_class($oRouter), 'MakeOperationMethodNameFromOperation', $oRouter, [$aRouteParts[1]]);
$this->assertEquals($sExpectedMethodName, $sTestedMethodName, "Operation method name '$aRouteParts[1]' was not matching the expected one. Got '$sTestedMethodName', expected '$sExpectedMethodName'.");
}
public function GetOperationMethodNameFromRouteOperationProvider(): array
{
return [
'Simple operation' => [
'object.modify',
'OperationModify',
],
'Operation with an underscore' => [
'object.apply_modify',
'OperationApplyModify',
],
];
}
}