mirror of
https://github.com/Combodo/iTop.git
synced 2026-05-18 14:58:43 +02:00
Merge remote-tracking branch 'origin/feature/5655-edit-object-in-modal' into develop
# Conflicts: # pages/ajax.render.php
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -100,6 +100,6 @@ $(document).ready(function()
|
||||
};
|
||||
}
|
||||
|
||||
CombodoPortalToolbox.OpenModal(oOptions);
|
||||
CombodoModal.OpenModal(oOptions);
|
||||
});
|
||||
});
|
||||
@@ -397,7 +397,7 @@ $(function()
|
||||
if(bRedirectInModal === true)
|
||||
{
|
||||
// Creating a new modal
|
||||
CombodoPortalToolbox.OpenModal({
|
||||
CombodoModal.OpenModal({
|
||||
content: {
|
||||
endpoint: sRedirectUrl,
|
||||
data: {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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());
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
oEvent.stopPropagation();
|
||||
|
||||
// Creating a new modal
|
||||
CombodoPortalToolbox.OpenModal({
|
||||
CombodoModal.OpenModal({
|
||||
content: {
|
||||
endpoint: $(this).attr('href'),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
55
js/utils.js
55
js/utils.js
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
2819
pages/UI.php
2819
pages/UI.php
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
28
sources/Controller/AbstractController.php
Normal file
28
sources/Controller/AbstractController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
102
sources/Controller/Base/Layout/ObjectController.php
Normal file
102
sources/Controller/Base/Layout/ObjectController.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
30
sources/Controller/iController.php
Normal file
30
sources/Controller/iController.php
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
183
sources/Router/Router.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 <smart> & 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 <smart> & 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😃😃😃'],
|
||||
];
|
||||
}
|
||||
|
||||
205
test/sources/Router/RouterTest.php
Normal file
205
test/sources/Router/RouterTest.php
Normal 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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user