diff --git a/datamodels/2.x/installation.xml b/datamodels/2.x/installation.xml index 4decbfe83..dd61c6464 100755 --- a/datamodels/2.x/installation.xml +++ b/datamodels/2.x/installation.xml @@ -16,7 +16,10 @@ itop-profiles-itil itop-welcome-itil itop-tickets - itop-hub-connector + itop-files-information + itop-twig-base + combodo-db-tools + itop-core-update true diff --git a/datamodels/2.x/itop-core-update/README.md b/datamodels/2.x/itop-core-update/README.md new file mode 100644 index 000000000..59420f0cd --- /dev/null +++ b/datamodels/2.x/itop-core-update/README.md @@ -0,0 +1,6 @@ +# itop-core-update +Application upgrade + +This feature allows administrators to upgrade the application from a downloaded package. + +namespace Combodo\iTop\CoreUpdate; diff --git a/datamodels/2.x/itop-core-update/ajax.php b/datamodels/2.x/itop-core-update/ajax.php new file mode 100644 index 000000000..97f03b61e --- /dev/null +++ b/datamodels/2.x/itop-core-update/ajax.php @@ -0,0 +1,21 @@ +DisableInDemoMode(); +$oUpdateController->AllowOnlyAdmin(); + +// Allow parallel execution of ajax requests +session_write_close(); +$oUpdateController->HandleOperation(); diff --git a/datamodels/2.x/itop-core-update/datamodel.itop-core-update.xml b/datamodels/2.x/itop-core-update/datamodel.itop-core-update.xml new file mode 100644 index 000000000..dc1474eac --- /dev/null +++ b/datamodels/2.x/itop-core-update/datamodel.itop-core-update.xml @@ -0,0 +1,11 @@ + + + + + 60 + System + $pages/exec.php?exec_module=itop-core-update&exec_page=index.php&c[menu]=iTopUpdate&maintenance=true + 1 + + + diff --git a/datamodels/2.x/itop-core-update/en.dict.itop-core-update.php b/datamodels/2.x/itop-core-update/en.dict.itop-core-update.php new file mode 100644 index 000000000..fd36e98ad --- /dev/null +++ b/datamodels/2.x/itop-core-update/en.dict.itop-core-update.php @@ -0,0 +1,108 @@ + + */ + +Dict::Add('EN US', 'English', 'English', array( + 'iTopUpdate:UI:PageTitle' => 'Application Upgrade', + 'itop-core-update:UI:SelectUpdateFile' => 'Application Upgrade', + 'itop-core-update:UI:ConfirmUpdate' => 'Application Upgrade', + 'itop-core-update:UI:UpdateCoreFiles' => 'Application Upgrade', + 'itop-core-update:UI:UpdateDone' => 'Application Upgrade', + + 'iTopUpdate:UI:SelectUpdateFile' => 'Select an upgrade file to upload', + 'iTopUpdate:UI:CheckUpdate' => 'Verify upgrade file', + 'iTopUpdate:UI:ConfirmInstallFile' => 'You are about to install %1$s', + 'iTopUpdate:UI:DoUpdate' => 'Upgrade', + 'iTopUpdate:UI:CurrentVersion' => 'Current installed version', + 'iTopUpdate:UI:Back' => 'Back', + 'iTopUpdate:UI:Cancel' => 'Cancel', + 'iTopUpdate:UI:Continue' => 'Continue', + 'iTopUpdate:UI:WithDBBackup' => 'Database backup', + 'iTopUpdate:UI:WithFilesBackup' => 'Application files backup', + 'iTopUpdate:UI:WithoutBackup' => 'No backup is planned', + 'iTopUpdate:UI:Backup' => 'Backup generated before update', + 'iTopUpdate:UI:DoFilesArchive' => 'Archive application files', + 'iTopUpdate:UI:UploadArchive' => 'Select a package to upload', + 'iTopUpdate:UI:ServerFile' => 'Path of a package already on the server', + + 'iTopUpdate:UI:Status' => 'Status', + 'iTopUpdate:UI:Action' => 'Update', + 'iTopUpdate:UI:History' => 'Versions History', + 'iTopUpdate:UI:Progress' => 'Progress of the upgrade', + + 'iTopUpdate:UI:DoBackup:Label' => 'Backup files and database', + 'iTopUpdate:UI:DoBackup:Warning' => 'Backup is not recommended due to limited available disk space', + + 'iTopUpdate:UI:DiskFreeSpace' => 'Disk free space', + 'iTopUpdate:UI:ItopDiskSpace' => 'iTop disk space', + 'iTopUpdate:UI:DBDiskSpace' => 'Database disk space', + 'iTopUpdate:UI:FileUploadMaxSize' => 'File upload max size', + + 'iTopUpdate:UI:PostMaxSize' => 'PHP ini value post_max_size: %1$s', + 'iTopUpdate:UI:UploadMaxFileSize' => 'PHP ini value upload_max_filesize: %1$s', + + 'iTopUpdate:UI:CanCoreUpdate:Loading' => 'Checking filesystem', + 'iTopUpdate:UI:CanCoreUpdate:Error' => 'Checking filesystem failed (%1$s)', + 'iTopUpdate:UI:CanCoreUpdate:ErrorFileNotExist' => 'Checking filesystem failed (File not exist %1$s)', + 'iTopUpdate:UI:CanCoreUpdate:Failed' => 'Checking filesystem failed', + 'iTopUpdate:UI:CanCoreUpdate:Yes' => 'Application can be updated', + 'iTopUpdate:UI:CanCoreUpdate:No' => 'Application cannot be updated: %1$s', + + // Setup Messages + 'iTopUpdate:UI:SetupMessage:Ready' => 'Ready to start', + 'iTopUpdate:UI:SetupMessage:EnterMaintenance' => 'Entering maintenance mode', + 'iTopUpdate:UI:SetupMessage:Backup' => 'Database backup', + 'iTopUpdate:UI:SetupMessage:FilesArchive' => 'Archive application files', + 'iTopUpdate:UI:SetupMessage:CopyFiles' => 'Copy new version files', + 'iTopUpdate:UI:SetupMessage:Compile' => 'Upgrade application and database', + 'iTopUpdate:UI:SetupMessage:ExitMaintenance' => 'Exiting maintenance mode', + 'iTopUpdate:UI:SetupMessage:UpdateDone' => 'Upgrade completed', + + // Errors + 'iTopUpdate:Error:MissingFunction' => 'Impossible to start upgrade, missing function', + 'iTopUpdate:Error:MissingFile' => 'Missing file: %1$s', + 'iTopUpdate:Error:CorruptedFile' => 'File %1$s is corrupted', + 'iTopUpdate:Error:BadFileFormat' => 'Upgrade file is not a zip file', + 'iTopUpdate:Error:BadFileContent' => 'Upgrade file is not an application archive', + 'iTopUpdate:Error:BadItopProduct' => 'Upgrade file is not compatible with your application', + 'iTopUpdate:Error:Copy' => 'Error, cannot copy \'%1$s\' to \'%2$s\'', + 'iTopUpdate:Error:FileNotFound' => 'File not found', + 'iTopUpdate:Error:NoFile' => 'No file provided', + 'iTopUpdate:Error:InvalidToken' => 'Invalid token', + 'iTopUpdate:Error:UpdateFailed' => 'Upgrade failed ', + 'iTopUpdate:Error:FileUploadMaxSizeTooSmall' => 'The upload max size seems too small for update. Please change the PHP configuration.', + + 'iTopUpdate:UI:RestoreArchive' => 'You can restore your application from the archive \'%1$s\'', + 'iTopUpdate:UI:RestoreBackup' => 'You can restore the database from \'%1$s\'', + 'iTopUpdate:UI:MaintenanceModeActive' => 'The application is currently under maintenance, no user can access the application. You have to run a setup or restore the application archive to return in normal mode.', + 'iTopUpdate:UI:UpdateDone' => 'Upgrade successful', + 'Menu:iTopUpdate' => 'Application Upgrade', + 'Menu:iTopUpdate+' => 'Application Upgrade', + + // Missing itop entries + 'Class:ModuleInstallation/Attribute:installed' => 'Installed on', + 'Class:ModuleInstallation/Attribute:name' => 'Name', + 'Class:ModuleInstallation/Attribute:version' => 'Version', + 'Class:ModuleInstallation/Attribute:comment' => 'Comment', +)); + + diff --git a/datamodels/2.x/itop-core-update/fr.dict.itop-core-update.php b/datamodels/2.x/itop-core-update/fr.dict.itop-core-update.php new file mode 100644 index 000000000..7ef3a679f --- /dev/null +++ b/datamodels/2.x/itop-core-update/fr.dict.itop-core-update.php @@ -0,0 +1,108 @@ + + */ + +Dict::Add('FR FR', 'French', 'Français', array( + 'iTopUpdate:UI:PageTitle' => 'Mise à jour de l\'application', + 'itop-core-update:UI:SelectUpdateFile' => 'Mise à jour', + 'itop-core-update:UI:ConfirmUpdate' => 'Mise à jour', + 'itop-core-update:UI:UpdateCoreFiles' => 'Mise à jour', + 'iTopUpdate:UI:SelectUpdateFile' => 'Sélectionner un fichier de mise à jour', + 'iTopUpdate:UI:CheckUpdate' => 'Vérification de la mise à jour', + 'iTopUpdate:UI:ConfirmInstallFile' => 'La mise à jour %1$s va être installée', + 'iTopUpdate:UI:DoUpdate' => 'Mettre à jour', + 'iTopUpdate:UI:DoBackup' => 'Faire une sauvegarde des fichiers et de la base', + 'iTopUpdate:UI:CurrentVersion' => 'Version installée', + 'iTopUpdate:UI:Back' => 'Annuler', + 'iTopUpdate:UI:WithBackup' => 'Avec sauvegarde de l\'application, l\'archive sera dans \'%1$s\'', + 'iTopUpdate:UI:WithoutBackup' => 'Pas de sauvegarde', + 'iTopUpdate:UI:Status' => 'Versions installées', + 'iTopUpdate:UI:InstallationCanBeUpdated' => 'L\'application peut être mise à jour', + 'iTopUpdate:UI:InstallationCanNotBeUpdated' => 'L\'application ne peut pas être mise à jour', + 'iTopUpdate:UI:DiskFreeSpace' => 'Taille disque disponible', + 'iTopUpdate:UI:ItopDiskSpace' => 'Taille disque utilisée par l\'application', + 'iTopUpdate:UI:DBDiskSpace' => 'Taille disque utilisée par la base de données', + 'iTopUpdate:UI:FileUploadMaxSize' => 'Taille maximale de chargement de fichier', + + 'iTopUpdate:UI:PostMaxSize' => 'Valeur PHP ini post_max_size : %1$s', + 'iTopUpdate:UI:UploadMaxFileSize' => 'Valeur PHP ini upload_max_filesize : %1$s', + + 'iTopUpdate:UI:Cancel' => 'Annuler', + 'iTopUpdate:UI:Continue' => 'Continuer', + 'iTopUpdate:UI:WithDBBackup' => 'Sauvegarde de la base de données', + 'iTopUpdate:UI:WithFilesBackup' => 'Archive des fichiers de l\'application' , + 'iTopUpdate:UI:Backup' => 'Sauvegarde effectuée avant la mise à jour', + 'iTopUpdate:UI:DoFilesArchive' => 'Archive les fichiers de l\'application', + + 'iTopUpdate:UI:Action' => 'Mettre à jour', + 'iTopUpdate:UI:History' => 'Historique des versions', + 'iTopUpdate:UI:Progress' => 'Progression de la mise à jour', + 'iTopUpdate:UI:DoBackup:Label' => 'Sauvegarde de la base de données', + 'iTopUpdate:UI:DoBackup:Warning' => 'La sauvegarde n\'est pas conseillée à cause du manque de place disque disponible', + 'iTopUpdate:UI:CanCoreUpdate:Loading' => 'Vérification des fichiers', + 'iTopUpdate:UI:CanCoreUpdate:Error' => 'Échec de la vérification des fichiers (%1$s)', + 'iTopUpdate:UI:CanCoreUpdate:ErrorFileNotExist' => 'Échec de la vérification des fichiers (Fichier manquant %1$s)', + 'iTopUpdate:UI:CanCoreUpdate:Failed' => 'Échec de la vérification des fichiers', + 'iTopUpdate:UI:CanCoreUpdate:Yes' => 'L\'application peut être mise à jour', + 'iTopUpdate:UI:CanCoreUpdate:No' => 'L\'application ne peut pas être mise à jour : %1$s', + + // Setup Messages + 'iTopUpdate:UI:SetupMessage:Ready' => 'Prêt pour l\installation', + 'iTopUpdate:UI:SetupMessage:EnterMaintenance' => 'Application en maintenance', + 'iTopUpdate:UI:SetupMessage:Backup' => 'Sauvegarde des fichiers de l\'application', + 'iTopUpdate:UI:SetupMessage:FilesArchive' => 'Archivage des fichiers de l\'application', + 'iTopUpdate:UI:SetupMessage:CopyFiles' => 'Copie des fichiers de la nouvelle version', + 'iTopUpdate:UI:SetupMessage:Compile' => 'Mise à jour de l\'application et de la base de données', + 'iTopUpdate:UI:SetupMessage:ExitMaintenance' => 'Application en utilisation normale', + 'iTopUpdate:UI:SetupMessage:UpdateDone' => 'Installation terminée', + + // Errors + 'iTopUpdate:Error:MissingFunction' => 'Impossible de mettre à jour', + 'iTopUpdate:Error:MissingFile' => 'Ficher manquant : %1$s', + 'iTopUpdate:Error:CorruptedFile' => 'Le fichier %1$s est corrompu', + 'iTopUpdate:Error:BadFileFormat' => 'Le fichier de mise à jour n\'est pas au format "zip"' , + 'iTopUpdate:Error:Copy' => 'Erreur : impossible de copier le fichier \'%1$s\' dans \'%2$s\'', + 'iTopUpdate:Error:FileNotFound' => 'Fichier manquant', + 'iTopUpdate:Error:InvalidToken' => 'Information manquante', + 'iTopUpdate:Error:NoUpdate' => 'La mise à jour a échoué', + 'iTopUpdate:Error:FileUploadMaxSizeTooSmall' => 'La taille maximale de chargement de fichier semble trop petite pour faire la mise à jour. Veuillez changer la configuration de PHP.', + + 'iTopUpdate:UI:RestoreArchive' => 'Vous pouvez restaurer l\'application depuis \'%1$s\'', + 'iTopUpdate:UI:RestoreBackup' => 'Vous pouvez restaurer la base de données depuis \'%1$s\'', + 'iTopUpdate:UI:MaintenanceModeActive' => 'L\'application est actuellement en maintenance, aucun utilisateur n\'a accès à l\'application. Vous pouvez lancer un setup ou réinstaller l\'application depuis une archive pour retourner dans un mode normal.', + + 'iTopUpdate:UI:UpdateDone' => 'Mise à jour effectuée', + 'Menu:iTopUpdate' => 'Mise à jour de l\'application', + 'Menu:iTopUpdate+' => 'Mise à jour de l\'application', + 'iTopUpdate:Error:BadFileContent' => 'Le fichier n\'est pas une archive valide', + 'iTopUpdate:Error:BadItopProduct' => 'L\'archive n\'est pas compatible avec votre application', + 'iTopUpdate:Error:NoFile' => 'Pas d\'archive', + 'iTopUpdate:Error:UpdateFailed' => 'La mise à jour à échoué ', + + // Missing itop entries + 'Class:ModuleInstallation/Attribute:installed' => 'Installé le', + 'Class:ModuleInstallation/Attribute:name' => 'Nom', + 'Class:ModuleInstallation/Attribute:version' => 'Version', + 'Class:ModuleInstallation/Attribute:comment' => 'Commentaire', +)); + + diff --git a/datamodels/2.x/itop-core-update/index.php b/datamodels/2.x/itop-core-update/index.php new file mode 100644 index 000000000..91bf6212c --- /dev/null +++ b/datamodels/2.x/itop-core-update/index.php @@ -0,0 +1,19 @@ +DisableInDemoMode(); +$oUpdateController->AllowOnlyAdmin(); +$oUpdateController->SetDefaultOperation('SelectUpdateFile'); +$oUpdateController->HandleOperation(); diff --git a/datamodels/2.x/itop-core-update/module.itop-core-update.php b/datamodels/2.x/itop-core-update/module.itop-core-update.php new file mode 100644 index 000000000..ed043339a --- /dev/null +++ b/datamodels/2.x/itop-core-update/module.itop-core-update.php @@ -0,0 +1,52 @@ + 'iTop Core Update', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + 'itop-twig-base/1.0.0', + 'itop-files-information/1.0.0', + 'combodo-db-tools/1.0.8', + ), + 'mandatory' => false, + 'visible' => true, + + // Components + // + 'datamodel' => array( + 'model.itop-core-update.php', + 'src/Service/RunTimeEnvironmentCoreUpdater.php', + 'src/Service/CoreUpdater.php', + 'src/Controller/UpdateController.php', + 'src/Controller/AjaxController.php', + ), + 'webservice' => array(), + 'data.struct' => array(), + 'data.sample' => array(), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array(), + ) +); diff --git a/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php b/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php new file mode 100644 index 000000000..38670804c --- /dev/null +++ b/datamodels/2.x/itop-core-update/src/Controller/AjaxController.php @@ -0,0 +1,184 @@ +getMessage()); + } + catch(Exception $e) + { + $aParams['bStatus'] = false; + $aParams['sMessage'] = Dict::Format('iTopUpdate:UI:CanCoreUpdate:Error', $e->getMessage()); + } + + $this->DisplayJSONPage($aParams); + } + + public function OperationGetItopDiskSpace() + { + $aParams = array(); + $aParams['iItopDiskSpace'] = FilesInformation::GetItopDiskSpace(); + $aParams['sItopDiskSpace'] = utils::BytesToFriendlyFormat($aParams['iItopDiskSpace']); + $this->DisplayJSONPage($aParams); + } + + public function OperationGetDBDiskSpace() + { + $aParams = array(); + $aParams['iDBDiskSpace'] = DBToolsUtils::GetDatabaseSize(); + $aParams['sDBDiskSpace'] = utils::BytesToFriendlyFormat($aParams['iDBDiskSpace']); + $this->DisplayJSONPage($aParams); + } + + public function OperationGetCurrentVersion() + { + $aParams = array(); + $aParams['sVersion'] = Dict::Format('UI:iTopVersion:Long', ITOP_APPLICATION, ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE); + $this->DisplayJSONPage($aParams); + } + + public function OperationEnterMaintenance() + { + $aParams = array(); + try + { + SetupUtils::EnterReadOnlyMode(MetaModel::GetConfig()); + $iResponseCode = 200; + } + catch (Exception $e) + { + IssueLog::Error("EnterMaintenance: ".$e->getMessage()); + $aParams['sError'] = $e->getMessage(); + $iResponseCode = 500; + } + $this->DisplayJSONPage($aParams, $iResponseCode); + } + + public function OperationExitMaintenance() + { + $aParams = array(); + try + { + SetupUtils::ExitReadOnlyMode(); + $iResponseCode = 200; + } + catch (Exception $e) + { + IssueLog::Error("ExitMaintenance: ".$e->getMessage()); + $aParams['sError'] = $e->getMessage(); + $iResponseCode = 500; + } + $this->DisplayJSONPage($aParams, $iResponseCode); + } + + public function OperationBackup() + { + $aParams = array(); + try + { + CoreUpdater::Backup(); + $iResponseCode = 200; + } + catch (Exception $e) + { + IssueLog::Error("Backup: ".$e->getMessage()); + $aParams['sError'] = $e->getMessage(); + $iResponseCode = 500; + } + $this->DisplayJSONPage($aParams, $iResponseCode); + } + + public function OperationFilesArchive() + { + $aParams = array(); + try + { + CoreUpdater::CreateItopArchive(); + $iResponseCode = 200; + } + catch (Exception $e) + { + IssueLog::Error("FilesArchive: ".$e->getMessage()); + $aParams['sError'] = $e->getMessage(); + $iResponseCode = 500; + } + $this->DisplayJSONPage($aParams, $iResponseCode); + } + + public function OperationCopyFiles() + { + $aParams = array(); + try + { + CoreUpdater::CopyCoreFiles(); + $iResponseCode = 200; + } + catch (Exception $e) + { + IssueLog::Error("CopyFiles: ".$e->getMessage()); + $aParams['sError'] = $e->getMessage(); + $iResponseCode = 500; + } + + $this->DisplayJSONPage($aParams, $iResponseCode); + } + + public function OperationCompile() + { + $aParams = array(); + try + { + CoreUpdater::Compile(); + $iResponseCode = 200; + } + catch (Exception $e) + { + IssueLog::Error("Compile: ".$e->getMessage()); + $aParams['sError'] = $e->getMessage(); + $iResponseCode = 500; + } + + $this->DisplayJSONPage($aParams, $iResponseCode); + } +} diff --git a/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php b/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php new file mode 100644 index 000000000..3b4174a29 --- /dev/null +++ b/datamodels/2.x/itop-core-update/src/Controller/UpdateController.php @@ -0,0 +1,190 @@ +GetPreviousInstallations(); + $aParams['sAjaxURL'] = utils::GetAbsoluteUrlModulePage('itop-core-update', 'ajax.php', array('maintenance' => 'true')); + $aParams['iDiskFreeSpace'] = disk_free_space(APPROOT); + $aParams['sDiskFreeSpace'] = utils::BytesToFriendlyFormat($aParams['iDiskFreeSpace']); + $aParams['iFileUploadMaxSize'] = $this->GetFileUploadMaxSize(); + $aParams['sFileUploadMaxSize'] = utils::BytesToFriendlyFormat($aParams['iFileUploadMaxSize']); + $aParams['sPostMaxSize'] = ini_get('post_max_size'); + $aParams['sUploadMaxSize'] = ini_get('upload_max_filesize'); + + $this->DisplayPage($aParams); + } + + /** + * @throws \Exception + */ + public function OperationConfirmUpdate() + { + $sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id'); + if (!utils::IsTransactionValid($sTransactionId)) + { + throw new Exception(Dict::S('iTopUpdate:Error:InvalidToken')); + } + + $bDoBackup = utils::ReadPostedParam('doBackup', 0, 'integer') == 1; + $bDoFilesArchive = utils::ReadPostedParam('doFilesArchive', 0, 'integer') == 1; + + $sNewVersion = null; + $sName = ''; + $sVersionToInstall = ''; + $sError = ''; + try + { + if (isset($_FILES['file'])) + { + $aFileInfo = $_FILES['file']; + $iError = $aFileInfo['error']; + if ($iError === UPLOAD_ERR_OK) + { + $sDownloadDir = CoreUpdater::DOWNLOAD_DIR; + if (is_dir($sDownloadDir)) + { + SetupUtils::rrmdir($sDownloadDir); + } + SetupUtils::builddir($sDownloadDir); + $sTmpName = $aFileInfo['tmp_name']; + $sName = $aFileInfo['name']; + $sNewVersion = $sDownloadDir.$sName; + if (@move_uploaded_file($sTmpName, $sNewVersion) === false) + { + throw new Exception(Dict::S('iTopUpdate:Error:FileNotFound')); + } + CoreUpdater::ExtractDownloadedFile($sNewVersion); + $sVersionToInstall = CoreUpdater::GetVersionToInstall(); + } + else + { + throw new Exception(Dict::S('iTopUpdate:Error:NoFile')); + } + } + else + { + throw new Exception(Dict::S('iTopUpdate:Error:NoFile')); + } + } + catch (Exception $e) + { + $iError = UPLOAD_ERR_NO_FILE; + $sError = $e->getMessage(); + } + + + $aParams = array(); + $aParams['sName'] = $sName; + $aParams['bSuccess'] = ($iError == 0); + $aParams['sError'] = $sError; + + $aParams['bDoBackup'] = $bDoBackup; + $aParams['bDoFilesArchive'] = $bDoFilesArchive; + $aParams['sItopArchive'] = CoreUpdater::GetItopArchiveFile(); + $aParams['sBackupFile'] = CoreUpdater::GetBackupFile(); + + $sQuestion = Dict::Format('iTopUpdate:UI:ConfirmInstallFile', $sVersionToInstall); + $aParams['sQuestion'] = $sQuestion; + + $sTransactionId = utils::GetNewTransactionId(); + $aParams['sTransactionId'] = $sTransactionId; + + $this->DisplayPage($aParams); + } + + public function OperationUpdateCoreFiles() + { + $sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id'); + if (!utils::IsTransactionValid($sTransactionId)) + { + throw new Exception(Dict::S('iTopUpdate:Error:InvalidToken')); + } + + $sNewVersion = utils::ReadPostedParam('filename', null, 'filename'); + $bDoBackup = utils::ReadPostedParam('doBackup', 0, 'integer') == 1; + $bDoFilesArchive = utils::ReadPostedParam('doFilesArchive', 0, 'integer') == 1; + $sCurrentVersion = Dict::Format('UI:iTopVersion:Long', ITOP_APPLICATION, ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE); + + $aParams = array( + 'sCurrentVersion' => $sCurrentVersion, + 'bDoBackup' => $bDoBackup, + 'sBackupFile' => CoreUpdater::GetBackupFile(), + 'bDoFilesArchive' => $bDoFilesArchive, + 'sItopArchive' => CoreUpdater::GetItopArchiveFile(), + 'sNewVersion' => $sNewVersion, + 'sProgressImage' => utils::GetAbsoluteUrlAppRoot().'setup/orange-progress.gif', + 'sSetupToken' => SetupUtils::CreateSetupToken(), + 'sAjaxURL' => utils::GetAbsoluteUrlModulePage('itop-core-update', 'ajax.php', array('maintenance' => 'true')), + ); + $this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().'setup/jquery.progression.js'); + + $this->DisplayPage($aParams); + } + + private function GetPreviousInstallations() + { + return DBToolsUtils::GetPreviousInstallations(); + } + + // Returns a file size limit in bytes based on the PHP upload_max_filesize + // and post_max_size + private function GetFileUploadMaxSize() + { + static $iMaxSize = -1; + + if ($iMaxSize < 0) + { + // Start with post_max_size. + $iPostMaxSize = $this->ParseSize(ini_get('post_max_size')); + if ($iPostMaxSize > 0) + { + $iMaxSize = $iPostMaxSize; + } + + // If upload_max_size is less, then reduce. Except if upload_max_size is + // zero, which indicates no limit. + $iUploadMax = $this->ParseSize(ini_get('upload_max_filesize')); + if ($iUploadMax > 0 && $iUploadMax < $iMaxSize) + { + $iMaxSize = $iUploadMax; + } + } + return $iMaxSize; + } + + private function ParseSize($iSize) + { + $sUnit = preg_replace('/[^bkmgtpezy]/i', '', $iSize); // Remove the non-unit characters from the size. + $iSize = preg_replace('/[^0-9.]/', '', $iSize); // Remove the non-numeric characters from the size. + if ($sUnit) + { + // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. + return round($iSize * pow(1024, stripos('bkmgtpezy', $sUnit[0]))); + } + else + { + return round($iSize); + } + } +} diff --git a/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php b/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php new file mode 100644 index 000000000..fa1f7aa5a --- /dev/null +++ b/datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php @@ -0,0 +1,522 @@ +getMessage()); + IssueLog::Info('itop-core-update: ended'); + throw $e; + } + finally + { + self::RRmdir(self::UPDATE_DIR); + } + } + + /** + * @throws \Exception + */ + public static function Compile() + { + try + { + // Compile code + IssueLog::Info('itop-core-update: Start compilation'); + IssueLog::Info('itop-core-update: Version Dev'); + + $sTargetEnv = 'production'; + $oRuntimeEnv = new RunTimeEnvironmentCoreUpdater($sTargetEnv); + $oRuntimeEnv->CheckDirectories($sTargetEnv); + $oRuntimeEnv->CompileFrom('production'); + $oConfig = $oRuntimeEnv->MakeConfigFile($sTargetEnv.' (built on '.date('Y-m-d').')'); + $oConfig->Set('access_mode', ACCESS_FULL); + $oRuntimeEnv->WriteConfigFileSafe($oConfig); + $oRuntimeEnv->InitDataModel($oConfig, true); + + $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir()); + $aSelectedModules = array(); + foreach ($aAvailableModules as $sModuleId => $aModule) + { + if (($sModuleId == ROOT_MODULE) || ($sModuleId == DATAMODEL_MODULE)) + { + continue; + } + else + { + $aSelectedModules[] = $sModuleId; + } + } + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'BeforeDatabaseCreation'); + $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseCreation'); + $oRuntimeEnv->UpdatePredefinedObjects(); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDatabaseSetup'); + $oRuntimeEnv->LoadData($aAvailableModules, $aSelectedModules, false /* no sample data*/); + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, $aSelectedModules, 'AfterDataLoad'); + $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); + $oExtensionsMap = new iTopExtensionsMap(); + // Default choices = as before + $oExtensionsMap->LoadChoicesFromDatabase($oConfig); + foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) + { + // Plus all "remote" extensions + if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) + { + $oExtensionsMap->MarkAsChosen($oExtension->sCode); + } + } + $aSelectedExtensionCodes = array(); + foreach ($oExtensionsMap->GetChoices() as $oExtension) + { + $aSelectedExtensionCodes[] = $oExtension->sCode; + } + $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aSelectedModules, + $aSelectedExtensionCodes, 'Done by the iTop Core Updater'); + + IssueLog::Info('itop-core-update: Compilation done'); + } + catch (Exception $e) + { + IssueLog::error($e->getMessage()); + throw $e; + } + } + + /** + * @param $sFromDir + * + * @throws \Exception + */ + private static function LocalUpdateCoreFiles($sFromDir) + { + self::CopyDir($sFromDir, APPROOT); + } + + /** + * @param $sArchiveFile + * + * @throws \Exception + */ + private static function ExtractUpdateFile($sArchiveFile) + { + if (!utils::EndsWith($sArchiveFile, '.zip')) + { + throw new Exception(Dict::S('iTopUpdate:Error:BadFileFormat')); + } + + $oArchive = new ZipArchive(); + $oArchive->open($sArchiveFile); + + self::RRmdir(self::UPDATE_DIR); + SetupUtils::builddir(self::UPDATE_DIR); + $oArchive->extractTo(self::UPDATE_DIR); + } + + /** + * @throws \Exception + */ + public static function Backup() + { + $sBackupName = self::GetBackupName(); + $sBackupFile = self::GetBackupFile(); + if (file_exists($sBackupFile)) + { + @unlink($sBackupFile); + } + + self::DoBackup($sBackupName); + } + + /** + * @throws \Exception + */ + public static function CreateItopArchive() + { + set_time_limit(0); + $sItopArchiveFile = self::GetItopArchiveFile(); + if (file_exists($sItopArchiveFile)) + { + @unlink($sItopArchiveFile); + } + + $sTempFile = sys_get_temp_dir().'/'.basename($sItopArchiveFile); + if (file_exists($sTempFile)) + { + @unlink($sTempFile); + } + + $aPathInfo = pathInfo(realpath(APPROOT)); + $sParentPath = $aPathInfo['dirname']; + $sDirName = $aPathInfo['basename']; + + $oZipArchive = new ZipArchive(); + $oZipArchive->open($sTempFile, ZIPARCHIVE::CREATE); + $oZipArchive->addEmptyDir($sDirName); + self::ZipFolder(realpath(APPROOT), $oZipArchive, strlen("$sParentPath/")); + $oZipArchive->close(); + + if (!file_exists($sTempFile)) + { + IssueLog::Error("Failed to create itop archive $sTempFile"); + } + + if (@rename($sTempFile, $sItopArchiveFile)) + { + IssueLog::Info("Archive $sItopArchiveFile Created"); + } + else + { + IssueLog::Error("Failed to create archive $sItopArchiveFile"); + } + } + + /** + * + * @param string $sTargetFile + * @throws Exception + */ + private static function DoBackup($sTargetFile) + { + // Make sure the target directory exists + $sBackupDir = dirname($sTargetFile); + SetupUtils::builddir($sBackupDir); + + $oBackup = new DBBackup(); + $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); + + $oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment()); + $oMutex->Lock(); + try + { + $oBackup->CreateCompressedBackup($sTargetFile); + IssueLog::Info('itop-core-update: Backup done: '.$sTargetFile); + } + catch (Exception $e) + { + $oMutex->Unlock(); + throw $e; + } + $oMutex->Unlock(); + } + + /** + * @param $sSource + * @param $sDest + * + * @throws \Exception + */ + public static function CopyDir($sSource, $sDest) + { + if (is_dir($sSource)) + { + if (!is_dir($sDest)) + { + @mkdir($sDest, 0755); + } + $aFiles = scandir($sSource); + if(sizeof($aFiles) > 0 ) + { + foreach($aFiles as $sFile) + { + if ($sFile == '.' || $sFile == '..' || $sFile == '.svn' || $sFile == '.git') + { + // Skip + continue; + } + + if (is_dir($sSource.'/'.$sFile)) + { + // Recurse + self::CopyDir($sSource.'/'.$sFile, $sDest.'/'.$sFile); + } + else + { + if (is_link($sDest.'/'.$sFile)) + { + unlink($sDest.'/'.$sFile); + } + self::CopyFile($sSource.'/'.$sFile, $sDest.'/'.$sFile); + } + } + } + } + elseif (is_file($sSource)) + { + self::CopyFile($sSource, $sDest); + } + } + + public static function RRmdir($sDir) { + if (is_dir($sDir)) + { + $oDir = @opendir($sDir); + while (false !== ($sFile = @readdir($oDir))) + { + if (($sFile != '.') && ($sFile != '..')) + { + $sFull = $sDir.'/'.$sFile; + if (is_dir($sFull)) + { + self::RRmdir($sFull); + } + else + { + @unlink($sFull); + } + } + } + @closedir($oDir); + @rmdir($sDir); + } + } + + /** + * @param $sSource + * @param $sDest + * + * @throws \Exception + */ + public static function CopyFile($sSource, $sDest) + { + if (is_file($sSource)) + { + if (!@copy($sSource, $sDest)) + { + // Try changing the mode of the file + @chmod($sDest, 0644); + if (!@copy($sSource, $sDest)) + { + throw new Exception(Dict::Format('iTopUpdate:Error:Copy', $sSource, $sDest)); + } + } + } + } + + /** + * Add files and sub-directories in a folder to zip file. + * + * @param string $sFolder + * @param ZipArchive $oZipArchive + * @param int $iStrippedLength Number of text to be removed from the file path. + */ + private static function ZipFolder($sFolder, &$oZipArchive, $iStrippedLength) { + $oFolder = opendir($sFolder); + while (false !== ($sFile = readdir($oFolder))) { + if (($sFile == '.') || ($sFile == '..')) + { + continue; + } + $sFilePath = "$sFolder/$sFile"; + + $sLocalItopPath = utils::LocalPath($sFilePath); + if ($sLocalItopPath == 'data/backups' || $sLocalItopPath == 'log') + { + continue; + } + + // Remove prefix from file path before add to zip. + $sLocalPath = substr($sFilePath, $iStrippedLength); + if (is_file($sFilePath)) { + $oZipArchive->addFile($sFilePath, $sLocalPath); + } elseif (is_dir($sFilePath)) { + // Add sub-directory. + $oZipArchive->addEmptyDir($sLocalPath); + self::ZipFolder($sFilePath, $oZipArchive, $iStrippedLength); + } + } + closedir($oFolder); + } + + /** + * @return string + */ + private static function GetItopArchiveName() + { + $sItopArchiveName = APPROOT.'data/backups/itop'; + return $sItopArchiveName; + } + + /** + * @return string + */ + public static function GetItopArchiveFile() + { + $sItopArchiveFile = self::GetItopArchiveName().'.zip'; + return $sItopArchiveFile; + } + + /** + * @return string + */ + private static function GetBackupName() + { + $sBackupName = APPROOT.'data/backups/manual/backup-core-update'; + return $sBackupName; + } + + /** + * @return string + */ + public static function GetBackupFile() + { + $sBackupFile = self::GetBackupName().'.tar.gz'; + return $sBackupFile; + } + + /** + * @param $sArchiveFile + * + * @throws \Exception + */ + public static function ExtractDownloadedFile($sArchiveFile) + { + try + { + // Extract archive file + self::ExtractUpdateFile($sArchiveFile); + + IssueLog::Info('itop-core-update: Archive extracted, check files integrity'); + + // Check files integrity + FilesIntegrity::CheckInstallationIntegrity(self::UPDATE_DIR.'web/'); + + IssueLog::Info('itop-core-update: Files integrity OK'); + } + catch (Exception $e) + { + self::RRmdir(self::UPDATE_DIR); + throw $e; + } + finally + { + self::RRmdir(self::DOWNLOAD_DIR); + } + } + + /** + * @return string + * @throws \Exception + */ + public static function GetVersionToInstall() + { + try + { + $sConfigFile = self::UPDATE_DIR.'web/core/config.class.inc.php'; + if (!is_file($sConfigFile)) + { + throw new Exception(Dict::S(Dict::S('iTopUpdate:Error:BadFileContent'))); + } + + $sContents = file_get_contents($sConfigFile); + preg_match_all("@define\('(?ITOP_[^']*)', '(?[^']*)'\);@", $sContents, $aMatches); + if (empty($aMatches)) + { + throw new Exception(Dict::S(Dict::S('iTopUpdate:Error:BadFileContent'))); + } + $aValues = array(); + foreach ($aMatches['name'] as $index => $sName) + { + $aValues[$sName] = $aMatches['value'][$index]; + } + + if ($aValues['ITOP_APPLICATION'] != ITOP_APPLICATION) + { + throw new Exception(Dict::S('iTopUpdate:Error:BadItopProduct')); + } + + // Extract updater file from the new version if available + if (is_file(APPROOT.'setup/appupgradecheck.php')) + { + // Remove previous specific updater + @unlink(APPROOT.'setup/appupgradecheck.php'); + } + if (is_file(self::UPDATE_DIR.'web/setup/appupgradecheck.php')) + { + IssueLog::Info('itop-core-update: Use updater provided in the archive'); + self::CopyFile(self::UPDATE_DIR.'web/setup/appupgradecheck.php', APPROOT.'setup/appupgradecheck.php'); + @include_once(APPROOT.'setup/appupgradecheck.php'); + } + if (function_exists('AppUpgradeCheckInstall')) + { + AppUpgradeCheckInstall(); + } + + return Dict::Format('UI:iTopVersion:Long', $aValues['ITOP_APPLICATION'], $aValues['ITOP_VERSION'], $aValues['ITOP_REVISION'], $aValues['ITOP_BUILD_DATE']); + } + catch (Exception $e) + { + self::RRmdir(self::UPDATE_DIR); + self::RRmdir(self::DOWNLOAD_DIR); + throw $e; + } + } +} diff --git a/datamodels/2.x/itop-core-update/src/Service/RunTimeEnvironmentCoreUpdater.php b/datamodels/2.x/itop-core-update/src/Service/RunTimeEnvironmentCoreUpdater.php new file mode 100644 index 000000000..e5a9f1e92 --- /dev/null +++ b/datamodels/2.x/itop-core-update/src/Service/RunTimeEnvironmentCoreUpdater.php @@ -0,0 +1,96 @@ +GetConfig(); + if (!is_null($oConfig)) + { + // Return the existing one + $oConfig->UpdateIncludes('env-'.$this->sTargetEnv); + } + else + { + // Clone the default 'production' config file + // + $oConfig = clone($this->GetConfig('production')); + + $oConfig->UpdateIncludes('env-'.$this->sTargetEnv); + + if (is_null($sEnvironmentLabel)) + { + $sEnvironmentLabel = $this->sTargetEnv; + } + $oConfig->Set('app_env_label', $sEnvironmentLabel); + if ($this->sFinalEnv !== 'production') + { + $oConfig->Set('db_name', $oConfig->Get('db_name').'_'.$this->sFinalEnv); + } + } + + return $oConfig; + } + + protected function GetConfig($sEnvironment = null) + { + if (is_null($sEnvironment)) + { + $sEnvironment = $this->sTargetEnv; + } + $sFile = APPCONF.$sEnvironment.'/'.ITOP_CONFIG_FILE; + if (file_exists($sFile)) + { + $oConfig = new Config($sFile); + return $oConfig; + } + else + { + return null; + } + } +} diff --git a/datamodels/2.x/itop-core-update/view/ConfirmUpdate.html.twig b/datamodels/2.x/itop-core-update/view/ConfirmUpdate.html.twig new file mode 100644 index 000000000..38ed9e5d7 --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/ConfirmUpdate.html.twig @@ -0,0 +1,110 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + + + +
+ {% if bSuccess %} + +
+ {{ 'iTopUpdate:UI:Status'|dict_s }} +
+
+ {{ sQuestion }} +
+
+ + + + + + + +
+ {{ include('DisplayCurrentVersion.html.twig') }} +
+
+ +
+ {{ 'iTopUpdate:UI:Backup'|dict_s }} + {% if bDoBackup or bDoFilesArchive %} + + + + + + +
+ {% if bDoBackup %} +
+
+
{{ 'iTopUpdate:UI:WithDBBackup'|dict_s }}
+
+
{{ sBackupFile }}
+
+
+
+ {% endif %} + {% if bDoFilesArchive %} +
+
+
{{ 'iTopUpdate:UI:WithFilesBackup'|dict_s }}
+
+
{{ sItopArchive }}
+
+
+
+ {% endif %} +
+ {% else %} +
+
+ {{ 'iTopUpdate:UI:WithoutBackup'|dict_s }} +
+
+ {% endif %} +
+ +
+ {{ 'iTopUpdate:UI:Action'|dict_s }} + +
+ + + + + +
+
+

