Compare commits

..

4 Commits

Author SHA1 Message Date
Eric Espie
9f34e9a0db N°9455 - Feature removal - console screen behavior 2026-06-01 09:26:23 +02:00
Eric Espie
f2a5909b43 Setup fast track (keep current options) 2026-05-29 17:56:28 +02:00
Eric Espie
1aa249b59f Setup fast track (fix setup token) 2026-05-29 17:40:05 +02:00
Eric Espie
6f34a4799c Setup fast track 2026-05-29 17:22:59 +02:00
35 changed files with 259 additions and 711 deletions

View File

@@ -5,9 +5,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Exception\ItopException;
class CoreException extends ItopException
class CoreException extends Exception
{
protected $m_sIssue;
protected $m_sImpact;

View File

@@ -16,7 +16,6 @@
//
// 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');
@@ -64,9 +63,6 @@ register_shutdown_function(function () {
}
}
});
set_exception_handler([ExceptionHandlerHelper::class, 'HandleException']);
$oKPI = new ExecutionKPI();
Session::Start();
$oKPI->ComputeAndReport("Session Start");

View File

@@ -1917,9 +1917,7 @@ SQL;
$aResponseHeaders[$sName] = $sValue;
}
}
if (PHP_VERSION_ID < 80000) {
curl_close($ch);
}
curl_close($ch);
return $response;
}

View File

@@ -614,12 +614,6 @@ class LogChannels
* @since 3.2.0
*/
public const SECURITY = 'Security';
/**
* @var string
* @since 3.3.0
*/
public const EXCEPTION = 'Exception';
}
abstract class LogAPI
@@ -714,14 +708,8 @@ abstract class LogAPI
private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array
{
$aContext['exception'] = get_class($oException);
$aContext['message'] = $oException->getMessage();
$aContext['stack'] = $oException->getTraceAsString();
if(method_exists($oException, 'GetContext')) {
$aContext = array_merge($aContext, $oException->GetContext());
}
$aContext['Error Message'] = $oException->getMessage();
$aContext['Stack Trace'] = $oException->getTraceAsString();
return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext];
}

View File

@@ -340,14 +340,13 @@ 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, $bAllowAllData = false)
public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null)
{
try {
$oObj = MetaModel::GetObject($sClass, $id, false, $bAllowAllData);
$oObj = MetaModel::GetObject($sClass, $id, false, false);
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);

View File

@@ -26,7 +26,6 @@ use Dict;
use Exception;
use MetaModel;
use RunTimeEnvironment;
use SecurityException;
use SetupUtils;
use utils;
@@ -84,11 +83,10 @@ class DataFeatureRemovalController extends Controller
{
$aParams = [];
try {
if (SetupUtils::IsSessionSetupTokenValid()) {
//from setup wizard/mtp
SetupUtils::CheckSetupToken();
SetupUtils::EraseSetupToken();
} catch (SecurityException $e) {
} else {
//from same module
$this->ValidateTransactionId();
}
@@ -102,7 +100,6 @@ class DataFeatureRemovalController extends Controller
'removed_extensions' => '[]',
'extensions_not_uninstallable' => '[]',
'copy_setup_files' => 1,
'return_button_label' => '',
];
$aHiddenInputs = [];
@@ -111,10 +108,6 @@ 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);
@@ -186,10 +179,6 @@ 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);

View File

@@ -87,15 +87,11 @@
{% endif %}
{% endif %}
{% 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 %}
{% 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 %}
{% EndUIPanel %}

View File

@@ -132,7 +132,6 @@ 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',
@@ -445,7 +444,6 @@ 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',

View File

@@ -533,7 +533,6 @@ 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',
@@ -846,7 +845,6 @@ 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',

View File

@@ -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,9 +1303,39 @@ try {
$oKPI->ComputeAndReport('Compute page');
$oP->output();
} catch (Exception $e) {
throw new ItopException("Unable to handle UI operation", previous: $e, aContext: [
'operation' => ($operation ?? 'N/A'),
]);
$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);
}
class UI

View File

