From 8089d5d53f7ec4bc43f1fbe379c4c63c8b0f7f54 Mon Sep 17 00:00:00 2001 From: Benjamin DALSASS Date: Wed, 22 Jul 2026 08:55:55 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B09776=20-=20Fix=20compatibility=20with=20?= =?UTF-8?q?iTop=203.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- legacywizardcontroller.class.inc.php | 684 +++++++++++++++++++++++++++ quizzwizard.class.inc.php | 6 +- 2 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 legacywizardcontroller.class.inc.php diff --git a/legacywizardcontroller.class.inc.php b/legacywizardcontroller.class.inc.php new file mode 100644 index 0000000..b567227 --- /dev/null +++ b/legacywizardcontroller.class.inc.php @@ -0,0 +1,684 @@ + +use Combodo\iTop\Application\UI\Base\Component\Html\Html; +use Combodo\iTop\Application\WebPage\WebPage; + +/** + * Engine for displaying the various pages of a "wizard" + * Each "step" of the wizard must be implemented as + * separate class derived from WizardStep. each 'step' can also have its own + * internal 'state' for developing complex wizards. + * The WizardController provides the "<< Back" feature by storing a stack + * of the previous screens. The WizardController also maintains from page + * to page a list of "parameters" to be dispayed/edited by each of the steps. + * + * @copyright Copyright (C) 2010-2024 Combodo SAS + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class WizardController +{ + protected $aSteps; + protected $sInitialStepClass; + protected $sInitialState; + protected $aParameters; + + /** + * Initiailization of the wizard controller + * @param string $sInitialStepClass Class of the initial step/page of the wizard + * @param string $sInitialState Initial state of the initial page (if this class manages states) + */ + public function __construct($sInitialStepClass, $sInitialState = '') + { + $this->sInitialStepClass = $sInitialStepClass; + $this->sInitialState = $sInitialState; + $this->aParameters = []; + $this->aSteps = []; + } + + /** + * Pushes information about the current step onto the stack + * @param hash $aStepInfo Array('class' => , 'state' => ) + */ + protected function PushStep($aStepInfo) + { + array_push($this->aSteps, $aStepInfo); + } + + /** + * Removes information about the previous step from the stack + * @return hash Array('class' => , 'state' => ) + */ + protected function PopStep() + { + return array_pop($this->aSteps); + } + + /** + * Reads a "persistent" parameter from the wizard's context + * @param string $sParamCode The code identifying this parameter + * @param mixed $defaultValue The default value of the parameter in case it was not set + */ + public function GetParameter($sParamCode, $defaultValue = '') + { + if (array_key_exists($sParamCode, $this->aParameters)) { + return $this->aParameters[$sParamCode]; + } + + return $defaultValue; + } + + /** + * @return array Allow to update config using {@see Config::UpdateFromParams()} + * + * @since 3.1.0 N°2013 + */ + public function GetParamForConfigArray(): array + { + /** @noinspection PhpUnnecessaryLocalVariableInspection */ + $aParamValues = [ + 'db_server' => $this->GetParameter('db_server', ''), + 'db_user' => $this->GetParameter('db_user', ''), + 'db_pwd' => $this->GetParameter('db_pwd', ''), + 'db_name' => $this->GetParameter('db_name', ''), + 'db_prefix' => $this->GetParameter('db_prefix', ''), + 'db_tls_enabled' => $this->GetParameter('db_tls_enabled', false), + 'db_tls_ca' => $this->GetParameter('db_tls_ca', ''), + ]; + + return $aParamValues; + } + + /** + * Stores a "persistent" parameter in the wizard's context + * + * @param string $sParamCode The code identifying this parameter + * @param mixed $value The value to store + */ + public function SetParameter($sParamCode, $value) + { + $this->aParameters[$sParamCode] = $value; + } + + /** + * Stores the value of the page's parameter in a "persistent" parameter in the wizard's context + * @param string $sParamCode The code identifying this parameter + * @param mixed $defaultValue The default value for the parameter + * @param string $sSanitizationFilter A 'sanitization' fitler. Default is 'raw_data', which means no filtering + */ + public function SaveParameter($sParamCode, $defaultValue, $sSanitizationFilter = 'raw_data') + { + $value = utils::ReadParam($sParamCode, $defaultValue, false, $sSanitizationFilter); + $this->aParameters[$sParamCode] = $value; + } + + /** + * Starts the wizard by displaying it in its initial state + */ + public function Start() + { + $sCurrentStepClass = $this->sInitialStepClass; + $oStep = new $sCurrentStepClass($this, $this->sInitialState); + $this->DisplayStep($oStep); + } + /** + * Progress towards the next step of the wizard + * @throws Exception + */ + protected function Next() + { + $sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass); + $sCurrentState = utils::ReadParam('_state', $this->sInitialState); + /** @var \WizardStep $oStep */ + $oStep = new $sCurrentStepClass($this, $sCurrentState); + if ($oStep->ValidateParams()) { + $this->PushStep(['class' => $sCurrentStepClass, 'state' => $sCurrentState]); + $aPossibleSteps = $oStep->GetPossibleSteps(); + $aNextStepInfo = $oStep->ProcessParams(true); // true => moving forward + if (in_array($aNextStepInfo['class'], $aPossibleSteps)) { + $oNextStep = new $aNextStepInfo['class']($this, $aNextStepInfo['state']); + $this->DisplayStep($oNextStep); + } else { + throw new Exception("Internal error: Unexpected next step '{$aNextStepInfo['class']}'. The possible next steps are: ".implode(', ', $aPossibleSteps)); + } + } else { + $this->DisplayStep($oStep); + } + } + /** + * Move one step back + */ + protected function Back() + { + // let the current step save its parameters + $sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass); + $sCurrentState = utils::ReadParam('_state', $this->sInitialState); + $oStep = new $sCurrentStepClass($this, $sCurrentState); + $aNextStepInfo = $oStep->ProcessParams(false); // false => Moving backwards + + // Display the previous step + $aCurrentStepInfo = $this->PopStep(); + $oStep = new $aCurrentStepInfo['class']($this, $aCurrentStepInfo['state']); + $this->DisplayStep($oStep); + } + + /** + * Displays the specified 'step' of the wizard + * @param WizardStep $oStep The 'step' to display + */ + protected function DisplayStep(WizardStep $oStep) + { + $oPage = new SetupPage($oStep->GetTitle()); + if ($oStep->RequiresWritableConfig()) { + $sConfigFile = utils::GetConfigFilePath(); + if (file_exists($sConfigFile)) { + // The configuration file already exists + if (!is_writable($sConfigFile)) { + SetupUtils::ExitReadOnlyMode(false); // Reset readonly mode in case of problem + SetupUtils::EraseSetupToken(); + $sRelativePath = utils::GetConfigFilePathRelative(); + $oP = new SetupPage('Installation Cannot Continue'); + $oP->add("

Fatal error

\n"); + $oP->error("Error: the configuration file '".$sRelativePath."' already exists and cannot be overwritten."); + $oP->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '".$sRelativePath."' can be modified by the web server."); + + $sButtonsHtml = <<Reload +HTML; + $oP->p($sButtonsHtml); + + $oP->output(); + // Prevent token creation + exit; + } + } + } + $oPage->LinkScriptFromAppRoot('setup/setup.js'); + $oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n"); + $oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n"); + $oPage->add('
'); + $oPage->add('
'); + $oStep->Display($oPage); + $oPage->add('
'); + + // Add the back / next buttons and the hidden form + // to store the parameters + $oPage->add(''); + $oPage->add(''); + foreach ($this->aParameters as $sCode => $value) { + $oPage->add(''); + } + + $oPage->add(''); + $oPage->add(''); + if ((count($this->aSteps) > 0) && ($oStep->CanMoveBackward())) { + $oPage->add(''); + } + if ($oStep->CanMoveForward()) { + $oPage->add(''); + } + $oPage->add('
'); + $oPage->add("
"); + $oPage->add(''); // The div may become visible in case of error + + // Hack to have the "Next >>" button, be the default button, since the first submit button in the form is the default one + $oPage->add_ready_script( + <<output(); + } + /** + * Make the wizard run: Start, Next or Back depending WizardUpdateButtons(); +on the page's parameters + */ + public function Run() + { + /** + * @since 3.2.0 Add the ContextTag init + * @noinspection PhpUnusedLocalVariableInspection + */ + $oContextTag = new ContextTag(ContextTag::TAG_SETUP); + + $sOperation = utils::ReadParam('operation'); + $this->aParameters = utils::ReadParam('_params', [], false, 'raw_data'); + $this->aSteps = json_decode(utils::ReadParam('_steps', '[]', false, 'raw_data'), true /* bAssoc */); + + switch ($sOperation) { + case 'next': + $this->Next(); + break; + + case 'back': + $this->Back(); + break; + + default: + $this->Start(); + } + } + + /** + * Provides information about the structure/workflow of the wizard by listing + * the possible list of 'steps' and their dependencies + * @param string $sStep Name of the class to start from (used for recursion) + * @param hash $aAllSteps List of steps (used for recursion) + */ + public function DumpStructure($sStep = '', $aAllSteps = null) + { + if ($aAllSteps == null) { + $aAllSteps = []; + } + if ($sStep == '') { + $sStep = $this->sInitialStepClass; + } + + $oStep = new $sStep($this, ''); + $aAllSteps[$sStep] = $oStep->GetPossibleSteps(); + foreach ($aAllSteps[$sStep] as $sNextStep) { + if (!array_key_exists($sNextStep, $aAllSteps)) { + $aAllSteps = $this->DumpStructure($sNextStep, $aAllSteps); + } + } + + return $aAllSteps; + } + + /** + * Dump the wizard's structure as a string suitable to produce a chart + * using graphviz's "dot" program + * @return string The 'dot' formatted output + */ + public function DumpStructureAsDot() + { + $aAllSteps = $this->DumpStructure(); + $sOutput = "digraph finite_state_machine {\n"; + //$sOutput .= "\trankdir=LR;"; + $sOutput .= "\tsize=\"10,12\"\n"; + + $aDeadEnds = [$this->sInitialStepClass]; + foreach ($aAllSteps as $sStep => $aNextSteps) { + if (count($aNextSteps) == 0) { + $aDeadEnds[] = $sStep; + } + } + $sOutput .= "\tnode [shape = doublecircle]; ".implode(' ', $aDeadEnds).";\n"; + $sOutput .= "\tnode [shape = box];\n"; + foreach ($aAllSteps as $sStep => $aNextSteps) { + $oStep = new $sStep($this, ''); + $sOutput .= "\t$sStep [ label = \"".$oStep->GetTitle()."\"];\n"; + if (count($aNextSteps) > 0) { + foreach ($aNextSteps as $sNextStep) { + $sOutput .= "\t$sStep -> $sNextStep;\n"; + } + } + } + $sOutput .= "}\n"; + return $sOutput; + } +} + +/** + * Abstract class to build "steps" for the wizard controller + * If a step needs to maintain an internal "state" (for complex steps) + * then it's up to the derived class to implement the behavior based on + * the internal 'sCurrentState' variable. + * @copyright Copyright (C) 2010-2024 Combodo SAS + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +abstract class WizardStep +{ + /** + * A reference to the WizardController + * @var WizardController + */ + protected $oWizard; + /** + * Current 'state' of the wizard step. Simple 'steps' can ignore it + * @var string + */ + protected $sCurrentState; + + public function __construct(WizardController $oWizard, $sCurrentState) + { + $this->oWizard = $oWizard; + $this->sCurrentState = $sCurrentState; + } + + public function GetState() + { + return $this->sCurrentState; + } + + /** + * Displays the wizard page for the current class/state + * The page can contain any number of "" fields, but no "
...
" tag + * The name of the input fields (and their id if one is supplied) MUST NOT start with "_" + * (this is reserved for the wizard's own parameters) + * @return void + */ + abstract public function Display(WebPage $oPage); + /** + * Displays the wizard page for the current class/state + * return UIBlock + * The name of the input fields (and their id if one is supplied) MUST NOT start with "_" + * (this is reserved for the wizard's own parameters) + * @return \Combodo\iTop\Application\UI\Base\UIBlock + * @since 3.0.0 + */ + public function DisplayBlock(WebPage $oPage) + { + return new Html($this->Display($oPage)); + } + + /** + * Processes the page's parameters and (if moving forward) returns the next step/state to be displayed + * @param bool $bMoveForward True if the wizard is moving forward 'Next >>' button pressed, false otherwise + * @return hash array('class' => $sNextClass, 'state' => $sNextState) + */ + abstract public function ProcessParams($bMoveForward = true); + + /** + * Returns the list of possible steps from this step forward + * @return array Array of strings (step classes) + */ + abstract public function GetPossibleSteps(); + + /** + * Returns title of the current step + * @return string The title of the wizard page for the current step + */ + abstract public function GetTitle(); + + /** + * Tells whether the parameters are Ok to move forward + * @return boolean True to move forward, false to stey on the same step + */ + public function ValidateParams() + { + return true; + } + + /** + * Tells whether this step/state is the last one of the wizard (dead-end) + * @return boolean True if the 'Next >>' button should be displayed + */ + public function CanMoveForward() + { + return true; + } + + /** + * Tells whether the "Next" button should be enabled interactively + * @return string A piece of javascript code returning either true or false + */ + public function JSCanMoveForward() + { + return 'return true;'; + } + + /** + * Returns the label for the " Next >> " button + * @return string The label for the button + */ + public function GetNextButtonLabel() + { + return 'Next'; + } + + /** + * 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 true; + } + + /** + * Tells whether the "Back" button should be enabled interactively + * @return string A piece of javascript code returning either true or false + */ + public function JSCanMoveBackward() + { + return 'return true;'; + } + + /** + * 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 + */ + public function RequiresWritableConfig() + { + return true; + } + + /** + * Overload this function to implement asynchronous action(s) (AJAX) + * @param string $sCode The code of the action (if several actions need to be distinguished) + * @param hash $aParameters The action's parameters name => value + */ + public function AsyncAction(WebPage $oPage, $sCode, $aParameters) + { + } +} + +/* + * Example of a simple Setup Wizard with some parameters to store + * the installation mode (install | upgrade) and a simple asynchronous + * (AJAX) action. + * + * The setup wizard is executed by the following code: + * + * $oWizard = new WizardController('Step1'); + * $oWizard->Run(); + * +class Step1 extends WizardStep +{ + public function GetTitle() + { + return 'Welcome'; + } + + public function GetPossibleSteps() + { + return array('Step2', 'Step2bis'); + } + + public function ProcessParams($bMoveForward = true) + { + $sNextStep = ''; + $sInstallMode = utils::ReadParam('install_mode'); + if ($sInstallMode == 'install') + { + $this->oWizard->SetParameter('install_mode', 'install'); + $sNextStep = 'Step2'; + } + else + { + $this->oWizard->SetParameter('install_mode', 'upgrade'); + $sNextStep = 'Step2bis'; + + } + return array('class' => $sNextStep, 'state' => ''); + } + + public function Display(WebPage $oPage) + { + $oPage->p('This is Step 1!'); + $sInstallMode = $this->oWizard->GetParameter('install_mode', 'install'); + $sChecked = ($sInstallMode == 'install') ? ' checked ' : ''; + $oPage->p(' Install'); + $sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : ''; + $oPage->p(' Upgrade'); + } +} + +class Step2 extends WizardStep +{ + public function GetTitle() + { + return 'Installation Parameters'; + } + + public function GetPossibleSteps() + { + return array('Step3'); + } + + public function ProcessParams($bMoveForward = true) + { + return array('class' => 'Step3', 'state' => ''); + } + + public function Display(WebPage $oPage) + { + $oPage->p('This is Step 2! (Installation)'); + } +} + +class Step2bis extends WizardStep +{ + public function GetTitle() + { + return 'Upgrade Parameters'; + } + + public function GetPossibleSteps() + { + return array('Step2ter'); + } + + public function ProcessParams($bMoveForward = true) + { + $sUpgradeInfo = utils::ReadParam('upgrade_info'); + $this->oWizard->SetParameter('upgrade_info', $sUpgradeInfo); + $sAdditionalUpgradeInfo = utils::ReadParam('additional_upgrade_info'); + $this->oWizard->SetParameter('additional_upgrade_info', $sAdditionalUpgradeInfo); + return array('class' => 'Step2ter', 'state' => ''); + } + + public function Display(WebPage $oPage) + { + $oPage->p('This is Step 2bis! (Upgrade)'); + $sUpgradeInfo = $this->oWizard->GetParameter('upgrade_info', ''); + $oPage->p('Type your name here: '); + $sAdditionalUpgradeInfo = $this->oWizard->GetParameter('additional_upgrade_info', ''); + $oPage->p('The installer replies: '); + + $oPage->add_ready_script("$('#upgrade_info').change(function() { + $('#v_upgrade_info').html(''); + WizardAsyncAction('', { upgrade_info: $('#upgrade_info').val() }); });"); + } + + public function AsyncAction(WebPage $oPage, $sCode, $aParameters) + { + usleep(300000); // 300 ms + $sName = $aParameters['upgrade_info']; + $sReply = addslashes("Hello ".$sName); + + $oPage->add_ready_script( +<< 'Step3', 'state' => ''); + } + + public function Display(WebPage $oPage) + { + $oPage->p('This is Step 2ter! (Upgrade)'); + } +} + +class Step3 extends WizardStep +{ + public function GetTitle() + { + return 'Installation Complete'; + } + + public function GetPossibleSteps() + { + return array(); + } + + public function ProcessParams($bMoveForward = true) + { + return array('class' => '', 'state' => ''); + } + + public function Display(WebPage $oPage) + { + $oPage->p('This is the FINAL Step'); + } + + public function CanMoveForward() + { + return false; + } +} + +End of the example */ diff --git a/quizzwizard.class.inc.php b/quizzwizard.class.inc.php index 60705b0..54b0d2e 100644 --- a/quizzwizard.class.inc.php +++ b/quizzwizard.class.inc.php @@ -1,6 +1,10 @@