N°2249 - Supportability - Updater module (application upgrade)

This commit is contained in:
Eric
2019-12-13 17:28:35 +01:00
parent 2741a446ea
commit dd5ac38dd4
36 changed files with 3158 additions and 1 deletions

View File

@@ -16,7 +16,10 @@
<module>itop-profiles-itil</module>
<module>itop-welcome-itil</module>
<module>itop-tickets</module>
<module>itop-hub-connector</module>
<module>itop-files-information</module>
<module>itop-twig-base</module>
<module>combodo-db-tools</module>
<module>itop-core-update</module>
</modules>
<mandatory>true</mandatory>
</choice>

View File

@@ -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;

View File

@@ -0,0 +1,21 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\CoreUpdate;
use Combodo\iTop\CoreUpdate\Controller\AjaxController;
use ContextTag;
require_once(APPROOT.'application/startup.inc.php');
new ContextTag('Setup');
$oUpdateController = new AjaxController();
$oUpdateController->DisableInDemoMode();
$oUpdateController->AllowOnlyAdmin();
// Allow parallel execution of ajax requests
session_write_close();
$oUpdateController->HandleOperation();

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
<menus>
<menu id="iTopUpdate" xsi:type="WebPageMenuNode" _delta="define">
<rank>60</rank>
<parent>System</parent>
<url>$pages/exec.php?exec_module=itop-core-update&amp;exec_page=index.php&amp;c[menu]=iTopUpdate&amp;maintenance=true</url>
<enable_admin_only>1</enable_admin_only>
</menu>
</menus>
</itop_design>

View File

@@ -0,0 +1,108 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
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',
));

View File

@@ -0,0 +1,108 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
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',
));

View File

@@ -0,0 +1,19 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\CoreUpdate;
use Combodo\iTop\CoreUpdate\Controller\UpdateController;
use ContextTag;
require_once(APPROOT.'application/startup.inc.php');
new ContextTag('Setup');
$oUpdateController = new UpdateController();
$oUpdateController->DisableInDemoMode();
$oUpdateController->AllowOnlyAdmin();
$oUpdateController->SetDefaultOperation('SelectUpdateFile');
$oUpdateController->HandleOperation();

View File

@@ -0,0 +1,52 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-core-update/1.0.0',
array(
// Identification
//
'label' => '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(),
)
);

View File

@@ -0,0 +1,184 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\CoreUpdate\Controller;
use Combodo\iTop\CoreUpdate\Service\CoreUpdater;
use Combodo\iTop\DBTools\Service\DBToolsUtils;
use Combodo\iTop\FilesInformation\Service\FileNotExistException;
use Combodo\iTop\FilesInformation\Service\FilesInformation;
use Combodo\iTop\TwigBase\Controller\Controller;
use Dict;
use Exception;
use IssueLog;
use MetaModel;
use SetupUtils;
use utils;
class AjaxController extends Controller
{
const LOCAL_DIR = __DIR__;
public function OperationCanUpdateCore()
{
$aParams = array();
try
{
$bCanUpdateCore = FilesInformation::CanUpdateCore($sMessage);
$aParams['bStatus'] = $bCanUpdateCore;
if ($bCanUpdateCore)
{
$aParams['sMessage'] = Dict::S('iTopUpdate:UI:CanCoreUpdate:Yes');
}
else
{
$aParams['sMessage'] = Dict::Format('iTopUpdate:UI:CanCoreUpdate:No', $sMessage);
}
}
catch (FileNotExistException $e)
{
$aParams['bStatus'] = false;
$aParams['sMessage'] = Dict::Format('iTopUpdate:UI:CanCoreUpdate:ErrorFileNotExist', $e->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);
}
}

View File

@@ -0,0 +1,190 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\CoreUpdate\Controller;
use Combodo\iTop\CoreUpdate\Service\CoreUpdater;
use Combodo\iTop\DBTools\Service\DBToolsUtils;
use Combodo\iTop\TwigBase\Controller\Controller;
use Dict;
use Exception;
use SetupUtils;
use utils;
class UpdateController extends Controller
{
const LOCAL_DIR = __DIR__;
public function OperationSelectUpdateFile()
{
$sTransactionId = utils::GetNewTransactionId();
$aParams = array();
$aParams['sTransactionId'] = $sTransactionId;
$aParams['aPreviousInstall'] = $this->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);
}
}
}