@@ -123,6 +123,7 @@ require_once('./xmldataloader.class.inc.php');
// Never cache this page
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past
$oCtx = new ContextTag(ContextTag::TAG_SETUP);
/**
* Main program

View File

@@ -81,13 +81,13 @@ class ApplicationInstallSequencer extends StepSequencer
return $this->ComputeNextStep($sStep);
case 'log-parameters':
if ($this->HasOptionalStep($sStep)) {
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
$this->DoLogParameters();
}
return $this->ComputeNextStep($sStep);
case 'backup':
if ($this->HasOptionalStep($sStep, false)) {
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
$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 ($this->HasOptionalStep($sStep)) {
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
$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 ($this->HasOptionalStep($sStep)) {
if (array_key_exists($sStep, $this->oParams->Get('optional_steps', []))) {
$this->oRunTimeEnvironment->MigrateDataAfterUpdateStructure($this->oParams->Get('mode'), $this->GetConfig());
}
return $this->ComputeNextStep($sStep);
@@ -191,17 +191,13 @@ class ApplicationInstallSequencer extends StepSequencer
public function GetStepNames(): array
{
$aStepNames = [ ''];
if ($this->HasOptionalStep('log-parameters')) {
$aStepNames [] = 'log-parameters';
}
if ($this->HasOptionalStep('backup', false)) {
$aStepNames [] = 'backup';
}
if ($this->HasOptionalStep('migrate-before')) {
$aStepNames [] = 'migrate-before';
foreach (['log-parameters', 'backup', 'migrate-before'] as $sStepName) {
if (array_key_exists($sStepName, $this->oParams->Get('optional_steps', []))) {
$aStepNames [] = $sStepName;
}
}
$aStepNames [] = 'db-schema';
if ($this->HasOptionalStep('migrate-after')) {
if (array_key_exists('migrate-after', $this->oParams->Get('optional_steps', []))) {
$aStepNames [] = 'migrate-after';
}

View File

@@ -102,7 +102,7 @@ class DataAuditSequencer extends StepSequencer
protected function IsDataAuditRequired(): bool
{
if (!$this->HasOptionalStep('setup-audit', false)) {
if (! array_key_exists('setup-audit', $this->oParams->Get('optional_steps', []))) {
return false;
}
@@ -124,7 +124,7 @@ class DataAuditSequencer extends StepSequencer
public function GetStepNames(): array
{
$aStepNames = [''];
if ($this->HasOptionalStep('copy')) {
if (array_key_exists('copy', $this->oParams->Get('optional_steps', []))) {
$aStepNames[] = 'copy';
}
$aStepNames[] = 'compile';

View File

@@ -29,9 +29,6 @@ abstract class StepSequencer
protected RunTimeEnvironment $oRunTimeEnvironment;
protected string $sSourceDesc;
protected const LABELS = [];
protected const SUCCESS_LABELS = [];
/**
* @param \Parameters $oParams
* @param \RunTimeEnvironment|null $oRunTimeEnvironment
@@ -236,26 +233,4 @@ 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');
}
}

View File

@@ -26,17 +26,17 @@ function WizardAsyncAction(sActionCode, oParams, OnErrorFunction)
function WizardUpdateButtons()
{
if (CanMoveForward()) {
$("#btn_next").removeClass('ibo-is-hidden');
$("#btn_next").prop('disabled', false);
}
else {
$("#btn_next").addClass('ibo-is-hidden');
$("#btn_next").prop('disabled', true);
}
if (CanMoveBackward()) {
$("#btn_back").removeClass('ibo-is-hidden');
$("#btn_back").prop('disabled', false);
}
else {
$("#btn_back").addClass('ibo-is-hidden');
$("#btn_back").prop('disabled', true);
}
}

View File

@@ -61,6 +61,7 @@ if (!function_exists('json_decode')) {
//N°3671 setup context: force $bForceTrustProxy to be persisted in next calls
utils::GetAbsoluteUrlAppRoot(true);
$oWizard = new WizardController('WizStepWelcome');
$oCtx = new ContextTag(ContextTag::TAG_SETUP);
//N°3952
if (SetupUtils::IsSessionSetupTokenValid()) {
// Normal operation
@@ -69,5 +70,5 @@ if (SetupUtils::IsSessionSetupTokenValid()) {
SetupUtils::ExitMaintenanceMode(false);
// Force initializing the setup
$oWizard->Start();
SetupUtils::CreateSetupToken();
//SetupUtils::CreateSetupToken();
}

View File

@@ -151,10 +151,10 @@ class WizardController
$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
$oStep = $this->GetWizardStep($sCurrentStepClass, $sCurrentState);
if ($oStep->ValidateParams()) {
$aPossibleSteps = $oStep->GetPossibleSteps();
if ($oStep->CanMoveBackward()) {
if ($oStep->CanComeBack()) {
$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());
@@ -220,6 +220,8 @@ HTML;
}
}
$oPage->LinkScriptFromAppRoot('setup/setup.js');
$oStep->PreFormDisplay($oPage);
$oPage->add('<form id="wiz_form" class="ibo-setup--wizard" method="post">');
$oPage->add('<div class="ibo-setup--wizard--content">');
$oStep->Display($oPage);
@@ -237,12 +239,8 @@ 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) {
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 ((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 ($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>');
@@ -287,8 +285,8 @@ EOF
$oPage->output();
}
/**
* Make the wizard run: Start, Next or Back depending WizardUpdateButtons();
on the page's parameters
* Make the wizard run: 'Start', 'Next' or 'Back' depending WizardUpdateButtons();
* on the page's parameters
*/
public function Run()
{

View File

@@ -54,20 +54,16 @@ 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
{
@@ -112,7 +108,6 @@ JS);
'removed_extensions' => '[]',
'extensions_not_uninstallable' => '[]',
'copy_setup_files' => 1,
'return_button_label' => '',
];
$aHiddenInputs = '';
foreach ($aParams as $sParamName => $defaultValue) {
@@ -128,43 +123,23 @@ 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="authent" value="$sUID"/>
<input type="hidden" name="setup_token" 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(
<<<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>');
<<<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>');
$('#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(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>&nbsp;<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(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>&nbsp;<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");
@@ -173,7 +148,7 @@ JS
$("#wiz_form").data("installation_status", "cleanup_needed");
$('#btn_next').hide();
JS
EOF
);
}
@@ -186,10 +161,6 @@ JS
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;';
}
}