+
+
+
+

+ + + + + +
+
+
+
+ + {% else %} + +
+
+
+ {{ sError }} +
+
+ +
+

+
+ +
+ + {% endif %} +
diff --git a/datamodels/2.x/itop-core-update/view/ConfirmUpdate.ready.js.twig b/datamodels/2.x/itop-core-update/view/ConfirmUpdate.ready.js.twig new file mode 100644 index 000000000..b5596aaaa --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/ConfirmUpdate.ready.js.twig @@ -0,0 +1,12 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +$("#do-update").on("click", function() { + $("#submit-wait").show(); + $(this).prop("disabled", true); + $("#cancel-update").prop("disabled", true); + $(this).parents('form').submit(); + e.preventDefault(); + e.stopPropagation(); + return false; +}); diff --git a/datamodels/2.x/itop-core-update/view/DisplayCurrentVersion.html.twig b/datamodels/2.x/itop-core-update/view/DisplayCurrentVersion.html.twig new file mode 100644 index 000000000..457d8eab2 --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/DisplayCurrentVersion.html.twig @@ -0,0 +1,10 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +{% set sVersion = 'UI:iTopVersion:Long'|dict_format(constant('ITOP_APPLICATION'), constant('ITOP_VERSION'), constant('ITOP_REVISION'), constant('ITOP_BUILD_DATE')) %} +
+
+
{{ 'iTopUpdate:UI:CurrentVersion'|dict_s }}
+
{{ sVersion }}
+
+
diff --git a/datamodels/2.x/itop-core-update/view/SelectUpdateFile.html.twig b/datamodels/2.x/itop-core-update/view/SelectUpdateFile.html.twig new file mode 100644 index 000000000..959c0d99e --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/SelectUpdateFile.html.twig @@ -0,0 +1,134 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + + + +
+
+ {{ 'iTopUpdate:UI:Status'|dict_s }} + +
+
+ {{ 'iTopUpdate:UI:CanCoreUpdate:Loading'|dict_s }} +
+
+ + + + + + + +
+ + {{ include('DisplayCurrentVersion.html.twig') }} + +
+
+
{{ 'iTopUpdate:UI:DiskFreeSpace'|dict_s }}
+
{{ sDiskFreeSpace }}
+
+
+ +
+
+
{{ 'iTopUpdate:UI:ItopDiskSpace'|dict_s }}
+
+
+
+ +
+
+
{{ 'iTopUpdate:UI:DBDiskSpace'|dict_s }}
+
+
+
+ +
+
+
{{ 'iTopUpdate:UI:FileUploadMaxSize'|dict_s }}
+
{{ sFileUploadMaxSize }}
+
+
+
+ +
+ +
+ {{ 'iTopUpdate:UI:SelectUpdateFile'|dict_s }} + +
+
+ + + + + +
+
+
+ +
+
+ + +
+ + +
+
+ + +
+
+