View File

@@ -0,0 +1,522 @@
<?php
/**
* iTop
* @copyright Copyright (C) 2010,2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*
*/
namespace Combodo\iTop\CoreUpdate\Service;
use Combodo\iTop\FilesInformation\Service\FilesIntegrity;
use DBBackup;
use Dict;
use Exception;
use IssueLog;
use iTopExtension;
use iTopExtensionsMap;
use iTopMutex;
use MetaModel;
use SetupUtils;
use utils;
use ZipArchive;
require_once APPROOT.'setup/applicationinstaller.class.inc.php';
require_once APPROOT.'setup/runtimeenv.class.inc.php';
final class CoreUpdater
{
const DOWNLOAD_DIR = APPROOT.'data/downloaded-core/';
const UPDATE_DIR = APPROOT.'data/core-update/';
/**
* @param bool $bDoBackup
*
* @throws \Exception
*/
public static function CopyCoreFiles()
{
set_time_limit(600);
// Extract updater file from the new version if available
if (is_file(APPROOT.'setup/appupgradecopy.php'))
{
// Remove previous specific updater
@unlink(APPROOT.'setup/appupgradecopy.php');
}
if (is_file(self::UPDATE_DIR.'web/setup/appupgradecopy.php'))
{
IssueLog::Info('itop-core-update: Use updater provided in the archive');
self::CopyFile(self::UPDATE_DIR.'web/setup/appupgradecopy.php', APPROOT.'setup/appupgradecopy.php');
@include_once(APPROOT.'setup/appupgradecopy.php');
}
try
{
if (function_exists('AppUpgradeCopyFiles'))
{
// start the update
set_time_limit(600);
AppUpgradeCopyFiles(self::UPDATE_DIR.'web/');
}
else
{
// Local function for older iTop versions
IssueLog::Info('itop-core-update: Use default updater');
self::LocalUpdateCoreFiles(self::UPDATE_DIR.'web/');
}
IssueLog::Info('itop-core-update: Update done, check files integrity');
FilesIntegrity::CheckInstallationIntegrity(APPROOT);
IssueLog::Info('itop-core-update: Files integrity OK');
}
catch (Exception $e)
{
IssueLog::error($e->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\('(?<name>ITOP_[^']*)', '(?<value>[^']*)'\);@", $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;
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\CoreUpdate\Service;
require_once(APPROOT."setup/runtimeenv.class.inc.php");
use Config;
use Exception;
use RunTimeEnvironment;
class RunTimeEnvironmentCoreUpdater extends RunTimeEnvironment
{
public function CheckDirectories($sTargetEnv)
{
$sTargetDir = APPROOT.'env-'.$sTargetEnv;
$sBuildDir = $sTargetDir.'-build';
self::CheckDirectory($sTargetDir);
self::CheckDirectory($sBuildDir);
}
/**
* @param $sDir
* @throws Exception
*/
public static function CheckDirectory($sDir)
{
if (!is_dir($sDir))
{
if (!@mkdir($sDir,0770))
{
throw new Exception('Creating directory '.$sDir.' is denied (Check access rights)');
}
}
// Try create a file
$sTempFile = $sDir.'/__itop_temp_file__';
if (!@touch($sTempFile))
{
throw new Exception('Write access to '.$sDir.' is denied (Check access rights)');
}
@unlink($sTempFile);
}
public function MakeConfigFile($sEnvironmentLabel = null)
{
$oConfig = $this->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;
}
}
}

View File

@@ -0,0 +1,110 @@
{# @copyright Copyright (C) 2010-2019 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div class="page_header"><h1>{{ 'iTopUpdate:UI:PageTitle'|dict_s }}</h1></div>
<div class="display_block display-files">
{% if bSuccess %}
<fieldset>
<legend>{{ 'iTopUpdate:UI:Status'|dict_s }}</legend>
<div class="header_message message_info">
<div>
<span>{{ sQuestion }}</span>
</div>
</div>
<table style="vertical-align:top" class="one-col-details" data-mode="view">
<tbody>
<tr>
<td style="vertical-align:top" class="">
{{ include('DisplayCurrentVersion.html.twig') }}
</td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset>
<legend>{{ 'iTopUpdate:UI:Backup'|dict_s }}</legend>
{% if bDoBackup or bDoFilesArchive %}
<table style="vertical-align:top" class="one-col-details" data-mode="view">
<tbody>
<tr>
<td style="vertical-align:top" class="">
{% if bDoBackup %}
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:WithDBBackup'|dict_s }}</span></div>
<div class="field_data">
<div class="field_value">{{ sBackupFile }}</div>
</div>
</div>
</div>
{% endif %}
{% if bDoFilesArchive %}
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:WithFilesBackup'|dict_s }}</span></div>
<div class="field_data">
<div class="field_value">{{ sItopArchive }}</div>
</div>
</div>
</div>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div class="header_message message_info">
<div>
<span>{{ 'iTopUpdate:UI:WithoutBackup'|dict_s }}</span>
</div>
</div>
{% endif %}
</fieldset>
<fieldset>
<legend>{{ 'iTopUpdate:UI:Action'|dict_s }}</legend>
<div>
<table>
<tr>
<td>
<form enctype="multipart/form-data" method="post">
<p><input id="cancel-update" type="submit" value="{{ 'iTopUpdate:UI:Cancel'|dict_s }}"/></p>
</form>
</td>
<td>
<form enctype="multipart/form-data" method="post">
<p><input id="do-update" type="submit" value="{{ 'iTopUpdate:UI:DoUpdate'|dict_s }}"/> <i id="submit-wait" style="display: none" class="fas fa-spinner fa-spin" aria-hidden="true"></i></p>
<input type="hidden" name="operation" value="UpdateCoreFiles"/>
<input type="hidden" name="transaction_id" value="{{ sTransactionId }}">
<input type="hidden" name="filename" value="{{ sName }}">
<input type="hidden" name="doBackup" value="{{ bDoBackup }}">
<input type="hidden" name="doFilesArchive" value="{{ bDoFilesArchive }}">
</form>
</td>
</tr>
</table>
</div>
</fieldset>
{% else %}
<fieldset>
<div class="header_message message_error">
<div>
<span>{{ sError }}</span>
</div>
</div>
<form enctype="multipart/form-data" method="post">
<p><input type="submit" value="{{ 'iTopUpdate:UI:Back'|dict_s }}"/></p>
</form>
</fieldset>
{% endif %}
</div>

View File

@@ -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;
});

View File

@@ -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')) %}
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:CurrentVersion'|dict_s }}</span></div>
<div class="field_data"><div class="field_value">{{ sVersion }}</div></div>
</div>
</div>

View File

@@ -0,0 +1,134 @@
{# @copyright Copyright (C) 2010-2019 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div class="page_header"><h1>{{ 'iTopUpdate:UI:PageTitle'|dict_s }}</h1></div>
<div class="display_block display-files">
<fieldset>
<legend>{{ 'iTopUpdate:UI:Status'|dict_s }}</legend>
<div id="header-requirements" class="header_message message_info">
<div>
<span id="can-core-update">{{ 'iTopUpdate:UI:CanCoreUpdate:Loading'|dict_s }} <i class="ajax-spin fas fa-spinner fa-spin" aria-hidden="true"></i></span>
</div>
</div>
<table style="vertical-align:top" class="one-col-details" data-mode="view">
<tbody>
<tr>
<td style="vertical-align:top" class="">
{{ include('DisplayCurrentVersion.html.twig') }}
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:DiskFreeSpace'|dict_s }}</span></div>
<div class="field_data"><div class="field_value">{{ sDiskFreeSpace }}</div></div>
</div>
</div>
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:ItopDiskSpace'|dict_s }}</span></div>
<div class="field_data"><div id="itop-disk-space" class="field_value"><i class="ajax-spin fas fa-spinner fa-spin" aria-hidden="true"></i></div></div>
</div>
</div>
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:DBDiskSpace'|dict_s }}</span></div>
<div class="field_data"><div id="db-disk-space" class="field_value"><i class="ajax-spin fas fa-spinner fa-spin" aria-hidden="true"></i></div></div>
</div>
</div>
<div class="details">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:FileUploadMaxSize'|dict_s }}</span></div>
<div class="field_data"><div class="field_value">{{ sFileUploadMaxSize }}</div></div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset>
<legend>{{ 'iTopUpdate:UI:SelectUpdateFile'|dict_s }}</legend>
<div class="details">
<form enctype="multipart/form-data" method="post">
<input type="hidden" name="operation" value="ConfirmUpdate"/>
<input type="hidden" name="transaction_id" value="{{ sTransactionId }}">
<div id="header-file-size" class="header_message message_error" style="display: none">
<div>
<span>{{ 'iTopUpdate:Error:FileUploadMaxSizeTooSmall'|dict_s }}</span>
</div>
<div>
<span>{{ 'iTopUpdate:UI:PostMaxSize'|dict_format(sPostMaxSize) }}</span>
</div>
<div>
<span>{{ 'iTopUpdate:UI:UploadMaxFileSize'|dict_format(sUploadMaxSize) }}</span>
</div>
</div>
<div class="field_container field_large">
<div class="field_data">
<div class="upload_container">
<input type="file" id="file" name="file"/>
</div>
</div>
<div id="dobackup-warning" class="header_message message_info" style="display: none">{{ 'iTopUpdate:UI:DoBackup:Warning'|dict_s }}</div>
<div class="field_data">
<label id="dobackup-label" for="doBackup">{{ 'iTopUpdate:UI:DoBackup:Label'|dict_s }}</label>
<input type="checkbox" id="doBackup" name="doBackup" checked="checked" value="1"/>
</div>
<div class="field_data">
<label id="dofilesarchive-label" for="doFilesArchive">{{ 'iTopUpdate:UI:DoFilesArchive'|dict_s }}</label>
<input type="checkbox" id="doFilesArchive" name="doFilesArchive" checked="checked" value="1"/>
</div>
</div>
<p><input id="check-update" type="submit" value="{{ 'iTopUpdate:UI:CheckUpdate'|dict_s }}"/> <i id="submit-wait" style="display: none" class="fas fa-spinner fa-spin" aria-hidden="true"></i></p>
</form>
</div>
</fieldset>
<fieldset>
<legend>{{ 'iTopUpdate:UI:History'|dict_s }}</legend>
<div class="details">
{% for aInstall in aPreviousInstall %}
{% if loop.first %}
<table class="listResults">
<tr>
<th>{{ 'Class:ModuleInstallation/Attribute:name'|dict_s }}</th>
<th>{{ 'Class:ModuleInstallation/Attribute:version'|dict_s }}</th>
<th>{{ 'Class:ModuleInstallation/Attribute:installed'|dict_s }}</th>
<th>{{ 'Class:ModuleInstallation/Attribute:comment'|dict_s }}</th>
</tr>
{% endif %}
{% if (loop.index0 % 2) == 0 %}
{% set sRowClass = "odd" %}
{% else %}
{% set sRowClass = "even" %}
{% endif %}
<tr class="{{ sRowClass }}">
<td>{{ aInstall.name }}</td>
<td>{{ aInstall.version }}</td>
<td>{{ aInstall.installed }}</td>
<td>{{ aInstall.comment }}</td>
</tr>
{% if loop.last %}
</table>
{% endif %}
{% endfor %}
</div>
</fieldset>
</div>

View File

@@ -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;
});

View File

@@ -0,0 +1,90 @@
{# @copyright Copyright (C) 2010-2019 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div class="page_header"><h1>{{ 'iTopUpdate:UI:PageTitle'|dict_s }}</h1></div>
<div class="display_block display-files">
<fieldset id="installation_progress">
<legend>{{ 'iTopUpdate:UI:Status'|dict_s }}</legend>
<div id="current_version" class="header_message message_info">
<div>
<span id="current-version">{{ sCurrentVersion }}</span>
</div>
</div>
<table style="vertical-align:top" class="one-col-details" data-mode="view">
<tbody>
<tr>
<td style="vertical-align:top" class="">
{% if bDoBackup %}
<div id="do_backup_done" class="details" style="display: none;">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:WithDBBackup'|dict_s }}</span></div>
<div class="field_data">
<div class="field_value">{{ sBackupFile }}</div>
</div>
</div>
</div>
{% endif %}
{% if bDoFilesArchive %}
<div id="do_files_archive_done" class="details" style="display: none;">
<div class="field_container field_small">
<div class="field_label label"><span title="">{{ 'iTopUpdate:UI:WithFilesBackup'|dict_s }}</span></div>
<div class="field_data">
<div class="field_value">{{ sItopArchive }}</div>
</div>
</div>
</div>
{% endif %}
</td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset id="installation_progress">
<legend>{{ 'iTopUpdate:UI:Progress'|dict_s }}</legend>
<div id="progress_content" style="height: 100px; overflow: auto; text-align: center;">
<p class="center">
<span id="setup_msg">{{ 'iTopUpdate:UI:SetupMessage:Ready'|dict_s }}</span>
</p>
<div id="progress" style="
margin: 5px auto;
display:block;
border:1px solid #000000;
width: 280px;
height: 20px;
line-height: 20px;
text-align: center;
">0%</div>
</div>
</fieldset>
<fieldset>
<div id="setup_error_outer" class="header_message message_error" style="display: none;">
<div>
<span id="setup_error"></span>
</div>
{% if bDoFilesArchive %}
<div>
<span>{{ 'iTopUpdate:UI:RestoreArchive'|dict_format(sItopArchive) }}</span>
</div>
{% endif %}
{% if bDoBackup %}
<div>
<span>{{ 'iTopUpdate:UI:RestoreBackup'|dict_format(sBackupFile) }}</span>
</div>
{% endif %}
<div>
<span>{{ 'iTopUpdate:UI:MaintenanceModeActive'|dict_s }}</span>
</div>
</div>
<form enctype="multipart/form-data" method="post">
<p><input id="setup_continue" type="submit" value="{{ 'iTopUpdate:UI:Continue'|dict_s }}" disabled/></p>
</form>
</fieldset>
</div>

View File

@@ -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();

View File

@@ -0,0 +1 @@
# itop-files-information

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0">
</itop_design>

View File

@@ -0,0 +1,31 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
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',
));