View File

@@ -144,11 +144,6 @@ 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

View File

@@ -54,13 +54,6 @@ 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);
@@ -116,20 +109,6 @@ 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
*/
@@ -146,25 +125,27 @@ 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(
<<<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
<<<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
);
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(
<<<JS
$('#progress').progression( {Current:100, Maximum: 100} );
$("#wiz_form").data("installation_status", "completed");
$("#btn_next").trigger('click');
JS
<<<EOF
$("#wiz_form").data("installation_status", "completed");
$('#progress').progression( {Current:100, Maximum: 100} );
WizardUpdateButtons();
$("#btn_next").off("click.install");
$("#btn_next").trigger('click');
EOF
);
static::AddPrevStepSuccessMessage($oPage, $aRes['prev-step-success-message']);
} else {
@@ -172,12 +153,12 @@ JS
$sMessage = addslashes(utils::EscapeHtml($aRes['message']));
$sMessage = str_replace("\n", '<br>', $sMessage);
$oPage->add_ready_script(
<<<JS
$("#wiz_form").data("installation_status", "error");
$("#progress .progress").addClass('progress-error');
WizardUpdateButtons();
$('#setup_error').html('$sMessage').show();
JS
<<<EOF
$("#wiz_form").data("installation_status", "error");
$("#progress .progress").addClass('progress-error');
WizardUpdateButtons();
$('#setup_error').html('$sMessage').show();
EOF
);
$this->AddProgressErrorScript($oPage, $aRes);
}
@@ -185,13 +166,7 @@ JS
protected function AddProgressErrorScript($oPage, $aRes)
{
$oPage->add_ready_script(
<<<JS
if ($('#return-button').length > 0) {
$('#return-button').removeClass('ibo-is-hidden');
}
JS
);
}
/**

View File

@@ -21,7 +21,7 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
$oWizard->SetParameter('datamodel_version', ITOP_CORE_VERSION);
$oWizard->SetParameter('upgrade_type', 'use-compatible');
$oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks());
$oWizard->SaveParameter('use_symbolic_links', MFCompiler::UseSymbolicLinks() ? 'on' : 'off');
$oWizard->SaveParameter('force-uninstall', '');
// should be done at the end
@@ -40,8 +40,8 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
*/
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$oProductionEnv = new RunTimeEnvironment();
$sBuildConfigFile = APPCONF.$oProductionEnv->GetBuildEnv().'/'.ITOP_CONFIG_FILE;
$oRuntimeEnv = new RunTimeEnvironment();
$sBuildConfigFile = APPCONF.$oRuntimeEnv->GetBuildEnv().'/'.ITOP_CONFIG_FILE;
@chmod($sBuildConfigFile, 0770); // In case it exists: RWX for owner and group, nothing for others
$oConfig = new Config($sBuildConfigFile);
@@ -56,6 +56,14 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
$this->oWizard->SetParameter('display_choices', '[]');
$this->oWizard->SetParameter('extensions_not_uninstallable', '[]');
if ($this->oWizard->GetParameter('skip_wizard', false)) {
$oExtensionMap = new iTopExtensionsMap($oRuntimeEnv->GetBuildEnv());
$aExtensionsFromDatabase = $oExtensionMap->GetChoicesFromDatabase($oConfig);
$this->oWizard->SetParameter('selected_extensions', json_encode($aExtensionsFromDatabase));
$adModulesFromDatabase = ModuleInstallationRepository::GetInstance()->ReadComputeInstalledModules($oConfig);
$this->oWizard->SetParameter('selected_modules', json_encode(array_keys($adModulesFromDatabase)));
}
$aWizardSteps = $this->GetWizardSteps();
$this->oWizard->SetWizardSteps($aWizardSteps);
$this->sCurrentState = count($aWizardSteps) - 1;
@@ -84,7 +92,7 @@ class WizStepLandingBeforeAudit extends WizStepModulesChoice
return 'Next';
}
public function CanMoveBackward()
public function CanComeBack()
{
return false;
}