+
+
+
+ +
+ {{ 'iTopUpdate:UI:History'|dict_s }} + +
+ {% for aInstall in aPreviousInstall %} + {% if loop.first %} + + + + + + + + {% endif %} + + {% if (loop.index0 % 2) == 0 %} + {% set sRowClass = "odd" %} + {% else %} + {% set sRowClass = "even" %} + {% endif %} + + + + + + + + + {% if loop.last %} +
{{ 'Class:ModuleInstallation/Attribute:name'|dict_s }}{{ 'Class:ModuleInstallation/Attribute:version'|dict_s }}{{ 'Class:ModuleInstallation/Attribute:installed'|dict_s }}{{ 'Class:ModuleInstallation/Attribute:comment'|dict_s }}
{{ aInstall.name }}{{ aInstall.version }}{{ aInstall.installed }}{{ aInstall.comment }}
+ {% endif %} + {% endfor %} +
+
+ +
diff --git a/datamodels/2.x/itop-core-update/view/SelectUpdateFile.ready.js.twig b/datamodels/2.x/itop-core-update/view/SelectUpdateFile.ready.js.twig new file mode 100644 index 000000000..f52ac52cb --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/SelectUpdateFile.ready.js.twig @@ -0,0 +1,95 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +var iDiskFreeSpace = {{ iDiskFreeSpace }}; + +$.ajax({ + method: "POST", + url: "{{ sAjaxURL|raw }}", + data: { + "operation": "CanUpdateCore" + }, + dataType: "json", + success: function(data) + { + var oRequirements = $("#header-requirements"); + var oCanCoreUpdate = $("#can-core-update"); + oCanCoreUpdate.html(data.sMessage); + oRequirements.removeClass("message_info"); + if (data.bStatus) + { + oRequirements.addClass("message_ok"); + } + else + { + oRequirements.addClass("message_error"); + } + } +}); + +var oGetItopDiskSpace = $.ajax({ + method: "POST", + url: "{{ sAjaxURL|raw }}", + data: { + "operation": "GetItopDiskSpace" + }, + dataType: "json", + success: function(data) + { + var oRequirement = $("#itop-disk-space"); + oRequirement.html(data.sItopDiskSpace); + } +}); + +var oGetDBDiskSpace = $.ajax({ + method: "POST", + url: "{{ sAjaxURL|raw }}", + data: { + "operation": "GetDBDiskSpace" + }, + dataType: "json", + success: function(data) + { + var oRequirement = $("#db-disk-space"); + oRequirement.html(data.sDBDiskSpace); + } +}); + +$.when(oGetItopDiskSpace, oGetDBDiskSpace).then( + function(data1, data2) + { + var iItopDiskSpace = data1[0].iItopDiskSpace; + var iDBDiskSpace = data2[0].iDBDiskSpace; + if ((2 * (iItopDiskSpace + iDBDiskSpace)) > iDiskFreeSpace) + { + $("#dobackup-warning").show(); + } + } +); + +$("#file").on("change", function(e) { + var selectedFile = $('#file').get(0).files[0]; + var errorMsg = $("#header-file-size"); + var submitButton = $("#check-update"); + if (selectedFile) + { + if (selectedFile.size > {{ iFileUploadMaxSize }}) + { + errorMsg.show(); + submitButton.prop("disabled", true); + return; + } + } + errorMsg.hide(); + submitButton.prop("disabled", false); +}); + +$("#check-update").on("click", function(e) { + $("#submit-wait").show(); + $(this).prop("disabled", true); + $(".ajax-spin").removeClass("fa-spinner").removeClass("fa-spin").addClass("fa-times"); + $(this).parents('form').submit(); + e.preventDefault(); + e.stopPropagation(); + return false; +}); diff --git a/datamodels/2.x/itop-core-update/view/UpdateCoreFiles.html.twig b/datamodels/2.x/itop-core-update/view/UpdateCoreFiles.html.twig new file mode 100644 index 000000000..f058db775 --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/UpdateCoreFiles.html.twig @@ -0,0 +1,90 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + + + +
+
+ {{ 'iTopUpdate:UI:Status'|dict_s }} + +
+
+ {{ sCurrentVersion }} +
+
+ + + + + + + +
+ {% if bDoBackup %} + + {% endif %} + {% if bDoFilesArchive %} + + {% endif %} +
+ +
+ +
+ {{ 'iTopUpdate:UI:Progress'|dict_s }} + +
+

