mirror of
https://github.com/Combodo/iTop.git
synced 2026-06-13 11:22:18 +02:00
Compare commits
14 Commits
feature/95
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
806a5b92df | ||
|
|
7d0005acf3 | ||
|
|
6999000d69 | ||
|
|
9c782c41df | ||
|
|
eedcdf32fe | ||
|
|
2321d89981 | ||
|
|
3bf47d1e8c | ||
|
|
feb89650e4 | ||
|
|
b672dfc9f2 | ||
|
|
eef06ee032 | ||
|
|
d6ce202fa8 | ||
|
|
1c38d989e4 | ||
|
|
e65542f978 | ||
|
|
829857ec85 |
@@ -5,7 +5,9 @@
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
class CoreException extends Exception
|
||||
use Combodo\iTop\Exception\ItopException;
|
||||
|
||||
class CoreException extends ItopException
|
||||
{
|
||||
protected $m_sIssue;
|
||||
protected $m_sImpact;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
use Combodo\iTop\Application\Helper\ExceptionHandlerHelper;
|
||||
use Combodo\iTop\Application\Helper\Session;
|
||||
|
||||
require_once(APPROOT.'core/cmdbobject.class.inc.php');
|
||||
@@ -63,6 +64,9 @@ register_shutdown_function(function () {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
set_exception_handler([ExceptionHandlerHelper::class, 'HandleException']);
|
||||
|
||||
$oKPI = new ExecutionKPI();
|
||||
Session::Start();
|
||||
$oKPI->ComputeAndReport("Session Start");
|
||||
|
||||
@@ -1917,7 +1917,9 @@ SQL;
|
||||
$aResponseHeaders[$sName] = $sValue;
|
||||
}
|
||||
}
|
||||
curl_close($ch);
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -614,6 +614,12 @@ class LogChannels
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public const SECURITY = 'Security';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public const EXCEPTION = 'Exception';
|
||||
}
|
||||
|
||||
abstract class LogAPI
|
||||
@@ -708,8 +714,14 @@ abstract class LogAPI
|
||||
|
||||
private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array
|
||||
{
|
||||
$aContext['Error Message'] = $oException->getMessage();
|
||||
$aContext['Stack Trace'] = $oException->getTraceAsString();
|
||||
$aContext['exception'] = get_class($oException);
|
||||
$aContext['message'] = $oException->getMessage();
|
||||
$aContext['stack'] = $oException->getTraceAsString();
|
||||
|
||||
if(method_exists($oException, 'GetContext')) {
|
||||
$aContext = array_merge($aContext, $oException->GetContext());
|
||||
}
|
||||
|
||||
return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext];
|
||||
}
|
||||
|
||||
|
||||
@@ -340,13 +340,14 @@ class ormDocument
|
||||
* @param string $sContentDisposition Either 'inline' or 'attachment'
|
||||
* @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file
|
||||
* @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField
|
||||
* @param bool $bAllowAllData If true, no rights filtering is applied
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null)
|
||||
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null, $bAllowAllData = false)
|
||||
{
|
||||
try {
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false, false);
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false, $bAllowAllData);
|
||||
if (!is_object($oObj)) {
|
||||
// If access to the document is not granted, check if the access to the host object is allowed
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false, true);
|
||||
|
||||
@@ -26,6 +26,7 @@ use Dict;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use RunTimeEnvironment;
|
||||
use SecurityException;
|
||||
use SetupUtils;
|
||||
use utils;
|
||||
|
||||
@@ -83,10 +84,11 @@ class DataFeatureRemovalController extends Controller
|
||||
{
|
||||
$aParams = [];
|
||||
|
||||
if (SetupUtils::IsSessionSetupTokenValid()) {
|
||||
try {
|
||||
//from setup wizard/mtp
|
||||
SetupUtils::CheckSetupToken();
|
||||
SetupUtils::EraseSetupToken();
|
||||
} else {
|
||||
} catch (SecurityException $e) {
|
||||
//from same module
|
||||
$this->ValidateTransactionId();
|
||||
}
|
||||
@@ -100,6 +102,7 @@ class DataFeatureRemovalController extends Controller
|
||||
'removed_extensions' => '[]',
|
||||
'extensions_not_uninstallable' => '[]',
|
||||
'copy_setup_files' => 1,
|
||||
'return_button_label' => '',
|
||||
];
|
||||
|
||||
$aHiddenInputs = [];
|
||||
@@ -108,6 +111,10 @@ class DataFeatureRemovalController extends Controller
|
||||
}
|
||||
$aParams['aHiddenInputs'] = $aHiddenInputs;
|
||||
|
||||
if ($aHiddenInputs['return_button_label'] !== '') {
|
||||
$aParams['sReturnButtonURL'] = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
}
|
||||
|
||||
$aAddedExtensions = json_decode($aHiddenInputs['added_extensions'], true);
|
||||
|
||||
$aRemovedExtensions = json_decode($aHiddenInputs['removed_extensions'], true);
|
||||
@@ -179,6 +186,10 @@ class DataFeatureRemovalController extends Controller
|
||||
$aParams['aSetupParams']["_params[$sInputName]"] = $sInputValue;
|
||||
}
|
||||
|
||||
if ($aHiddenInputs['return_button_label'] !== '') {
|
||||
$aParams['sReturnButtonURL'] = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
}
|
||||
|
||||
[$aParams['aDeletionPlanSummary'], $aParams['iQueryCount'], $aParams['bDeletionPossible']] = $this->GetDeletionPlanSummaryTable($aGetRemovedClasses);
|
||||
[$aParams['aDeletionExecutionSummary'], $aParams['bHasDeletionExecution']] = $this->GetExecutionSummaryTable();
|
||||
$aParams['bDeletionNeeded'] = ($aParams['iQueryCount'] > 0);
|
||||
|
||||
@@ -87,11 +87,15 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% if aHiddenInputs.return_button_label != '' %}
|
||||
{% UIButton ForAlternativeSecondaryAction { sLabel:aHiddenInputs.return_button_label, OnClickJsCode: 'window.location.href="' ~ sReturnButtonURL ~ '"'} %}
|
||||
{% else %}
|
||||
{% UIForm Standard {} %}
|
||||
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
|
||||
{% UIInput ForHidden { sName:'operation', sValue:'Main'} %}
|
||||
{% UIToolbar ForButton {} %}
|
||||
{% UIButton ForPrimaryAction {sLabel:'UI:Button:BackToMain'|dict_s, sName:'btn_back', sId:'btn_back', bIsSubmit:true} %}
|
||||
{% EndUIToolbar %}
|
||||
{% EndUIForm %}
|
||||
{% endif %}
|
||||
{% EndUIPanel %}
|
||||
|
||||
@@ -26,6 +26,7 @@ SetupWebPage::AddModule(
|
||||
],
|
||||
'data.struct' => [
|
||||
'data/en_us.data.itop-brand.xml',
|
||||
'data/en_us.data.itop-networkdevicetype.xml',
|
||||
'data/en_us.data.itop-osfamily.xml',
|
||||
'data/en_us.data.itop-osversion.xml',
|
||||
],
|
||||
@@ -101,8 +102,6 @@ if (!class_exists('ConfigMgmtInstaller')) {
|
||||
*/
|
||||
public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion)
|
||||
{
|
||||
// load localized data for NetworkDeviceType
|
||||
static::LoadLocalizedDataOnCrossingVersion($oConfiguration, $sPreviousVersion, $sCurrentVersion,'3.3.0',__DIR__."/data/{{language_code}}.data.itop-networkdevicetype.xml" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,13 @@ if (!class_exists('ServiceMgmtProviderInstaller')) {
|
||||
public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion)
|
||||
{
|
||||
// Load localized structural data: contract types
|
||||
static::LoadLocalizedDataOnNewInstall($oConfiguration, $sPreviousVersion, __DIR__."/data/{{language_code}}.data.itop-contracttype.xml");
|
||||
static::LoadLocalizedData(
|
||||
$oConfiguration,
|
||||
$sPreviousVersion,
|
||||
$sCurrentVersion,
|
||||
'3.3.0',
|
||||
__DIR__."/data/{{language_code}}.data.itop-contracttype.xml"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,13 @@ if (!class_exists('ServiceMgmtInstaller')) {
|
||||
public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion)
|
||||
{
|
||||
// Load localized structural data: contact types and document types
|
||||
static::LoadLocalizedDataOnNewInstall($oConfiguration, $sPreviousVersion, __DIR__."/data/{{language_code}}.data.itop-contracttype.xml");
|
||||
static::LoadLocalizedData(
|
||||
$oConfiguration,
|
||||
$sPreviousVersion,
|
||||
$sCurrentVersion,
|
||||
'3.3.0',
|
||||
__DIR__."/data/{{language_code}}.data.itop-contracttype.xml"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,20 @@ if (!class_exists('StructureInstaller')) {
|
||||
public static function AfterDatabaseCreation(Config $oConfiguration, $sPreviousVersion, $sCurrentVersion)
|
||||
{
|
||||
// Load localized structural data: contact types and document types
|
||||
static::LoadLocalizedDataOnNewInstall($oConfiguration, $sPreviousVersion, __DIR__."/data/{{language_code}}.data.itop-contacttype.xml");
|
||||
static::LoadLocalizedDataOnNewInstall($oConfiguration, $sPreviousVersion, __DIR__."/data/{{language_code}}.data.itop-documenttype.xml");
|
||||
static::LoadLocalizedData(
|
||||
$oConfiguration,
|
||||
$sPreviousVersion,
|
||||
$sCurrentVersion,
|
||||
'3.3.0',
|
||||
__DIR__."/data/{{language_code}}.data.itop-contacttype.xml"
|
||||
);
|
||||
static::LoadLocalizedData(
|
||||
$oConfiguration,
|
||||
$sPreviousVersion,
|
||||
$sCurrentVersion,
|
||||
'3.3.0',
|
||||
__DIR__."/data/{{language_code}}.data.itop-documenttype.xml"
|
||||
);
|
||||
|
||||
// Default language will be used for actions
|
||||
// Note: There is a issue when upgrading, default language cannot be retrieved from the passed configuration, we have to read it from the disk
|
||||
|
||||
@@ -61,6 +61,6 @@ class TicketsInstaller extends ModuleInstallerAPI
|
||||
}
|
||||
}
|
||||
// Load localized structural data: predefined query phrases for notifications
|
||||
static::LoadLocalizedDataOnCrossingVersion($oConfiguration, $sPreviousVersion, $sCurrentVersion, '3.0.0', __DIR__."/data/{{language_code}}.data.itop-tickets.xml");
|
||||
static::LoadLocalizedData($oConfiguration, $sPreviousVersion, $sCurrentVersion, '3.0.0', __DIR__."/data/{{language_code}}.data.itop-tickets.xml");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ return array(
|
||||
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/Application/Branding.php',
|
||||
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExceptionHandlerHelper' => $baseDir . '/sources/Application/Helper/ExceptionHandlerHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\FormHelper' => $baseDir . '/sources/Application/Helper/FormHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => $baseDir . '/sources/Application/Helper/ImportHelper.php',
|
||||
@@ -444,6 +445,7 @@ return array(
|
||||
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => $baseDir . '/sources/Dependencies/NPM/iTopNPM.php',
|
||||
'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php',
|
||||
'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php',
|
||||
'Combodo\\iTop\\Exception\\ItopException' => $baseDir . '/sources/Exception/ItopException.php',
|
||||
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => $baseDir . '/sources/Form/Field/AbstractSimpleField.php',
|
||||
'Combodo\\iTop\\Form\\Field\\BlobField' => $baseDir . '/sources/Form/Field/BlobField.php',
|
||||
'Combodo\\iTop\\Form\\Field\\CaseLogField' => $baseDir . '/sources/Form/Field/CaseLogField.php',
|
||||
|
||||
@@ -533,6 +533,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
|
||||
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/Application/Branding.php',
|
||||
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExceptionHandlerHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExceptionHandlerHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\FormHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/FormHelper.php',
|
||||
'Combodo\\iTop\\Application\\Helper\\ImportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ImportHelper.php',
|
||||
@@ -845,6 +846,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685
|
||||
'Combodo\\iTop\\Dependencies\\NPM\\iTopNPM' => __DIR__ . '/../..' . '/sources/Dependencies/NPM/iTopNPM.php',
|
||||
'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
||||
'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php',
|
||||
'Combodo\\iTop\\Exception\\ItopException' => __DIR__ . '/../..' . '/sources/Exception/ItopException.php',
|
||||
'Combodo\\iTop\\Form\\Field\\AbstractSimpleField' => __DIR__ . '/../..' . '/sources/Form/Field/AbstractSimpleField.php',
|
||||
'Combodo\\iTop\\Form\\Field\\BlobField' => __DIR__ . '/../..' . '/sources/Form/Field/BlobField.php',
|
||||
'Combodo\\iTop\\Form\\Field\\CaseLogField' => __DIR__ . '/../..' . '/sources/Form/Field/CaseLogField.php',
|
||||
|
||||
38
pages/UI.php
38
pages/UI.php
@@ -19,12 +19,12 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Application\WelcomePopup\WelcomePopupService;
|
||||
use Combodo\iTop\Controller\Base\Layout\ObjectController;
|
||||
use Combodo\iTop\Controller\WelcomePopupController;
|
||||
use Combodo\iTop\Exception\ItopException;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
|
||||
/**
|
||||
@@ -1303,39 +1303,9 @@ try {
|
||||
$oKPI->ComputeAndReport('Compute page');
|
||||
$oP->output();
|
||||
} catch (Exception $e) {
|
||||
$oErrorPage = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
if ($e instanceof SecurityException) {
|
||||
$oErrorPage->add("<h1>".Dict::S('UI:SystemIntrusion')."</h1>\n");
|
||||
} else {
|
||||
$oErrorPage->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
}
|
||||
$sErrorDetails = ($e instanceof CoreException) ? $e->getHtmlDesc() : $e->getMessage();
|
||||
$oErrorPage->error(Dict::Format('UI:Error_Details', utils::EscapeHtml($sErrorDetails)), $e);
|
||||
$oErrorPage->output();
|
||||
|
||||
$sErrorStackTrace = ($e instanceof CoreException) ? $e->getFullStackTraceAsString() : $e->getTraceAsString();
|
||||
if (MetaModel::IsLogEnabledIssue()) {
|
||||
if (MetaModel::IsValidClass('EventIssue')) {
|
||||
try {
|
||||
$oLog = new EventIssue();
|
||||
|
||||
$oLog->Set('message', $e->getMessage());
|
||||
$oLog->Set('userinfo', '');
|
||||
$sIssue = ($e instanceof CoreException) ? $e->GetIssue() : 'PHP Exception';
|
||||
$oLog->Set('issue', $sIssue);
|
||||
$oLog->Set('impact', 'Page could not be displayed');
|
||||
$oLog->Set('callstack', $sErrorStackTrace);
|
||||
$aData = ($e instanceof CoreException) ? $e->getContextData() : [];
|
||||
$oLog->Set('data', $aData);
|
||||
$oLog->DBInsertNoReload();
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Exception("Failed to log issue into the DB", $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sOperationToLog = $operation ?? 'N/A';
|
||||
IssueLog::Debug('UI.php operation='.$sOperationToLog.', error='.$e->getMessage()."\n".$sErrorStackTrace, LogChannels::CONSOLE);
|
||||
throw new ItopException("Unable to handle UI operation", previous: $e, aContext: [
|
||||
'operation' => ($operation ?? 'N/A'),
|
||||
]);
|
||||
}
|
||||
|
||||
class UI
|
||||
|
||||
@@ -311,7 +311,7 @@ abstract class ModuleInstallerAPI
|
||||
|
||||
/**
|
||||
* @param \Config $oConfiguration
|
||||
* @param string $sPreviousVersion The previous version of the module (empty string in case of first install)
|
||||
* @param string $sPreviousVersion The previous version of the module (empty string will force the loading)
|
||||
* @param string $sCurrentVersion The current version of the module
|
||||
* @param string $sFirstLoadingVersion The first module version for which the data loading should be performed (e.g. '3.0.0')
|
||||
* @param string $sFilePattern The pattern of the file to load, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
|
||||
@@ -321,51 +321,74 @@ abstract class ModuleInstallerAPI
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
*/
|
||||
public static function LoadLocalizedDataOnCrossingVersion(Config $oConfiguration, ?string $sPreviousVersion, ?string $sCurrentVersion, string $sFirstLoadingVersion, string $sFilePattern): void
|
||||
public static function LoadLocalizedData(Config $oConfiguration, string $sPreviousVersion, string $sCurrentVersion, string $sFirstLoadingVersion, string $sFilePattern): void
|
||||
{
|
||||
self::AssertLoadLocalizedDataParametersAreValid($sPreviousVersion, $sCurrentVersion, $sFirstLoadingVersion);
|
||||
self::AssertLoadLocalizedDataParametersAreValid($sPreviousVersion, $sCurrentVersion, $sFirstLoadingVersion, $sFilePattern);
|
||||
|
||||
// The loading is done only if
|
||||
// - it's a first install of the module
|
||||
// - or it's an upgrade of that module (PreviousVersion is less than the CurrentVersion), which means that we are really upgrading (and not reinstalling the same version or downgrading), and
|
||||
// - either the FirstLoadingVersion is between the PreviousVersion and the CurrentVersion
|
||||
// - or the FirstLoadingVersion is empty, forcing the loading on all upgrades,
|
||||
// It's not very clear if it makes sense to test a particular version,
|
||||
// as the loading mechanism checks object existence using reconc_keys
|
||||
// and do not recreate them, nor update existing.
|
||||
// Without test, new entries added to the data files, would be automatically loaded
|
||||
if (($sPreviousVersion === '') ||
|
||||
(version_compare($sPreviousVersion, $sCurrentVersion, '<')
|
||||
&& version_compare($sPreviousVersion, $sFirstLoadingVersion, '<')
|
||||
&& version_compare($sFirstLoadingVersion, $sCurrentVersion, '<='))) {
|
||||
&& version_compare($sPreviousVersion, $sFirstLoadingVersion, '<'))) {
|
||||
|
||||
self::LoadLocalizedData($oConfiguration, $sFilePattern);
|
||||
// Note: There is an issue when upgrading, default language cannot be retrieved from the passed configuration, we have to read it from the disk
|
||||
if (utils::IsNullOrEmptyString($sPreviousVersion)) {
|
||||
// Fresh install
|
||||
$sDefaultLanguage = $oConfiguration->GetDefaultLanguage();
|
||||
} else {
|
||||
// Upgrade
|
||||
$sDefaultLanguage = utils::GetConfig(true)->GetDefaultLanguage();
|
||||
}
|
||||
|
||||
$sFileName = self::GetLocalizedFileName($sDefaultLanguage, $sFilePattern);
|
||||
if ($sFileName !== '') {
|
||||
self::XMLFileLoad($sFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Config $oConfiguration
|
||||
* @param string $sPreviousVersion The previous version of the module (empty string in case of first install)
|
||||
* @param string $sFilePattern The pattern of the file to load, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function LoadLocalizedDataOnNewInstall(Config $oConfiguration, ?string $sPreviousVersion, string $sFilePattern): void
|
||||
* @throws \CoreUnexpectedValue
|
||||
*/
|
||||
private static function AssertLoadLocalizedDataParametersAreValid(string $sPreviousVersion, string $sCurrentVersion, string $sFirstLoadingVersion, string $sFilePattern): void
|
||||
{
|
||||
if (utils::IsNullOrEmptyString($sPreviousVersion)) {
|
||||
self::LoadLocalizedData($oConfiguration, $sFilePattern);
|
||||
if (($sPreviousVersion !== '') && !self::IsValidLocalizedDataVersion($sPreviousVersion)) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sPreviousVersion to be empty or match x.y[.z][-name], got '{$sPreviousVersion}'");
|
||||
}
|
||||
|
||||
if (!self::IsValidLocalizedDataVersion($sCurrentVersion)) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sCurrentVersion to match x.y[.z][-name], got '{$sCurrentVersion}'");
|
||||
}
|
||||
|
||||
if (!self::IsValidLocalizedDataVersion($sFirstLoadingVersion)) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sFirstLoadingVersion to match x.y[.z][-name], got '{$sFirstLoadingVersion}'");
|
||||
}
|
||||
|
||||
if (utils::IsNullOrEmptyString($sFilePattern)) {
|
||||
throw new CoreUnexpectedValue('LoadLocalizedData expects sFilePattern to be a non-empty string');
|
||||
}
|
||||
|
||||
if (substr_count($sFilePattern, '{{language_code}}') !== 1) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sFilePattern to contain the exact placeholder '{{language_code}}' exactly once");
|
||||
}
|
||||
}
|
||||
|
||||
private static function IsValidLocalizedDataVersion(string $sVersion): bool
|
||||
{
|
||||
return (preg_match('/^\d+\.\d+(?:\.\d+)?(?:-[A-Za-z0-9]+)?$/', $sVersion) === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Config $oConfiguration
|
||||
* @param string $sFilePattern The pattern of the file to load, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
|
||||
* @param array|string $sFileName
|
||||
* @param \XMLDataLoader $oDataLoader
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected static function LoadLocalizedData(Config $oConfiguration, string $sFilePattern): void
|
||||
public static function XMLFileLoad(string $sFileName): void
|
||||
{
|
||||
if (substr_count($sFilePattern, '{{language_code}}') !== 1) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects $sFilePattern to contain the exact placeholder '{{language_code}}' exactly once");
|
||||
}
|
||||
$sDefaultLanguage = $oConfiguration->GetDefaultLanguage();
|
||||
$sFileName = self::GetLocalizedFileName($sDefaultLanguage, $sFilePattern);
|
||||
if (!file_exists($sFileName)) {
|
||||
throw new Exception("File $sFileName not found");
|
||||
}
|
||||
@@ -378,39 +401,19 @@ abstract class ModuleInstallerAPI
|
||||
$oDataLoader->EndSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \CoreUnexpectedValue
|
||||
*/
|
||||
private static function AssertLoadLocalizedDataParametersAreValid(?string $sPreviousVersion, ?string $sCurrentVersion, string $sFirstLoadingVersion): void
|
||||
{
|
||||
if (($sPreviousVersion !== '') && !self::IsValidLocalizedDataVersion($sPreviousVersion)) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sPreviousVersion to be empty or match x.y[.z][-name], got '{$sPreviousVersion}'");
|
||||
}
|
||||
if (!self::IsValidLocalizedDataVersion($sCurrentVersion)) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sCurrentVersion to match x.y[.z][-name], got '{$sCurrentVersion}'");
|
||||
}
|
||||
if (($sFirstLoadingVersion !== '') && !self::IsValidLocalizedDataVersion($sFirstLoadingVersion)) {
|
||||
throw new CoreUnexpectedValue("LoadLocalizedData expects sFirstLoadingVersion to match x.y[.z][-name], got '{$sFirstLoadingVersion}'");
|
||||
}
|
||||
}
|
||||
|
||||
private static function IsValidLocalizedDataVersion(string $sVersion): bool
|
||||
{
|
||||
return (preg_match('/^\d+\.\d+(?:\.\d+)?(?:-[A-Za-z0-9]+)?$/', $sVersion) === 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sLanguage The language code to use for localization (e.g. 'EN US')
|
||||
* @param string $sFilePattern The full path+name of the file to localize, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
|
||||
*
|
||||
* @return string The localized file name if found, or an empty string if not found
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
private static function GetLocalizedFileName($sLanguage, string $sFilePattern): string
|
||||
public static function GetLocalizedFileName($sLanguage, string $sFilePattern): string
|
||||
{
|
||||
$sLang = str_replace(' ', '_', strtolower($sLanguage));
|
||||
$sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern);
|
||||
if (!file_exists($sFileName)) {
|
||||
SetupLog::Debug("No data file found matching the pattern $sFilePattern and language_code $sLang. Trying with 'en_us' as fallback.");
|
||||
$sLang = 'en_us';
|
||||
$sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern);
|
||||
}
|
||||
|
||||
@@ -81,13 +81,13 @@ class ApplicationInstallSequencer extends StepSequencer
|
||||
return $this->ComputeNextStep($sStep);
|
||||
|
||||
case 'log-parameters':
|
||||
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
|
||||
if ($this->HasOptionalStep($sStep)) {
|
||||
$this->DoLogParameters();
|
||||
}
|
||||
return $this->ComputeNextStep($sStep);
|
||||
|
||||
case 'backup':
|
||||
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
|
||||
if ($this->HasOptionalStep($sStep, false)) {
|
||||
$aBackupOptions = $this->oParams->Get('optional_steps')['backup'];
|
||||
// __DB__-%Y-%m-%d
|
||||
$sDestination = $aBackupOptions['destination'];
|
||||
@@ -100,7 +100,7 @@ class ApplicationInstallSequencer extends StepSequencer
|
||||
|
||||
case 'migrate-before':
|
||||
$this->oRunTimeEnvironment->EnterMaintenanceMode($this->GetConfig());
|
||||
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
|
||||
if ($this->HasOptionalStep($sStep)) {
|
||||
$this->oRunTimeEnvironment->MigrateDataBeforeUpdateStructure($this->oParams->Get('mode'), $this->GetConfig());
|
||||
}
|
||||
return $this->ComputeNextStep($sStep);
|
||||
@@ -111,7 +111,7 @@ class ApplicationInstallSequencer extends StepSequencer
|
||||
return $this->ComputeNextStep($sStep);
|
||||
|
||||
case 'migrate-after':
|
||||
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
|
||||
if ($this->HasOptionalStep($sStep)) {
|
||||
$this->oRunTimeEnvironment->MigrateDataAfterUpdateStructure($this->oParams->Get('mode'), $this->GetConfig());
|
||||
}
|
||||
return $this->ComputeNextStep($sStep);
|
||||
@@ -191,13 +191,17 @@ class ApplicationInstallSequencer extends StepSequencer
|
||||
public function GetStepNames(): array
|
||||
{
|
||||
$aStepNames = [ ''];
|
||||
foreach (['log-parameters', 'backup', 'migrate-before'] as $sStepName) {
|
||||
if (array_key_exists($sStepName, $this->oParams->Get('optional_steps', []))) {
|
||||
$aStepNames [] = $sStepName;
|
||||
}
|
||||
if ($this->HasOptionalStep('log-parameters')) {
|
||||
$aStepNames [] = 'log-parameters';
|
||||
}
|
||||
if ($this->HasOptionalStep('backup', false)) {
|
||||
$aStepNames [] = 'backup';
|
||||
}
|
||||
if ($this->HasOptionalStep('migrate-before')) {
|
||||
$aStepNames [] = 'migrate-before';
|
||||
}
|
||||
$aStepNames [] = 'db-schema';
|
||||
if (array_key_exists('migrate-after', $this->oParams->Get('optional_steps', []))) {
|
||||
if ($this->HasOptionalStep('migrate-after')) {
|
||||
$aStepNames [] = 'migrate-after';
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class DataAuditSequencer extends StepSequencer
|
||||
|
||||
protected function IsDataAuditRequired(): bool
|
||||
{
|
||||
if (! array_key_exists('setup-audit', $this->oParams->Get('optional_steps', []))) {
|
||||
if (!$this->HasOptionalStep('setup-audit', false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ class DataAuditSequencer extends StepSequencer
|
||||
public function GetStepNames(): array
|
||||
{
|
||||
$aStepNames = [''];
|
||||
if (array_key_exists('copy', $this->oParams->Get('optional_steps', []))) {
|
||||
if ($this->HasOptionalStep('copy')) {
|
||||
$aStepNames[] = 'copy';
|
||||
}
|
||||
$aStepNames[] = 'compile';
|
||||
|
||||
@@ -29,6 +29,9 @@ abstract class StepSequencer
|
||||
protected RunTimeEnvironment $oRunTimeEnvironment;
|
||||
protected string $sSourceDesc;
|
||||
|
||||
protected const LABELS = [];
|
||||
protected const SUCCESS_LABELS = [];
|
||||
|
||||
/**
|
||||
* @param \Parameters $oParams
|
||||
* @param \RunTimeEnvironment|null $oRunTimeEnvironment
|
||||
@@ -233,4 +236,26 @@ abstract class StepSequencer
|
||||
* @return array (status => , message => , percentage-completed => , next-step => , next-step-label => )
|
||||
*/
|
||||
abstract public function ExecuteStep($sStep = '', $sInstallComment = null): array;
|
||||
|
||||
/**
|
||||
* Check whether an optional step is enabled in the "optional_steps" parameters. If optional_steps is not set, use $bDefaultValue as its default value
|
||||
*
|
||||
* @param $sStep
|
||||
* @param bool $bDefaultValue
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function HasOptionalStep($sStep, bool $bDefaultValue = true)
|
||||
{
|
||||
$aOptionalSteps = $this->oParams->Get('optional_steps', null);
|
||||
if (is_null($aOptionalSteps)) {
|
||||
return $bDefaultValue;
|
||||
}
|
||||
if (is_array($aOptionalSteps)) {
|
||||
return array_key_exists($sStep, $aOptionalSteps);
|
||||
}
|
||||
throw new Exception('Incorrect value for parameter optional_steps');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,17 +26,17 @@ function WizardAsyncAction(sActionCode, oParams, OnErrorFunction)
|
||||
function WizardUpdateButtons()
|
||||
{
|
||||
if (CanMoveForward()) {
|
||||
$("#btn_next").prop('disabled', false);
|
||||
$("#btn_next").removeClass('ibo-is-hidden');
|
||||
}
|
||||
else {
|
||||
$("#btn_next").prop('disabled', true);
|
||||
$("#btn_next").addClass('ibo-is-hidden');
|
||||
}
|
||||
|
||||
if (CanMoveBackward()) {
|
||||
$("#btn_back").prop('disabled', false);
|
||||
$("#btn_back").removeClass('ibo-is-hidden');
|
||||
}
|
||||
else {
|
||||
$("#btn_back").prop('disabled', true);
|
||||
$("#btn_back").addClass('ibo-is-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,10 +151,10 @@ class WizardController
|
||||
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
|
||||
$oStep = $this->GetWizardStep($sCurrentStepClass, $sCurrentState);
|
||||
if ($oStep->ValidateParams()) {
|
||||
if ($oStep->CanComeBack()) {
|
||||
$aPossibleSteps = $oStep->GetPossibleSteps();
|
||||
if ($oStep->CanMoveBackward()) {
|
||||
$this->PushStep(['class' => $sCurrentStepClass, 'state' => $sCurrentState]);
|
||||
}
|
||||
$aPossibleSteps = $oStep->GetPossibleSteps();
|
||||
$oWizardState = $oStep->UpdateWizardStateAndGetNextStep(true); // true => moving forward
|
||||
if (in_array($oWizardState->GetNextStep(), $aPossibleSteps)) {
|
||||
$oNextStep = $this->GetWizardStep($oWizardState->GetNextStep(), $oWizardState->GetState());
|
||||
@@ -237,8 +237,12 @@ HTML;
|
||||
|
||||
$oPage->add('<input type="hidden" name="_steps" value="'.utils::EscapeHtml(json_encode($this->aWizardSteps)).'"/>');
|
||||
$oPage->add('<table style="width:100%;" class="ibo-setup--wizard--buttons-container"><tr>');
|
||||
if ((count($this->aWizardSteps) > 0) && ($oStep->CanMoveBackward())) {
|
||||
$oPage->add('<td style="text-align: left"><button id="btn_back" class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="back"><span class="ibo-button--label">Back</span></button></td>');
|
||||
if (count($this->aWizardSteps) > 0) {
|
||||
if ($oStep->CanMoveBackward()) {
|
||||
$oPage->add('<td style="text-align: left"><button id="btn_back" class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="back"><span class="ibo-button--label">Back</span></button></td>');
|
||||
} else {
|
||||
$oPage->add('<td style="text-align: left"><button id="btn_back" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="submit" name="operation" value="back"><span class="ibo-button--label">Back</span></button></td>');
|
||||
}
|
||||
}
|
||||
if ($oStep->CanMoveForward()) {
|
||||
$oPage->add('<td style="text-align:right;"><button id="btn_next" class="default ibo-button ibo-is-regular ibo-is-primary" type="submit" name="operation" value="next"><span class="ibo-button--label">'.utils::EscapeHtml($oStep->GetNextButtonLabel()).'</span></button></td>');
|
||||
|
||||
@@ -54,16 +54,20 @@ class WizStepDataAudit extends WizStepInstall
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this step/state allows to go back or not
|
||||
* @return boolean True if the '<< Back' button should be displayed
|
||||
*/
|
||||
public function CanMoveBackward()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
|
||||
{
|
||||
return new WizardState(WizStepSummary::class);
|
||||
}
|
||||
|
||||
public function CanComeBack()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function Display(SetupPage $oPage): void
|
||||
{
|
||||
|
||||
@@ -108,6 +112,7 @@ JS);
|
||||
'removed_extensions' => '[]',
|
||||
'extensions_not_uninstallable' => '[]',
|
||||
'copy_setup_files' => 1,
|
||||
'return_button_label' => '',
|
||||
];
|
||||
$aHiddenInputs = '';
|
||||
foreach ($aParams as $sParamName => $defaultValue) {
|
||||
@@ -123,23 +128,43 @@ INPUT;
|
||||
<<<HTML
|
||||
<form id="data-feature-removal" class="ibo-setup--wizard ibo-is-hidden" method="post" action="$sApplicationUrl">
|
||||
<input type="hidden" name="operation" value="AnalysisResult"/>
|
||||
<input type="hidden" name="setup_token" value="$sUID"/>
|
||||
<input type="hidden" name="authent" value="$sUID"/>
|
||||
$aHiddenInputs
|
||||
</form>
|
||||
HTML
|
||||
);
|
||||
|
||||
$sButtonLabel = $this->oWizard->GetParameter('return_button_label', '');
|
||||
if ($sButtonLabel !== '') {
|
||||
$sButtonLabel = utils::HtmlEntities($sButtonLabel);
|
||||
$sButtonUrl = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
$sButtonUrl = utils::HtmlEntities($sButtonUrl);
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').after('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
|
||||
JS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function AddProgressErrorScript($oPage, $aRes)
|
||||
{
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
if ($('#return-button').length > 0) {
|
||||
$('#return-button').removeClass('ibo-is-hidden');
|
||||
}
|
||||
JS
|
||||
);
|
||||
|
||||
if (isset($aRes['error_code']) && $aRes['error_code'] === DataAuditSequencer::DATA_AUDIT_FAILED) {
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').before('<td style="text-align:center;"><button class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="next" id="ignore_and_continue"><span class="ibo-button--label">Ignore and continue</span></button></td>');
|
||||
<<<JS
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').after('<td style="text-align:center;"><button class="ibo-button ibo-is-alternative ibo-is-neutral" type="submit" name="operation" value="next" id="ignore_and_continue"><span class="ibo-button--label">Ignore and continue</span></button></td>');
|
||||
$('#ignore_and_continue').on('click', function() {
|
||||
return confirm("If you skip the cleanup you won't be able to run the process later. You'll have to migrate or delete unconsistent data manually.");
|
||||
});
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(2)').after('<td style="text-align:center;"><span id="submit-wait" class="ibo-spinner ibo-is-inline ibo-is-hidden ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"></i></span> <button id="goto-data-feature-removal" class="default ibo-button ibo-is-regular ibo-is-primary" type="button"><span class="ibo-button--label">Cleanup my data</span></button></td>');
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(3)').after('<td style="text-align:center;"><span id="submit-wait" class="ibo-spinner ibo-is-inline ibo-is-hidden ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"></i></span> <button id="goto-data-feature-removal" class="default ibo-button ibo-is-regular ibo-is-primary" type="button"><span class="ibo-button--label">Cleanup my data</span></button></td>');
|
||||
$('#goto-data-feature-removal').on("click", function() {
|
||||
$('#goto-data-feature-removal').prop('disabled', true);
|
||||
$('#submit-wait').removeClass("ibo-is-hidden");
|
||||
@@ -148,7 +173,7 @@ HTML
|
||||
|
||||
$("#wiz_form").data("installation_status", "cleanup_needed");
|
||||
$('#btn_next').hide();
|
||||
EOF
|
||||
JS
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,6 +186,10 @@ EOF
|
||||
|
||||
public function JSCanMoveBackward()
|
||||
{
|
||||
if ($this->oWizard->GetParameter('return_button_label', '') !== '') {
|
||||
return 'return false;';
|
||||
}
|
||||
|
||||
return 'return ["not started", "error", "cleanup_needed"].indexOf($("#wiz_form").data("installation_status")) !== -1;';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ class WizStepDone extends WizardStep
|
||||
return false;
|
||||
}
|
||||
|
||||
public function JSCanMoveBackward()
|
||||
{
|
||||
return 'return false;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this step of the wizard requires that the configuration file be writable
|
||||
* @return bool True if the wizard will possibly need to modify the configuration at some point
|
||||
|
||||
@@ -54,6 +54,13 @@ class WizStepInstall extends AbstractWizStepInstall
|
||||
}
|
||||
}
|
||||
|
||||
public function CanMoveBackward()
|
||||
{
|
||||
$sLabel = $this->oWizard->GetParameter('return_button_label', '');
|
||||
SetupLog::Info(__METHOD__.": return_button_label [$sLabel]");
|
||||
return $sLabel === '';
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
|
||||
{
|
||||
return new WizardState(WizStepDone::class);
|
||||
@@ -109,6 +116,20 @@ JS);
|
||||
JS);
|
||||
}
|
||||
|
||||
public function PostFormDisplay(SetupPage $oPage)
|
||||
{
|
||||
$sButtonLabel = $this->oWizard->GetParameter('return_button_label', '');
|
||||
SetupLog::Info(__METHOD__.": return_button_label [$sButtonLabel]");
|
||||
if ($sButtonLabel !== '') {
|
||||
$sButtonUrl = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').after('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral ibo-is-hidden" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
|
||||
JS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
@@ -125,27 +146,25 @@ JS);
|
||||
// Tell the web page to move the progress bar and to launch the next step
|
||||
$sMessage = addslashes(utils::EscapeHtml($aRes['next-step-label']));
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$("#wiz_form").data("installation_status", "running");
|
||||
WizardUpdateButtons();
|
||||
$('#setup_msg').html('$sMessage');
|
||||
$('#progress').progression( {Current:{$aRes['percentage-completed']}, Maximum: 100} );
|
||||
|
||||
//$("#percentage").html('{$aRes['percentage-completed']} % completed<br/>{$aRes['next-step-label']}');
|
||||
ExecuteStep('{$aRes['next-step']}');
|
||||
EOF
|
||||
<<<JS
|
||||
$("#wiz_form").data("installation_status", "running");
|
||||
WizardUpdateButtons();
|
||||
$('#setup_msg').html('$sMessage');
|
||||
$('#progress').progression( {Current:{$aRes['percentage-completed']}, Maximum: 100} );
|
||||
|
||||
//$("#percentage").html('{$aRes['percentage-completed']} % completed<br/>{$aRes['next-step-label']}');
|
||||
ExecuteStep('{$aRes['next-step']}');
|
||||
JS
|
||||
);
|
||||
static::AddPrevStepSuccessMessage($oPage, $aRes['prev-step-success-message']);
|
||||
} elseif ($aRes['status'] !== StepSequencer::ERROR) {
|
||||
// Installation complete, move to the next step of the wizard
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$("#wiz_form").data("installation_status", "completed");
|
||||
$('#progress').progression( {Current:100, Maximum: 100} );
|
||||
WizardUpdateButtons();
|
||||
$("#btn_next").off("click.install");
|
||||
$("#btn_next").trigger('click');
|
||||
EOF
|
||||
<<<JS
|
||||
$('#progress').progression( {Current:100, Maximum: 100} );
|
||||
$("#wiz_form").data("installation_status", "completed");
|
||||
$("#btn_next").trigger('click');
|
||||
JS
|
||||
);
|
||||
static::AddPrevStepSuccessMessage($oPage, $aRes['prev-step-success-message']);
|
||||
} else {
|
||||
@@ -153,12 +172,12 @@ EOF
|
||||
$sMessage = addslashes(utils::EscapeHtml($aRes['message']));
|
||||
$sMessage = str_replace("\n", '<br>', $sMessage);
|
||||
$oPage->add_ready_script(
|
||||
<<<EOF
|
||||
$("#wiz_form").data("installation_status", "error");
|
||||
$("#progress .progress").addClass('progress-error');
|
||||
WizardUpdateButtons();
|
||||
$('#setup_error').html('$sMessage').show();
|
||||
EOF
|
||||
<<<JS
|
||||
$("#wiz_form").data("installation_status", "error");
|
||||
$("#progress .progress").addClass('progress-error');
|
||||
WizardUpdateButtons();
|
||||
$('#setup_error').html('$sMessage').show();
|
||||
JS
|
||||
);
|
||||
$this->AddProgressErrorScript($oPage, $aRes);
|
||||
}
|
||||
@@ -166,7 +185,13 @@ EOF
|
||||
|
||||
protected function AddProgressErrorScript($oPage, $aRes)
|
||||
{
|
||||
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
if ($('#return-button').length > 0) {
|
||||
$('#return-button').removeClass('ibo-is-hidden');
|
||||
}
|
||||
JS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,7 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
|
||||
return 'Next';
|
||||
}
|
||||
|
||||
public function CanComeBack()
|
||||
public function CanMoveBackward()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,14 @@ class WizStepSummary extends AbstractWizStepInstall
|
||||
}
|
||||
}
|
||||
|
||||
public function CanMoveBackward()
|
||||
{
|
||||
$sLabel = $this->oWizard->GetParameter('return_button_label', '');
|
||||
SetupLog::Info(__METHOD__.": return_button_label [$sLabel]");
|
||||
|
||||
return $sLabel === '';
|
||||
}
|
||||
|
||||
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
|
||||
{
|
||||
$this->oWizard->SaveParameter('db_backup', false);
|
||||
@@ -242,6 +250,19 @@ JS
|
||||
);
|
||||
}
|
||||
|
||||
public function PostFormDisplay(SetupPage $oPage)
|
||||
{
|
||||
$sButtonLabel = $this->oWizard->GetParameter('return_button_label', '');
|
||||
if ($sButtonLabel !== '') {
|
||||
$sButtonUrl = utils::GetAbsoluteUrlModulePage('itsm-designer-connector', 'launch.php');
|
||||
$oPage->add_ready_script(
|
||||
<<<JS
|
||||
$('.ibo-setup--wizard--buttons-container tr td:nth-child(1)').after('<td style="text-align:center;"><button id="return-button" class="ibo-button ibo-is-alternative ibo-is-neutral" type="button" onclick="window.location.href=\'$sButtonUrl\'"><span class="ibo-button--label">$sButtonLabel</span></button></td>');
|
||||
JS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Next" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
@@ -257,7 +278,11 @@ JS
|
||||
*/
|
||||
public function JSCanMoveBackward()
|
||||
{
|
||||
return 'return true;';
|
||||
if ($this->oWizard->GetParameter('return_button_label', '') === '') {
|
||||
return 'return true;';
|
||||
}
|
||||
|
||||
return 'return false;';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
|
||||
/**
|
||||
* First step of the iTop Installation Wizard: Welcome screen, requirements
|
||||
*/
|
||||
@@ -98,7 +96,7 @@ EOF
|
||||
$sStyle = 'style="display:none;overflow:auto;"';
|
||||
$sToggleButtons = '<button type="button" id="show_details" class="ibo-button ibo-is-alternative ibo-is-neutral" onclick="$(\'#details\').toggle(); $(this).toggle(); $(\'#hide_details\').toggle();"><span class="ibo-button--icon fa fa-caret-down"></span><span class="ibo-button--label">Show details</span></button><button type="button" id="hide_details" class="ibo-button ibo-is-alternative ibo-is-neutral" style="display:none;" onclick="$(\'#details\').toggle(); $(this).toggle(); $(\'#show_details\').toggle();"><span class="ibo-button--icon fa fa-caret-up"></span><span class="ibo-button--label">Hide details</span></button>';
|
||||
if (count($aErrors) > 0) {
|
||||
$sStyle = 'overflow:auto;"';
|
||||
$sStyle = 'style="overflow:auto;"';
|
||||
$sTitle = count($aErrors).' Error(s), '.count($aWarnings).' Warning(s).';
|
||||
$sH2Class = 'text-error';
|
||||
} elseif (count($aWarnings) > 0) {
|
||||
|
||||
@@ -151,15 +151,6 @@ abstract class WizardStep
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the user will come back to this step/state if he click on "Back"
|
||||
* @return boolean True if the 'Back' button should display this step
|
||||
*/
|
||||
public function CanComeBack()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the "Next" button should be enabled interactively
|
||||
* @return string A piece of javascript code returning either true or false
|
||||
|
||||
119
sources/Application/Helper/ExceptionHandlerHelper.php
Normal file
119
sources/Application/Helper/ExceptionHandlerHelper.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Application\Helper;
|
||||
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use IssueLog;
|
||||
use SimpleXMLElement;
|
||||
use Throwable;
|
||||
|
||||
class ExceptionHandlerHelper
|
||||
{
|
||||
public static array $aSupportedMimeTypes = [
|
||||
'application/json' => 'json',
|
||||
'application/xml' => 'xml',
|
||||
'text/html' => 'html',
|
||||
'text/plain' => 'text',
|
||||
];
|
||||
|
||||
public static function HandleException(Throwable $oException)
|
||||
{
|
||||
$aStatus = ob_get_status();
|
||||
if (count($aStatus) !== 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Log the exception
|
||||
IssueLog::Exception('Fatal error', $oException);
|
||||
|
||||
$mime = self::NegotiateMimeType();
|
||||
|
||||
if ($mime === null) {
|
||||
http_response_code(406);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Vary: Accept');
|
||||
echo json_encode(['error' => 'Not Acceptable'], JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(500);
|
||||
header("Content-Type: {$mime}; charset=utf-8");
|
||||
header('Vary: Accept');
|
||||
|
||||
$aData = [
|
||||
'error' => 'Fatal error',
|
||||
'message' => 'We are sorry, an unexpected error has occurred. Please try again later.',
|
||||
|
||||
];
|
||||
|
||||
switch (self::$aSupportedMimeTypes[$mime]) {
|
||||
case 'json':
|
||||
echo json_encode($aData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
break;
|
||||
|
||||
case 'xml':
|
||||
$oXml = new SimpleXMLElement('<response/>');
|
||||
array_walk_recursive($aData, function ($sValue, $sKey) use ($oXml) {
|
||||
$oXml->addChild((string)$sKey, htmlspecialchars((string)$sValue, ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
});
|
||||
echo $oXml->asXML();
|
||||
break;
|
||||
|
||||
case 'html':
|
||||
// Create error page
|
||||
$oErrorPage = new ErrorPage('Fatal error');
|
||||
$oErrorPage->error('We are sorry, an unexpected error has occurred. Please try again later.<br><br>', $oException);
|
||||
$oErrorPage->output();
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
echo "Fatal error\n";
|
||||
echo "We are sorry, an unexpected error has occurred. Please try again later.\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static function NegotiateMimeType(): ?string
|
||||
{
|
||||
$supportedMimes = array_keys(self::$aSupportedMimeTypes);
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '*/*';
|
||||
|
||||
if (trim($acceptHeader) === '' || $acceptHeader === '*/*') {
|
||||
return in_array('application/json', $supportedMimes, true) ? 'application/json' : $supportedMimes[0];
|
||||
}
|
||||
|
||||
$accepted = [];
|
||||
foreach (explode(',', $acceptHeader) as $part) {
|
||||
$part = trim($part);
|
||||
$q = 1.0;
|
||||
if (str_contains($part, ';')) {
|
||||
[$type, $params] = array_map('trim', explode(';', $part, 2));
|
||||
if (preg_match('/q=([0-9.]+)/', $params, $m)) {
|
||||
$q = (float)$m[1];
|
||||
}
|
||||
} else {
|
||||
$type = $part;
|
||||
}
|
||||
$accepted[] = ['type' => $type, 'q' => $q];
|
||||
}
|
||||
|
||||
usort($accepted, fn ($a, $b) => $b['q'] <=> $a['q']);
|
||||
|
||||
foreach ($accepted as $a) {
|
||||
foreach ($supportedMimes as $mime) {
|
||||
if ($a['type'] === $mime || $a['type'] === '*/*') {
|
||||
return $mime;
|
||||
}
|
||||
// Ex: application/* match application/json
|
||||
if (str_ends_with($a['type'], '/*')) {
|
||||
$prefix = explode('/', $a['type'])[0].'/';
|
||||
if (str_starts_with($mime, $prefix)) {
|
||||
return $mime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ namespace Combodo\iTop\Application\TwigBase\Controller;
|
||||
use ApplicationMenu;
|
||||
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
|
||||
use Combodo\iTop\Application\WebPage\AjaxPage;
|
||||
use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Controller\AbstractController;
|
||||
@@ -261,29 +260,29 @@ abstract class Controller extends AbstractController
|
||||
*/
|
||||
public function HandleOperation(): void
|
||||
{
|
||||
try {
|
||||
$this->CheckAccess();
|
||||
$this->m_sOperation = utils::ReadParam('operation', $this->sDefaultOperation);
|
||||
// try {
|
||||
$this->CheckAccess();
|
||||
$this->m_sOperation = utils::ReadParam('operation', $this->sDefaultOperation);
|
||||
|
||||
if ($this->CallOperation(utils::ToCamelCase($this->m_sOperation))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to unchanged names for compatibility
|
||||
if ($this->CallOperation($this->m_sOperation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->DisplayBadRequest();
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
$oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
$oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
$oP->add(get_class($e).' : '.utils::EscapeHtml($e->GetMessage()));
|
||||
$oP->output();
|
||||
|
||||
IssueLog::Exception('HandleOperation failed for '.json_encode($this->m_sOperation), $e);
|
||||
if ($this->CallOperation(utils::ToCamelCase($this->m_sOperation))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to unchanged names for compatibility
|
||||
if ($this->CallOperation($this->m_sOperation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->DisplayBadRequest();
|
||||
// } catch (Exception $e) {
|
||||
// http_response_code(500);
|
||||
// $oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
|
||||
// $oP->add("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\n");
|
||||
// $oP->add(get_class($e).' : '.utils::EscapeHtml($e->GetMessage()));
|
||||
// $oP->output();
|
||||
//
|
||||
// IssueLog::Exception('HandleOperation failed for '.json_encode($this->m_sOperation), $e);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,6 @@ use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\ButtonGroup\ButtonGroupUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\PopoverMenuItemFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\CaseLogEntryForm\CaseLogEntryForm;
|
||||
use DBObject;
|
||||
use DBObjectSet;
|
||||
use Dict;
|
||||
@@ -39,7 +38,9 @@ class CaseLogEntryFormFactory
|
||||
->AddMainActionButtons(static::PrepareCancelButton());
|
||||
|
||||
$oSaveButton = static::PrepareSaveButton();
|
||||
$oTransitionsMenu = static::PrepareTransitionsSelectionPopoverMenu($oObject, $sCaseLogAttCode);
|
||||
$oTransitionsMenu = static::PrepareTransitionsSelectionPopoverMenu($oObject, $sCaseLogAttCode, $oCaseLogEntryForm->GetId());
|
||||
// Prevent popover menu from landing behind caselog editor
|
||||
$oTransitionsMenu->SetContainer(PopoverMenu::ENUM_CONTAINER_BODY);
|
||||
if (true === $oTransitionsMenu->HasItems()) {
|
||||
$oButtonGroup = ButtonGroupUIBlockFactory::MakeButtonWithOptionsMenu($oSaveButton, $oTransitionsMenu);
|
||||
$oCaseLogEntryForm->AddMainActionButtons($oButtonGroup);
|
||||
@@ -69,7 +70,16 @@ class CaseLogEntryFormFactory
|
||||
return $oButton;
|
||||
}
|
||||
|
||||
protected static function PrepareTransitionsSelectionPopoverMenu(DBObject $oObject, string $sCaseLogAttCode): PopoverMenu
|
||||
/**
|
||||
* @param DBObject $oObject
|
||||
* @param string $sCaseLogAttCode
|
||||
* @param string $sCaseLogEntryFormId
|
||||
* @since 3.2.3 Add mandatory $sCaseLogEntryFormId parameter
|
||||
* @return PopoverMenu
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
protected static function PrepareTransitionsSelectionPopoverMenu(DBObject $oObject, string $sCaseLogAttCode, string $sCaseLogEntryFormId): PopoverMenu
|
||||
{
|
||||
$sObjClass = get_class($oObject);
|
||||
|
||||
@@ -77,8 +87,6 @@ class CaseLogEntryFormFactory
|
||||
$sSectionId = 'send-actions';
|
||||
$oMenu->AddSection($sSectionId);
|
||||
|
||||
$sCaseLogEntryFormDataRole = CaseLogEntryForm::BLOCK_CODE;
|
||||
|
||||
// Note: This code is inspired from cmdbAbstract::DisplayModifyForm(), it might be better to factorize it
|
||||
$aTransitions = $oObject->EnumTransitions();
|
||||
if (!isset($aExtraParams['custom_operation']) && count($aTransitions)) {
|
||||
@@ -97,7 +105,7 @@ class CaseLogEntryFormFactory
|
||||
CaseLogEntryForm::BLOCK_CODE.'--add-action--'.$sCaseLogAttCode.'--stimulus--'.$sStimulusCode,
|
||||
Dict::Format('UI:Button:SendAnd', $aStimuli[$sStimulusCode]->GetLabel()),
|
||||
<<<JS
|
||||
$(this).closest('[data-role="{$sCaseLogEntryFormDataRole}"]').trigger('save_entry.caselog_entry_form.itop', {stimulus_code: '{$sStimulusCode}'});
|
||||
$('#$sCaseLogEntryFormId').trigger('save_entry.caselog_entry_form.itop', {stimulus_code: '{$sStimulusCode}'});
|
||||
JS
|
||||
)
|
||||
);
|
||||
|
||||
@@ -16,10 +16,12 @@ use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectSummary;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Combodo\iTop\Application\WebPage\DownloadPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\JsonPage;
|
||||
use Combodo\iTop\Application\WebPage\JsonPPage;
|
||||
use Combodo\iTop\Controller\Notifications\NotificationsCenterController;
|
||||
use Combodo\iTop\Service\Notification\Event\EventNotificationNewsroomService;
|
||||
use Combodo\iTop\Service\Notification\NotificationsRepository;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use CoreException;
|
||||
@@ -27,6 +29,7 @@ use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use Dict;
|
||||
use EventNotificationNewsroom;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use SecurityException;
|
||||
use UserRights;
|
||||
@@ -379,9 +382,10 @@ JS
|
||||
$oEventBlock->SetCSSColorClass($sReadColor);
|
||||
$oEventBlock->SetSubTitle($sReadLabel);
|
||||
$oEventBlock->SetClassLabel('');
|
||||
/** @var \ormDocument $oImage */
|
||||
$oImage = $oEvent->Get('icon');
|
||||
if (!$oImage->IsEmpty()) {
|
||||
$sIconUrl = $oImage->GetDisplayURL(get_class($oEvent), $iEventId, 'icon');
|
||||
$sIconUrl = self::GetDisplayIconUrl($iEventId, $oImage->GetSignature());
|
||||
$oEventBlock->SetIcon($sIconUrl, Panel::ENUM_ICON_COVER_METHOD_COVER, true);
|
||||
}
|
||||
|
||||
@@ -545,7 +549,7 @@ $sMessage
|
||||
HTML;
|
||||
|
||||
$sIcon = $oMessage->Get('icon') !== null ?
|
||||
$oMessage->Get('icon')->GetDisplayURL(EventNotificationNewsroom::class, $oMessage->GetKey(), 'icon') :
|
||||
$this->GetDisplayIconUrl($oMessage->GetKey(), $oMessage->Get('icon')->GetSignature()) :
|
||||
Branding::GetCompactMainLogoAbsoluteUrl();
|
||||
$aMessages[] = [
|
||||
'id' => $oMessage->GetKey(),
|
||||
@@ -692,6 +696,35 @@ HTML;
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the icon of an EventNotificationNewsroom
|
||||
* (copy of ajax.render.php?operation=display_document but with the bAllowAllData parameter set to true in order to bypass the data access restrictions since the icon is not a critical information)
|
||||
* @return void
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public function OperationViewIcon(): void
|
||||
{
|
||||
$sId = utils::ReadParam('id', '');
|
||||
if (!empty($sId)) {
|
||||
$oPage = new DownloadPage('');
|
||||
// X-Frame http header : set in page constructor, but we need to allow frame integration for this specific page
|
||||
// so we're resetting its value ! (see N°3416)
|
||||
$oPage->add_xframe_options('');
|
||||
$iCacheSec = (int)utils::ReadParam('cache', 0);
|
||||
$oPage->set_cache($iCacheSec);
|
||||
|
||||
// N°4129 - Prevent XSS attacks & other script executions
|
||||
if (utils::GetConfig()->Get('security.disable_inline_documents_sandbox') === false) {
|
||||
$oPage->add_header('Content-Security-Policy: sandbox;');
|
||||
}
|
||||
|
||||
if (EventNotificationNewsroomService::DownloadIcon($oPage, $sId, UserRights::GetContactId()) === true) {
|
||||
$oPage->Output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sAction
|
||||
*
|
||||
@@ -784,4 +817,9 @@ HTML;
|
||||
|
||||
return $aReturnData;
|
||||
}
|
||||
|
||||
protected function GetDisplayIconUrl(string $sId, string $sSignature): string
|
||||
{
|
||||
return utils::GetAbsoluteUrlAppRoot()."pages/UI.php?route=itopnewsroom.view_icon&id=$sId&s=$sSignature&cache=86400";
|
||||
}
|
||||
}
|
||||
|
||||
28
sources/Exception/ItopException.php
Normal file
28
sources/Exception/ItopException.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Exception;
|
||||
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use LogChannels;
|
||||
use Throwable;
|
||||
|
||||
class ItopException extends Exception
|
||||
{
|
||||
private array $aContext;
|
||||
|
||||
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null, array $aContext = [])
|
||||
{
|
||||
$aContext['code'] = $code;
|
||||
IssueLog::Debug($message, LogChannels::EXCEPTION, $aContext);
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->aContext = $aContext;
|
||||
}
|
||||
|
||||
public function getContext(): array
|
||||
{
|
||||
return $this->aContext;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -4,8 +4,10 @@ namespace Combodo\iTop\Service\Notification\Event;
|
||||
|
||||
use Action;
|
||||
use Combodo\iTop\Application\Branding;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use EventNotificationNewsroom;
|
||||
use MetaModel;
|
||||
use ormDocument;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
@@ -71,4 +73,31 @@ class EventNotificationNewsroomService
|
||||
|
||||
return $oEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\Application\WebPage\WebPage $oPage
|
||||
* @param string $sId
|
||||
* @param int $iContactId
|
||||
*
|
||||
* @return bool Returns true if the download has been launched, false otherwise (e.g. if the event doesn't exist or doesn't belong to the current user)
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public static function DownloadIcon(WebPage $oPage, string $sId, int $iContactId): bool
|
||||
{
|
||||
$oEvent = MetaModel::GetObject(EventNotificationNewsroom::class, $sId, false, true);
|
||||
if (($oEvent !== null) && ($oEvent->Get('contact_id') === $iContactId)) {
|
||||
ormDocument::DownloadDocument(
|
||||
$oPage,
|
||||
EventNotificationNewsroom::class,
|
||||
$sId,
|
||||
'icon',
|
||||
ormDocument::ENUM_CONTENT_DISPOSITION_INLINE,
|
||||
bAllowAllData: true
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +706,9 @@ abstract class ItopTestCase extends KernelTestCase
|
||||
$this->aLastCurlGetInfo = $info;
|
||||
$sErrorMsg = curl_error($ch);
|
||||
$iErrorCode = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
\IssueLog::Info(__METHOD__, null, ['url' => $sUrl, 'error' => $sErrorMsg, 'error_code' => $iErrorCode, 'post_fields' => $aPostFields, 'info' => $info]);
|
||||
|
||||
|
||||
@@ -306,6 +306,21 @@ class ormDocumentTest extends ItopDataTestCase
|
||||
$this->assertStringNotContainsString('the object does not exist or you are not allowed to view it', $sAllowedHtml, 'Unexpected error message when rights are sufficient.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider DownloadDocumentRightsProvider
|
||||
*/
|
||||
public function testAllowsDownloadingDocumentWhenBypassingRightsChecksWithAllowAllData(string $sTargetClass, string $sAttCode, string $sData, string $sFileName, ?string $sHostClass)
|
||||
{
|
||||
$iDeniedDocumentId = $this->CreateDownloadTargetInOrg($sTargetClass, $sAttCode, $this->iOrgDifferentFromUser, $sData, $sFileName, $sHostClass);
|
||||
|
||||
$oPageAllowed = new CaptureWebPage();
|
||||
ormDocument::DownloadDocument($oPageAllowed, $sTargetClass, $iDeniedDocumentId, $sAttCode, ormDocument::ENUM_CONTENT_DISPOSITION_INLINE, bAllowAllData: true);
|
||||
$sAllowedHtml = $oPageAllowed->GetHtml();
|
||||
|
||||
$this->assertStringContainsString($sData, $sAllowedHtml, 'Expected file data present when bypassing rights checks.');
|
||||
$this->assertStringNotContainsString("Invalid id ($iDeniedDocumentId) for class '$sTargetClass' - the object does not exist or you are not allowed to view it", $sAllowedHtml, 'Unexpected invalid id error message when bypassing rights checks.');
|
||||
}
|
||||
|
||||
public function DownloadDocumentRightsProvider(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -285,89 +285,61 @@ SQL
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion
|
||||
* @dataProvider LoadLocalizedData_RequiredLanguageProvider
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedData
|
||||
*/
|
||||
public function testLoadLocalizedData_LoadRequiredLanguageOnFirstInstall(string $sRequiredLanguage, array $aAvailableLanguages, array $aExpectedCountByLanguage): void
|
||||
public function testLoadLocalizedData_LoadsOnFirstInstall(): void
|
||||
{
|
||||
// Given
|
||||
[$oConfig, $sOrgName, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_RequiredLanguage_', $sRequiredLanguage, $aAvailableLanguages);
|
||||
|
||||
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_FirstInstall_', 'fr_fr');
|
||||
$this->GivenLocalizedDataFile($sTmpDir, "en_us", $sOrgName);
|
||||
$this->GivenLocalizedDataFile($sTmpDir, "fr_fr", $sOrgName);
|
||||
// When no previous version, and current version higher than the first loading version
|
||||
ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion($oConfig, '', '3.3.0', '3.0.0', $sPattern);
|
||||
|
||||
ModuleInstallerAPI::LoadLocalizedData($oConfig, '', '3.3.0', '3.0.0', $sPattern);
|
||||
// Then data loaded
|
||||
foreach ($aExpectedCountByLanguage as $sLanguage => $iExpectedCount) {
|
||||
$this->AssertOrganizationCountByName($sOrgName, $sLanguage, $iExpectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
public function LoadLocalizedData_RequiredLanguageProvider(): array
|
||||
{
|
||||
return [
|
||||
'Required fr_fr and file exists' => [
|
||||
'required language' => 'fr_fr',
|
||||
'available languages' => ['en_us', 'fr_fr'],
|
||||
'expected counts' => ['en_us' => 0, 'fr_fr' => 1],
|
||||
],
|
||||
'Required en_us and file exists' => [
|
||||
'required language' => 'en_us',
|
||||
'available languages' => ['en_us', 'fr_fr'],
|
||||
'expected counts' => ['en_us' => 1, 'fr_fr' => 0],
|
||||
],
|
||||
'Required fr_fr but fallback to en_us' => [
|
||||
'required language' => 'fr_fr',
|
||||
'available languages' => ['en_us'],
|
||||
'expected counts' => ['en_us' => 1, 'fr_fr' => 0],
|
||||
],
|
||||
'Required de_de and file exists' => [
|
||||
'required language' => 'de_de',
|
||||
'available languages' => ['en_us', 'fr_fr', 'de_de'],
|
||||
'expected counts' => ['en_us' => 0, 'fr_fr' => 0, 'de_de' => 1],
|
||||
],
|
||||
'Required de_de but fallback to en_us' => [
|
||||
'required language' => 'de_de',
|
||||
'available languages' => ['en_us', 'fr_fr'],
|
||||
'expected counts' => ['en_us' => 1, 'fr_fr' => 0, 'de_de' => 0],
|
||||
],
|
||||
];
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 0);
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion
|
||||
* @dataProvider LoadLocalizedData_VersionConditionNotMetProvider
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedData
|
||||
*/
|
||||
public function testLoadLocalizedData_DoesNotLoadWhenVersionConditionIsNotMet(string $sPreviousVersion, string $sCurrentVersion, string $sFirstLoadingVersion): void
|
||||
public function testLoadLocalizedData_DoesNotLoadWhenVersionConditionIsNotMet(): void
|
||||
{
|
||||
// Given
|
||||
[$oConfig, $sOrgName, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_NoLoad_', 'en_us', ['en_us']);
|
||||
// When version gate conditions are not met
|
||||
ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion($oConfig, $sPreviousVersion, $sCurrentVersion, $sFirstLoadingVersion, $sPattern);
|
||||
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_NoLoad_', 'en_us');
|
||||
$this->GivenLocalizedDataFile($sTmpDir, "en_us", $sOrgName);
|
||||
|
||||
// When a previous version that is lower than the first loading version, but higher or equal to the current version
|
||||
ModuleInstallerAPI::LoadLocalizedData($oConfig, '3.0.0', '3.1.0', '3.0.0', $sPattern);
|
||||
// Then no data loaded
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 0);
|
||||
}
|
||||
|
||||
public function LoadLocalizedData_VersionConditionNotMetProvider(): array
|
||||
/**
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedData
|
||||
*/
|
||||
public function testLoadLocalizedData_FallbacksToEnUsWhenLanguageFileIsMissing(): void
|
||||
{
|
||||
return [
|
||||
'Equal versions (reinstall)' => ['3.1.0', '3.1.0', '3.0.0'],
|
||||
'Downgrade attempt' => ['3.2.0', '3.1.0', '3.0.0'],
|
||||
'Upgrade but first loading version already passed' => ['3.1.0', '3.2.0', '3.0.0'],
|
||||
'Upgrade with boundary equality on first loading version' => ['3.0.0', '3.1.0', '3.0.0'],
|
||||
'Upgrade but first loading version empty' => ['3.1.0', '3.2.0', ''],
|
||||
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_Fallback_', 'fr_fr');
|
||||
// Intentionally create ONLY en_us file
|
||||
$this->GivenLocalizedDataFile($sTmpDir, 'en_us', $sOrgName);
|
||||
// When loading localized data in fr_fr, but only en_us file exists
|
||||
ModuleInstallerAPI::LoadLocalizedData($oConfig, '', '3.3.0', '3.0.0', $sPattern);
|
||||
|
||||
];
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 0);
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedData
|
||||
* @dataProvider LoadLocalizedData_ValidVersionFormatsProvider
|
||||
*/
|
||||
public function testLoadLocalizedData_AcceptsSupportedVersionFormats(string $sCurrentVersion, string $sFirstLoadingVersion): void
|
||||
{
|
||||
[$oConfig, $sOrgName, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_ValidVersion_', 'en_us', ['en_us']);
|
||||
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_ValidVersion_', 'en_us');
|
||||
$this->GivenLocalizedDataFile($sTmpDir, 'en_us', $sOrgName);
|
||||
|
||||
ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion($oConfig, '', $sCurrentVersion, $sFirstLoadingVersion, $sPattern);
|
||||
ModuleInstallerAPI::LoadLocalizedData($oConfig, '', $sCurrentVersion, $sFirstLoadingVersion, $sPattern);
|
||||
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 1);
|
||||
}
|
||||
@@ -376,39 +348,11 @@ SQL
|
||||
{
|
||||
return [
|
||||
'Current version with suffix' => ['3.2-dev', '3.0.0'],
|
||||
'Current version x.y.z' => ['10.12.140-Tagada34', '1.0'],
|
||||
'Current version x.y.z-suffix' => ['2.3.3-beta', '2.3.3-alpha'],
|
||||
'Current version x.y.z' => ['1.2.4', '1.0'],
|
||||
'Current version x.y.z-suffix' => ['2.3.3-beta', '2.0.0'],
|
||||
'Current version x.y.z-1' => ['1.2.4-1', '1.0.3-2'],
|
||||
];
|
||||
}
|
||||
// Test when a file is loaded twice because of the version conditions, it doesn't create duplicates (idempotent loading)
|
||||
public function testLoadLocalizedData_IdempotentLoading(): void
|
||||
{
|
||||
// Given
|
||||
[$oConfig, $sOrgName, $sPattern] = $this->GivenLocalizedDataTestContext('XML_Load_Idempotent_', 'en_us', ['en_us']);
|
||||
|
||||
// When LoadLocalizedData is called twice with conditions that would load the file both times
|
||||
ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion($oConfig, '', '3.1.0', '3.0.0', $sPattern);
|
||||
ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion($oConfig, '3.1.0', '3.2.0', '', $sPattern);
|
||||
|
||||
// Then no duplicate data loaded
|
||||
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion
|
||||
* @dataProvider LoadLocalizedData_InvalidParametersProvider
|
||||
*/
|
||||
public function testLoadLocalizedData_ThrowsOnInvalidParameters(string $sPreviousVersion, string $sCurrentVersion, string $sFirstLoadingVersion, string $sPattern, string $sExpectedMessage): void
|
||||
{
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$this->assertNotNull($oConfig);
|
||||
|
||||
$this->expectException(\CoreUnexpectedValue::class);
|
||||
$this->expectExceptionMessage($sExpectedMessage);
|
||||
|
||||
ModuleInstallerAPI::LoadLocalizedDataOnCrossingVersion($oConfig, $sPreviousVersion, $sCurrentVersion, $sFirstLoadingVersion, $sPattern);
|
||||
}
|
||||
|
||||
public function LoadLocalizedData_InvalidParametersProvider(): array
|
||||
{
|
||||
@@ -444,6 +388,13 @@ SQL
|
||||
'pattern' => $sTmpDir.DIRECTORY_SEPARATOR.'data.{{LANGUAGE_CODE}}.xml',
|
||||
'message' => "{{language_code}}",
|
||||
],
|
||||
'Parent directory does not exist' => [
|
||||
'previous' => '',
|
||||
'current' => '3.2.0',
|
||||
'first' => '3.0.0',
|
||||
'pattern' => $sTmpDir.DIRECTORY_SEPARATOR.'missing'.DIRECTORY_SEPARATOR.'data.{{language_code}}.xml',
|
||||
'message' => 'parent directory',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -452,7 +403,7 @@ SQL
|
||||
*
|
||||
* @return array{0: Config, 1: string, 2: string, 3: string, 4: string}
|
||||
*/
|
||||
private function GivenLocalizedDataTestContext(string $sOrgNamePrefix, string $sLanguage, array $aAvailableLanguages = []): array
|
||||
private function GivenLocalizedDataTestContext(string $sOrgNamePrefix, string $sLanguage): array
|
||||
{
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$oConfig->SetDefaultLanguage($sLanguage);
|
||||
@@ -464,11 +415,7 @@ SQL
|
||||
$this->aFileToClean[] = $sTmpDir;
|
||||
$sPattern = $sTmpDir.DIRECTORY_SEPARATOR.'data.{{language_code}}.xml';
|
||||
|
||||
foreach ($aAvailableLanguages as $sAvailableLanguage) {
|
||||
$this->GivenLocalizedDataFile($sTmpDir, $sAvailableLanguage, $sOrgName);
|
||||
}
|
||||
|
||||
return [$oConfig, $sOrgName, $sPattern];
|
||||
return [$oConfig, $sOrgName, $sTmpDir, $sPattern];
|
||||
}
|
||||
|
||||
private function GivenLocalizedDataFile(string $sDir, string $sLang, string $sOrgName): string
|
||||
|
||||
@@ -101,10 +101,10 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
|
||||
$aExpected = [
|
||||
'status' => 1,
|
||||
'message' => '',
|
||||
'next-step' => 'db-schema',
|
||||
'next-step-label' => 'Updating database schema',
|
||||
'next-step' => 'migrate-before',
|
||||
'next-step-label' => 'Migrate data before database upgrade',
|
||||
'prev-step-success-message' => 'Parameters logged',
|
||||
'percentage-completed' => 16,
|
||||
'percentage-completed' => 22,
|
||||
];
|
||||
$this->assertEquals($aExpected, $aRes);
|
||||
}
|
||||
@@ -284,7 +284,7 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
|
||||
'next-step' => 'load-data',
|
||||
'next-step-label' => 'Loading data',
|
||||
'prev-step-success-message' => 'Post-creation data loaded',
|
||||
'percentage-completed' => 50,
|
||||
'percentage-completed' => 66,
|
||||
];
|
||||
$this->assertEquals($aExpected, $aRes);
|
||||
}
|
||||
@@ -306,7 +306,7 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
|
||||
'next-step' => 'create-config',
|
||||
'next-step-label' => 'Creating the configuration File',
|
||||
'prev-step-success-message' => 'Data loaded',
|
||||
'percentage-completed' => 66,
|
||||
'percentage-completed' => 77,
|
||||
'status' => 1,
|
||||
];
|
||||
$this->assertEquals($aExpected, $aRes);
|
||||
@@ -330,7 +330,7 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
|
||||
'next-step' => 'commit',
|
||||
'next-step-label' => 'Finalize',
|
||||
'prev-step-success-message' => 'Configuration file created',
|
||||
'percentage-completed' => 83,
|
||||
'percentage-completed' => 88,
|
||||
'status' => 1,
|
||||
];
|
||||
$this->assertEquals($aExpected, $aRes);
|
||||
@@ -485,7 +485,10 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
|
||||
$this->GivenApplicationInstallSequencer([], true);
|
||||
$expected = [
|
||||
'',
|
||||
'log-parameters',
|
||||
'migrate-before',
|
||||
'db-schema',
|
||||
'migrate-after',
|
||||
'after-db-create',
|
||||
'load-data',
|
||||
'create-config',
|
||||
@@ -497,11 +500,11 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
|
||||
public function testGetStepAfterWithPercent()
|
||||
{
|
||||
$this->GivenApplicationInstallSequencer([], true);
|
||||
$this->assertEquals(['db-schema', 16], $this->oSequencer->GetStepAfterWithPercent(''));
|
||||
$this->assertEquals(['after-db-create', 33], $this->oSequencer->GetStepAfterWithPercent('db-schema'));
|
||||
$this->assertEquals(['load-data', 50], $this->oSequencer->GetStepAfterWithPercent('after-db-create'));
|
||||
$this->assertEquals(['create-config', 66], $this->oSequencer->GetStepAfterWithPercent('load-data'));
|
||||
$this->assertEquals(['commit', 83], $this->oSequencer->GetStepAfterWithPercent('create-config'));
|
||||
$this->assertEquals(['log-parameters', 11], $this->oSequencer->GetStepAfterWithPercent(''));
|
||||
$this->assertEquals(['migrate-after', 44], $this->oSequencer->GetStepAfterWithPercent('db-schema'));
|
||||
$this->assertEquals(['load-data', 66], $this->oSequencer->GetStepAfterWithPercent('after-db-create'));
|
||||
$this->assertEquals(['create-config', 77], $this->oSequencer->GetStepAfterWithPercent('load-data'));
|
||||
$this->assertEquals(['commit', 88], $this->oSequencer->GetStepAfterWithPercent('create-config'));
|
||||
$this->assertEquals(['', 100], $this->oSequencer->GetStepAfterWithPercent('commit'));
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ class UnattendedInstallTest extends ItopDataTestCase
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
|
||||
$sJson = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($ch);
|
||||
}
|
||||
return $sJson;
|
||||
}
|
||||
public function testCallUnattendedInstallFromHttp()
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Service\Notification\Event;
|
||||
|
||||
use Action;
|
||||
use ActionNewsroom;
|
||||
use Combodo\iTop\Application\WebPage\CaptureWebPage;
|
||||
use Combodo\iTop\Service\Notification\Event\EventNotificationNewsroomService;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use Contact;
|
||||
use EventNotificationNewsroom;
|
||||
use Person;
|
||||
use Ticket;
|
||||
use Trigger;
|
||||
use TriggerOnObjectMention;
|
||||
use UserRequest;
|
||||
use UserRights;
|
||||
|
||||
class EventNotificationNewsroomServiceTest extends ItopDataTestCase
|
||||
{
|
||||
public const CREATE_TEST_ORG = true;
|
||||
|
||||
private Contact $oContact;
|
||||
private Trigger $oTrigger;
|
||||
private Action $oAction;
|
||||
private Ticket $oTicket;
|
||||
private EventNotificationNewsroom $oEvent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
/** @var Contact $oContact */
|
||||
$oContact = $this->createObject(Person::class, [
|
||||
'name' => 'Khalo',
|
||||
'first_name' => 'Frida',
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
]);
|
||||
$this->oContact = $oContact;
|
||||
|
||||
/** @var Trigger $oTrigger */
|
||||
$oTrigger = $this->createObject(TriggerOnObjectMention::class, [
|
||||
'description' => 'Person mentioned on Ticket',
|
||||
'target_class' => 'Ticket',
|
||||
]);
|
||||
$this->oTrigger = $oTrigger;
|
||||
|
||||
/** @var Action $oAction */
|
||||
$oAction = $this->createObject(ActionNewsroom::class, [
|
||||
'name' => 'Notification to persons mentioned in logs',
|
||||
'status' => 'enabled',
|
||||
'title' => '$this->friendlyname$',
|
||||
'message' => 'You have been mentioned by $current_contact->friendlyname$',
|
||||
'recipients' => 'SELECT Person WHERE id = :mentioned->id',
|
||||
]);
|
||||
$this->oAction = $oAction;
|
||||
|
||||
/** @var Ticket $oTicket */
|
||||
$oTicket = $this->createObject(UserRequest::class, [
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
'title' => 'Houston, got a problem',
|
||||
'description' => 'Test description',
|
||||
]);
|
||||
$this->oTicket = $oTicket;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
$this->oEvent->DBDelete();
|
||||
}
|
||||
|
||||
public function testDownloadIsTriggeredWhenDownloaderIsNotificationRecipient(): void
|
||||
{
|
||||
$this->oEvent = EventNotificationNewsroomService::MakeEventFromAction(
|
||||
oAction: $this->oAction,
|
||||
iContactId: $this->oContact->GetKey(),
|
||||
iTriggerId: $this->oTrigger->GetKey(),
|
||||
sMessage: 'Test message',
|
||||
sTitle: 'Test event',
|
||||
sUrl: 'https://localhost/itop/pages/UI.php?operation=details&class=UserRequest&id=1',
|
||||
iObjectId: $this->oTicket->GetKey(),
|
||||
sObjectClass: UserRequest::class,
|
||||
);
|
||||
$this->oEvent->DBInsert();
|
||||
|
||||
$oPage = new CaptureWebPage();
|
||||
$bDownloadIcon = EventNotificationNewsroomService::DownloadIcon($oPage, $this->oEvent->GetKey(), $this->oContact->GetKey());
|
||||
$sHtml = $oPage->GetHtml();
|
||||
|
||||
$this->assertTrue($bDownloadIcon);
|
||||
$this->assertNotEquals('', $sHtml);
|
||||
}
|
||||
|
||||
public function testDownloadIsNotTriggeredWhenDownloaderIsNotNotificationRecipient(): void
|
||||
{
|
||||
$oContact = $this->createObject(Person::class, [
|
||||
'name' => 'Doe',
|
||||
'first_name' => 'John',
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
]);
|
||||
$this->oEvent = EventNotificationNewsroomService::MakeEventFromAction(
|
||||
oAction: $this->oAction,
|
||||
iContactId: $oContact->GetKey(),
|
||||
iTriggerId: $this->oTrigger->GetKey(),
|
||||
sMessage: 'Test message',
|
||||
sTitle: 'Test event',
|
||||
sUrl: 'https://localhost/itop/pages/UI.php?operation=details&class=UserRequest&id=1',
|
||||
iObjectId: $this->oTicket->GetKey(),
|
||||
sObjectClass: UserRequest::class,
|
||||
);
|
||||
$this->oEvent->DBInsert();
|
||||
|
||||
$oPage = new CaptureWebPage();
|
||||
$bDownloadIcon = EventNotificationNewsroomService::DownloadIcon($oPage, $this->oEvent->GetKey(), $this->oContact->GetKey());
|
||||
$sHtml = $oPage->GetHtml();
|
||||
|
||||
$this->assertFalse($bDownloadIcon);
|
||||
$this->assertEquals('', $sHtml);
|
||||
}
|
||||
|
||||
public function testDownloadIconIsTriggeredEvenWhenUserCannotReadIconAttribute(): void
|
||||
{
|
||||
// Create a user with Support Agent Profile
|
||||
$sLogin = uniqid('EventNotificationNewsroomServiceTest');
|
||||
$oUser = $this->CreateContactlessUser($sLogin, self::$aURP_Profiles['Support Agent'], '1234@Abcdefg');
|
||||
$oUser->Set('contactid', $this->oContact->GetKey());
|
||||
UserRights::Login($sLogin);
|
||||
|
||||
$this->oEvent = EventNotificationNewsroomService::MakeEventFromAction(
|
||||
oAction: $this->oAction,
|
||||
iContactId: $this->oContact->GetKey(),
|
||||
iTriggerId: $this->oTrigger->GetKey(),
|
||||
sMessage: 'Test message',
|
||||
sTitle: 'Test event',
|
||||
sUrl: 'https://localhost/itop/pages/UI.php?operation=details&class=UserRequest&id=1',
|
||||
iObjectId: $this->oTicket->GetKey(),
|
||||
sObjectClass: UserRequest::class,
|
||||
);
|
||||
$this->oEvent->DBInsert();
|
||||
|
||||
$iURValue = UserRights::IsActionAllowedOnAttribute(EventNotificationNewsroom::class, 'icon', UR_ACTION_READ, $this->oEvent, $oUser);
|
||||
$this->assertEquals(UR_ALLOWED_NO, $iURValue);
|
||||
|
||||
$oPage = new CaptureWebPage();
|
||||
$bDownloadIcon = EventNotificationNewsroomService::DownloadIcon($oPage, $this->oEvent->GetKey(), $this->oContact->GetKey());
|
||||
$sHtml = $oPage->GetHtml();
|
||||
|
||||
$this->assertTrue($bDownloadIcon);
|
||||
$this->assertNotEquals('', $sHtml);
|
||||
$this->assertStringNotContainsString('the object does not exist or you are not allowed to view it', $sHtml);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user