View File

@@ -57,14 +57,6 @@ 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);
@@ -250,19 +242,6 @@ 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
@@ -278,11 +257,7 @@ JS
*/
public function JSCanMoveBackward()
{
if ($this->oWizard->GetParameter('return_button_label', '') === '') {
return 'return true;';
}
return 'return false;';
return 'return true;';
}
}

View File

@@ -24,6 +24,17 @@
class WizStepWelcome extends WizardStep
{
protected $bCanMoveForward;
private array $aInfo;
private array $aWarnings;
private array $aErrors;
private string $sUID;
public function __construct(WizardController $oWizard, $sCurrentState)
{
parent::__construct($oWizard, $sCurrentState);
$this->CheckInstallation();
$this->sUID = SetupUtils::CreateSetupToken();
}
public function GetTitle()
{
@@ -46,8 +57,7 @@ class WizStepWelcome extends WizardStep
public function UpdateWizardStateAndGetNextStep($bMoveForward = true): WizardState
{
$sUID = SetupUtils::CreateSetupToken();
$this->oWizard->SetParameter('authent', $sUID);
$this->oWizard->SetParameter('authent', $this->sUID);
return new WizardState(WizStepInstallOrUpgrade::class);
}
@@ -68,39 +78,14 @@ class WizStepWelcome extends WizardStep
EOF
);
$oPage->add('<h1>'.ITOP_APPLICATION.' Installation Wizard</h1>');
$aResults = SetupUtils::CheckPhpAndExtensions();
$this->bCanMoveForward = true;
$aInfo = [];
$aWarnings = [];
$aErrors = [];
foreach ($aResults as $oCheckResult) {
switch ($oCheckResult->iSeverity) {
case CheckResult::ERROR:
$aErrors[] = $oCheckResult->sLabel;
$this->bCanMoveForward = false;
break;
case CheckResult::WARNING:
$aWarnings[] = $oCheckResult->sLabel;
break;
case CheckResult::INFO:
$aInfo[] = $oCheckResult->sLabel;
break;
case CheckResult::TRACE:
SetupLog::Ok($oCheckResult->sLabel);
break;
}
}
$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 = 'style="overflow:auto;"';
$sTitle = count($aErrors).' Error(s), '.count($aWarnings).' Warning(s).';
if (count($this->aErrors) > 0) {
$sStyle = 'overflow:auto;"';
$sTitle = count($this->aErrors).' Error(s), '.count($this->aWarnings).' Warning(s).';
$sH2Class = 'text-error';
} elseif (count($aWarnings) > 0) {
$sTitle = count($aWarnings).' Warning(s) '.$sToggleButtons;
} elseif (count($this->aWarnings) > 0) {
$sTitle = count($this->aWarnings).' Warning(s) '.$sToggleButtons;
$sH2Class = 'text-warning';
} else {
$sTitle = 'Ok. '.$sToggleButtons;
@@ -112,13 +97,13 @@ EOF
<div id="details" $sStyle>
HTML
);
foreach ($aErrors as $sText) {
foreach ($this->aErrors as $sText) {
$oPage->error($sText);
}
foreach ($aWarnings as $sText) {
foreach ($this->aWarnings as $sText) {
$oPage->warning($sText);
}
foreach ($aInfo as $sText) {
foreach ($this->aInfo as $sText) {
$oPage->ok($sText);
}
$oPage->add('</div>');
@@ -129,8 +114,70 @@ HTML
$oPage->add_ready_script('CheckDirectoryConfFilesPermissions("'.utils::GetItopVersionWikiSyntax().'")');
}
/**
* Add post display stuff to the setup screen
* @param \SetupPage $oPage
*
* @return void
*/
public function PostFormDisplay(SetupPage $oPage)
{
if ($this->bCanMoveForward) {
$sBuildConfigFile = APPCONF.ITOP_DEFAULT_ENV.'/'.ITOP_CONFIG_FILE;
if (file_exists($sBuildConfigFile)) {
$oPage->add(
<<<HTML
<form method="post">
<input type="hidden" name="_class" value="WizStepLandingBeforeAudit"/>
<input type="hidden" name="operation" value="next"/>
<input type="hidden" name="_params[skip_wizard]" value="1"/>
<input type="hidden" name="authent" value="{$this->sUID}"/>
<input type="hidden" name="_params[authent]" value="{$this->sUID}"/>
<table style="width:100%;" class="ibo-setup--wizard--buttons-container">
<tr>
<td style="text-align: right"><button type="submit" class="ibo-button ibo-is-regular ibo-is-secondary">Keep current choices</button></td>
</tr>
</table>
</form>
HTML
);
}
}
}
public function CanMoveForward()
{
return $this->bCanMoveForward;
}
/**
*/
public function CheckInstallation(): void
{
$aResults = SetupUtils::CheckPhpAndExtensions();
$this->bCanMoveForward = true;
$this->aInfo = [];
$this->aWarnings = [];
$this->aErrors = [];
foreach ($aResults as $oCheckResult) {
switch ($oCheckResult->iSeverity) {
case CheckResult::ERROR:
$this->aErrors[] = $oCheckResult->sLabel;
$this->bCanMoveForward = false;
break;
case CheckResult::WARNING:
$this->aWarnings[] = $oCheckResult->sLabel;
break;
case CheckResult::INFO:
$this->aInfo[] = $oCheckResult->sLabel;
break;
case CheckResult::TRACE:
SetupLog::Ok($oCheckResult->sLabel);
break;
}
}
}
}

View File

@@ -76,6 +76,10 @@ abstract class WizardStep
{
}
public function PreFormDisplay(SetupPage $oPage)
{
}
protected function CheckDependencies()
{
if (is_null($this->bDependencyCheck)) {
@@ -151,6 +155,15 @@ 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

View File

@@ -1,119 +0,0 @@
<?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;
}
}

View File

@@ -23,6 +23,7 @@ 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;
@@ -260,29 +261,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;
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);
}
// 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);
// }
}
/**

View File

@@ -13,6 +13,7 @@ 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;
@@ -38,9 +39,7 @@ class CaseLogEntryFormFactory
->AddMainActionButtons(static::PrepareCancelButton());
$oSaveButton = static::PrepareSaveButton();
$oTransitionsMenu = static::PrepareTransitionsSelectionPopoverMenu($oObject, $sCaseLogAttCode, $oCaseLogEntryForm->GetId());
// Prevent popover menu from landing behind caselog editor
$oTransitionsMenu->SetContainer(PopoverMenu::ENUM_CONTAINER_BODY);
$oTransitionsMenu = static::PrepareTransitionsSelectionPopoverMenu($oObject, $sCaseLogAttCode);
if (true === $oTransitionsMenu->HasItems()) {
$oButtonGroup = ButtonGroupUIBlockFactory::MakeButtonWithOptionsMenu($oSaveButton, $oTransitionsMenu);
$oCaseLogEntryForm->AddMainActionButtons($oButtonGroup);
@@ -70,16 +69,7 @@ class CaseLogEntryFormFactory
return $oButton;
}
/**
* @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
protected static function PrepareTransitionsSelectionPopoverMenu(DBObject $oObject, string $sCaseLogAttCode): PopoverMenu
{
$sObjClass = get_class($oObject);
@@ -87,6 +77,8 @@ 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)) {
@@ -105,7 +97,7 @@ class CaseLogEntryFormFactory
CaseLogEntryForm::BLOCK_CODE.'--add-action--'.$sCaseLogAttCode.'--stimulus--'.$sStimulusCode,
Dict::Format('UI:Button:SendAnd', $aStimuli[$sStimulusCode]->GetLabel()),
<<<JS
$('#$sCaseLogEntryFormId').trigger('save_entry.caselog_entry_form.itop', {stimulus_code: '{$sStimulusCode}'});
$(this).closest('[data-role="{$sCaseLogEntryFormDataRole}"]').trigger('save_entry.caselog_entry_form.itop', {stimulus_code: '{$sStimulusCode}'});
JS
)
);

View File

@@ -16,12 +16,10 @@ 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;
@@ -29,7 +27,6 @@ use DBObjectSearch;
use DBObjectSet;
use Dict;
use EventNotificationNewsroom;
use Exception;
use MetaModel;
use SecurityException;
use UserRights;
@@ -382,10 +379,9 @@ JS
$oEventBlock->SetCSSColorClass($sReadColor);
$oEventBlock->SetSubTitle($sReadLabel);
$oEventBlock->SetClassLabel('');
/** @var \ormDocument $oImage */
$oImage = $oEvent->Get('icon');
if (!$oImage->IsEmpty()) {
$sIconUrl = self::GetDisplayIconUrl($iEventId, $oImage->GetSignature());
$sIconUrl = $oImage->GetDisplayURL(get_class($oEvent), $iEventId, 'icon');
$oEventBlock->SetIcon($sIconUrl, Panel::ENUM_ICON_COVER_METHOD_COVER, true);
}
@@ -549,7 +545,7 @@ $sMessage
HTML;
$sIcon = $oMessage->Get('icon') !== null ?
$this->GetDisplayIconUrl($oMessage->GetKey(), $oMessage->Get('icon')->GetSignature()) :
$oMessage->Get('icon')->GetDisplayURL(EventNotificationNewsroom::class, $oMessage->GetKey(), 'icon') :
Branding::GetCompactMainLogoAbsoluteUrl();
$aMessages[] = [
'id' => $oMessage->GetKey(),
@@ -696,35 +692,6 @@ 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
*
@@ -817,9 +784,4 @@ 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";
}
}