View File

@@ -0,0 +1,31 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*/
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',
));

View File

@@ -0,0 +1,6 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/

View File

@@ -0,0 +1,49 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-files-information/1.0.0',
array(
// Identification
//
'label' => '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(),
)
);

View File

@@ -0,0 +1,133 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FilesInformation\Service;
use Dict;
use MetaModel;
use utils;
class FilesInformation
{
private static $sItopOwner;
/**
* Check iTop files access rights to tell if core update is possible
*
* @param string $sMessage
*
* @return bool true if core update is possible
* @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException
*/
public static function CanUpdateCore(&$sMessage)
{
self::Init();
// Check than iTop can write everywhere
if (!self::CanWriteRecursive('', $sMessage))
{
return false;
}
return true;
}
/**
* @param string $sRootPath
* @param string $sMessage
*
* @return bool
* @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException
*/
private static function CanWriteRecursive($sRootPath = '', &$sMessage = null)
{
$aDirStats = FilesInformationUtils::Scan($sRootPath, false);
foreach ($aDirStats as $sFileName => $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);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FilesInformation\Service;
use Exception;
class FilesInformationException extends Exception
{
}
class FileNotExistException extends FilesInformationException
{
}
class FileIntegrityException extends FilesInformationException
{
}

View File

@@ -0,0 +1,179 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FilesInformation\Service;
use utils;
class FilesInformationUtils
{
public static function Init()
{
clearstatcache();
}
/**
* @param string $sPath
* @param bool $bGetDirSize
*
* @return array
* @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException
*/
public static function Scan($sPath = '', $bGetDirSize = true)
{
$sRealRootPath = utils::AbsolutePath($sPath);
$aFiles = scandir($sRealRootPath);
$aFileStats = array();
foreach ($aFiles as $sScanFile)
{
if ($sScanFile == '..')
{
continue;
}
$sFile = $sRealRootPath.DIRECTORY_SEPARATOR.$sScanFile;
$sFileName = utils::LocalPath($sFile);
$aFileStat = self::GetFileStat($sFileName, $bGetDirSize);
$aFileStat['basename'] = $sScanFile;
$aFileStats[$sScanFile] = $aFileStat;
}
return $aFileStats;
}
/**
* @param string $sFilename
*
* @param bool $bGetDirSize
*
* @return array
* @throws \Combodo\iTop\FilesInformation\Service\FileNotExistException
*/
public static function GetFileStat($sFilename, $bGetDirSize = true)
{
$sFile = realpath(APPROOT.$sFilename);
$aStats = @stat($sFile);
if (!$aStats)
{
throw new FileNotExistException($sFilename);
}
$aFileStats = array();
$aFileStats['name'] = $sFilename;
$aFileStats['size'] = $aStats['size'];
if (is_dir($sFile))
{
// Special dir case
if ($bGetDirSize)
{
// The size is computed by aggregating the sizes on the children
$aFileStats['size'] = self::GetDirSize($sFile);
}
}
$aFileStats['display_size'] = utils::BytesToFriendlyFormat($aFileStats['size']);
$aFileStats['perms'] = sprintf("0%o", 0777 & $aStats['mode']);
$aFileStats['mode'] = $aStats['mode'];
$aTypes = array(
0140000=>'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;
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\FilesInformation\Service;
use Dict;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMXPath;
class FilesIntegrity
{
/**
* Get the files defined in the manifest.xml
*
* @param string $sManifest full path of the manifest file
*
* @return array|false list of file info (path, size, md5)
* @throws \Exception
*/
private static function GetInstalledFiles($sManifest)
{
$aFiles = array();
$aManifestStats = @stat($sManifest);
if ($aManifestStats === false)
{
// No manifest
return false;
}
$oManifestDocument = new DOMDocument('1.0', 'UTF-8');
@$oManifestDocument->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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1 @@
<?php

View File

@@ -0,0 +1 @@
<?php

View File

@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
//
// iTop module definition file
//
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-twig-base/1.0.0',
array(
// Identification
//
'label' => '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(),
)
);

View File

@@ -0,0 +1,470 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\TwigBase\Controller;
use ajax_page;
use ApplicationMenu;
use Combodo\iTop\TwigBase\Twig\TwigHelper;
use Dict;
use Exception;
use IssueLog;
use iTopWebPage;
use LoginWebPage;
use MetaModel;
use ReflectionClass;
use SetupPage;
use SetupUtils;
use Twig_Error;
use utils;
use ZipArchive;
abstract class Controller
{
/** @var \Twig\Environment */
private $m_oTwig;
/** @var string */
private $m_sOperation;
/** @var string */
private $m_sModule;
/** @var iTopWebPage|\ajax_page */
private $m_oPage;
/** @var bool */
private $m_bCheckDemoMode = false;
/** @var bool */
private $m_bMustBeAdmin = false;
/** @var string */
private $m_sMenuId = null;
/** @var string */
private $m_sDefaultOperation = 'Default';
private $m_aLinkedScripts;
private $m_aLinkedStylesheets;
private $m_aAjaxTabs;
public function __construct()
{
$sModulePath = dirname(dirname($this->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("<h1>".Dict::S('UI:FatalErrorMessage')."</h1>\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 <b>demonstration mode</b>: 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();
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\TwigBase\Twig;
use AttributeDateTime;
use Dict;
use Exception;
use MetaModel;
use Twig_Environment;
use Twig_SimpleFilter;
use Twig_SimpleFunction;
use utils;
class Extension
{
/**
* Registers Twig extensions such as filters or functions.
* It allows us to access some stuff directly in twig.
*
* @param \Twig_Environment $oTwigEnv
*/
public static function RegisterTwigExtensions(Twig_Environment &$oTwigEnv)
{
// Filter to translate a string via the Dict::S function
// Usage in twig: {{ 'String:ToTranslate'|dict_s }}
$oTwigEnv->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);
}));
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\TwigBase\Twig;
use Twig_Environment;
use Twig_Loader_Filesystem;
@include_once(APPROOT.'/lib/silex/vendor/autoload.php');
class TwigHelper
{
public static function GetTwigEnvironment($sViewPath)
{
$oLoader = new Twig_Loader_Filesystem($sViewPath);
$oTwig = new Twig_Environment($oLoader);
Extension::RegisterTwigExtensions($oTwig);
return $oTwig;
}
}