diff --git a/setup/unattended-install/.htaccess b/setup/unattended-install/.htaccess
new file mode 100644
index 000000000..782472c78
--- /dev/null
+++ b/setup/unattended-install/.htaccess
@@ -0,0 +1,13 @@
+# Apache 2.4
+
+Require all denied
+
+
+# Apache 2.2
+
+deny from all
+Satisfy All
+
+
+# Apache 2.2 and 2.4
+IndexIgnore *
diff --git a/setup/unattended-install/README.md b/setup/unattended-install/README.md
new file mode 100644
index 000000000..d4ca4249b
--- /dev/null
+++ b/setup/unattended-install/README.md
@@ -0,0 +1,5 @@
+# Unattended-install
+
+This script allows to install and update iTop via CLI.
+
+For more information, see the official Wiki : [Automated installation [iTop Documentation]](https://www.itophub.io/wiki/page?id=latest:advancedtopics:automatic_install)
diff --git a/setup/unattended-install/unattended-install.php b/setup/unattended-install/unattended-install.php
new file mode 100644
index 000000000..09af751ec
--- /dev/null
+++ b/setup/unattended-install/unattended-install.php
@@ -0,0 +1,307 @@
+Get('mode');
+
+$sTargetEnvironment = $oParams->Get('target_env', '');
+if ($sTargetEnvironment == '')
+{
+ $sTargetEnvironment = 'production';
+}
+
+//unattended run based on db settings coming from response_file (XML file)
+$aDBXmlSettings = $oParams->Get('database', array());
+$sDBServer = $aDBXmlSettings['server'];
+$sDBUser = $aDBXmlSettings['user'];
+$sDBPwd = $aDBXmlSettings['pwd'];
+$sDBName = $aDBXmlSettings['name'];
+$sDBPrefix = $aDBXmlSettings['prefix'];
+
+if ($sMode == 'install')
+{
+ echo "Installation mode detected.\n";
+
+ $bClean = utils::ReadParam('clean', false, true /* CLI allowed */);
+ if ($bClean)
+ {
+ echo "Cleanup mode detected.\n";
+
+ // Configuration file
+ $sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE;
+ if (file_exists($sConfigFile))
+ {
+ echo "Trying to delete the configuration file: '$sConfigFile'.\n";
+ @chmod($sConfigFile, 0770); // RWX for owner and group, nothing for others
+ unlink($sConfigFile);
+ }
+ else
+ {
+ echo "No config file to delete ($sConfigFile does not exist).\n";
+ }
+
+ // Starting with iTop 2.7.0, a failed setup leaves some lock files, let's remove them
+ $aLockFiles = array(
+ 'data/.readonly' => 'read-only lock file',
+ 'data/.maintenance' => 'maintenance mode lock file',
+ );
+ foreach($aLockFiles as $sFile => $sDescription)
+ {
+ $sLockFile = APPROOT.$sFile;
+ if (file_exists($sLockFile))
+ {
+ echo "Trying to delete the $sDescription: '$sLockFile'.\n";
+ unlink($sLockFile);
+ }
+ }
+
+ // Starting with iTop 2.6.0, let's remove the cache directory as well
+ // Can cause some strange issues in the setup (apparently due to the Dict class being automatically loaded ??)
+ $sCacheDir = APPROOT.'data/cache-'.$sTargetEnvironment;
+ if (file_exists($sCacheDir))
+ {
+ if (is_dir($sCacheDir))
+ {
+ echo "Emptying the cache directory '$sCacheDir'.\n";
+ SetupUtils::tidydir($sCacheDir);
+ }
+ else
+ {
+ die("ERROR the cache directory '$sCacheDir' exists, but is NOT a directory !!!\nExiting.\n");
+ }
+ }
+
+ // env-xxx directory
+ $sTargetDir = APPROOT.'env-'.$sTargetEnvironment;
+ if (file_exists($sTargetDir))
+ {
+ if (is_dir($sTargetDir))
+ {
+ echo "Emptying the target directory '$sTargetDir'.\n";
+ SetupUtils::tidydir($sTargetDir);
+ }
+ else
+ {
+ die("ERROR the target dir '$sTargetDir' exists, but is NOT a directory !!!\nExiting.\n");
+ }
+ }
+ else
+ {
+ echo "No target directory to delete ($sTargetDir does not exist).\n";
+ }
+
+ if ($sDBPrefix != '')
+ {
+ die("Cleanup not implemented for a partial database (prefix= '$sDBPrefix')\nExiting.");
+ }
+
+ $oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd);
+ if ($oMysqli->connect_errno)
+ {
+ die("Cannot connect to the MySQL server (".$oMysqli->connect_errno . ") ".$oMysqli->connect_error."\nExiting");
+ }
+ else
+ {
+ if ($oMysqli->select_db($sDBName))
+ {
+ echo "Deleting database '$sDBName'\n";
+ $oMysqli->query("DROP DATABASE `$sDBName`");
+ }
+ else
+ {
+ echo "The database '$sDBName' does not seem to exist. Nothing to cleanup.\n";
+ }
+ }
+ }
+}
+else
+{
+ //use settings from itop conf
+ $sTargetEnvironment = $oParams->Get('target_env', '');
+ if ($sTargetEnvironment == '')
+ {
+ $sTargetEnvironment = 'production';
+ }
+ $sTargetDir = APPROOT.'env-'.$sTargetEnvironment;
+}
+
+$bHasErrors = false;
+$aChecks = SetupUtils::CheckBackupPrerequisites(APPROOT.'data'); // mmm should be the backup destination dir
+
+$aSelectedModules = $oParams->Get('selected_modules');
+$sSourceDir = $oParams->Get('source_dir', 'datamodels/latest');
+$sExtensionDir = $oParams->Get('extensions_dir', 'extensions');
+$aChecks = array_merge($aChecks, SetupUtils::CheckSelectedModules($sSourceDir, $sExtensionDir, $aSelectedModules));
+
+foreach($aChecks as $oCheckResult)
+{
+ switch ($oCheckResult->iSeverity)
+ {
+ case CheckResult::ERROR:
+ $bHasErrors = true;
+ $sHeader = "Error";
+ break;
+
+ case CheckResult::WARNING:
+ $sHeader = "Warning";
+ break;
+
+ case 3: // CheckResult::TRACE added in iTop 3.0.0
+ // does nothing : those are old debug traces, see N°2214
+ $sHeader = 'Trace';
+ break;
+
+ case CheckResult::INFO:
+ default:
+ $sHeader = "Info";
+ break;
+ }
+ echo $sHeader.": ".$oCheckResult->sLabel;
+ if (strlen($oCheckResult->sDescription))
+ {
+ echo ' - '.$oCheckResult->sDescription;
+ }
+ echo "\n";
+}
+
+if ($bHasErrors)
+{
+ echo "Encountered stopper issues. Aborting...\n";
+ $sLogMsg = "Encountered stopper issues. Aborting...";
+ echo "$sLogMsg\n";
+ SetupLog::Error($sLogMsg);
+ die;
+}
+
+$bFoundIssues = false;
+
+$bInstall = utils::ReadParam('install', true, true /* CLI allowed */);
+if ($bInstall)
+{
+ echo "Starting the unattended installation...\n";
+ $oWizard = new ApplicationInstaller($oParams);
+ $bRes = $oWizard->ExecuteAllSteps();
+ if (!$bRes)
+ {
+ echo "\nencountered installation issues!";
+ $bFoundIssues = true;
+ }
+ else
+ {
+ $oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd);
+ if (!$oMysqli->connect_errno)
+ {
+ if ($oMysqli->select_db($sDBName))
+ {
+ // Check the presence of a table to record information about the MTP (from the Designer)
+ $sDesignerUpdatesTable = $sDBPrefix.'priv_designer_update';
+ $sSQL = "SELECT id FROM `$sDesignerUpdatesTable`";
+ if ($oMysqli->query($sSQL) !== false)
+ {
+ // Record the Designer Udpates in the priv_designer_update table
+ $sDeltaFile = APPROOT.'data/'.$sTargetEnvironment.'.delta.xml';
+ if (is_readable($sDeltaFile))
+ {
+ // Retrieve the revision
+ $oDoc = new DOMDocument();
+ $oDoc->load($sDeltaFile);
+ $iRevision = 0;
+ $iRevision = $oDoc->firstChild->getAttribute('revision_id');
+ if ($iRevision > 0) // Safety net, just in case...
+ {
+ $sDate = date('Y-m-d H:i:s');
+ $sSQL = "INSERT INTO `$sDesignerUpdatesTable` (revision_id, compilation_date, comment) VALUES ($iRevision, '$sDate', 'Deployed using unattended.php.')";
+ if ($oMysqli->query($sSQL) !== false)
+ {
+ echo "\nDesigner update (MTP at revision $iRevision) successfully recorded.\n";
+ }
+ else
+ {
+ echo "\nFailed to record designer updates(".$oMysqli->error.").\n";
+ }
+ }
+ else
+ {
+ echo "\nFailed to read the revision from $sDeltaFile file. No designer update information will be recorded.\n";
+
+ }
+ }
+ else
+ {
+ echo "\nNo $sDeltaFile file (or the file is not accessible). No designer update information to record.\n";
+ }
+ }
+ }
+ }
+ }
+}
+else
+{
+ echo "No installation requested.\n";
+}
+if (!$bFoundIssues && $bCheckConsistency)
+{
+ echo "Checking data model consistency.\n";
+ ob_start();
+ $sCheckRes = '';
+ try
+ {
+ MetaModel::CheckDefinitions(false);
+ $sCheckRes = ob_get_clean();
+ }
+ catch(Exception $e)
+ {
+ $sCheckRes = ob_get_clean()."\nException: ".$e->getMessage();
+ }
+ if (strlen($sCheckRes) > 0)
+ {
+ echo $sCheckRes;
+ echo "\nfound consistency issues!";
+ $bFoundIssues = true;
+ }
+}
+
+if (!$bFoundIssues)
+{
+ // last line: used to check the install
+ // the only way to track issues in case of Fatal error or even parsing error!
+ $sLogMsg = "installed!";
+ SetupLog::Info($sLogMsg);
+ echo "\n$sLogMsg";
+ exit(0);
+}
+
+$sLogMsg = "installation failed!";
+SetupLog::Error($sLogMsg);
+echo "\n$sLogMsg";
+exit(-1);
diff --git a/setup/unattended-install/web.config b/setup/unattended-install/web.config
new file mode 100644
index 000000000..58c9c3ac3
--- /dev/null
+++ b/setup/unattended-install/web.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php
new file mode 100644
index 000000000..ee4bdd1d3
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/setup/unattended-install/UnattendedInstallTest.php
@@ -0,0 +1,71 @@
+Get('app_root_url');
+ curl_setopt($ch, CURLOPT_URL, "$sUrl/setup/unattended-install/unattended-install.php");
+ curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
+ curl_setopt($ch, CURLOPT_POSTFIELDS, []);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ // Force disable of certificate check as most of dev / test env have a self-signed certificate
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+
+ $sJson = curl_exec($ch);
+ curl_close ($ch);
+ return $sJson;
+ }
+ public function testCallUnattendedInstallFromHttp(){
+ $sJson = $this->callUnattendedFromHttp();
+ if (false !== strpos($sJson, "403 Forbidden")){
+ //.htaccess / webconfig effect
+ $aFiles = [
+ 'web.config',
+ '.htaccess',
+ ];
+ foreach ($aFiles as $sFile){
+ $sPath = APPROOT."setup/unattended-install/$sFile";
+ if (is_file("$sPath")) {
+ rename($sPath, "$sPath.back");
+ }
+ }
+
+ $sJson = $this->callUnattendedFromHttp();
+ }
+
+ $this->assertEquals("Mode CLI only", $sJson, "even without HTTP protection, script should NOT be called directly by HTTP");
+ }
+
+ public function testCallUnattendedInstallFromCLI() {
+ $cliPath = realpath(APPROOT."/setup/unattended-install/unattended-install.php");
+ $res = exec("php ".$cliPath);
+
+ $this->assertEquals("Param file `default-params.xml` doesn't exist ! Exiting...", $res);
+ }
+}