View File

@@ -1,28 +0,0 @@
<?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;
}
}

View File

@@ -4,10 +4,8 @@ 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;
/**
@@ -73,31 +71,4 @@ 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;
}
}

View File

@@ -706,9 +706,7 @@ abstract class ItopTestCase extends KernelTestCase
$this->aLastCurlGetInfo = $info;
$sErrorMsg = curl_error($ch);
$iErrorCode = curl_errno($ch);
if (PHP_VERSION_ID < 80000) {
curl_close($ch);
}
curl_close($ch);
\IssueLog::Info(__METHOD__, null, ['url' => $sUrl, 'error' => $sErrorMsg, 'error_code' => $iErrorCode, 'post_fields' => $aPostFields, 'info' => $info]);

View File

@@ -306,21 +306,6 @@ 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 [

View File

@@ -101,10 +101,10 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
$aExpected = [
'status' => 1,
'message' => '',
'next-step' => 'migrate-before',
'next-step-label' => 'Migrate data before database upgrade',
'next-step' => 'db-schema',
'next-step-label' => 'Updating database schema',
'prev-step-success-message' => 'Parameters logged',
'percentage-completed' => 22,
'percentage-completed' => 16,
];
$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' => 66,
'percentage-completed' => 50,
];
$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' => 77,
'percentage-completed' => 66,
'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' => 88,
'percentage-completed' => 83,
'status' => 1,
];
$this->assertEquals($aExpected, $aRes);
@@ -485,10 +485,7 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
$this->GivenApplicationInstallSequencer([], true);
$expected = [
'',
'log-parameters',
'migrate-before',
'db-schema',
'migrate-after',
'after-db-create',
'load-data',
'create-config',
@@ -500,11 +497,11 @@ class ApplicationInstallerSequencerTest extends ItopTestCase
public function testGetStepAfterWithPercent()
{
$this->GivenApplicationInstallSequencer([], true);
$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(['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(['', 100], $this->oSequencer->GetStepAfterWithPercent('commit'));
}

View File

@@ -39,9 +39,7 @@ class UnattendedInstallTest extends ItopDataTestCase
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$sJson = curl_exec($ch);
if (PHP_VERSION_ID < 80000) {
curl_close($ch);
}
curl_close($ch);
return $sJson;
}
public function testCallUnattendedInstallFromHttp()

View File

@@ -1,154 +0,0 @@
<?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);
}
}