wizardcontroller.class.inc.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. <?php
  2. // Copyright (C) 2010-2012 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Engine for displaying the various pages of a "wizard"
  20. * Each "step" of the wizard must be implemented as
  21. * separate class derived from WizardStep. each 'step' can also have its own
  22. * internal 'state' for developing complex wizards.
  23. * The WizardController provides the "<< Back" feature by storing a stack
  24. * of the previous screens. The WizardController also maintains from page
  25. * to page a list of "parameters" to be dispayed/edited by each of the steps.
  26. * @copyright Copyright (C) 2010-2012 Combodo SARL
  27. * @license http://opensource.org/licenses/AGPL-3.0
  28. */
  29. class WizardController
  30. {
  31. protected $aSteps;
  32. protected $sInitialStepClass;
  33. protected $sInitialState;
  34. protected $aParameters;
  35. /**
  36. * Initiailization of the wizard controller
  37. * @param string $sInitialStepClass Class of the initial step/page of the wizard
  38. * @param string $sInitialState Initial state of the initial page (if this class manages states)
  39. */
  40. public function __construct($sInitialStepClass, $sInitialState = '')
  41. {
  42. $this->sInitialStepClass = $sInitialStepClass;
  43. $this->sInitialState = $sInitialState;
  44. $this->aParameters = array();
  45. $this->aSteps = array();
  46. }
  47. /**
  48. * Pushes information about the current step onto the stack
  49. * @param hash $aStepInfo Array('class' => , 'state' => )
  50. */
  51. protected function PushStep($aStepInfo)
  52. {
  53. array_push($this->aSteps, $aStepInfo);
  54. }
  55. /**
  56. * Removes information about the previous step from the stack
  57. * @return hash Array('class' => , 'state' => )
  58. */
  59. protected function PopStep()
  60. {
  61. return array_pop($this->aSteps);
  62. }
  63. /**
  64. * Reads a "persistent" parameter from the wizard's context
  65. * @param string $sParamCode The code identifying this parameter
  66. * @param mixed $defaultValue The default value of the parameter in case it was not set
  67. */
  68. public function GetParameter($sParamCode, $defaultValue = '')
  69. {
  70. if (array_key_exists($sParamCode, $this->aParameters))
  71. {
  72. return $this->aParameters[$sParamCode];
  73. }
  74. return $defaultValue;
  75. }
  76. /**
  77. * Stores a "persistent" parameter in the wizard's context
  78. * @param string $sParamCode The code identifying this parameter
  79. * @param mixed $value The value to store
  80. */
  81. public function SetParameter($sParamCode, $value)
  82. {
  83. $this->aParameters[$sParamCode] = $value;
  84. }
  85. /**
  86. * Stores the value of the page's parameter in a "persistent" parameter in the wizard's context
  87. * @param string $sParamCode The code identifying this parameter
  88. * @param mixed $defaultValue The default value for the parameter
  89. * @param string $sSanitizationFilter A 'sanitization' fitler. Default is 'raw_data', which means no filtering
  90. */
  91. public function SaveParameter($sParamCode, $defaultValue, $sSanitizationFilter = 'raw_data')
  92. {
  93. $value = utils::ReadParam($sParamCode, $defaultValue, false, $sSanitizationFilter);
  94. $this->aParameters[$sParamCode] = $value;
  95. }
  96. /**
  97. * Starts the wizard by displaying it in its initial state
  98. */
  99. protected function Start()
  100. {
  101. $sCurrentStepClass = $this->sInitialStepClass;
  102. $oStep = new $sCurrentStepClass($this, $this->sInitialState);
  103. $this->DisplayStep($oStep);
  104. }
  105. /**
  106. * Progress towards the next step of the wizard
  107. * @throws Exception
  108. */
  109. protected function Next()
  110. {
  111. $sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
  112. $sCurrentState = utils::ReadParam('_state', $this->sInitialState);
  113. $oStep = new $sCurrentStepClass($this, $sCurrentState);
  114. if ($oStep->ValidateParams($sCurrentState))
  115. {
  116. $this->PushStep(array('class' => $sCurrentStepClass, 'state' => $sCurrentState));
  117. $aPossibleSteps = $oStep->GetPossibleSteps();
  118. $aNextStepInfo = $oStep->ProcessParams(true); // true => moving forward
  119. if (in_array($aNextStepInfo['class'], $aPossibleSteps))
  120. {
  121. $oNextStep = new $aNextStepInfo['class']($this, $aNextStepInfo['state']);
  122. $this->DisplayStep($oNextStep);
  123. }
  124. else
  125. {
  126. throw new Exception("Internal error: Unexpected next step '{$aNextStepInfo['class']}'. The possible next steps are: ".implode(', ', $aPossibleSteps));
  127. }
  128. }
  129. else
  130. {
  131. $this->DisplayStep($oStep);
  132. }
  133. }
  134. /**
  135. * Move one step back
  136. */
  137. protected function Back()
  138. {
  139. // let the current step save its parameters
  140. $sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
  141. $sCurrentState = utils::ReadParam('_state', $this->sInitialState);
  142. $oStep = new $sCurrentStepClass($this, $sCurrentState);
  143. $aNextStepInfo = $oStep->ProcessParams(false); // false => Moving backwards
  144. // Display the previous step
  145. $aCurrentStepInfo = $this->PopStep();
  146. $oStep = new $aCurrentStepInfo['class']($this, $aCurrentStepInfo['state']);
  147. $this->DisplayStep($oStep);
  148. }
  149. /**
  150. * Displays the specified 'step' of the wizard
  151. * @param WizardStep $oStep The 'step' to display
  152. */
  153. protected function DisplayStep(WizardStep $oStep)
  154. {
  155. $oPage = new SetupPage($oStep->GetTitle());
  156. if ($oStep->RequiresWritableConfig())
  157. {
  158. $sConfigFile = utils::GetConfigFilePath();
  159. if (file_exists($sConfigFile))
  160. {
  161. // The configuration file already exists
  162. if (!is_writable($sConfigFile))
  163. {
  164. $oP = new SetupPage('Installation Cannot Continue');
  165. $oP->add("<h2>Fatal error</h2>\n");
  166. $oP->error("<b>Error:</b> the configuration file '".$sConfigFile."' already exists and cannot be overwritten.");
  167. $oP->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '<b>".realpath($sConfigFile)."</b>' can be modified by the web server.");
  168. $oP->p('<button type="button" onclick="window.location.reload()">Reload</button>');
  169. $oP->output();
  170. return;
  171. }
  172. }
  173. }
  174. $oPage->add_linked_script('../setup/setup.js');
  175. $oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
  176. $oPage->add_script("function CanMoveBackward()\n{\n".$oStep->JSCanMoveBackward()."\n}\n");
  177. $oPage->add('<form id="wiz_form" method="post">');
  178. $oStep->Display($oPage);
  179. // Add the back / next buttons and the hidden form
  180. // to store the parameters
  181. $oPage->add('<input type="hidden" id="_class" name="_class" value="'.get_class($oStep).'"/>');
  182. $oPage->add('<input type="hidden" id="_state" name="_state" value="'.$oStep->GetState().'"/>');
  183. foreach($this->aParameters as $sCode => $value)
  184. {
  185. $oPage->add('<input type="hidden" name="_params['.$sCode.']" value="'.htmlentities($value, ENT_QUOTES, 'UTF-8').'"/>');
  186. }
  187. $oPage->add('<input type="hidden" name="_steps" value="'.htmlentities(json_encode($this->aSteps), ENT_QUOTES, 'UTF-8').'"/>');
  188. $oPage->add('<table style="width:100%;"><tr>');
  189. if ((count($this->aSteps) > 0) && ($oStep->CanMoveBackward()))
  190. {
  191. $oPage->add('<td style="text-align: left"><button id="btn_back" type="submit" name="operation" value="back"> &lt;&lt; Back </button></td>');
  192. }
  193. if ($oStep->CanMoveForward())
  194. {
  195. $oPage->add('<td style="text-align:right;"><button id="btn_next" class="default" type="submit" name="operation" value="next">'.htmlentities($oStep->GetNextButtonLabel(), ENT_QUOTES, 'UTF-8').'</button></td>');
  196. }
  197. $oPage->add('</tr></table>');
  198. $oPage->add("</form>");
  199. $oPage->add('<div id="async_action" style="display:none;overflow:auto;max-height:100px;color:#F00;font-size:small;"></div>'); // The div may become visible in case of error
  200. // Hack to have the "Next >>" button, be the default button, since the first submit button in the form is the default one
  201. $oPage->add_ready_script(
  202. <<<EOF
  203. $('form').each(function () {
  204. var thisform = $(this);
  205. thisform.prepend(thisform.find('button.default').clone().removeAttr('id').removeAttr('disabled').css({
  206. position: 'absolute',
  207. left: '-999px',
  208. top: '-999px',
  209. height: 0,
  210. width: 0
  211. }));
  212. });
  213. $('#btn_back').click(function() { $('#wiz_form').data('back', true); });
  214. $('#wiz_form').submit(function() {
  215. if ($(this).data('back'))
  216. {
  217. return CanMoveBackward();
  218. }
  219. else
  220. {
  221. return CanMoveForward();
  222. }
  223. });
  224. $('#wiz_form').data('back', false);
  225. WizardUpdateButtons();
  226. EOF
  227. );
  228. $oPage->output();
  229. }
  230. /**
  231. * Make the wizard run: Start, Next or Back depending WizardUpdateButtons();
  232. on the page's parameters
  233. */
  234. public function Run()
  235. {
  236. $sOperation = utils::ReadParam('operation');
  237. $this->aParameters = utils::ReadParam('_params', array(), false, 'raw_data');
  238. $this->aSteps = json_decode(utils::ReadParam('_steps', '[]', false, 'raw_data'), true /* bAssoc */);
  239. switch($sOperation)
  240. {
  241. case 'next':
  242. $this->Next();
  243. break;
  244. case 'back':
  245. $this->Back();
  246. break;
  247. default:
  248. $this->Start();
  249. }
  250. }
  251. /**
  252. * Provides information about the structure/workflow of the wizard by listing
  253. * the possible list of 'steps' and their dependencies
  254. * @param string $sStep Name of the class to start from (used for recursion)
  255. * @param hash $aAllSteps List of steps (used for recursion)
  256. */
  257. public function DumpStructure($sStep = '', $aAllSteps = null)
  258. {
  259. if ($aAllSteps == null) $aAllSteps = array();
  260. if ($sStep == '') $sStep = $this->sInitialStepClass;
  261. $oStep = new $sStep($this, '');
  262. $aAllSteps[$sStep] = $oStep->GetPossibleSteps();
  263. foreach($aAllSteps[$sStep] as $sNextStep)
  264. {
  265. if (!array_key_exists($sNextStep, $aAllSteps))
  266. {
  267. $aAllSteps = $this->DumpStructure($sNextStep , $aAllSteps);
  268. }
  269. }
  270. return $aAllSteps;
  271. }
  272. /**
  273. * Dump the wizard's structure as a string suitable to produce a chart
  274. * using graphviz's "dot" program
  275. * @return string The 'dot' formatted output
  276. */
  277. public function DumpStructureAsDot()
  278. {
  279. $aAllSteps = $this->DumpStructure();
  280. $sOutput = "digraph finite_state_machine {\n";
  281. //$sOutput .= "\trankdir=LR;";
  282. $sOutput .= "\tsize=\"10,12\"\n";
  283. $aDeadEnds = array($this->sInitialStepClass);
  284. foreach($aAllSteps as $sStep => $aNextSteps)
  285. {
  286. if (count($aNextSteps) == 0)
  287. {
  288. $aDeadEnds[] = $sStep;
  289. }
  290. }
  291. $sOutput .= "\tnode [shape = doublecircle]; ".implode(' ', $aDeadEnds).";\n";
  292. $sOutput .= "\tnode [shape = box];\n";
  293. foreach($aAllSteps as $sStep => $aNextSteps)
  294. {
  295. $oStep = new $sStep($this, '');
  296. $sOutput .= "\t$sStep [ label = \"".$oStep->GetTitle()."\"];\n";
  297. if (count($aNextSteps) > 0)
  298. {
  299. foreach($aNextSteps as $sNextStep)
  300. {
  301. $sOutput .= "\t$sStep -> $sNextStep;\n";
  302. }
  303. }
  304. }
  305. $sOutput .= "}\n";
  306. return $sOutput;
  307. }
  308. }
  309. /**
  310. * Abstract class to build "steps" for the wizard controller
  311. * If a step needs to maintain an internal "state" (for complex steps)
  312. * then it's up to the derived class to implement the behavior based on
  313. * the internal 'sCurrentState' variable.
  314. * @copyright Copyright (C) 2010-2012 Combodo SARL
  315. * @license http://opensource.org/licenses/AGPL-3.0
  316. */
  317. abstract class WizardStep
  318. {
  319. /**
  320. * A reference to the WizardController
  321. * @var WizardController
  322. */
  323. protected $oWizard;
  324. /**
  325. * Current 'state' of the wizard step. Simple 'steps' can ignore it
  326. * @var string
  327. */
  328. protected $sCurrentState;
  329. public function __construct(WizardController $oWizard, $sCurrentState)
  330. {
  331. $this->oWizard = $oWizard;
  332. $this->sCurrentState = $sCurrentState;
  333. }
  334. public function GetState()
  335. {
  336. return $this->sCurrentState;
  337. }
  338. /**
  339. * Displays the wizard page for the current class/state
  340. * The page can contain any number of "<input/>" fields, but no "<form>...</form>" tag
  341. * The name of the input fields (and their id if one is supplied) MUST NOT start with "_"
  342. * (this is reserved for the wizard's own parameters)
  343. * @return void
  344. */
  345. abstract public function Display(WebPage $oPage);
  346. /**
  347. * Processes the page's parameters and (if moving forward) returns the next step/state to be displayed
  348. * @param bool $bMoveForward True if the wizard is moving forward 'Next >>' button pressed, false otherwise
  349. * @return hash array('class' => $sNextClass, 'state' => $sNextState)
  350. */
  351. abstract public function ProcessParams($bMoveForward = true);
  352. /**
  353. * Returns the list of possible steps from this step forward
  354. * @return array Array of strings (step classes)
  355. */
  356. abstract public function GetPossibleSteps();
  357. /**
  358. * Returns title of the current step
  359. * @return string The title of the wizard page for the current step
  360. */
  361. abstract public function GetTitle();
  362. /**
  363. * Tells whether the parameters are Ok to move forward
  364. * @return boolean True to move forward, false to stey on the same step
  365. */
  366. public function ValidateParams()
  367. {
  368. return true;
  369. }
  370. /**
  371. * Tells whether this step/state is the last one of the wizard (dead-end)
  372. * @return boolean True if the 'Next >>' button should be displayed
  373. */
  374. public function CanMoveForward()
  375. {
  376. return true;
  377. }
  378. /**
  379. * Tells whether the "Next" button should be enabled interactively
  380. * @return string A piece of javascript code returning either true or false
  381. */
  382. public function JSCanMoveForward()
  383. {
  384. return 'return true;';
  385. }
  386. /**
  387. * Returns the label for the " Next >> " button
  388. * @return string The label for the button
  389. */
  390. public function GetNextButtonLabel()
  391. {
  392. return ' Next >> ';
  393. }
  394. /**
  395. * Tells whether this step/state allows to go back or not
  396. * @return boolean True if the '<< Back' button should be displayed
  397. */
  398. public function CanMoveBackward()
  399. {
  400. return true;
  401. }
  402. /**
  403. * Tells whether the "Back" button should be enabled interactively
  404. * @return string A piece of javascript code returning either true or false
  405. */
  406. public function JSCanMoveBackward()
  407. {
  408. return 'return true;';
  409. }
  410. /**
  411. * Tells whether this step of the wizard requires that the configuration file be writable
  412. * @return bool True if the wizard will possibly need to modify the configuration at some point
  413. */
  414. public function RequiresWritableConfig()
  415. {
  416. return true;
  417. }
  418. /**
  419. * Overload this function to implement asynchronous action(s) (AJAX)
  420. * @param string $sCode The code of the action (if several actions need to be distinguished)
  421. * @param hash $aParameters The action's parameters name => value
  422. */
  423. public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
  424. {
  425. }
  426. }
  427. /*
  428. * Example of a simple Setup Wizard with some parameters to store
  429. * the installation mode (install | upgrade) and a simple asynchronous
  430. * (AJAX) action.
  431. *
  432. * The setup wizard is executed by the following code:
  433. *
  434. * $oWizard = new WizardController('Step1');
  435. * $oWizard->Run();
  436. *
  437. class Step1 extends WizardStep
  438. {
  439. public function GetTitle()
  440. {
  441. return 'Welcome';
  442. }
  443. public function GetPossibleSteps()
  444. {
  445. return array('Step2', 'Step2bis');
  446. }
  447. public function ProcessParams($bMoveForward = true)
  448. {
  449. $sNextStep = '';
  450. $sInstallMode = utils::ReadParam('install_mode');
  451. if ($sInstallMode == 'install')
  452. {
  453. $this->oWizard->SetParameter('install_mode', 'install');
  454. $sNextStep = 'Step2';
  455. }
  456. else
  457. {
  458. $this->oWizard->SetParameter('install_mode', 'upgrade');
  459. $sNextStep = 'Step2bis';
  460. }
  461. return array('class' => $sNextStep, 'state' => '');
  462. }
  463. public function Display(WebPage $oPage)
  464. {
  465. $oPage->p('This is Step 1!');
  466. $sInstallMode = $this->oWizard->GetParameter('install_mode', 'install');
  467. $sChecked = ($sInstallMode == 'install') ? ' checked ' : '';
  468. $oPage->p('<input type="radio" name="install_mode" value="install"'.$sChecked.'/> Install');
  469. $sChecked = ($sInstallMode == 'upgrade') ? ' checked ' : '';
  470. $oPage->p('<input type="radio" name="install_mode" value="upgrade"'.$sChecked.'/> Upgrade');
  471. }
  472. }
  473. class Step2 extends WizardStep
  474. {
  475. public function GetTitle()
  476. {
  477. return 'Installation Parameters';
  478. }
  479. public function GetPossibleSteps()
  480. {
  481. return array('Step3');
  482. }
  483. public function ProcessParams($bMoveForward = true)
  484. {
  485. return array('class' => 'Step3', 'state' => '');
  486. }
  487. public function Display(WebPage $oPage)
  488. {
  489. $oPage->p('This is Step 2! (Installation)');
  490. }
  491. }
  492. class Step2bis extends WizardStep
  493. {
  494. public function GetTitle()
  495. {
  496. return 'Upgrade Parameters';
  497. }
  498. public function GetPossibleSteps()
  499. {
  500. return array('Step2ter');
  501. }
  502. public function ProcessParams($bMoveForward = true)
  503. {
  504. $sUpgradeInfo = utils::ReadParam('upgrade_info');
  505. $this->oWizard->SetParameter('upgrade_info', $sUpgradeInfo);
  506. $sAdditionalUpgradeInfo = utils::ReadParam('additional_upgrade_info');
  507. $this->oWizard->SetParameter('additional_upgrade_info', $sAdditionalUpgradeInfo);
  508. return array('class' => 'Step2ter', 'state' => '');
  509. }
  510. public function Display(WebPage $oPage)
  511. {
  512. $oPage->p('This is Step 2bis! (Upgrade)');
  513. $sUpgradeInfo = $this->oWizard->GetParameter('upgrade_info', '');
  514. $oPage->p('Type your name here: <input type="text" id="upgrade_info" name="upgrade_info" value="'.$sUpgradeInfo.'" size="20"/><span id="v_upgrade_info"></span>');
  515. $sAdditionalUpgradeInfo = $this->oWizard->GetParameter('additional_upgrade_info', '');
  516. $oPage->p('The installer replies: <input type="text" name="additional_upgrade_info" value="'.$sAdditionalUpgradeInfo.'" size="20"/>');
  517. $oPage->add_ready_script("$('#upgrade_info').change(function() {
  518. $('#v_upgrade_info').html('<img src=\"../images/indicator.gif\"/>');
  519. WizardAsyncAction('', { upgrade_info: $('#upgrade_info').val() }); });");
  520. }
  521. public function AsyncAction(WebPage $oPage, $sCode, $aParameters)
  522. {
  523. usleep(300000); // 300 ms
  524. $sName = $aParameters['upgrade_info'];
  525. $sReply = addslashes("Hello ".$sName);
  526. $oPage->add_ready_script(
  527. <<<EOF
  528. $("#v_upgrade_info").html('');
  529. $("input[name=additional_upgrade_info]").val("$sReply");
  530. EOF
  531. );
  532. }
  533. }
  534. class Step2ter extends WizardStep
  535. {
  536. public function GetTitle()
  537. {
  538. return 'Additional Upgrade Info';
  539. }
  540. public function GetPossibleSteps()
  541. {
  542. return array('Step3');
  543. }
  544. public function ProcessParams($bMoveForward = true)
  545. {
  546. return array('class' => 'Step3', 'state' => '');
  547. }
  548. public function Display(WebPage $oPage)
  549. {
  550. $oPage->p('This is Step 2ter! (Upgrade)');
  551. }
  552. }
  553. class Step3 extends WizardStep
  554. {
  555. public function GetTitle()
  556. {
  557. return 'Installation Complete';
  558. }
  559. public function GetPossibleSteps()
  560. {
  561. return array();
  562. }
  563. public function ProcessParams($bMoveForward = true)
  564. {
  565. return array('class' => '', 'state' => '');
  566. }
  567. public function Display(WebPage $oPage)
  568. {
  569. $oPage->p('This is the FINAL Step');
  570. }
  571. public function CanMoveForward()
  572. {
  573. return false;
  574. }
  575. }
  576. End of the example */