+ {{ 'iTopUpdate:UI:SetupMessage:Ready'|dict_s }} +

+
0%
+
+
+ +
+ + +
+

+
+
+
diff --git a/datamodels/2.x/itop-core-update/view/UpdateCoreFiles.ready.js.twig b/datamodels/2.x/itop-core-update/view/UpdateCoreFiles.ready.js.twig new file mode 100644 index 000000000..41e008d3c --- /dev/null +++ b/datamodels/2.x/itop-core-update/view/UpdateCoreFiles.ready.js.twig @@ -0,0 +1,122 @@ +{# @copyright Copyright (C) 2010-2019 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +function UpdateProgress(sMessage, iPercent) { + $('#setup_msg').html(sMessage); + $('#progress').progression({ + Current: iPercent, + Maximum: 100, + aBackgroundImg: "{{ sProgressImage }}", + aTextColor: '#000000' + }); +} + +var oGetCurrentVersion = { + method: "POST", + url: "{{ sAjaxURL|raw }}", + data: { + "operation": "GetCurrentVersion" + }, + dataType: "json", + success: function(data) + { + var oCurrentVersion = $("#current-version"); + if (oCurrentVersion) + { + oCurrentVersion.html(data.sVersion); + } + } +} + +function GetAjaxRequest(sOperation) +{ + oAjaxRequest = { + method: "POST", + url: "{{ sAjaxURL|raw }}", + data: { + "operation": sOperation, + "authent": "{{ sSetupToken }}" + }, + dataType: "json" + }; + + if (sOperation === "Backup") + { + oAjaxRequest.success = function() { + $("#do_backup_done").show(); + }; + } + + if (sOperation === "FilesArchive") + { + oAjaxRequest.success = function() { + $("#do_files_archive_done").show(); + }; + } + + return oAjaxRequest; +} + +{% set aSteps = ['EnterMaintenance', 'Backup', 'FilesArchive', 'CopyFiles', 'Compile', 'ExitMaintenance', 'UpdateDone'] %} + +aStepsName = []; + +{% for sStep in aSteps %} +aStepsName.push({{ ('iTopUpdate:UI:SetupMessage:' ~ sStep)|dict_s|json_encode|raw }}); +{% endfor %} + +var sBackupStep; +{% if bDoBackup %} +sBackupStep = "Backup"; +{% endif %} + +var sFilesArchiveStep; +{% if bDoFilesArchive %} +sFilesArchiveStep = "FilesArchive"; +{% endif %} + +var aStepsAjaxOperation = ["EnterMaintenance", sBackupStep, sFilesArchiveStep, "CopyFiles", "Compile", "ExitMaintenance", null]; + +var iNextStep = 0; +function ExecNextStep() { + if (iNextStep < aStepsAjaxOperation.length) + { + var sAjaxOperation = aStepsAjaxOperation[iNextStep]; + var iPercent = (iNextStep + 1) * 100 / aStepsAjaxOperation.length; + UpdateProgress(aStepsName[iNextStep], iPercent); + iNextStep++; + if (sAjaxOperation) { + $.ajax(GetAjaxRequest(sAjaxOperation)) + .done(function () { + setTimeout(ExecNextStep, 500); + }) + .fail(function ( jqXHR) { + $("#setup_continue").removeAttr("disabled"); + if (jqXHR && jqXHR.responseJSON) { + $("#setup_error").html({{ 'iTopUpdate:Error:UpdateFailed'|dict_s|json_encode|raw }}+" "+jqXHR.responseJSON.sError); + } + else + { + $("#setup_error").html({{ 'iTopUpdate:Error:UpdateFailed'|dict_s|json_encode|raw }}); + } + $('.progress').css("background-image", "none").css("background-color", "#fcc"); + $("#setup_error_outer").show(); + }) + ; + } + else + { + setTimeout(ExecNextStep, 500); + } + } + else + { + $.ajax(oGetCurrentVersion); + $("#setup_continue").removeAttr("disabled"); + $("#current_version").removeClass("message_info").addClass("message_ok"); + $('.progress').css("background-image", "none").css("background-color", "#cfc"); + } +} + +ExecNextStep(); + diff --git a/datamodels/2.x/itop-files-information/README.md b/datamodels/2.x/itop-files-information/README.md new file mode 100644 index 000000000..6abf9e9fc --- /dev/null +++ b/datamodels/2.x/itop-files-information/README.md @@ -0,0 +1 @@ +# itop-files-information \ No newline at end of file diff --git a/datamodels/2.x/itop-files-information/datamodel.itop-files-information.xml b/datamodels/2.x/itop-files-information/datamodel.itop-files-information.xml new file mode 100644 index 000000000..ab2d5ebb0 --- /dev/null +++ b/datamodels/2.x/itop-files-information/datamodel.itop-files-information.xml @@ -0,0 +1,3 @@ + + + diff --git a/datamodels/2.x/itop-files-information/en.dict.itop-files-information.php b/datamodels/2.x/itop-files-information/en.dict.itop-files-information.php new file mode 100644 index 000000000..f91eb17cd --- /dev/null +++ b/datamodels/2.x/itop-files-information/en.dict.itop-files-information.php @@ -0,0 +1,31 @@ + + */ + +Dict::Add('EN US', 'English', 'English', array( + // Errors + 'FilesInformation:Error:MissingFile' => 'Missing file: %1$s', + 'FilesInformation:Error:CorruptedFile' => 'File %1$s is corrupted', + 'FilesInformation:Error:CantWriteToFile' => 'Can not write to file %1$s', +)); + + diff --git a/datamodels/2.x/itop-files-information/fr.dict.itop-files-information.php b/datamodels/2.x/itop-files-information/fr.dict.itop-files-information.php new file mode 100644 index 000000000..edc859c96 --- /dev/null +++ b/datamodels/2.x/itop-files-information/fr.dict.itop-files-information.php @@ -0,0 +1,31 @@ + + */ + +Dict::Add('FR FR', 'French', 'Français', array( + // Errors + 'FilesInformation:Error:MissingFile' => 'Ficher manquant : %1$s', + 'FilesInformation:Error:CorruptedFile' => 'Le fichier %1$s est corrompu', + 'FilesInformation:Error:CantWriteToFile' => 'Impossible de modifier le fichier %1$s', +)); + + diff --git a/datamodels/2.x/itop-files-information/model.itop-files-information.php b/datamodels/2.x/itop-files-information/model.itop-files-information.php new file mode 100644 index 000000000..19140b5bf --- /dev/null +++ b/datamodels/2.x/itop-files-information/model.itop-files-information.php @@ -0,0 +1,6 @@ + 'iTop files information', + 'category' => 'business', + + // Setup + // + 'dependencies' => array( + ), + 'mandatory' => false, + 'visible' => false, + + // Components + // + 'datamodel' => array( + 'model.itop-files-information.php', + 'src/Service/FilesInformation.php', + 'src/Service/FilesInformationException.php', + 'src/Service/FilesInformationUtils.php', + 'src/Service/FilesIntegrity.php', + ), + 'webservice' => array(), + 'data.struct' => array(), + 'data.sample' => array(), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array(), + ) +); diff --git a/datamodels/2.x/itop-files-information/src/Service/FilesInformation.php b/datamodels/2.x/itop-files-information/src/Service/FilesInformation.php new file mode 100644 index 000000000..251ea6e7d --- /dev/null +++ b/datamodels/2.x/itop-files-information/src/Service/FilesInformation.php @@ -0,0 +1,133 @@ + $aFileStats) + { + if (!self::CanWriteToFile($aFileStats)) + { + $sMessage = Dict::Format('FilesInformation:Error:CantWriteToFile', $sRootPath.DIRECTORY_SEPARATOR.$sFileName); + return false; + } + if (($sFileName != '.') && ($aFileStats['type'] == 'dir')) + { + if (!self::CanWriteRecursive($sRootPath.DIRECTORY_SEPARATOR.$sFileName, $sMessage)) + { + return false; + } + } + } + return true; + } + + /** + * Check if iTop can write + * @param string $sFilename absolute path to chack + * + * @return bool + * @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException + */ + public static function IsWritable($sFilename) + { + $aFileStats = FilesInformationUtils::GetFileStat(utils::LocalPath($sFilename)); + return self::CanWriteToFile($aFileStats); + } + + private static function CanWriteToFile($aFileStats) + { + if ($aFileStats['writable']) + { + return true; + } + if ($aFileStats['file_owner'] == self::$sItopOwner) + { + // If iTop owns the file, no pb to write + return true; + } + return false; + } + + /** + * @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException + */ + private static function Init() + { + clearstatcache(); + + $sSourceConfigFile = MetaModel::GetConfig()->GetLoadedFile(); + $sSourceConfigFile = utils::LocalPath($sSourceConfigFile); + + $aConfigFiles = FilesInformationUtils::Scan(dirname($sSourceConfigFile)); + if (!isset($aConfigFiles[basename($sSourceConfigFile)])) + { + return; + } + $aConfigStats = $aConfigFiles[basename($sSourceConfigFile)]; + self::$sItopOwner = $aConfigStats['file_owner']; + } + + public static function GetItopDiskSpace() + { + return FilesInformationUtils::GetDirSize(realpath(APPROOT)); + } + + /** + * @param $sLocalDirPath + * + * @return array + * @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException + */ + public static function GetDirInfo($sLocalDirPath) + { + if (utils::AbsolutePath($sLocalDirPath) === false) + { + return array(); + } + return FilesInformationUtils::Scan($sLocalDirPath); + } + +} diff --git a/datamodels/2.x/itop-files-information/src/Service/FilesInformationException.php b/datamodels/2.x/itop-files-information/src/Service/FilesInformationException.php new file mode 100644 index 000000000..339ba150a --- /dev/null +++ b/datamodels/2.x/itop-files-information/src/Service/FilesInformationException.php @@ -0,0 +1,25 @@ +'socket', + 0120000=>'link', + 0100000=>'file', + 0060000=>'block', + 0040000=>'dir', + 0020000=>'char', + 0010000=>'fifo' + ); + $iRawMode = $aStats['mode']; + $iMode = decoct($iRawMode & 0170000); // File Encoding Bit + + $sDisplayMode =(array_key_exists(octdec($iMode),$aTypes))?$aTypes[octdec($iMode)]{0}:'u'; + $sDisplayMode.=(($iRawMode&0x0100)?'r':'-').(($iRawMode&0x0080)?'w':'-'); + $sDisplayMode.=(($iRawMode&0x0040)?(($iRawMode&0x0800)?'s':'x'):(($iRawMode&0x0800)?'S':'-')); + $sDisplayMode.=(($iRawMode&0x0020)?'r':'-').(($iRawMode&0x0010)?'w':'-'); + $sDisplayMode.=(($iRawMode&0x0008)?(($iRawMode&0x0400)?'s':'x'):(($iRawMode&0x0400)?'S':'-')); + $sDisplayMode.=(($iRawMode&0x0004)?'r':'-').(($iRawMode&0x0002)?'w':'-'); + $sDisplayMode.=(($iRawMode&0x0001)?(($iRawMode&0x0200)?'t':'x'):(($iRawMode&0x0200)?'T':'-')); + + $aFileStats['display_mode'] = $sDisplayMode; + $aFileStats['type'] = $aTypes[octdec($iMode)]; + $aFileStats['readable'] = is_readable($sFile); + $aFileStats['writable'] = is_writable($sFile); + $aFileStats['file_owner'] = $aStats['uid']; + $aFileStats['file_group'] = $aStats['gid']; + if (function_exists('posix_getpwuid')) + { + $aPwUid = @posix_getpwuid($aStats['uid']); + if (isset($aPwUid['name'])) + { + $aFileStats['owner_name'] = $aPwUid['name']; + } + } + if (empty($aFileStats['owner_name'])) + { + $aFileStats['owner_name'] = ''; + } + if (function_exists('posix_getgrgid')) + { + $aGrGid = @posix_getgrgid($aStats['gid']); + if (isset($aGrGid['name'])) + { + $aFileStats['group_name'] = $aGrGid['name']; + } + } + if (empty($aFileStats['group_name'])) + { + $aFileStats['group_name'] = ''; + } + $aFileStats['mtime'] = date('Y-m-d H:i:s', $aStats['mtime']); + $aFileStats['ctime'] = date('Y-m-d H:i:s', $aStats['ctime']); + + return $aFileStats; + } + + /** + * @param string $sPath relative iTop path + * + * @return string absolute path + * @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException + */ + public static function GetAbsolutePath($sPath) + { + $sRootPath = realpath(APPROOT); + $sFullPath = realpath($sRootPath.DIRECTORY_SEPARATOR.$sPath); + if (($sFullPath === false) || !utils::StartsWith($sFullPath, $sRootPath)) + { + throw new FileNotExistException($sPath); + } + return $sFullPath; + } + + public static function GetDirSize($sRealRootPath) + { + $aFiles = scandir($sRealRootPath); + $iSize = 0; + foreach ($aFiles as $sScanFile) + { + if (($sScanFile == '.') || ($sScanFile == '..')) + { + continue; + } + $sFile = $sRealRootPath.DIRECTORY_SEPARATOR.$sScanFile; + if (is_dir($sFile)) + { + $iSize += self::GetDirSize($sFile); + } + else + { + $aStats = @stat($sFile); + $iSize += $aStats['size']; + } + } + return $iSize; + } +} diff --git a/datamodels/2.x/itop-files-information/src/Service/FilesIntegrity.php b/datamodels/2.x/itop-files-information/src/Service/FilesIntegrity.php new file mode 100644 index 000000000..25753b3b7 --- /dev/null +++ b/datamodels/2.x/itop-files-information/src/Service/FilesIntegrity.php @@ -0,0 +1,123 @@ +load($sManifest); + $oXPath = new DOMXPath($oManifestDocument); + $oNodeList = $oXPath->query('/files'); + if ($oNodeList->length == 0) + { + // no files + return false; + } + foreach ($oNodeList as $oItems) + { + foreach ($oItems->childNodes as $oFileNode) + { + if (($oFileNode instanceof DOMNode)) + { + if ($oFileNode->hasChildNodes()) + { + $aFileInfo = array(); + foreach ($oFileNode->childNodes as $oFileInfo) + { + if ($oFileInfo instanceof DOMElement) + { + $aFileInfo[$oFileInfo->tagName] = $oFileInfo->textContent; + } + } + $aFiles[] = $aFileInfo; + } + } + } + } + + return $aFiles; + } + + /** + * Check that files present in iTop folder corresponds to the manifest + * + * @param string $sRootPath + * + * @throws \Combodo\iTop\FilesInformation\Service\FileIntegrityException + */ + public static function CheckInstallationIntegrity($sRootPath = APPROOT) + { + $aFilesInfo = FilesIntegrity::GetInstalledFiles($sRootPath.'manifest.xml'); + + if ($aFilesInfo === false) + { + throw new FileIntegrityException(Dict::Format('FilesInformation:Error:MissingFile', 'manifest.xml')); + } + + @clearstatcache(); + foreach ($aFilesInfo as $aFileInfo) + { + $sFile = $sRootPath.$aFileInfo['path']; + if (is_file($sFile)) + { + $aStats = @stat($sFile); + $iSize = $aStats['size']; + $sContent = file_get_contents($sFile); + $sChecksum = md5($sContent); + if (($iSize != $aFileInfo['size']) || ($sChecksum != $aFileInfo['md5'])) + { + + throw new FileIntegrityException(Dict::Format('FilesInformation:Error:CorruptedFile', basename($sFile))); + } + } + // Packed with missing files... + } + } + + public static function IsInstallationConform($sRootPath, &$sErrorMsg) + { + $sErrorMsg = ''; + try + { + self::CheckInstallationIntegrity($sRootPath); + return true; + } + catch (FileIntegrityException $e) + { + $sErrorMsg = $e->getMessage(); + } + return false; + } +} diff --git a/datamodels/2.x/itop-twig-base/README.md b/datamodels/2.x/itop-twig-base/README.md new file mode 100644 index 000000000..d93037a34 --- /dev/null +++ b/datamodels/2.x/itop-twig-base/README.md @@ -0,0 +1,7 @@ +# itop-twig-base + +Provide Twig service to other modules. + +BEWARE: This feature is subject to change! The API if not guaranteed future versions may break the compatibility. + +namespace Combodo\iTop\TwigBase; diff --git a/datamodels/2.x/itop-twig-base/index.php b/datamodels/2.x/itop-twig-base/index.php new file mode 100644 index 000000000..a81436628 --- /dev/null +++ b/datamodels/2.x/itop-twig-base/index.php @@ -0,0 +1 @@ + 'iTop Twig Base', + 'category' => 'business', + + // Setup + // + 'dependencies' => array(), + 'mandatory' => false, + 'visible' => false, + + // Components + // + 'datamodel' => array( + 'model.itop-twig-base.php', + 'src/Controller/Controller.php', + 'src/Twig/Extension.php', + 'src/Twig/TwigHelper.php', + ), + 'webservice' => array(), + 'data.struct' => array(), + 'data.sample' => array(), + + // Documentation + // + 'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any + 'doc.more_information' => '', // hyperlink to more information, if any + + // Default settings + // + 'settings' => array(), + ) +); diff --git a/datamodels/2.x/itop-twig-base/src/Controller/Controller.php b/datamodels/2.x/itop-twig-base/src/Controller/Controller.php new file mode 100644 index 000000000..b082035a8 --- /dev/null +++ b/datamodels/2.x/itop-twig-base/src/Controller/Controller.php @@ -0,0 +1,470 @@ +getDir())); + $this->m_sModule = basename($sModulePath); + $oTwig = TwigHelper::GetTwigEnvironment($sModulePath.'/view'); + $this->m_oTwig = $oTwig; + $this->m_aLinkedScripts = array(); + $this->m_aLinkedStylesheets = array(); + $this->m_aAjaxTabs = array(); + } + + private function getDir() + { + return dirname((new ReflectionClass(static::class))->getFileName()); + } + + /** + * Entry point to handle requests + * + * @api + */ + public function HandleOperation() + { + try + { + $this->CheckAccess(); + $this->m_sOperation = utils::ReadParam('operation', $this->m_sDefaultOperation); + + $sMethodName = 'Operation'.$this->m_sOperation; + if (method_exists($this, $sMethodName)) + { + $this->$sMethodName(); + } + else + { + $this->DisplayPageNotFound(); + } + } + catch (Exception $e) + { + require_once(APPROOT."/setup/setuppage.class.inc.php"); + + http_response_code(500); + $oP = new SetupPage(Dict::S('UI:PageTitle:FatalError')); + $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); + $oP->add(get_class($e).' : '.htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8')); + $oP->output(); + + IssueLog::Error($e->getMessage()); + } + } + + /** + * Overridable "page not found" which is more an "operation not found" + */ + public function DisplayPageNotFound() + { + http_response_code(404); + die("Page not found"); + } + + /** + * @throws \Exception + */ + private function CheckAccess() + { + if ($this->m_bCheckDemoMode && MetaModel::GetConfig()->Get('demo_mode')) + { + throw new Exception("Sorry, iTop is in demonstration mode: this feature is disabled."); + } + + LoginWebPage::DoLogin($this->m_bMustBeAdmin); + if (!empty($this->m_sMenuId)) + { + ApplicationMenu::CheckMenuIdEnabled($this->m_sMenuId); + } + } + + /** + * @return array + * @throws \Exception + */ + private function GetDefaultParameters() + { + $aParams = array(); + $aParams['sIndexURL'] = utils::GetAbsoluteUrlModulePage($this->m_sModule, 'index.php'); + + return $aParams; + } + + /** + * Disable this feature if in demo mode + * + * @api + */ + public function DisableInDemoMode() + { + $this->m_bCheckDemoMode = true; + } + + /** + * Allow only admin users for this feature + * + * @api + */ + public function AllowOnlyAdmin() + { + $this->m_bMustBeAdmin = true; + } + + /** + * Set the Id of the menu to check for user access rights + * + * @api + * + * @param string $sMenuId + */ + public function SetMenuId($sMenuId) + { + $this->m_sMenuId = $sMenuId; + } + + /** + * Set the default operation when no 'operation' parameter is given on URL + * + * @api + * + * @param string $sDefaultOperation + */ + public function SetDefaultOperation($sDefaultOperation) + { + $this->m_sDefaultOperation = $sDefaultOperation; + } + + /** + * Display an AJAX page (ajax_page) + * + * @api + * + * @param array $aParams Params used by the twig template + * @param null $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig + * + * @throws \Exception + */ + public function DisplayAjaxPage($aParams = array(), $sTemplateName = null) + { + $this->DisplayPage($aParams, $sTemplateName, 'ajax'); + } + + /** + * Display the twig page based on the name or the operation + * + * @api + * + * @param array $aParams Params used by the twig template + * @param string $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig + * @param string $sPageType ('html' or 'ajax') + * + * @throws \Exception + */ + public function DisplayPage($aParams = array(), $sTemplateName = null, $sPageType = 'html') + { + if (empty($sTemplateName)) + { + $sTemplateName = $this->m_sOperation; + } + $aParams = array_merge($this->GetDefaultParameters(), $aParams); + $this->CreatePage($sPageType); + $this->AddToPage($this->RenderTemplate($aParams, $sTemplateName, 'html')); + $this->AddScriptToPage($this->RenderTemplate($aParams, $sTemplateName, 'js')); + $this->AddReadyScriptToPage($this->RenderTemplate($aParams, $sTemplateName, 'ready.js')); + if (!empty($this->m_aAjaxTabs)) + { + $this->m_oPage->AddTabContainer(''); + $this->m_oPage->SetCurrentTabContainer(''); + } + foreach ($this->m_aAjaxTabs as $aTab) + { + $this->AddAjaxTabToPage($aTab['label'], $aTab['url'], $aTab['cache']); + } + foreach ($this->m_aLinkedScripts as $sLinkedScript) + { + $this->AddLinkedScriptToPage($sLinkedScript); + } + foreach ($this->m_aLinkedStylesheets as $sLinkedStylesheet) + { + $this->AddLinkedStylesheetToPage($sLinkedStylesheet); + } + $this->OutputPage(); + } + + /** + * Return a JSON response + * + * @api + * + * @param array $aParams Content of the response, will be converted to JSON + * @param int $iResponseCode HTTP response code + * @param array $aHeaders additional HTTP headers + */ + public function DisplayJSONPage($aParams = array(), $iResponseCode = 200, $aHeaders = array()) + { + http_response_code($iResponseCode); + header('Content-Type: application/json'); + foreach ($aHeaders as $sHeader) + { + header($sHeader); + } + echo json_encode($aParams); + } + + /** + * Generate a page, zip it and propose the zipped file for download + * + * @api + * + * @param array $aParams Params used by the twig template + * @param null $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig + */ + public function DownloadZippedPage($aParams = array(), $sTemplateName = null) + { + if (empty($sTemplateName)) + { + $sTemplateName = $this->m_sOperation; + } + $sReportFolder = str_replace("\\", '/', APPROOT.'log/'); + $sReportFile = 'itop-system-information-report-'.date('Y-m-d-H-i-s'); + $sHTMLReport = $sReportFolder.$sReportFile.'.html'; + $sZIPReportFile = $sReportFile; + + file_put_contents($sHTMLReport, $this->RenderTemplate($aParams, $sTemplateName, 'html')); + + $this->ZipDownloadRemoveFile(array($sHTMLReport), $sZIPReportFile, true); + } + + /** + * Create an archive and launch download, remove original file and archive when done + * + * @param string[] $aFiles + * @param string $sDownloadArchiveName file name to download, without the extension (.zip is automatically added) + * @param bool $bUnlinkFiles if true then will unlink each source file + */ + final protected function ZipDownloadRemoveFile($aFiles, $sDownloadArchiveName, $bUnlinkFiles = false) + { + $sArchiveFileFullPath = tempnam(SetupUtils::GetTmpDir(), 'itop_download-').'.zip'; + $oArchive = new ZipArchive(); + $oArchive->open($sArchiveFileFullPath, ZipArchive::CREATE); + foreach ($aFiles as $sFile) + { + $oArchive->addFile($sFile, basename($sFile)); + } + $oArchive->close(); + + if ($bUnlinkFiles) + { + foreach ($aFiles as $sFile) + { + unlink($sFile); + } + } + + $this->SendFileContent($sArchiveFileFullPath, $sDownloadArchiveName.'.zip', true, true); + } + + final protected function SendFileContent($sFilePath, $sDownloadArchiveName = null, $bFileTransfer = true, $bRemoveFile = false, $aHeaders = array()) + { + $sFileMimeType = utils::GetFileMimeType($sFilePath); + header('Content-Type: '.$sFileMimeType); + + if ($bFileTransfer) + { + header('Content-Description: File Transfer'); + header('Content-Disposition: inline; filename="'.$sDownloadArchiveName); + } + + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + + foreach ($aHeaders as $sKey => $sValue) + { + header($sKey.': '.$sValue); + } + + header('Content-Length: '.filesize($sFilePath)); + + readfile($sFilePath); + + if ($bRemoveFile) + { + unlink($sFilePath); + } + exit(0); + } + + /** + * Add a linked script to the current Page + * + * @api + * + * @param string $sScript Script path to link + */ + public function AddLinkedScript($sScript) + { + $this->m_aLinkedScripts[] = $sScript; + } + + /** + * Add an linked stylesheet to the current Page + * + * @api + * + * @param string $sStylesheet Stylesheet path to link + */ + public function AddLinkedStylesheet($sStylesheet) + { + $this->m_aLinkedStylesheets[] = $sStylesheet; + } + + /** + * Add an AJAX tab to the current page + * + * @api + * + * @param string $sLabel Label of the tab + * @param string $sURL URL to call when the tab is activated + * @param bool $bCache If true, cache the result for the current web page + */ + public function AddAjaxTab($sLabel, $sURL, $bCache = true) + { + $this->m_aAjaxTabs[] = array('label' => $sLabel, 'url' => $sURL, 'cache' => $bCache); + } + + private function RenderTemplate($aParams, $sName, $sTemplateFileExtension) + { + try + { + return $this->m_oTwig->render($sName.'.'.$sTemplateFileExtension.'.twig', $aParams); + } + catch (Twig_Error $e) + { + // Ignore errors + if (!utils::StartsWith($e->getMessage(), 'Unable to find template')) + { + IssueLog::Error($e->getMessage()); + } + } + + return ''; + } + + /** + * @param $sPageType + * + * @throws \Exception + */ + private function CreatePage($sPageType) + { + switch ($sPageType) + { + case 'html': + $this->m_oPage = new iTopWebPage($this->GetOperationTitle()); + break; + + case 'ajax': + $this->m_oPage = new ajax_page($this->GetOperationTitle()); + break; + } + } + + /** + * Get the title of the operation + * + * @return string + */ + public function GetOperationTitle() + { + return Dict::S($this->m_sModule.':UI:'.$this->m_sOperation); + } + + /** + * @param $sContent + * + * @throws \Exception + */ + private function AddToPage($sContent) + { + $this->m_oPage->add($sContent); + } + + private function AddReadyScriptToPage($sScript) + { + $this->m_oPage->add_ready_script($sScript); + } + + private function AddScriptToPage($sScript) + { + $this->m_oPage->add_script($sScript); + } + + private function AddLinkedScriptToPage($sLinkedScript) + { + $this->m_oPage->add_linked_script($sLinkedScript); + } + + private function AddLinkedStylesheetToPage($sLinkedStylesheet) + { + $this->m_oPage->add_linked_stylesheet($sLinkedStylesheet); + } + + private function AddAjaxTabToPage($sLabel, $sURL, $bCache) + { + $this->m_oPage->AddAjaxTab($sLabel, $sURL, $bCache); + } + + /** + * @throws \Exception + */ + private function OutputPage() + { + $this->m_oPage->output(); + } +} diff --git a/datamodels/2.x/itop-twig-base/src/Twig/Extension.php b/datamodels/2.x/itop-twig-base/src/Twig/Extension.php new file mode 100644 index 000000000..88de79adc --- /dev/null +++ b/datamodels/2.x/itop-twig-base/src/Twig/Extension.php @@ -0,0 +1,132 @@ +addFilter(new Twig_SimpleFilter('dict_s', + function ($sStringCode, $sDefault = null, $bUserLanguageOnly = false) { + return Dict::S($sStringCode, $sDefault, $bUserLanguageOnly); + }) + ); + + // Filter to format a string via the Dict::Format function + // Usage in twig: {{ 'String:ToTranslate'|dict_format() }} + $oTwigEnv->addFilter(new Twig_SimpleFilter('dict_format', + function ($sStringCode, $sParam01 = null, $sParam02 = null, $sParam03 = null, $sParam04 = null) { + return Dict::Format($sStringCode, $sParam01, $sParam02, $sParam03, $sParam04); + }) + ); + + // Filter to format output + // example a DateTime is converted to user format + // Usage in twig: {{ 'String:ToFormat'|output_format }} + $oTwigEnv->addFilter(new Twig_SimpleFilter('date_format', + function ($sDate) { + try + { + if (preg_match('@^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$@', trim($sDate))) + { + return AttributeDateTime::GetFormat()->Format($sDate); + } + } + catch (Exception $e) + { + } + + return $sDate; + }) + ); + + + // Filter to format output + // example a DateTime is converted to user format + // Usage in twig: {{ 'String:ToFormat'|output_format }} + $oTwigEnv->addFilter(new Twig_SimpleFilter('size_format', + function ($sSize) { + return utils::BytesToFriendlyFormat($sSize); + }) + ); + + // Filter to enable base64 encode/decode + // Usage in twig: {{ 'String to encode'|base64_encode }} + $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_encode', 'base64_encode')); + $oTwigEnv->addFilter(new Twig_SimpleFilter('base64_decode', 'base64_decode')); + + // Filter to enable json decode (encode already exists) + // Usage in twig: {{ aSomeArray|json_decode }} + $oTwigEnv->addFilter(new Twig_SimpleFilter('json_decode', function ($sJsonString, $bAssoc = false) { + return json_decode($sJsonString, $bAssoc); + }) + ); + + // Filter to add itopversion to an url + $oTwigEnv->addFilter(new Twig_SimpleFilter('add_itop_version', function ($sUrl) { + if (strpos($sUrl, '?') === false) + { + $sUrl = $sUrl."?itopversion=".ITOP_VERSION; + } + else + { + $sUrl = $sUrl."&itopversion=".ITOP_VERSION; + } + + return $sUrl; + })); + + // Filter to add a module's version to an url + $oTwigEnv->addFilter(new Twig_SimpleFilter('add_module_version', function ($sUrl, $sModuleName) { + $sModuleVersion = utils::GetCompiledModuleVersion($sModuleName); + + if (strpos($sUrl, '?') === false) + { + $sUrl = $sUrl."?moduleversion=".$sModuleVersion; + } + else + { + $sUrl = $sUrl."&moduleversion=".$sModuleVersion; + } + + return $sUrl; + })); + + // Function to check our current environment + // Usage in twig: {% if is_development_environment() %} + $oTwigEnv->addFunction(new Twig_SimpleFunction('is_development_environment', function () { + return utils::IsDevelopmentEnvironment(); + })); + + // Function to get configuration parameter + // Usage in twig: {{ get_config_parameter('foo') }} + $oTwigEnv->addFunction(new Twig_SimpleFunction('get_config_parameter', function ($sParamName) { + $oConfig = MetaModel::GetConfig(); + + return $oConfig->Get($sParamName); + })); + } + +} \ No newline at end of file diff --git a/datamodels/2.x/itop-twig-base/src/Twig/TwigHelper.php b/datamodels/2.x/itop-twig-base/src/Twig/TwigHelper.php new file mode 100644 index 000000000..215b70dd9 --- /dev/null +++ b/datamodels/2.x/itop-twig-base/src/Twig/TwigHelper.php @@ -0,0 +1,25 @@ +