diff --git a/application/utils.inc.php b/application/utils.inc.php index 026da66f5..581b63089 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1,1983 +1,1983 @@ - - - -/** - * Static class utils - * - * @copyright Copyright (C) 2010-2017 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -require_once(APPROOT.'/core/config.class.inc.php'); -require_once(APPROOT.'/application/transaction.class.inc.php'); -require_once(APPROOT.'application/Html2Text.php'); -require_once(APPROOT.'application/Html2TextException.php'); - -define('ITOP_CONFIG_FILE', 'config-itop.php'); -define('ITOP_DEFAULT_CONFIG_FILE', APPCONF.ITOP_DEFAULT_ENV.'/'.ITOP_CONFIG_FILE); - -define('SERVER_NAME_PLACEHOLDER', '$SERVER_NAME$'); - -define('SERVER_MAX_URL_LENGTH', 2048); - -class FileUploadException extends Exception -{ -} - - -/** - * Helper functions to interact with forms: read parameters, upload files... - * @package iTop - */ -class utils -{ - private static $oConfig = null; - private static $m_bCASClient = false; - - // Parameters loaded from a file, parameters of the page/command line still have precedence - private static $m_aParamsFromFile = null; - private static $m_aParamSource = array(); - - protected static function LoadParamFile($sParamFile) - { - if (!file_exists($sParamFile)) - { - throw new Exception("Could not find the parameter file: '$sParamFile'"); - } - if (!is_readable($sParamFile)) - { - throw new Exception("Could not load parameter file: '$sParamFile'"); - } - $sParams = file_get_contents($sParamFile); - - if (is_null(self::$m_aParamsFromFile)) - { - self::$m_aParamsFromFile = array(); - } - - $aParamLines = explode("\n", $sParams); - foreach ($aParamLines as $sLine) - { - $sLine = trim($sLine); - - // Ignore the line after a '#' - if (($iCommentPos = strpos($sLine, '#')) !== false) - { - $sLine = substr($sLine, 0, $iCommentPos); - $sLine = trim($sLine); - } - - // Note: the line is supposed to be already trimmed - if (preg_match('/^(\S*)\s*=(.*)$/', $sLine, $aMatches)) - { - $sParam = $aMatches[1]; - $value = trim($aMatches[2]); - self::$m_aParamsFromFile[$sParam] = $value; - self::$m_aParamSource[$sParam] = $sParamFile; - } - } - } - - public static function UseParamFile($sParamFileArgName = 'param_file', $bAllowCLI = true) - { - $sFileSpec = self::ReadParam($sParamFileArgName, '', $bAllowCLI, 'raw_data'); - foreach(explode(',', $sFileSpec) as $sFile) - { - $sFile = trim($sFile); - if (!empty($sFile)) - { - self::LoadParamFile($sFile); - } - } - } - - /** - * Return the source file from which the parameter has been found, - * usefull when it comes to pass user credential to a process executed - * in the background - * @param $sName Parameter name - * @return The file name if any, or null - */ - public static function GetParamSourceFile($sName) - { - if (array_key_exists($sName, self::$m_aParamSource)) - { - return self::$m_aParamSource[$sName]; - } - else - { - return null; - } - } - - public static function IsModeCLI() - { - $sSAPIName = php_sapi_name(); - $sCleanName = strtolower(trim($sSAPIName)); - if ($sCleanName == 'cli') - { - return true; - } - else - { - return false; - } - } - - protected static $bPageMode = null; - /** - * @var boolean[] - */ - protected static $aModes = array(); - - public static function InitArchiveMode() - { - if (isset($_SESSION['archive_mode'])) - { - $iDefault = $_SESSION['archive_mode']; - } - else - { - $iDefault = 0; - } - // Read and record the value for switching the archive mode - $iCurrent = self::ReadParam('with-archive', $iDefault); - if (isset($_SESSION)) - { - $_SESSION['archive_mode'] = $iCurrent; - } - // Read and use the value for the current page (web services) - $iCurrent = self::ReadParam('with_archive', $iCurrent, true); - self::$bPageMode = ($iCurrent == 1); - } - - /** - * @param boolean $bMode if true then activate archive mode (archived objects are visible), otherwise archived objects are - * hidden (archive = "soft deletion") - */ - public static function PushArchiveMode($bMode) - { - array_push(self::$aModes, $bMode); - } - - public static function PopArchiveMode() - { - array_pop(self::$aModes); - } - - /** - * @return boolean true if archive mode is enabled - */ - public static function IsArchiveMode() - { - if (count(self::$aModes) > 0) - { - $bRet = end(self::$aModes); - } - else - { - if (self::$bPageMode === null) - { - self::InitArchiveMode(); - } - $bRet = self::$bPageMode; - } - return $bRet; - } - - /** - * Helper to be called by the GUI and define if the user will see obsolete data (otherwise, the user will have to dig further) - * @return bool - */ - public static function ShowObsoleteData() - { - $bDefault = MetaModel::GetConfig()->Get('obsolescence.show_obsolete_data'); // default is false - $bShow = appUserPreferences::GetPref('show_obsolete_data', $bDefault); - if (static::IsArchiveMode()) - { - $bShow = true; - } - return $bShow; - } - - public static function ReadParam($sName, $defaultValue = "", $bAllowCLI = false, $sSanitizationFilter = 'parameter') - { - global $argv; - $retValue = $defaultValue; - - if (!is_null(self::$m_aParamsFromFile)) - { - if (isset(self::$m_aParamsFromFile[$sName])) - { - $retValue = self::$m_aParamsFromFile[$sName]; - } - } - - if (isset($_REQUEST[$sName])) - { - $retValue = $_REQUEST[$sName]; - } - elseif ($bAllowCLI && isset($argv)) - { - foreach($argv as $iArg => $sArg) - { - if (preg_match('/^--'.$sName.'=(.*)$/', $sArg, $aMatches)) - { - $retValue = $aMatches[1]; - } - } - } - return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); - } - - public static function ReadPostedParam($sName, $defaultValue = '', $sSanitizationFilter = 'parameter') - { - $retValue = isset($_POST[$sName]) ? $_POST[$sName] : $defaultValue; - return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); - } - - public static function Sanitize($value, $defaultValue, $sSanitizationFilter) - { - if ($value === $defaultValue) - { - // Preserve the real default value (can be used to detect missing mandatory parameters) - $retValue = $value; - } - else - { - $retValue = self::Sanitize_Internal($value, $sSanitizationFilter); - if ($retValue === false) - { - $retValue = $defaultValue; - } - } - return $retValue; - } - - protected static function Sanitize_Internal($value, $sSanitizationFilter) - { - switch($sSanitizationFilter) - { - case 'integer': - $retValue = filter_var($value, FILTER_SANITIZE_NUMBER_INT); - break; - - case 'class': - $retValue = $value; - if (!MetaModel::IsValidClass($value)) - { - $retValue = false; - } - break; - - case 'string': - $retValue = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS); - break; - - case 'context_param': - case 'parameter': - case 'field_name': - if (is_array($value)) - { - $retValue = array(); - foreach($value as $key => $val) - { - $retValue[$key] = self::Sanitize_Internal($val, $sSanitizationFilter); // recursively check arrays - if ($retValue[$key] === false) - { - $retValue = false; - break; - } - } - } - else - { - switch($sSanitizationFilter) - { - case 'parameter': - $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^([ A-Za-z0-9_=-]|%3D|%2B|%2F)*$/'))); // the '=', '%3D, '%2B', '%2F' characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC) - break; - - case 'field_name': - $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name - break; - - case 'context_param': - $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[ A-Za-z0-9_=%:+-]*$/'))); - break; - - } - } - break; - - default: - case 'raw_data': - $retValue = $value; - // Do nothing - } - return $retValue; - } - - /** - * Reads an uploaded file and turns it into an ormDocument object - Triggers an exception in case of error - * @param string $sName Name of the input used from uploading the file - * @param string $sIndex If Name is an array of posted files, then the index must be used to point out the file - * @return ormDocument The uploaded file (can be 'empty' if nothing was uploaded) - */ - public static function ReadPostedDocument($sName, $sIndex = null) - { - $oDocument = new ormDocument(); // an empty document - if(isset($_FILES[$sName])) - { - $aFileInfo = $_FILES[$sName]; - - $sError = is_null($sIndex) ? $aFileInfo['error'] : $aFileInfo['error'][$sIndex]; - switch($sError) - { - case UPLOAD_ERR_OK: - $sTmpName = is_null($sIndex) ? $aFileInfo['tmp_name'] : $aFileInfo['tmp_name'][$sIndex]; - $sMimeType = is_null($sIndex) ? $aFileInfo['type'] : $aFileInfo['type'][$sIndex]; - $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; - - $doc_content = file_get_contents($sTmpName); - if (function_exists('finfo_file')) - { - // as of PHP 5.3 the fileinfo extension is bundled within PHP - // in which case we don't trust the mime type provided by the browser - $rInfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension - if ($rInfo !== false) - { - $sType = @finfo_file($rInfo, $sTmpName); - if ( ($sType !== false) - && is_string($sType) - && (strlen($sType)>0)) - { - $sMimeType = $sType; - } - } - @finfo_close($rInfo); - } - $oDocument = new ormDocument($doc_content, $sMimeType, $sName); - break; - - case UPLOAD_ERR_NO_FILE: - // no file to load, it's a normal case, just return an empty document - break; - - case UPLOAD_ERR_FORM_SIZE: - case UPLOAD_ERR_INI_SIZE: - throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize'))); - break; - - case UPLOAD_ERR_PARTIAL: - throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.')); - break; - - case UPLOAD_ERR_NO_TMP_DIR: - throw new FileUploadException(Dict::S('UI:Error:NoTmpDir')); - break; - - case UPLOAD_ERR_CANT_WRITE: - throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir'))); - break; - - case UPLOAD_ERR_EXTENSION: - $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; - throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName)); - break; - - default: - throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError)); - break; - - } - } - return $oDocument; - } - - /** - * Interprets the results posted by a normal or paginated list (in multiple selection mode) - * - * @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected - * - * @return Array An array of object IDs corresponding to the objects selected in the set - */ - public static function ReadMultipleSelection($oFullSetFilter) - { - $aSelectedObj = utils::ReadParam('selectObject', array()); - $sSelectionMode = utils::ReadParam('selectionMode', ''); - if ($sSelectionMode != '') - { - // Paginated selection - $aExceptions = utils::ReadParam('storedSelection', array()); - if ($sSelectionMode == 'positive') - { - // Only the explicitely listed items are selected - $aSelectedObj = $aExceptions; - } - else - { - // All items of the set are selected, except the one explicitely listed - $aSelectedObj = array(); - $oFullSet = new DBObjectSet($oFullSetFilter); - $sClassAlias = $oFullSetFilter->GetClassAlias(); - $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field - while($oObj = $oFullSet->Fetch()) - { - if (!in_array($oObj->GetKey(), $aExceptions)) - { - $aSelectedObj[] = $oObj->GetKey(); - } - } - } - } - return $aSelectedObj; - } - - /** - * Interprets the results posted by a normal or paginated list (in multiple selection mode) - * - * @param DBSearch $oFullSetFilter The criteria defining the whole sets of objects being selected - * - * @return Array An array of object IDs:friendlyname corresponding to the objects selected in the set - * @throws \CoreException - */ - public static function ReadMultipleSelectionWithFriendlyname($oFullSetFilter) - { - $sSelectionMode = utils::ReadParam('selectionMode', ''); - - if ($sSelectionMode === '') - { - throw new CoreException('selectionMode is mandatory'); - } - - // Paginated selection - $aSelectedIds = utils::ReadParam('storedSelection', array()); - if (count($aSelectedIds) > 0 ) - { - if ($sSelectionMode == 'positive') - { - // Only the explicitly listed items are selected - $oFullSetFilter->AddCondition('id', $aSelectedIds, 'IN'); - } - else - { - // All items of the set are selected, except the one explicitly listed - $oFullSetFilter->AddCondition('id', $aSelectedIds, 'NOTIN'); - } - } - - $aSelectedObj = array(); - $oFullSet = new DBObjectSet($oFullSetFilter); - $sClassAlias = $oFullSetFilter->GetClassAlias(); - $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field - while ($oObj = $oFullSet->Fetch()) - { - $aSelectedObj[$oObj->GetKey()] = $oObj->Get('friendlyname'); - } - - return $aSelectedObj; - } - - public static function GetNewTransactionId() - { - return privUITransaction::GetNewTransactionId(); - } - - public static function IsTransactionValid($sId, $bRemoveTransaction = true) - { - return privUITransaction::IsTransactionValid($sId, $bRemoveTransaction); - } - - public static function RemoveTransaction($sId) - { - return privUITransaction::RemoveTransaction($sId); - } - - /** - * Returns a unique tmp id for the current upload based on the transaction system (db). - * - * Build as session_id() . '_' . static::GetNewTransactionId() - * - * @return string - */ - public static function GetUploadTempId($sTransactionId = null) - { - if ($sTransactionId === null) - { - $sTransactionId = static::GetNewTransactionId(); - } - return session_id() . '_' . $sTransactionId; - } - - public static function ReadFromFile($sFileName) - { - if (!file_exists($sFileName)) return false; - return file_get_contents($sFileName); - } - - /** - * Helper function to convert a value expressed in a 'user friendly format' - * as in php.ini, e.g. 256k, 2M, 1G etc. Into a number of bytes - * @param mixed $value The value as read from php.ini - * @return number - */ - public static function ConvertToBytes( $value ) - { - $iReturn = $value; - if ( !is_numeric( $value ) ) - { - $iLength = strlen( $value ); - $iReturn = substr( $value, 0, $iLength - 1 ); - $sUnit = strtoupper( substr( $value, $iLength - 1 ) ); - switch ( $sUnit ) - { - case 'G': - $iReturn *= 1024; - case 'M': - $iReturn *= 1024; - case 'K': - $iReturn *= 1024; - } - } - return $iReturn; - } - - /** - * Checks if the memory limit is at least what is required - * - * @param int $memoryLimit set limit in bytes - * @param int $requiredLimit required limit in bytes - * @return bool - */ - public static function IsMemoryLimitOk($memoryLimit, $requiredLimit) - { - return ($memoryLimit >= $requiredLimit) || ($memoryLimit == -1); - } - - /** - * Format a value into a more friendly format (KB, MB, GB, TB) instead a juste a Bytes amount. - * - * @param type $value - * @return string - */ - public static function BytesToFriendlyFormat($value) - { - $sReturn = ''; - // Kilobytes - if ($value >= 1024) - { - $sReturn = 'K'; - $value = $value / 1024; - } - // Megabytes - if ($value >= 1024) - { - $sReturn = 'M'; - $value = $value / 1024; - } - // Gigabytes - if ($value >= 1024) - { - $sReturn = 'G'; - $value = $value / 1024; - } - // Terabytes - if ($value >= 1024) - { - $sReturn = 'T'; - $value = $value / 1024; - } - - $value = round($value, 1); - - return $value . '' . $sReturn . 'B'; - } - - /** - * Helper function to convert a string to a date, given a format specification. It replaces strtotime which does not allow for specifying a date in a french format (for instance) - * Example: StringToTime('01/05/11 12:03:45', '%d/%m/%y %H:%i:%s') - * @param string $sDate - * @param string $sFormat - * @return timestamp or false if the input format is not correct - */ - public static function StringToTime($sDate, $sFormat) - { - // Source: http://php.net/manual/fr/function.strftime.php - // (alternative: http://www.php.net/manual/fr/datetime.formats.date.php) - static $aDateTokens = null; - static $aDateRegexps = null; - if (is_null($aDateTokens)) - { - $aSpec = array( - '%d' =>'(?[0-9]{2})', - '%m' => '(?[0-9]{2})', - '%y' => '(?[0-9]{2})', - '%Y' => '(?[0-9]{4})', - '%H' => '(?[0-2][0-9])', - '%i' => '(?[0-5][0-9])', - '%s' => '(?[0-5][0-9])', - ); - $aDateTokens = array_keys($aSpec); - $aDateRegexps = array_values($aSpec); - } - - $sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat); - - if (preg_match('!^(?)'.$sDateRegexp.'(?)$!', $sDate, $aMatches)) - { - $sYear = isset($aMatches['year']) ? $aMatches['year'] : 0; - $sMonth = isset($aMatches['month']) ? $aMatches['month'] : 1; - $sDay = isset($aMatches['day']) ? $aMatches['day'] : 1; - $sHour = isset($aMatches['hour']) ? $aMatches['hour'] : 0; - $sMinute = isset($aMatches['minute']) ? $aMatches['minute'] : 0; - $sSecond = isset($aMatches['second']) ? $aMatches['second'] : 0; - return strtotime("$sYear-$sMonth-$sDay $sHour:$sMinute:$sSecond"); - } - else - { - return false; - } - // http://www.spaweditor.com/scripts/regex/index.php - } - - /** - * Convert an old date/time format specifciation (using % placeholders) - * to a format compatible with DateTime::createFromFormat - * @param string $sOldDateTimeFormat - * @return string - */ - static public function DateTimeFormatToPHP($sOldDateTimeFormat) - { - $aSearch = array('%d', '%m', '%y', '%Y', '%H', '%i', '%s'); - $aReplacement = array('d', 'm', 'y', 'Y', 'H', 'i', 's'); - return str_replace($aSearch, $aReplacement, $sOldDateTimeFormat); - } - - /** - * @return \Config from the current environement, or if not existing from the production env, else new Config made from scratch - * @uses \MetaModel::GetConfig() don't forget to add the needed require_once(APPROOT.'core/metamodel.class.php'); - */ - static public function GetConfig() - { - if (self::$oConfig == null) - { - self::$oConfig = MetaModel::GetConfig(); - - if (self::$oConfig == null) - { - $sConfigFile = self::GetConfigFilePath(); - if (!file_exists($sConfigFile)) - { - $sConfigFile = self::GetConfigFilePath('production'); - if (!file_exists($sConfigFile)) - { - $sConfigFile = null; - } - } - - self::$oConfig = new Config($sConfigFile); - } - } - return self::$oConfig; - } - - public static function InitTimeZone() { - $oConfig = self::GetConfig(); - $sItopTimeZone = $oConfig->Get('timezone'); - - if (!empty($sItopTimeZone)) - { - date_default_timezone_set($sItopTimeZone); - } - else - { - // Leave as is... up to the admin to set a value somewhere... - // see http://php.net/manual/en/datetime.configuration.php#ini.date.timezone - } - } - - /** - * Returns the absolute URL to the application root path - * - * @return string The absolute URL to the application root, without the first slash - * - * @throws \Exception - */ - static public function GetAbsoluteUrlAppRoot() - { - static $sUrl = null; - if ($sUrl === null) - { - $sUrl = self::GetConfig()->Get('app_root_url'); - if ($sUrl == '') - { - $sUrl = self::GetDefaultUrlAppRoot(); - } - elseif (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1) - { - if (isset($_SERVER['SERVER_NAME'])) - { - $sServerName = $_SERVER['SERVER_NAME']; - } - else - { - // CLI mode ? - $sServerName = php_uname('n'); - } - $sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl); - } - } - return $sUrl; - } - - /** - * Builds an root url from the server's variables. - * For most usages, when an root url is needed, use utils::GetAbsoluteUrlAppRoot() instead as uses this only as a fallback when the app_root_url conf parameter is not defined. - * - * @return string - * - * @throws \Exception - */ - static public function GetDefaultUrlAppRoot() - { - // Build an absolute URL to this page on this server/port - $sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; - $sProtocol = self::IsConnectionSecure() ? 'https' : 'http'; - $iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; - if ($sProtocol == 'http') - { - $sPort = ($iPort == 80) ? '' : ':'.$iPort; - } - else - { - $sPort = ($iPort == 443) ? '' : ':'.$iPort; - } - // $_SERVER['REQUEST_URI'] is empty when running on IIS - // Let's use Ivan Tcholakov's fix (found on www.dokeos.com) - if (!empty($_SERVER['REQUEST_URI'])) - { - $sPath = $_SERVER['REQUEST_URI']; - } - else - { - $sPath = $_SERVER['SCRIPT_NAME']; - if (!empty($_SERVER['QUERY_STRING'])) - { - $sPath .= '?'.$_SERVER['QUERY_STRING']; - } - $_SERVER['REQUEST_URI'] = $sPath; - } - $sPath = $_SERVER['REQUEST_URI']; - - // remove all the parameters from the query string - $iQuestionMarkPos = strpos($sPath, '?'); - if ($iQuestionMarkPos !== false) - { - $sPath = substr($sPath, 0, $iQuestionMarkPos); - } - $sAbsoluteUrl = "$sProtocol://{$sServerName}{$sPort}{$sPath}"; - - $sCurrentScript = realpath($_SERVER['SCRIPT_FILENAME']); - $sCurrentScript = str_replace('\\', '/', $sCurrentScript); // canonical path - $sAppRoot = str_replace('\\', '/', APPROOT); // canonical path - $sCurrentRelativePath = str_replace($sAppRoot, '', $sCurrentScript); - - $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); - if ($sAppRootPos !== false) - { - $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path - } - else - { - // Second attempt without index.php at the end... - $sCurrentRelativePath = str_replace('index.php', '', $sCurrentRelativePath); - $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); - if ($sAppRootPos !== false) - { - $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path - } - else - { - // No luck... - throw new Exception("Failed to determine application root path $sAbsoluteUrl ($sCurrentRelativePath) APPROOT:'$sAppRoot'"); - } - } - return $sAppRootUrl; - } - - /** - * Helper to handle the variety of HTTP servers - * See #286 (fixed in [896]), and #634 (this fix) - * - * Though the official specs says 'a non empty string', some servers like IIS do set it to 'off' ! - * nginx set it to an empty string - * Others might leave it unset (no array entry) - */ - static public function IsConnectionSecure() - { - $bSecured = false; - - if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off')) - { - $bSecured = true; - } - return $bSecured; - } - - /** - * Tells whether or not log off operation is supported. - * Actually in only one case: - * 1) iTop is using an internal authentication - * 2) the user did not log-in using the "basic" mode (i.e basic authentication) or by passing credentials in the URL - * @return boolean True if logoff is supported, false otherwise - */ - static function CanLogOff() - { - $bResult = false; - if(isset($_SESSION['login_mode'])) - { - $sLoginMode = $_SESSION['login_mode']; - switch($sLoginMode) - { - case 'external': - $bResult = false; - break; - - case 'form': - case 'basic': - case 'url': - case 'cas': - default: - $bResult = true; - - } - } - return $bResult; - } - - /** - * Initializes the CAS client - */ - static function InitCASClient() - { - $sCASIncludePath = self::GetConfig()->Get('cas_include_path'); - include_once($sCASIncludePath.'/CAS.php'); - - $bCASDebug = self::GetConfig()->Get('cas_debug'); - if ($bCASDebug) - { - phpCAS::setDebug(APPROOT.'log/error.log'); - } - - if (!self::$m_bCASClient) - { - // Initialize phpCAS - $sCASVersion = self::GetConfig()->Get('cas_version'); - $sCASHost = self::GetConfig()->Get('cas_host'); - $iCASPort = self::GetConfig()->Get('cas_port'); - $sCASContext = self::GetConfig()->Get('cas_context'); - phpCAS::client($sCASVersion, $sCASHost, $iCASPort, $sCASContext, false /* session already started */); - self::$m_bCASClient = true; - $sCASCACertPath = self::GetConfig()->Get('cas_server_ca_cert_path'); - if (empty($sCASCACertPath)) - { - // If no certificate authority is provided, do not attempt to validate - // the server's certificate - // THIS SETTING IS NOT RECOMMENDED FOR PRODUCTION. - // VALIDATING THE CAS SERVER IS CRUCIAL TO THE SECURITY OF THE CAS PROTOCOL! - phpCAS::setNoCasServerValidation(); - } - else - { - phpCAS::setCasServerCACert($sCASCACertPath); - } - } - } - - static function DebugBacktrace($iLimit = 5) - { - $aFullTrace = debug_backtrace(); - $aLightTrace = array(); - for($i=1; ($i<=$iLimit && $i < count($aFullTrace)); $i++) // Skip the last function call... which is the call to this function ! - { - $aLightTrace[$i] = $aFullTrace[$i]['function'].'(), called from line '.$aFullTrace[$i]['line'].' in '.$aFullTrace[$i]['file']; - } - echo "

".print_r($aLightTrace, true)."

\n"; - } - - /** - * Execute the given iTop PHP script, passing it the current credentials - * Only CLI mode is supported, because of the need to hand the credentials over to the next process - * Throws an exception if the execution fails or could not be attempted (config issue) - * @param string $sScript Name and relative path to the file (relative to the iTop root dir) - * @param hash $aArguments Associative array of 'arg' => 'value' - * @return array(iCode, array(output lines)) - */ - /** - */ - static function ExecITopScript($sScriptName, $aArguments) - { - $aDisabled = explode(', ', ini_get('disable_functions')); - if (in_array('exec', $aDisabled)) - { - throw new Exception("The PHP exec() function has been disabled on this server"); - } - - $sPHPExec = trim(self::GetConfig()->Get('php_path')); - if (strlen($sPHPExec) == 0) - { - throw new Exception("The path to php must not be empty. Please set a value for 'php_path' in your configuration file."); - } - - $sAuthUser = self::ReadParam('auth_user', '', 'raw_data'); - $sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data'); - $sParamFile = self::GetParamSourceFile('auth_user'); - if (is_null($sParamFile)) - { - $aArguments['auth_user'] = $sAuthUser; - $aArguments['auth_pwd'] = $sAuthPwd; - } - else - { - $aArguments['param_file'] = $sParamFile; - } - - $aArgs = array(); - foreach($aArguments as $sName => $value) - { - // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation - // It suggests to rely on pctnl_* function instead of using escapeshellargs - $aArgs[] = "--$sName=".escapeshellarg($value); - } - $sArgs = implode(' ', $aArgs); - - $sScript = realpath(APPROOT.$sScriptName); - if (!file_exists($sScript)) - { - throw new Exception("Could not find the script file '$sScriptName' from the directory '".APPROOT."'"); - } - - $sCommand = '"'.$sPHPExec.'" '.escapeshellarg($sScript).' -- '.$sArgs; - - if (version_compare(phpversion(), '5.3.0', '<')) - { - if (substr(PHP_OS,0,3) == 'WIN') - { - // Under Windows, and for PHP 5.2.x, the whole command has to be quoted - // Cf PHP doc: http://php.net/manual/fr/function.exec.php, comment from the 27-Dec-2010 - $sCommand = '"'.$sCommand.'"'; - } - } - - $sLastLine = exec($sCommand, $aOutput, $iRes); - if ($iRes == 1) - { - throw new Exception(Dict::S('Core:ExecProcess:Code1')." - ".$sCommand); - } - elseif ($iRes == 255) - { - $sErrors = implode("\n", $aOutput); - throw new Exception(Dict::S('Core:ExecProcess:Code255')." - ".$sCommand.":\n".$sErrors); - } - - //$aOutput[] = $sCommand; - return array($iRes, $aOutput); - } - - /** - * Get the current environment - */ - public static function GetCurrentEnvironment() - { - if (isset($_SESSION['itop_env'])) - { - return $_SESSION['itop_env']; - } - else - { - return ITOP_DEFAULT_ENV; - } - } - - /** - * Returns a path to a folder into which any module can store cache data - * The corresponding folder is created or cleaned upon code compilation - * @return string - */ - public static function GetCachePath() - { - return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/'; - } - /** - * Merge standard menu items with plugin provided menus items - */ - public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions, $sTableId = null, $sDataTableId = null) - { - // 1st - add standard built-in menu items - // - switch($iMenuId) - { - case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT: - // $param is a DBObjectSet - $oAppContext = new ApplicationContext(); - $sContext = $oAppContext->GetForLink(); - $sDataTableId = is_null($sDataTableId) ? '' : $sDataTableId; - $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($param->GetFilter()->GetClass()); - $sOQL = addslashes($param->GetFilter()->ToOQL(true)); - $sFilter = urlencode($param->GetFilter()->serialize()); - $sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}"; - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - - $aResult = array(); - if (strlen($sUrl) < SERVER_MAX_URL_LENGTH) - { - $aResult[] = new SeparatorPopupMenuItem(); - // Static menus: Email this page, CSV Export & Add to Dashboard - $aResult[] = new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), - "mailto:?body=".urlencode($sUrl).' ' // Add an extra space to make it work in Outlook - ); - } - - if (UserRights::IsActionAllowed($param->GetFilter()->GetClass(), UR_ACTION_BULK_READ, $param) != UR_ALLOWED_NO) - { - // Bulk export actions - $aResult[] = new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '$sDataTableId', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"); - $aResult[] = new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '$sDataTableId', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"); - if (extension_loaded('gd')) - { - // PDF export requires GD - $aResult[] = new JSPopupMenuItem('UI:Menu:ExportPDF', Dict::S('UI:Menu:ExportPDF'), "ExportListDlg('$sOQL', '$sDataTableId', 'pdf', ".json_encode(Dict::S('UI:Menu:ExportPDF')).")"); - } - } - $aResult[] = new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL', '$sContext')"); - $aResult[] = new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"); - - break; - - case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS: - // $param is a DBObject - $oObj = $param; - $sOQL = "SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey(); - $oFilter = DBObjectSearch::FromOQL($sOQL); - $sFilter = $oFilter->serialize(); - $sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey()); - $sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj)); - $oAppContext = new ApplicationContext(); - $sContext = $oAppContext->GetForLink(); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); - - $aResult = array( - new SeparatorPopupMenuItem(), - // Static menus: Email this page & CSV Export - new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook - new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"), - new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"), - new SeparatorPopupMenuItem(), - new URLPopupMenuItem('UI:Menu:PrintableVersion', Dict::S('UI:Menu:PrintableVersion'), $sUrl.'&printable=1', '_blank'), - ); - break; - - case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS: - // $param is a Dashboard - $oAppContext = new ApplicationContext(); - $aParams = $oAppContext->GetAsHash(); - $sMenuId = ApplicationMenu::GetActiveNodeId(); - $sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle')); - $sDlgText = addslashes(Dict::S('UI:ImportDashboardText')); - $sCloseBtn = addslashes(Dict::S('UI:Button:Cancel')); - $aResult = array( - new SeparatorPopupMenuItem(), - new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId), - new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"), - ); - break; - - default: - // Unknown type of menu, do nothing - $aResult = array(); - } - foreach($aResult as $oMenuItem) - { - $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); - } - - // Invoke the plugins - // - foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) - { - if (is_object($param) && !($param instanceof DBObject)) - { - $tmpParam = clone $param; // In case the parameter is an DBObjectSet, clone it to prevent alterations - } - else - { - $tmpParam = $param; - } - foreach($oExtensionInstance->EnumItems($iMenuId, $tmpParam) as $oMenuItem) - { - if (is_object($oMenuItem)) - { - $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); - - foreach($oMenuItem->GetLinkedScripts() as $sLinkedScript) - { - $oPage->add_linked_script($sLinkedScript); - } - } - } - } - } - /** - * Get target configuration file name (including full path) - */ - public static function GetConfigFilePath($sEnvironment = null) - { - if (is_null($sEnvironment)) - { - $sEnvironment = self::GetCurrentEnvironment(); - } - return APPCONF.$sEnvironment.'/'.ITOP_CONFIG_FILE; - } - - /** - * @return string the absolute URL to the modules root path - */ - static public function GetAbsoluteUrlModulesRoot() - { - $sUrl = self::GetAbsoluteUrlAppRoot().'env-'.self::GetCurrentEnvironment().'/'; - return $sUrl; - } - - /** - * To be compatible with this mechanism, the called page must include approot with an absolute path OR not include - * it at all (losing the direct access to the page) : - * - * ```php - * if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); - * require_once(__DIR__.'/../../approot.inc.php'); - * ``` - * - * @param string $sModule - * @param string $sPage - * @param string[] $aArguments - * @param string $sEnvironment - * - * @return string the URL to a page that will execute the requested module page, with query string values url encoded - * - * @see GetExecPageArguments can be used to submit using the GET method (see bug in N.1108) - * @see GetAbsoluteUrlExecPage - */ - static public function GetAbsoluteUrlModulePage($sModule, $sPage, $aArguments = array(), $sEnvironment = null) - { - $aArgs = self::GetExecPageArguments($sModule, $sPage, $aArguments, $sEnvironment); - $sArgs = http_build_query($aArgs); - - return self::GetAbsoluteUrlExecPage()."?".$sArgs; - } - - /** - * @param string $sModule - * @param string $sPage - * @param string[] $aArguments - * @param string $sEnvironment - * - * @return string[] key/value pair for the exec page query string. Warning : values are not url encoded ! - * @throws \Exception if one of the argument has a reserved name - */ - static public function GetExecPageArguments($sModule, $sPage, $aArguments = array(), $sEnvironment = null) - { - $sEnvironment = is_null($sEnvironment) ? self::GetCurrentEnvironment() : $sEnvironment; - $aArgs = array(); - $aArgs['exec_module'] = $sModule; - $aArgs['exec_page'] = $sPage; - $aArgs['exec_env'] = $sEnvironment; - foreach($aArguments as $sName => $sValue) - { - if (($sName == 'exec_module') || ($sName == 'exec_page') || ($sName == 'exec_env')) - { - throw new Exception("Module page: $sName is a reserved page argument name"); - } - $aArgs[$sName] = $sValue; - } - - return $aArgs; - } - - /** - * @return string - */ - static public function GetAbsoluteUrlExecPage() - { - return self::GetAbsoluteUrlAppRoot().'pages/exec.php'; - } - - /** - * Returns a name unique amongst the given list - * @param string $sProposed The default value - * @param array $aExisting An array of existing values (strings) - */ - static public function MakeUniqueName($sProposed, $aExisting) - { - if (in_array($sProposed, $aExisting)) - { - $i = 1; - while (in_array($sProposed.$i, $aExisting) && ($i < 50)) - { - $i++; - } - return $sProposed.$i; - } - else - { - return $sProposed; - } - } - - /** - * Some characters cause troubles with jQuery when used inside DOM IDs, so let's replace them by the safe _ (underscore) - * @param string $sId The ID to sanitize - * @return string The sanitized ID - */ - static public function GetSafeId($sId) - { - return str_replace(array(':', '[', ']', '+', '-'), '_', $sId); - } - - /** - * Helper to execute an HTTP POST request - * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl - * originaly named after do_post_request - * Does not require cUrl but requires openssl for performing https POSTs. - * - * @param string $sUrl The URL to POST the data to - * @param hash $aData The data to POST as an array('param_name' => value) - * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers - * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php - * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones. Example: CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3 - * @return string The result of the POST request - * @throws Exception - */ - static public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = array()) - { - // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request. - - if (function_exists('curl_init')) - { - // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options - // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 - // by setting the SSLVERSION to 3 as done below. - $aHeaders = explode("\n", $sOptionnalHeaders); - $aHTTPHeaders = array(); - foreach($aHeaders as $sHeaderString) - { - if(preg_match('/^([^:]): (.+)$/', $sHeaderString, $aMatches)) - { - $aHTTPHeaders[$aMatches[1]] = $aMatches[2]; - } - } - // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions - $aOptions = array( - CURLOPT_RETURNTRANSFER => true, // return the content of the request - CURLOPT_HEADER => false, // don't return the headers in the output - CURLOPT_FOLLOWLOCATION => true, // follow redirects - CURLOPT_ENCODING => "", // handle all encodings - CURLOPT_USERAGENT => "spider", // who am i - CURLOPT_AUTOREFERER => true, // set referer on redirect - CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect - CURLOPT_TIMEOUT => 120, // timeout on response - CURLOPT_MAXREDIRS => 10, // stop after 10 redirects - CURLOPT_SSL_VERIFYPEER => false, // Disabled SSL Cert checks - // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why - // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 - // CURLOPT_SSLVERSION => 3, - CURLOPT_POST => count($aData), - CURLOPT_POSTFIELDS => http_build_query($aData), - CURLOPT_HTTPHEADER => $aHTTPHeaders, - ); - - $aAllOptions = $aCurlOptions + $aOptions; - $ch = curl_init($sUrl); - curl_setopt_array($ch, $aAllOptions); - $response = curl_exec($ch); - $iErr = curl_errno($ch); - $sErrMsg = curl_error( $ch ); - $aHeaders = curl_getinfo( $ch ); - if ($iErr !== 0) - { - throw new Exception("Problem opening URL: $sUrl, $sErrMsg"); - } - if (is_array($aResponseHeaders)) - { - $aHeaders = curl_getinfo($ch); - foreach($aHeaders as $sCode => $sValue) - { - $sName = str_replace(' ' , '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type" - $aResponseHeaders[$sName] = $sValue; - } - } - curl_close( $ch ); - } - else - { - // cURL is not available let's try with streams and fopen... - - $sData = http_build_query($aData); - $aParams = array('http' => array( - 'method' => 'POST', - 'content' => $sData, - 'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n", - )); - if ($sOptionnalHeaders !== null) - { - $aParams['http']['header'] .= $sOptionnalHeaders; - } - $ctx = stream_context_create($aParams); - - $fp = @fopen($sUrl, 'rb', false, $ctx); - if (!$fp) - { - global $php_errormsg; - if (isset($php_errormsg)) - { - throw new Exception("Wrong URL: $sUrl, $php_errormsg"); - } - elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) - { - throw new Exception("Cannot connect to $sUrl: missing module 'openssl'"); - } - else - { - throw new Exception("Wrong URL: $sUrl"); - } - } - $response = @stream_get_contents($fp); - if ($response === false) - { - throw new Exception("Problem reading data from $sUrl, $php_errormsg"); - } - if (is_array($aResponseHeaders)) - { - $aMeta = stream_get_meta_data($fp); - $aHeaders = $aMeta['wrapper_data']; - foreach($aHeaders as $sHeaderString) - { - if(preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) - { - $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]); - } - } - } - } - return $response; - } - - /** - * Get a standard list of character sets - * - * @param array $aAdditionalEncodings Additional values - * @return array of iconv code => english label, sorted by label - */ - public static function GetPossibleEncodings($aAdditionalEncodings = array()) - { - // Encodings supported: - // ICONV_CODE => Display Name - // Each iconv installation supports different encodings - // Some reasonably common and useful encodings are listed here - $aPossibleEncodings = array( - 'UTF-8' => 'Unicode (UTF-8)', - 'ISO-8859-1' => 'Western (ISO-8859-1)', - 'WINDOWS-1251' => 'Cyrilic (Windows 1251)', - 'WINDOWS-1252' => 'Western (Windows 1252)', - 'ISO-8859-15' => 'Western (ISO-8859-15)', - ); - $aPossibleEncodings = array_merge($aPossibleEncodings, $aAdditionalEncodings); - asort($aPossibleEncodings); - return $aPossibleEncodings; - } - - /** - * Convert a string containing some (valid) HTML markup to plain text - * @param string $sHtml - * @return string - */ - public static function HtmlToText($sHtml) - { - try - { - //return ''.$sHtml; - return \Html2Text\Html2Text::convert(''.$sHtml); - } - catch(Exception $e) - { - return $e->getMessage(); - } - } - - /** - * Convert (?) plain text to some HTML markup by replacing newlines by
tags - * and escaping HTML entities - * @param string $sText - * @return string - */ - public static function TextToHtml($sText) - { - $sText = str_replace("\r\n", "\n", $sText); - $sText = str_replace("\r", "\n", $sText); - return str_replace("\n", '
', htmlentities($sText, ENT_QUOTES, 'UTF-8')); - } - - /** - * Eventually compiles the SASS (.scss) file into the CSS (.css) file - * - * @param string $sSassRelPath Relative path to the SCSS file (must have the extension .scss) - * @param array $aImportPaths Array of absolute paths to load imports from - * @return string Relative path to the CSS file (.css) - */ - static public function GetCSSFromSASS($sSassRelPath, $aImportPaths = null) - { - // Avoiding compilation if file is already a css file. - if (preg_match('/\.css$/', $sSassRelPath)) - { - return $sSassRelPath; - } - - // Setting import paths - if ($aImportPaths === null) - { - $aImportPaths = array(); - } - $aImportPaths[] = APPROOT . '/css'; - - $sSassPath = APPROOT.$sSassRelPath; - $sCssRelPath = preg_replace('/\.scss$/', '.css', $sSassRelPath); - $sCssPath = APPROOT.$sCssRelPath; - clearstatcache(); - if (!file_exists($sCssPath) || (is_writable($sCssPath) && (filemtime($sCssPath) < filemtime($sSassPath)))) - { - require_once(APPROOT.'lib/scssphp/scss.inc.php'); - $oScss = new Compiler(); - $oScss->setImportPaths($aImportPaths); - $oScss->setFormatter('Leafo\\ScssPhp\\Formatter\\Expanded'); - // Temporary disabling max exec time while compiling - $iCurrentMaxExecTime = (int) ini_get('max_execution_time'); - set_time_limit(0); - $sCss = $oScss->compile(file_get_contents($sSassPath)); - set_time_limit($iCurrentMaxExecTime); - file_put_contents($sCssPath, $sCss); - } - return $sCssRelPath; - } - - static public function GetImageSize($sImageData) - { - if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher - { - $aRet = @getimagesizefromstring($sImageData); - } - else if(ini_get('allow_url_fopen')) - { - // work around to avoid creating a tmp file - $sUri = 'data://application/octet-stream;base64,'.base64_encode($sImageData); - $aRet = @getimagesize($sUri); - } - else - { - // Damned, need to create a tmp file - $sTempFile = tempnam(SetupUtils::GetTmpDir(), 'img-'); - @file_put_contents($sTempFile, $sImageData); - $aRet = @getimagesize($sTempFile); - @unlink($sTempFile); - } - return $aRet; - } - - /** - * Resize an image attachment so that it fits in the given dimensions - * @param ormDocument $oImage The original image stored as an ormDocument - * @param int $iWidth Image's original width - * @param int $iHeight Image's original height - * @param int $iMaxImageWidth Maximum width for the resized image - * @param int $iMaxImageHeight Maximum height for the resized image - * @return ormDocument The resampled image - */ - public static function ResizeImageToFit(ormDocument $oImage, $iWidth, $iHeight, $iMaxImageWidth, $iMaxImageHeight) - { - // If image size smaller than maximums, we do nothing - if (($iWidth <= $iMaxImageWidth) && ($iHeight <= $iMaxImageHeight)) - { - return $oImage; - } - - - // If gd extension is not loaded, we put a warning in the log and return the image as is - if (extension_loaded('gd') === false) - { - IssueLog::Warning('Image could not be resized as the "gd" extension does not seem to be loaded. It will remain as ' . $iWidth . 'x' . $iHeight . ' instead of ' . $iMaxImageWidth . 'x' . $iMaxImageHeight); - return $oImage; - } - - - switch($oImage->GetMimeType()) - { - case 'image/gif': - case 'image/jpeg': - case 'image/png': - $img = @imagecreatefromstring($oImage->GetData()); - break; - - default: - // Unsupported image type, return the image as-is - //throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); - return $oImage; - } - if ($img === false) - { - //throw new Exception("Warning: corrupted image: '".$oImage->GetFileName()." / ".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); - return $oImage; - } - else - { - // Let's scale the image, preserving the transparency for GIFs and PNGs - - $fScale = min($iMaxImageWidth / $iWidth, $iMaxImageHeight / $iHeight); - - $iNewWidth = $iWidth * $fScale; - $iNewHeight = $iHeight * $fScale; - - $new = imagecreatetruecolor($iNewWidth, $iNewHeight); - - // Preserve transparency - if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png")) - { - imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127)); - imagealphablending($new, false); - imagesavealpha($new, true); - } - - imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight); - - ob_start(); - switch ($oImage->GetMimeType()) - { - case 'image/gif': - imagegif($new); // send image to output buffer - break; - - case 'image/jpeg': - imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality - break; - - case 'image/png': - imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression - break; - } - $oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName()); - @ob_end_clean(); - - imagedestroy($img); - imagedestroy($new); - - return $oResampledImage; - } - - } - - /** - * Create a 128 bit UUID in the format: {########-####-####-####-############} - * - * Note: this method can be run from the command line as well as from the web server. - * Note2: this method is not cryptographically secure! If you need a cryptographically secure value - * consider using open_ssl or PHP 7 methods. - * @param string $sPrefix - * @return string - */ - static public function CreateUUID($sPrefix = '') - { - $uid = uniqid("", true); - $data = $sPrefix; - $data .= __FILE__; - $data .= mt_rand(); - $hash = strtoupper(hash('ripemd128', $uid . md5($data))); - $sUUID = '{' . - substr($hash, 0, 8) . - '-' . - substr($hash, 8, 4) . - '-' . - substr($hash, 12, 4) . - '-' . - substr($hash, 16, 4) . - '-' . - substr($hash, 20, 12) . - '}'; - return $sUUID; - } - - /** - * Returns the name of the module containing the file where the call to this function is made - * or an empty string if no such module is found (or not called within a module file) - * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module - * @return string - */ - static public function GetCurrentModuleName($iCallDepth = 0) - { - $sCurrentModuleName = ''; - $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); - - foreach(GetModulesInfo() as $sModuleName => $aInfo) - { - if ($aInfo['root_dir'] !== '') - { - $sRootDir = realpath(APPROOT.$aInfo['root_dir']); - - if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) - { - $sCurrentModuleName = $sModuleName; - break; - } - } - } - return $sCurrentModuleName; - } - - /** - * Returns the relative (to APPROOT) path of the root directory of the module containing the file where the call to this function is made - * or an empty string if no such module is found (or not called within a module file) - * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module - * @return string - */ - static public function GetCurrentModuleDir($iCallDepth) - { - $sCurrentModuleDir = ''; - $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); - - foreach(GetModulesInfo() as $sModuleName => $aInfo) - { - if ($aInfo['root_dir'] !== '') - { - $sRootDir = realpath(APPROOT.$aInfo['root_dir']); - - if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) - { - $sCurrentModuleDir = basename($sRootDir); - break; - } - } - } - return $sCurrentModuleDir; - } - - /** - * Returns the base URL for all files in the current module from which this method is called - * or an empty string if no such module is found (or not called within a module file) - * @return string - */ - static public function GetCurrentModuleUrl() - { - $sDir = static::GetCurrentModuleDir(1); - if ( $sDir !== '') - { - return static::GetAbsoluteUrlModulesRoot().'/'.$sDir; - } - return ''; - } - - /** - * Get the value of a given setting for the current module - * @param string $sProperty The name of the property to retrieve - * @param mixed $defaultvalue - * @return mixed - */ - static public function GetCurrentModuleSetting($sProperty, $defaultvalue = null) - { - $sModuleName = static::GetCurrentModuleName(1); - return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue); - } - - /** - * Get the compiled version of a given module, as it was seen by the compiler - * @param string $sModuleName - * @return string|NULL - */ - static public function GetCompiledModuleVersion($sModuleName) - { - $aModulesInfo = GetModulesInfo(); - if (array_key_exists($sModuleName, $aModulesInfo)) - { - return $aModulesInfo[$sModuleName]['version']; - } - return null; - } - - /** - * Check if the given path/url is an http(s) URL - * @param string $sPath - * @return boolean - */ - public static function IsURL($sPath) - { - $bRet = false; - if ((substr($sPath, 0, 7) == 'http://') || (substr($sPath, 0, 8) == 'https://') || (substr($sPath, 0, 8) == 'ftp://')) - { - $bRet = true; - } - return $bRet; - } - - /** - * Check if the given URL is a link to download a document/image on the CURRENT iTop - * In such a case we can read the content of the file directly in the database (if the users rights allow) and return the ormDocument - * @param string $sPath - * @return false|ormDocument - * @throws Exception - */ - public static function IsSelfURL($sPath) - { - $result = false; - $sPageUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php'; - if (substr($sPath, 0, strlen($sPageUrl)) == $sPageUrl) - { - // If the URL is an URL pointing to this instance of iTop, then - // extract the "query" part of the URL and analyze it - $sQuery = parse_url($sPath, PHP_URL_QUERY); - if ($sQuery !== null) - { - $aParams = array(); - foreach(explode('&', $sQuery) as $sChunk) - { - $aParts = explode('=', $sChunk); - if (count($aParts) != 2) continue; - $aParams[$aParts[0]] = urldecode($aParts[1]); - } - $result = array_key_exists('operation', $aParams) && array_key_exists('class', $aParams) && array_key_exists('id', $aParams) && array_key_exists('field', $aParams) && ($aParams['operation'] == 'download_document'); - if ($result) - { - // This is a 'download_document' operation, let's retrieve the document directly from the database - $sClass = $aParams['class']; - $iKey = $aParams['id']; - $sAttCode = $aParams['field']; - - $oObj = MetaModel::GetObject($sClass, $iKey, false /* must exist */); // Users rights apply here !! - if ($oObj) - { - /** - * @var ormDocument $result - */ - $result = clone $oObj->Get($sAttCode); - return $result; - } - } - } - throw new Exception('Invalid URL. This iTop URL is not pointing to a valid Document/Image.'); - } - return $result; - } - - /** - * Read the content of a file (and retrieve its MIME type) from either: - * - an URL pointing to a blob (image/document) on the current iTop server - * - an http(s) URL - * - the local file system (but only if you are an administrator) - * @param string $sPath - * @return ormDocument|null - * @throws Exception - */ - public static function FileGetContentsAndMIMEType($sPath) - { - $oUploadedDoc = null; - $aKnownExtensions = array( - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', - 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', - 'pdf' => 'application/pdf', - 'doc' => 'application/msword', - 'dot' => 'application/msword', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', - 'vsd' => 'application/x-visio', - 'vdx' => 'application/visio.drawing', - 'odt' => 'application/vnd.oasis.opendocument.text', - 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', - 'odp' => 'application/vnd.oasis.opendocument.presentation', - 'zip' => 'application/zip', - 'txt' => 'text/plain', - 'htm' => 'text/html', - 'html' => 'text/html', - 'exe' => 'application/octet-stream' - ); - - $sData = null; - $sMimeType = 'text/plain'; // Default MIME Type: treat the file as a bunch a characters... - $sFileName = 'uploaded-file'; // Default name for downloaded-files - $sExtension = '.txt'; // Default file extension in case we don't know the MIME Type - - if(empty($sPath)) - { - // Empty path (NULL or '') means that there is no input, making an empty document. - $oUploadedDoc = new ormDocument('', '', ''); - } - elseif (static::IsURL($sPath)) - { - if ($oUploadedDoc = static::IsSelfURL($sPath)) - { - // Nothing more to do, we've got it !! - } - else - { - // Remote file, let's use the HTTP headers to find the MIME Type - $sData = @file_get_contents($sPath); - if ($sData === false) - { - throw new Exception("Failed to load the file from the URL '$sPath'."); - } - else - { - if (isset($http_response_header)) - { - $aHeaders = static::ParseHeaders($http_response_header); - $sMimeType = array_key_exists('Content-Type', $aHeaders) ? strtolower($aHeaders['Content-Type']) : 'application/x-octet-stream'; - // Compute the file extension from the MIME Type - foreach($aKnownExtensions as $sExtValue => $sMime) - { - if ($sMime === $sMimeType) - { - $sExtension = '.'.$sExtValue; - break; - } - } - } - $sFileName .= $sExtension; - } - $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); - } - } - else if (UserRights::IsAdministrator()) - { - // Only administrators are allowed to read local files - $sData = @file_get_contents($sPath); - if ($sData === false) - { - throw new Exception("Failed to load the file '$sPath'. The file does not exist or the current process is not allowed to access it."); - } - $sExtension = strtolower(pathinfo($sPath, PATHINFO_EXTENSION)); - $sFileName = basename($sPath); - - if (array_key_exists($sExtension, $aKnownExtensions)) - { - $sMimeType = $aKnownExtensions[$sExtension]; - } - else if (extension_loaded('fileinfo')) - { - $finfo = new finfo(FILEINFO_MIME); - $sMimeType = $finfo->file($sPath); - } - $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); - } - return $oUploadedDoc; - } - - protected static function ParseHeaders($aHeaders) - { - $aCleanHeaders = array(); - foreach( $aHeaders as $sKey => $sValue ) - { - $aTokens = explode(':', $sValue, 2); - if(isset($aTokens[1])) - { - $aCleanHeaders[trim($aTokens[0])] = trim($aTokens[1]); - } - else - { - // The header is not in the form Header-Code: Value - $aCleanHeaders[] = $sValue; // Store the value as-is - $aMatches = array(); - // Check if it's not the HTTP response code - if( preg_match("|HTTP/[0-9\.]+\s+([0-9]+)|", $sValue, $aMatches) ) - { - $aCleanHeaders['reponse_code'] = intval($aMatches[1]); - } - } - } - return $aCleanHeaders; - } - - /** - * Return a string based on compilation time or (if not available because the datamodel has not been loaded) - * the version of iTop. This string is useful to prevent browser side caching of content that may vary at each - * (re)installation of iTop (especially during development). - * @return string - */ - public static function GetCacheBusterTimestamp() - { - if(!defined('COMPILATION_TIMESTAMP')) - { - return ITOP_VERSION; - } - return COMPILATION_TIMESTAMP; - } - - /** - * Check if the given class if configured as a high cardinality class. - * - * @param $sClass - * - * @return bool - */ - public static function IsHighCardinality($sClass) - { - if (utils::GetConfig()->Get('search_manual_submit')) - { - return true; - } - $aHugeClasses = MetaModel::GetConfig()->Get('high_cardinality_classes'); - return in_array($sClass, $aHugeClasses); - } - - /** - * Check if iTop is in a development environment (VCS vs build number) - * - * @return bool - */ - public static function IsDevelopmentEnvironment() - { - return ITOP_REVISION === 'svn'; - } -} + + + +/** + * Static class utils + * + * @copyright Copyright (C) 2010-2017 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once(APPROOT.'/core/config.class.inc.php'); +require_once(APPROOT.'/application/transaction.class.inc.php'); +require_once(APPROOT.'application/Html2Text.php'); +require_once(APPROOT.'application/Html2TextException.php'); + +define('ITOP_CONFIG_FILE', 'config-itop.php'); +define('ITOP_DEFAULT_CONFIG_FILE', APPCONF.ITOP_DEFAULT_ENV.'/'.ITOP_CONFIG_FILE); + +define('SERVER_NAME_PLACEHOLDER', '$SERVER_NAME$'); + +define('SERVER_MAX_URL_LENGTH', 2048); + +class FileUploadException extends Exception +{ +} + + +/** + * Helper functions to interact with forms: read parameters, upload files... + * @package iTop + */ +class utils +{ + private static $oConfig = null; + private static $m_bCASClient = false; + + // Parameters loaded from a file, parameters of the page/command line still have precedence + private static $m_aParamsFromFile = null; + private static $m_aParamSource = array(); + + protected static function LoadParamFile($sParamFile) + { + if (!file_exists($sParamFile)) + { + throw new Exception("Could not find the parameter file: '$sParamFile'"); + } + if (!is_readable($sParamFile)) + { + throw new Exception("Could not load parameter file: '$sParamFile'"); + } + $sParams = file_get_contents($sParamFile); + + if (is_null(self::$m_aParamsFromFile)) + { + self::$m_aParamsFromFile = array(); + } + + $aParamLines = explode("\n", $sParams); + foreach ($aParamLines as $sLine) + { + $sLine = trim($sLine); + + // Ignore the line after a '#' + if (($iCommentPos = strpos($sLine, '#')) !== false) + { + $sLine = substr($sLine, 0, $iCommentPos); + $sLine = trim($sLine); + } + + // Note: the line is supposed to be already trimmed + if (preg_match('/^(\S*)\s*=(.*)$/', $sLine, $aMatches)) + { + $sParam = $aMatches[1]; + $value = trim($aMatches[2]); + self::$m_aParamsFromFile[$sParam] = $value; + self::$m_aParamSource[$sParam] = $sParamFile; + } + } + } + + public static function UseParamFile($sParamFileArgName = 'param_file', $bAllowCLI = true) + { + $sFileSpec = self::ReadParam($sParamFileArgName, '', $bAllowCLI, 'raw_data'); + foreach(explode(',', $sFileSpec) as $sFile) + { + $sFile = trim($sFile); + if (!empty($sFile)) + { + self::LoadParamFile($sFile); + } + } + } + + /** + * Return the source file from which the parameter has been found, + * usefull when it comes to pass user credential to a process executed + * in the background + * @param $sName Parameter name + * @return The file name if any, or null + */ + public static function GetParamSourceFile($sName) + { + if (array_key_exists($sName, self::$m_aParamSource)) + { + return self::$m_aParamSource[$sName]; + } + else + { + return null; + } + } + + public static function IsModeCLI() + { + $sSAPIName = php_sapi_name(); + $sCleanName = strtolower(trim($sSAPIName)); + if ($sCleanName == 'cli') + { + return true; + } + else + { + return false; + } + } + + protected static $bPageMode = null; + /** + * @var boolean[] + */ + protected static $aModes = array(); + + public static function InitArchiveMode() + { + if (isset($_SESSION['archive_mode'])) + { + $iDefault = $_SESSION['archive_mode']; + } + else + { + $iDefault = 0; + } + // Read and record the value for switching the archive mode + $iCurrent = self::ReadParam('with-archive', $iDefault); + if (isset($_SESSION)) + { + $_SESSION['archive_mode'] = $iCurrent; + } + // Read and use the value for the current page (web services) + $iCurrent = self::ReadParam('with_archive', $iCurrent, true); + self::$bPageMode = ($iCurrent == 1); + } + + /** + * @param boolean $bMode if true then activate archive mode (archived objects are visible), otherwise archived objects are + * hidden (archive = "soft deletion") + */ + public static function PushArchiveMode($bMode) + { + array_push(self::$aModes, $bMode); + } + + public static function PopArchiveMode() + { + array_pop(self::$aModes); + } + + /** + * @return boolean true if archive mode is enabled + */ + public static function IsArchiveMode() + { + if (count(self::$aModes) > 0) + { + $bRet = end(self::$aModes); + } + else + { + if (self::$bPageMode === null) + { + self::InitArchiveMode(); + } + $bRet = self::$bPageMode; + } + return $bRet; + } + + /** + * Helper to be called by the GUI and define if the user will see obsolete data (otherwise, the user will have to dig further) + * @return bool + */ + public static function ShowObsoleteData() + { + $bDefault = MetaModel::GetConfig()->Get('obsolescence.show_obsolete_data'); // default is false + $bShow = appUserPreferences::GetPref('show_obsolete_data', $bDefault); + if (static::IsArchiveMode()) + { + $bShow = true; + } + return $bShow; + } + + public static function ReadParam($sName, $defaultValue = "", $bAllowCLI = false, $sSanitizationFilter = 'parameter') + { + global $argv; + $retValue = $defaultValue; + + if (!is_null(self::$m_aParamsFromFile)) + { + if (isset(self::$m_aParamsFromFile[$sName])) + { + $retValue = self::$m_aParamsFromFile[$sName]; + } + } + + if (isset($_REQUEST[$sName])) + { + $retValue = $_REQUEST[$sName]; + } + elseif ($bAllowCLI && isset($argv)) + { + foreach($argv as $iArg => $sArg) + { + if (preg_match('/^--'.$sName.'=(.*)$/', $sArg, $aMatches)) + { + $retValue = $aMatches[1]; + } + } + } + return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); + } + + public static function ReadPostedParam($sName, $defaultValue = '', $sSanitizationFilter = 'parameter') + { + $retValue = isset($_POST[$sName]) ? $_POST[$sName] : $defaultValue; + return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter); + } + + public static function Sanitize($value, $defaultValue, $sSanitizationFilter) + { + if ($value === $defaultValue) + { + // Preserve the real default value (can be used to detect missing mandatory parameters) + $retValue = $value; + } + else + { + $retValue = self::Sanitize_Internal($value, $sSanitizationFilter); + if ($retValue === false) + { + $retValue = $defaultValue; + } + } + return $retValue; + } + + protected static function Sanitize_Internal($value, $sSanitizationFilter) + { + switch($sSanitizationFilter) + { + case 'integer': + $retValue = filter_var($value, FILTER_SANITIZE_NUMBER_INT); + break; + + case 'class': + $retValue = $value; + if (!MetaModel::IsValidClass($value)) + { + $retValue = false; + } + break; + + case 'string': + $retValue = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS); + break; + + case 'context_param': + case 'parameter': + case 'field_name': + if (is_array($value)) + { + $retValue = array(); + foreach($value as $key => $val) + { + $retValue[$key] = self::Sanitize_Internal($val, $sSanitizationFilter); // recursively check arrays + if ($retValue[$key] === false) + { + $retValue = false; + break; + } + } + } + else + { + switch($sSanitizationFilter) + { + case 'parameter': + $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^([ A-Za-z0-9_=-]|%3D|%2B|%2F)*$/'))); // the '=', '%3D, '%2B', '%2F' characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC) + break; + + case 'field_name': + $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name + break; + + case 'context_param': + $retValue = filter_var($value, FILTER_VALIDATE_REGEXP, array("options"=>array("regexp"=>'/^[ A-Za-z0-9_=%:+-]*$/'))); + break; + + } + } + break; + + default: + case 'raw_data': + $retValue = $value; + // Do nothing + } + return $retValue; + } + + /** + * Reads an uploaded file and turns it into an ormDocument object - Triggers an exception in case of error + * @param string $sName Name of the input used from uploading the file + * @param string $sIndex If Name is an array of posted files, then the index must be used to point out the file + * @return ormDocument The uploaded file (can be 'empty' if nothing was uploaded) + */ + public static function ReadPostedDocument($sName, $sIndex = null) + { + $oDocument = new ormDocument(); // an empty document + if(isset($_FILES[$sName])) + { + $aFileInfo = $_FILES[$sName]; + + $sError = is_null($sIndex) ? $aFileInfo['error'] : $aFileInfo['error'][$sIndex]; + switch($sError) + { + case UPLOAD_ERR_OK: + $sTmpName = is_null($sIndex) ? $aFileInfo['tmp_name'] : $aFileInfo['tmp_name'][$sIndex]; + $sMimeType = is_null($sIndex) ? $aFileInfo['type'] : $aFileInfo['type'][$sIndex]; + $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; + + $doc_content = file_get_contents($sTmpName); + if (function_exists('finfo_file')) + { + // as of PHP 5.3 the fileinfo extension is bundled within PHP + // in which case we don't trust the mime type provided by the browser + $rInfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension + if ($rInfo !== false) + { + $sType = @finfo_file($rInfo, $sTmpName); + if ( ($sType !== false) + && is_string($sType) + && (strlen($sType)>0)) + { + $sMimeType = $sType; + } + } + @finfo_close($rInfo); + } + $oDocument = new ormDocument($doc_content, $sMimeType, $sName); + break; + + case UPLOAD_ERR_NO_FILE: + // no file to load, it's a normal case, just return an empty document + break; + + case UPLOAD_ERR_FORM_SIZE: + case UPLOAD_ERR_INI_SIZE: + throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize'))); + break; + + case UPLOAD_ERR_PARTIAL: + throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.')); + break; + + case UPLOAD_ERR_NO_TMP_DIR: + throw new FileUploadException(Dict::S('UI:Error:NoTmpDir')); + break; + + case UPLOAD_ERR_CANT_WRITE: + throw new FileUploadException(Dict::Format('UI:Error:CannotWriteToTmp_Dir', ini_get('upload_tmp_dir'))); + break; + + case UPLOAD_ERR_EXTENSION: + $sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex]; + throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName)); + break; + + default: + throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError)); + break; + + } + } + return $oDocument; + } + + /** + * Interprets the results posted by a normal or paginated list (in multiple selection mode) + * + * @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected + * + * @return Array An array of object IDs corresponding to the objects selected in the set + */ + public static function ReadMultipleSelection($oFullSetFilter) + { + $aSelectedObj = utils::ReadParam('selectObject', array()); + $sSelectionMode = utils::ReadParam('selectionMode', ''); + if ($sSelectionMode != '') + { + // Paginated selection + $aExceptions = utils::ReadParam('storedSelection', array()); + if ($sSelectionMode == 'positive') + { + // Only the explicitely listed items are selected + $aSelectedObj = $aExceptions; + } + else + { + // All items of the set are selected, except the one explicitely listed + $aSelectedObj = array(); + $oFullSet = new DBObjectSet($oFullSetFilter); + $sClassAlias = $oFullSetFilter->GetClassAlias(); + $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field + while($oObj = $oFullSet->Fetch()) + { + if (!in_array($oObj->GetKey(), $aExceptions)) + { + $aSelectedObj[] = $oObj->GetKey(); + } + } + } + } + return $aSelectedObj; + } + + /** + * Interprets the results posted by a normal or paginated list (in multiple selection mode) + * + * @param DBSearch $oFullSetFilter The criteria defining the whole sets of objects being selected + * + * @return Array An array of object IDs:friendlyname corresponding to the objects selected in the set + * @throws \CoreException + */ + public static function ReadMultipleSelectionWithFriendlyname($oFullSetFilter) + { + $sSelectionMode = utils::ReadParam('selectionMode', ''); + + if ($sSelectionMode === '') + { + throw new CoreException('selectionMode is mandatory'); + } + + // Paginated selection + $aSelectedIds = utils::ReadParam('storedSelection', array()); + if (count($aSelectedIds) > 0 ) + { + if ($sSelectionMode == 'positive') + { + // Only the explicitly listed items are selected + $oFullSetFilter->AddCondition('id', $aSelectedIds, 'IN'); + } + else + { + // All items of the set are selected, except the one explicitly listed + $oFullSetFilter->AddCondition('id', $aSelectedIds, 'NOTIN'); + } + } + + $aSelectedObj = array(); + $oFullSet = new DBObjectSet($oFullSetFilter); + $sClassAlias = $oFullSetFilter->GetClassAlias(); + $oFullSet->OptimizeColumnLoad(array($sClassAlias => array('friendlyname'))); // We really need only the IDs but it does not work since id is not a real field + while ($oObj = $oFullSet->Fetch()) + { + $aSelectedObj[$oObj->GetKey()] = $oObj->Get('friendlyname'); + } + + return $aSelectedObj; + } + + public static function GetNewTransactionId() + { + return privUITransaction::GetNewTransactionId(); + } + + public static function IsTransactionValid($sId, $bRemoveTransaction = true) + { + return privUITransaction::IsTransactionValid($sId, $bRemoveTransaction); + } + + public static function RemoveTransaction($sId) + { + return privUITransaction::RemoveTransaction($sId); + } + + /** + * Returns a unique tmp id for the current upload based on the transaction system (db). + * + * Build as session_id() . '_' . static::GetNewTransactionId() + * + * @return string + */ + public static function GetUploadTempId($sTransactionId = null) + { + if ($sTransactionId === null) + { + $sTransactionId = static::GetNewTransactionId(); + } + return session_id() . '_' . $sTransactionId; + } + + public static function ReadFromFile($sFileName) + { + if (!file_exists($sFileName)) return false; + return file_get_contents($sFileName); + } + + /** + * Helper function to convert a value expressed in a 'user friendly format' + * as in php.ini, e.g. 256k, 2M, 1G etc. Into a number of bytes + * @param mixed $value The value as read from php.ini + * @return number + */ + public static function ConvertToBytes( $value ) + { + $iReturn = $value; + if ( !is_numeric( $value ) ) + { + $iLength = strlen( $value ); + $iReturn = substr( $value, 0, $iLength - 1 ); + $sUnit = strtoupper( substr( $value, $iLength - 1 ) ); + switch ( $sUnit ) + { + case 'G': + $iReturn *= 1024; + case 'M': + $iReturn *= 1024; + case 'K': + $iReturn *= 1024; + } + } + return $iReturn; + } + + /** + * Checks if the memory limit is at least what is required + * + * @param int $memoryLimit set limit in bytes + * @param int $requiredLimit required limit in bytes + * @return bool + */ + public static function IsMemoryLimitOk($memoryLimit, $requiredLimit) + { + return ($memoryLimit >= $requiredLimit) || ($memoryLimit == -1); + } + + /** + * Format a value into a more friendly format (KB, MB, GB, TB) instead a juste a Bytes amount. + * + * @param type $value + * @return string + */ + public static function BytesToFriendlyFormat($value) + { + $sReturn = ''; + // Kilobytes + if ($value >= 1024) + { + $sReturn = 'K'; + $value = $value / 1024; + } + // Megabytes + if ($value >= 1024) + { + $sReturn = 'M'; + $value = $value / 1024; + } + // Gigabytes + if ($value >= 1024) + { + $sReturn = 'G'; + $value = $value / 1024; + } + // Terabytes + if ($value >= 1024) + { + $sReturn = 'T'; + $value = $value / 1024; + } + + $value = round($value, 1); + + return $value . '' . $sReturn . 'B'; + } + + /** + * Helper function to convert a string to a date, given a format specification. It replaces strtotime which does not allow for specifying a date in a french format (for instance) + * Example: StringToTime('01/05/11 12:03:45', '%d/%m/%y %H:%i:%s') + * @param string $sDate + * @param string $sFormat + * @return timestamp or false if the input format is not correct + */ + public static function StringToTime($sDate, $sFormat) + { + // Source: http://php.net/manual/fr/function.strftime.php + // (alternative: http://www.php.net/manual/fr/datetime.formats.date.php) + static $aDateTokens = null; + static $aDateRegexps = null; + if (is_null($aDateTokens)) + { + $aSpec = array( + '%d' =>'(?[0-9]{2})', + '%m' => '(?[0-9]{2})', + '%y' => '(?[0-9]{2})', + '%Y' => '(?[0-9]{4})', + '%H' => '(?[0-2][0-9])', + '%i' => '(?[0-5][0-9])', + '%s' => '(?[0-5][0-9])', + ); + $aDateTokens = array_keys($aSpec); + $aDateRegexps = array_values($aSpec); + } + + $sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat); + + if (preg_match('!^(?)'.$sDateRegexp.'(?)$!', $sDate, $aMatches)) + { + $sYear = isset($aMatches['year']) ? $aMatches['year'] : 0; + $sMonth = isset($aMatches['month']) ? $aMatches['month'] : 1; + $sDay = isset($aMatches['day']) ? $aMatches['day'] : 1; + $sHour = isset($aMatches['hour']) ? $aMatches['hour'] : 0; + $sMinute = isset($aMatches['minute']) ? $aMatches['minute'] : 0; + $sSecond = isset($aMatches['second']) ? $aMatches['second'] : 0; + return strtotime("$sYear-$sMonth-$sDay $sHour:$sMinute:$sSecond"); + } + else + { + return false; + } + // http://www.spaweditor.com/scripts/regex/index.php + } + + /** + * Convert an old date/time format specifciation (using % placeholders) + * to a format compatible with DateTime::createFromFormat + * @param string $sOldDateTimeFormat + * @return string + */ + static public function DateTimeFormatToPHP($sOldDateTimeFormat) + { + $aSearch = array('%d', '%m', '%y', '%Y', '%H', '%i', '%s'); + $aReplacement = array('d', 'm', 'y', 'Y', 'H', 'i', 's'); + return str_replace($aSearch, $aReplacement, $sOldDateTimeFormat); + } + + /** + * @return \Config from the current environement, or if not existing from the production env, else new Config made from scratch + * @uses \MetaModel::GetConfig() don't forget to add the needed require_once(APPROOT.'core/metamodel.class.php'); + */ + static public function GetConfig() + { + if (self::$oConfig == null) + { + self::$oConfig = MetaModel::GetConfig(); + + if (self::$oConfig == null) + { + $sConfigFile = self::GetConfigFilePath(); + if (!file_exists($sConfigFile)) + { + $sConfigFile = self::GetConfigFilePath('production'); + if (!file_exists($sConfigFile)) + { + $sConfigFile = null; + } + } + + self::$oConfig = new Config($sConfigFile); + } + } + return self::$oConfig; + } + + public static function InitTimeZone() { + $oConfig = self::GetConfig(); + $sItopTimeZone = $oConfig->Get('timezone'); + + if (!empty($sItopTimeZone)) + { + date_default_timezone_set($sItopTimeZone); + } + else + { + // Leave as is... up to the admin to set a value somewhere... + // see http://php.net/manual/en/datetime.configuration.php#ini.date.timezone + } + } + + /** + * Returns the absolute URL to the application root path + * + * @return string The absolute URL to the application root, without the first slash + * + * @throws \Exception + */ + static public function GetAbsoluteUrlAppRoot() + { + static $sUrl = null; + if ($sUrl === null) + { + $sUrl = self::GetConfig()->Get('app_root_url'); + if ($sUrl == '') + { + $sUrl = self::GetDefaultUrlAppRoot(); + } + elseif (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1) + { + if (isset($_SERVER['SERVER_NAME'])) + { + $sServerName = $_SERVER['SERVER_NAME']; + } + else + { + // CLI mode ? + $sServerName = php_uname('n'); + } + $sUrl = str_replace(SERVER_NAME_PLACEHOLDER, $sServerName, $sUrl); + } + } + return $sUrl; + } + + /** + * Builds an root url from the server's variables. + * For most usages, when an root url is needed, use utils::GetAbsoluteUrlAppRoot() instead as uses this only as a fallback when the app_root_url conf parameter is not defined. + * + * @return string + * + * @throws \Exception + */ + static public function GetDefaultUrlAppRoot() + { + // Build an absolute URL to this page on this server/port + $sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; + $sProtocol = self::IsConnectionSecure() ? 'https' : 'http'; + $iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80; + if ($sProtocol == 'http') + { + $sPort = ($iPort == 80) ? '' : ':'.$iPort; + } + else + { + $sPort = ($iPort == 443) ? '' : ':'.$iPort; + } + // $_SERVER['REQUEST_URI'] is empty when running on IIS + // Let's use Ivan Tcholakov's fix (found on www.dokeos.com) + if (!empty($_SERVER['REQUEST_URI'])) + { + $sPath = $_SERVER['REQUEST_URI']; + } + else + { + $sPath = $_SERVER['SCRIPT_NAME']; + if (!empty($_SERVER['QUERY_STRING'])) + { + $sPath .= '?'.$_SERVER['QUERY_STRING']; + } + $_SERVER['REQUEST_URI'] = $sPath; + } + $sPath = $_SERVER['REQUEST_URI']; + + // remove all the parameters from the query string + $iQuestionMarkPos = strpos($sPath, '?'); + if ($iQuestionMarkPos !== false) + { + $sPath = substr($sPath, 0, $iQuestionMarkPos); + } + $sAbsoluteUrl = "$sProtocol://{$sServerName}{$sPort}{$sPath}"; + + $sCurrentScript = realpath($_SERVER['SCRIPT_FILENAME']); + $sCurrentScript = str_replace('\\', '/', $sCurrentScript); // canonical path + $sAppRoot = str_replace('\\', '/', APPROOT); // canonical path + $sCurrentRelativePath = str_replace($sAppRoot, '', $sCurrentScript); + + $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); + if ($sAppRootPos !== false) + { + $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path + } + else + { + // Second attempt without index.php at the end... + $sCurrentRelativePath = str_replace('index.php', '', $sCurrentRelativePath); + $sAppRootPos = strpos($sAbsoluteUrl, $sCurrentRelativePath); + if ($sAppRootPos !== false) + { + $sAppRootUrl = substr($sAbsoluteUrl, 0, $sAppRootPos); // remove the current page and path + } + else + { + // No luck... + throw new Exception("Failed to determine application root path $sAbsoluteUrl ($sCurrentRelativePath) APPROOT:'$sAppRoot'"); + } + } + return $sAppRootUrl; + } + + /** + * Helper to handle the variety of HTTP servers + * See #286 (fixed in [896]), and #634 (this fix) + * + * Though the official specs says 'a non empty string', some servers like IIS do set it to 'off' ! + * nginx set it to an empty string + * Others might leave it unset (no array entry) + */ + static public function IsConnectionSecure() + { + $bSecured = false; + + if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off')) + { + $bSecured = true; + } + return $bSecured; + } + + /** + * Tells whether or not log off operation is supported. + * Actually in only one case: + * 1) iTop is using an internal authentication + * 2) the user did not log-in using the "basic" mode (i.e basic authentication) or by passing credentials in the URL + * @return boolean True if logoff is supported, false otherwise + */ + static function CanLogOff() + { + $bResult = false; + if(isset($_SESSION['login_mode'])) + { + $sLoginMode = $_SESSION['login_mode']; + switch($sLoginMode) + { + case 'external': + $bResult = false; + break; + + case 'form': + case 'basic': + case 'url': + case 'cas': + default: + $bResult = true; + + } + } + return $bResult; + } + + /** + * Initializes the CAS client + */ + static function InitCASClient() + { + $sCASIncludePath = self::GetConfig()->Get('cas_include_path'); + include_once($sCASIncludePath.'/CAS.php'); + + $bCASDebug = self::GetConfig()->Get('cas_debug'); + if ($bCASDebug) + { + phpCAS::setDebug(APPROOT.'log/error.log'); + } + + if (!self::$m_bCASClient) + { + // Initialize phpCAS + $sCASVersion = self::GetConfig()->Get('cas_version'); + $sCASHost = self::GetConfig()->Get('cas_host'); + $iCASPort = self::GetConfig()->Get('cas_port'); + $sCASContext = self::GetConfig()->Get('cas_context'); + phpCAS::client($sCASVersion, $sCASHost, $iCASPort, $sCASContext, false /* session already started */); + self::$m_bCASClient = true; + $sCASCACertPath = self::GetConfig()->Get('cas_server_ca_cert_path'); + if (empty($sCASCACertPath)) + { + // If no certificate authority is provided, do not attempt to validate + // the server's certificate + // THIS SETTING IS NOT RECOMMENDED FOR PRODUCTION. + // VALIDATING THE CAS SERVER IS CRUCIAL TO THE SECURITY OF THE CAS PROTOCOL! + phpCAS::setNoCasServerValidation(); + } + else + { + phpCAS::setCasServerCACert($sCASCACertPath); + } + } + } + + static function DebugBacktrace($iLimit = 5) + { + $aFullTrace = debug_backtrace(); + $aLightTrace = array(); + for($i=1; ($i<=$iLimit && $i < count($aFullTrace)); $i++) // Skip the last function call... which is the call to this function ! + { + $aLightTrace[$i] = $aFullTrace[$i]['function'].'(), called from line '.$aFullTrace[$i]['line'].' in '.$aFullTrace[$i]['file']; + } + echo "

".print_r($aLightTrace, true)."

\n"; + } + + /** + * Execute the given iTop PHP script, passing it the current credentials + * Only CLI mode is supported, because of the need to hand the credentials over to the next process + * Throws an exception if the execution fails or could not be attempted (config issue) + * @param string $sScript Name and relative path to the file (relative to the iTop root dir) + * @param hash $aArguments Associative array of 'arg' => 'value' + * @return array(iCode, array(output lines)) + */ + /** + */ + static function ExecITopScript($sScriptName, $aArguments) + { + $aDisabled = explode(', ', ini_get('disable_functions')); + if (in_array('exec', $aDisabled)) + { + throw new Exception("The PHP exec() function has been disabled on this server"); + } + + $sPHPExec = trim(self::GetConfig()->Get('php_path')); + if (strlen($sPHPExec) == 0) + { + throw new Exception("The path to php must not be empty. Please set a value for 'php_path' in your configuration file."); + } + + $sAuthUser = self::ReadParam('auth_user', '', 'raw_data'); + $sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data'); + $sParamFile = self::GetParamSourceFile('auth_user'); + if (is_null($sParamFile)) + { + $aArguments['auth_user'] = $sAuthUser; + $aArguments['auth_pwd'] = $sAuthPwd; + } + else + { + $aArguments['param_file'] = $sParamFile; + } + + $aArgs = array(); + foreach($aArguments as $sName => $value) + { + // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation + // It suggests to rely on pctnl_* function instead of using escapeshellargs + $aArgs[] = "--$sName=".escapeshellarg($value); + } + $sArgs = implode(' ', $aArgs); + + $sScript = realpath(APPROOT.$sScriptName); + if (!file_exists($sScript)) + { + throw new Exception("Could not find the script file '$sScriptName' from the directory '".APPROOT."'"); + } + + $sCommand = '"'.$sPHPExec.'" '.escapeshellarg($sScript).' -- '.$sArgs; + + if (version_compare(phpversion(), '5.3.0', '<')) + { + if (substr(PHP_OS,0,3) == 'WIN') + { + // Under Windows, and for PHP 5.2.x, the whole command has to be quoted + // Cf PHP doc: http://php.net/manual/fr/function.exec.php, comment from the 27-Dec-2010 + $sCommand = '"'.$sCommand.'"'; + } + } + + $sLastLine = exec($sCommand, $aOutput, $iRes); + if ($iRes == 1) + { + throw new Exception(Dict::S('Core:ExecProcess:Code1')." - ".$sCommand); + } + elseif ($iRes == 255) + { + $sErrors = implode("\n", $aOutput); + throw new Exception(Dict::S('Core:ExecProcess:Code255')." - ".$sCommand.":\n".$sErrors); + } + + //$aOutput[] = $sCommand; + return array($iRes, $aOutput); + } + + /** + * Get the current environment + */ + public static function GetCurrentEnvironment() + { + if (isset($_SESSION['itop_env'])) + { + return $_SESSION['itop_env']; + } + else + { + return ITOP_DEFAULT_ENV; + } + } + + /** + * Returns a path to a folder into which any module can store cache data + * The corresponding folder is created or cleaned upon code compilation + * @return string + */ + public static function GetCachePath() + { + return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/'; + } + /** + * Merge standard menu items with plugin provided menus items + */ + public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions, $sTableId = null, $sDataTableId = null) + { + // 1st - add standard built-in menu items + // + switch($iMenuId) + { + case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT: + // $param is a DBObjectSet + $oAppContext = new ApplicationContext(); + $sContext = $oAppContext->GetForLink(); + $sDataTableId = is_null($sDataTableId) ? '' : $sDataTableId; + $sUIPage = cmdbAbstractObject::ComputeStandardUIPage($param->GetFilter()->GetClass()); + $sOQL = addslashes($param->GetFilter()->ToOQL(true)); + $sFilter = urlencode($param->GetFilter()->serialize()); + $sUrl = utils::GetAbsoluteUrlAppRoot()."pages/$sUIPage?operation=search&filter=".$sFilter."&{$sContext}"; + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + + $aResult = array(); + if (strlen($sUrl) < SERVER_MAX_URL_LENGTH) + { + $aResult[] = new SeparatorPopupMenuItem(); + // Static menus: Email this page, CSV Export & Add to Dashboard + $aResult[] = new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), + "mailto:?body=".urlencode($sUrl).' ' // Add an extra space to make it work in Outlook + ); + } + + if (UserRights::IsActionAllowed($param->GetFilter()->GetClass(), UR_ACTION_BULK_READ, $param) != UR_ALLOWED_NO) + { + // Bulk export actions + $aResult[] = new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '$sDataTableId', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"); + $aResult[] = new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '$sDataTableId', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"); + if (extension_loaded('gd')) + { + // PDF export requires GD + $aResult[] = new JSPopupMenuItem('UI:Menu:ExportPDF', Dict::S('UI:Menu:ExportPDF'), "ExportListDlg('$sOQL', '$sDataTableId', 'pdf', ".json_encode(Dict::S('UI:Menu:ExportPDF')).")"); + } + } + $aResult[] = new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL', '$sContext')"); + $aResult[] = new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')"); + + break; + + case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS: + // $param is a DBObject + $oObj = $param; + $sOQL = "SELECT ".get_class($oObj)." WHERE id=".$oObj->GetKey(); + $oFilter = DBObjectSearch::FromOQL($sOQL); + $sFilter = $oFilter->serialize(); + $sUrl = ApplicationContext::MakeObjectUrl(get_class($oObj), $oObj->GetKey()); + $sUIPage = cmdbAbstractObject::ComputeStandardUIPage(get_class($oObj)); + $oAppContext = new ApplicationContext(); + $sContext = $oAppContext->GetForLink(); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js'); + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js'); + $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css'); + + $aResult = array( + new SeparatorPopupMenuItem(), + // Static menus: Email this page & CSV Export + new URLPopupMenuItem('UI:Menu:EMail', Dict::S('UI:Menu:EMail'), "mailto:?subject=".urlencode($oObj->GetRawName())."&body=".urlencode($sUrl).' '), // Add an extra space to make it work in Outlook + new JSPopupMenuItem('UI:Menu:CSVExport', Dict::S('UI:Menu:CSVExport'), "ExportListDlg('$sOQL', '', 'csv', ".json_encode(Dict::S('UI:Menu:CSVExport')).")"), + new JSPopupMenuItem('UI:Menu:ExportXLSX', Dict::S('ExcelExporter:ExportMenu'), "ExportListDlg('$sOQL', '', 'xlsx', ".json_encode(Dict::S('ExcelExporter:ExportMenu')).")"), + new SeparatorPopupMenuItem(), + new URLPopupMenuItem('UI:Menu:PrintableVersion', Dict::S('UI:Menu:PrintableVersion'), $sUrl.'&printable=1', '_blank'), + ); + break; + + case iPopupMenuExtension::MENU_DASHBOARD_ACTIONS: + // $param is a Dashboard + $oAppContext = new ApplicationContext(); + $aParams = $oAppContext->GetAsHash(); + $sMenuId = ApplicationMenu::GetActiveNodeId(); + $sDlgTitle = addslashes(Dict::S('UI:ImportDashboardTitle')); + $sDlgText = addslashes(Dict::S('UI:ImportDashboardText')); + $sCloseBtn = addslashes(Dict::S('UI:Button:Cancel')); + $aResult = array( + new SeparatorPopupMenuItem(), + new URLPopupMenuItem('UI:ExportDashboard', Dict::S('UI:ExportDashBoard'), utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=export_dashboard&id='.$sMenuId), + new JSPopupMenuItem('UI:ImportDashboard', Dict::S('UI:ImportDashBoard'), "UploadDashboard({dashboard_id: '$sMenuId', title: '$sDlgTitle', text: '$sDlgText', close_btn: '$sCloseBtn' })"), + ); + break; + + default: + // Unknown type of menu, do nothing + $aResult = array(); + } + foreach($aResult as $oMenuItem) + { + $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); + } + + // Invoke the plugins + // + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) + { + if (is_object($param) && !($param instanceof DBObject)) + { + $tmpParam = clone $param; // In case the parameter is an DBObjectSet, clone it to prevent alterations + } + else + { + $tmpParam = $param; + } + foreach($oExtensionInstance->EnumItems($iMenuId, $tmpParam) as $oMenuItem) + { + if (is_object($oMenuItem)) + { + $aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem(); + + foreach($oMenuItem->GetLinkedScripts() as $sLinkedScript) + { + $oPage->add_linked_script($sLinkedScript); + } + } + } + } + } + /** + * Get target configuration file name (including full path) + */ + public static function GetConfigFilePath($sEnvironment = null) + { + if (is_null($sEnvironment)) + { + $sEnvironment = self::GetCurrentEnvironment(); + } + return APPCONF.$sEnvironment.'/'.ITOP_CONFIG_FILE; + } + + /** + * @return string the absolute URL to the modules root path + */ + static public function GetAbsoluteUrlModulesRoot() + { + $sUrl = self::GetAbsoluteUrlAppRoot().'env-'.self::GetCurrentEnvironment().'/'; + return $sUrl; + } + + /** + * To be compatible with this mechanism, the called page must include approot with an absolute path OR not include + * it at all (losing the direct access to the page) : + * + * ```php + * if (!defined('__DIR__')) define('__DIR__', dirname(__FILE__)); + * require_once(__DIR__.'/../../approot.inc.php'); + * ``` + * + * @param string $sModule + * @param string $sPage + * @param string[] $aArguments + * @param string $sEnvironment + * + * @return string the URL to a page that will execute the requested module page, with query string values url encoded + * + * @see GetExecPageArguments can be used to submit using the GET method (see bug in N.1108) + * @see GetAbsoluteUrlExecPage + */ + static public function GetAbsoluteUrlModulePage($sModule, $sPage, $aArguments = array(), $sEnvironment = null) + { + $aArgs = self::GetExecPageArguments($sModule, $sPage, $aArguments, $sEnvironment); + $sArgs = http_build_query($aArgs); + + return self::GetAbsoluteUrlExecPage()."?".$sArgs; + } + + /** + * @param string $sModule + * @param string $sPage + * @param string[] $aArguments + * @param string $sEnvironment + * + * @return string[] key/value pair for the exec page query string. Warning : values are not url encoded ! + * @throws \Exception if one of the argument has a reserved name + */ + static public function GetExecPageArguments($sModule, $sPage, $aArguments = array(), $sEnvironment = null) + { + $sEnvironment = is_null($sEnvironment) ? self::GetCurrentEnvironment() : $sEnvironment; + $aArgs = array(); + $aArgs['exec_module'] = $sModule; + $aArgs['exec_page'] = $sPage; + $aArgs['exec_env'] = $sEnvironment; + foreach($aArguments as $sName => $sValue) + { + if (($sName == 'exec_module') || ($sName == 'exec_page') || ($sName == 'exec_env')) + { + throw new Exception("Module page: $sName is a reserved page argument name"); + } + $aArgs[$sName] = $sValue; + } + + return $aArgs; + } + + /** + * @return string + */ + static public function GetAbsoluteUrlExecPage() + { + return self::GetAbsoluteUrlAppRoot().'pages/exec.php'; + } + + /** + * Returns a name unique amongst the given list + * @param string $sProposed The default value + * @param array $aExisting An array of existing values (strings) + */ + static public function MakeUniqueName($sProposed, $aExisting) + { + if (in_array($sProposed, $aExisting)) + { + $i = 1; + while (in_array($sProposed.$i, $aExisting) && ($i < 50)) + { + $i++; + } + return $sProposed.$i; + } + else + { + return $sProposed; + } + } + + /** + * Some characters cause troubles with jQuery when used inside DOM IDs, so let's replace them by the safe _ (underscore) + * @param string $sId The ID to sanitize + * @return string The sanitized ID + */ + static public function GetSafeId($sId) + { + return str_replace(array(':', '[', ']', '+', '-'), '_', $sId); + } + + /** + * Helper to execute an HTTP POST request + * Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl + * originaly named after do_post_request + * Does not require cUrl but requires openssl for performing https POSTs. + * + * @param string $sUrl The URL to POST the data to + * @param hash $aData The data to POST as an array('param_name' => value) + * @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers + * @param hash $aResponseHeaders An array to be filled with reponse headers: WARNING: the actual content of the array depends on the library used: cURL or fopen, test with both !! See: http://fr.php.net/manual/en/function.curl-getinfo.php + * @param hash $aCurlOptions An (optional) array of options to pass to curl_init. The format is 'option_code' => 'value'. These values have precedence over the default ones. Example: CURLOPT_SSLVERSION => CURL_SSLVERSION_SSLv3 + * @return string The result of the POST request + * @throws Exception + */ + static public function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = array()) + { + // $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request. + + if (function_exists('curl_init')) + { + // If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options + // For instance fopen does not allow to work around the bug: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 + // by setting the SSLVERSION to 3 as done below. + $aHeaders = explode("\n", $sOptionnalHeaders); + $aHTTPHeaders = array(); + foreach($aHeaders as $sHeaderString) + { + if(preg_match('/^([^:]): (.+)$/', $sHeaderString, $aMatches)) + { + $aHTTPHeaders[$aMatches[1]] = $aMatches[2]; + } + } + // Default options, can be overloaded/extended with the 4th parameter of this method, see above $aCurlOptions + $aOptions = array( + CURLOPT_RETURNTRANSFER => true, // return the content of the request + CURLOPT_HEADER => false, // don't return the headers in the output + CURLOPT_FOLLOWLOCATION => true, // follow redirects + CURLOPT_ENCODING => "", // handle all encodings + CURLOPT_USERAGENT => "spider", // who am i + CURLOPT_AUTOREFERER => true, // set referer on redirect + CURLOPT_CONNECTTIMEOUT => 120, // timeout on connect + CURLOPT_TIMEOUT => 120, // timeout on response + CURLOPT_MAXREDIRS => 10, // stop after 10 redirects + CURLOPT_SSL_VERIFYPEER => false, // Disabled SSL Cert checks + // SSLV3 (CURL_SSLVERSION_SSLv3 = 3) is now considered as obsolete/dangerous: http://disablessl3.com/#why + // but it used to be a MUST to prevent a strange SSL error: http://stackoverflow.com/questions/18191672/php-curl-ssl-routinesssl23-get-server-helloreason1112 + // CURLOPT_SSLVERSION => 3, + CURLOPT_POST => count($aData), + CURLOPT_POSTFIELDS => http_build_query($aData), + CURLOPT_HTTPHEADER => $aHTTPHeaders, + ); + + $aAllOptions = $aCurlOptions + $aOptions; + $ch = curl_init($sUrl); + curl_setopt_array($ch, $aAllOptions); + $response = curl_exec($ch); + $iErr = curl_errno($ch); + $sErrMsg = curl_error( $ch ); + $aHeaders = curl_getinfo( $ch ); + if ($iErr !== 0) + { + throw new Exception("Problem opening URL: $sUrl, $sErrMsg"); + } + if (is_array($aResponseHeaders)) + { + $aHeaders = curl_getinfo($ch); + foreach($aHeaders as $sCode => $sValue) + { + $sName = str_replace(' ' , '-', ucwords(str_replace('_', ' ', $sCode))); // Transform "content_type" into "Content-Type" + $aResponseHeaders[$sName] = $sValue; + } + } + curl_close( $ch ); + } + else + { + // cURL is not available let's try with streams and fopen... + + $sData = http_build_query($aData); + $aParams = array('http' => array( + 'method' => 'POST', + 'content' => $sData, + 'header'=> "Content-type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($sData)."\r\n", + )); + if ($sOptionnalHeaders !== null) + { + $aParams['http']['header'] .= $sOptionnalHeaders; + } + $ctx = stream_context_create($aParams); + + $fp = @fopen($sUrl, 'rb', false, $ctx); + if (!$fp) + { + global $php_errormsg; + if (isset($php_errormsg)) + { + throw new Exception("Wrong URL: $sUrl, $php_errormsg"); + } + elseif ((strtolower(substr($sUrl, 0, 5)) == 'https') && !extension_loaded('openssl')) + { + throw new Exception("Cannot connect to $sUrl: missing module 'openssl'"); + } + else + { + throw new Exception("Wrong URL: $sUrl"); + } + } + $response = @stream_get_contents($fp); + if ($response === false) + { + throw new Exception("Problem reading data from $sUrl, $php_errormsg"); + } + if (is_array($aResponseHeaders)) + { + $aMeta = stream_get_meta_data($fp); + $aHeaders = $aMeta['wrapper_data']; + foreach($aHeaders as $sHeaderString) + { + if(preg_match('/^([^:]+): (.+)$/', $sHeaderString, $aMatches)) + { + $aResponseHeaders[$aMatches[1]] = trim($aMatches[2]); + } + } + } + } + return $response; + } + + /** + * Get a standard list of character sets + * + * @param array $aAdditionalEncodings Additional values + * @return array of iconv code => english label, sorted by label + */ + public static function GetPossibleEncodings($aAdditionalEncodings = array()) + { + // Encodings supported: + // ICONV_CODE => Display Name + // Each iconv installation supports different encodings + // Some reasonably common and useful encodings are listed here + $aPossibleEncodings = array( + 'UTF-8' => 'Unicode (UTF-8)', + 'ISO-8859-1' => 'Western (ISO-8859-1)', + 'WINDOWS-1251' => 'Cyrilic (Windows 1251)', + 'WINDOWS-1252' => 'Western (Windows 1252)', + 'ISO-8859-15' => 'Western (ISO-8859-15)', + ); + $aPossibleEncodings = array_merge($aPossibleEncodings, $aAdditionalEncodings); + asort($aPossibleEncodings); + return $aPossibleEncodings; + } + + /** + * Convert a string containing some (valid) HTML markup to plain text + * @param string $sHtml + * @return string + */ + public static function HtmlToText($sHtml) + { + try + { + //return ''.$sHtml; + return \Html2Text\Html2Text::convert(''.$sHtml); + } + catch(Exception $e) + { + return $e->getMessage(); + } + } + + /** + * Convert (?) plain text to some HTML markup by replacing newlines by
tags + * and escaping HTML entities + * @param string $sText + * @return string + */ + public static function TextToHtml($sText) + { + $sText = str_replace("\r\n", "\n", $sText); + $sText = str_replace("\r", "\n", $sText); + return str_replace("\n", '
', htmlentities($sText, ENT_QUOTES, 'UTF-8')); + } + + /** + * Eventually compiles the SASS (.scss) file into the CSS (.css) file + * + * @param string $sSassRelPath Relative path to the SCSS file (must have the extension .scss) + * @param array $aImportPaths Array of absolute paths to load imports from + * @return string Relative path to the CSS file (.css) + */ + static public function GetCSSFromSASS($sSassRelPath, $aImportPaths = null) + { + // Avoiding compilation if file is already a css file. + if (preg_match('/\.css$/', $sSassRelPath)) + { + return $sSassRelPath; + } + + // Setting import paths + if ($aImportPaths === null) + { + $aImportPaths = array(); + } + $aImportPaths[] = APPROOT . '/css'; + + $sSassPath = APPROOT.$sSassRelPath; + $sCssRelPath = preg_replace('/\.scss$/', '.css', $sSassRelPath); + $sCssPath = APPROOT.$sCssRelPath; + clearstatcache(); + if (!file_exists($sCssPath) || (is_writable($sCssPath) && (filemtime($sCssPath) < filemtime($sSassPath)))) + { + require_once(APPROOT.'lib/scssphp/scss.inc.php'); + $oScss = new Compiler(); + $oScss->setImportPaths($aImportPaths); + $oScss->setFormatter('Leafo\\ScssPhp\\Formatter\\Expanded'); + // Temporary disabling max exec time while compiling + $iCurrentMaxExecTime = (int) ini_get('max_execution_time'); + set_time_limit(0); + $sCss = $oScss->compile(file_get_contents($sSassPath)); + set_time_limit($iCurrentMaxExecTime); + file_put_contents($sCssPath, $sCss); + } + return $sCssRelPath; + } + + static public function GetImageSize($sImageData) + { + if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher + { + $aRet = @getimagesizefromstring($sImageData); + } + else if(ini_get('allow_url_fopen')) + { + // work around to avoid creating a tmp file + $sUri = 'data://application/octet-stream;base64,'.base64_encode($sImageData); + $aRet = @getimagesize($sUri); + } + else + { + // Damned, need to create a tmp file + $sTempFile = tempnam(SetupUtils::GetTmpDir(), 'img-'); + @file_put_contents($sTempFile, $sImageData); + $aRet = @getimagesize($sTempFile); + @unlink($sTempFile); + } + return $aRet; + } + + /** + * Resize an image attachment so that it fits in the given dimensions + * @param ormDocument $oImage The original image stored as an ormDocument + * @param int $iWidth Image's original width + * @param int $iHeight Image's original height + * @param int $iMaxImageWidth Maximum width for the resized image + * @param int $iMaxImageHeight Maximum height for the resized image + * @return ormDocument The resampled image + */ + public static function ResizeImageToFit(ormDocument $oImage, $iWidth, $iHeight, $iMaxImageWidth, $iMaxImageHeight) + { + // If image size smaller than maximums, we do nothing + if (($iWidth <= $iMaxImageWidth) && ($iHeight <= $iMaxImageHeight)) + { + return $oImage; + } + + + // If gd extension is not loaded, we put a warning in the log and return the image as is + if (extension_loaded('gd') === false) + { + IssueLog::Warning('Image could not be resized as the "gd" extension does not seem to be loaded. It will remain as ' . $iWidth . 'x' . $iHeight . ' instead of ' . $iMaxImageWidth . 'x' . $iMaxImageHeight); + return $oImage; + } + + + switch($oImage->GetMimeType()) + { + case 'image/gif': + case 'image/jpeg': + case 'image/png': + $img = @imagecreatefromstring($oImage->GetData()); + break; + + default: + // Unsupported image type, return the image as-is + //throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); + return $oImage; + } + if ($img === false) + { + //throw new Exception("Warning: corrupted image: '".$oImage->GetFileName()." / ".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used."); + return $oImage; + } + else + { + // Let's scale the image, preserving the transparency for GIFs and PNGs + + $fScale = min($iMaxImageWidth / $iWidth, $iMaxImageHeight / $iHeight); + + $iNewWidth = $iWidth * $fScale; + $iNewHeight = $iHeight * $fScale; + + $new = imagecreatetruecolor($iNewWidth, $iNewHeight); + + // Preserve transparency + if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png")) + { + imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127)); + imagealphablending($new, false); + imagesavealpha($new, true); + } + + imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight); + + ob_start(); + switch ($oImage->GetMimeType()) + { + case 'image/gif': + imagegif($new); // send image to output buffer + break; + + case 'image/jpeg': + imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality + break; + + case 'image/png': + imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression + break; + } + $oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName()); + @ob_end_clean(); + + imagedestroy($img); + imagedestroy($new); + + return $oResampledImage; + } + + } + + /** + * Create a 128 bit UUID in the format: {########-####-####-####-############} + * + * Note: this method can be run from the command line as well as from the web server. + * Note2: this method is not cryptographically secure! If you need a cryptographically secure value + * consider using open_ssl or PHP 7 methods. + * @param string $sPrefix + * @return string + */ + static public function CreateUUID($sPrefix = '') + { + $uid = uniqid("", true); + $data = $sPrefix; + $data .= __FILE__; + $data .= mt_rand(); + $hash = strtoupper(hash('ripemd128', $uid . md5($data))); + $sUUID = '{' . + substr($hash, 0, 8) . + '-' . + substr($hash, 8, 4) . + '-' . + substr($hash, 12, 4) . + '-' . + substr($hash, 16, 4) . + '-' . + substr($hash, 20, 12) . + '}'; + return $sUUID; + } + + /** + * Returns the name of the module containing the file where the call to this function is made + * or an empty string if no such module is found (or not called within a module file) + * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module + * @return string + */ + static public function GetCurrentModuleName($iCallDepth = 0) + { + $sCurrentModuleName = ''; + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); + + foreach(GetModulesInfo() as $sModuleName => $aInfo) + { + if ($aInfo['root_dir'] !== '') + { + $sRootDir = realpath(APPROOT.$aInfo['root_dir']); + + if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) + { + $sCurrentModuleName = $sModuleName; + break; + } + } + } + return $sCurrentModuleName; + } + + /** + * Returns the relative (to APPROOT) path of the root directory of the module containing the file where the call to this function is made + * or an empty string if no such module is found (or not called within a module file) + * @param number $iCallDepth The depth of the module in the callstack. Zero when called directly from within the module + * @return string + */ + static public function GetCurrentModuleDir($iCallDepth) + { + $sCurrentModuleDir = ''; + $aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $sCallerFile = realpath($aCallStack[$iCallDepth]['file']); + + foreach(GetModulesInfo() as $sModuleName => $aInfo) + { + if ($aInfo['root_dir'] !== '') + { + $sRootDir = realpath(APPROOT.$aInfo['root_dir']); + + if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir) + { + $sCurrentModuleDir = basename($sRootDir); + break; + } + } + } + return $sCurrentModuleDir; + } + + /** + * Returns the base URL for all files in the current module from which this method is called + * or an empty string if no such module is found (or not called within a module file) + * @return string + */ + static public function GetCurrentModuleUrl() + { + $sDir = static::GetCurrentModuleDir(1); + if ( $sDir !== '') + { + return static::GetAbsoluteUrlModulesRoot().'/'.$sDir; + } + return ''; + } + + /** + * Get the value of a given setting for the current module + * @param string $sProperty The name of the property to retrieve + * @param mixed $defaultvalue + * @return mixed + */ + static public function GetCurrentModuleSetting($sProperty, $defaultvalue = null) + { + $sModuleName = static::GetCurrentModuleName(1); + return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue); + } + + /** + * Get the compiled version of a given module, as it was seen by the compiler + * @param string $sModuleName + * @return string|NULL + */ + static public function GetCompiledModuleVersion($sModuleName) + { + $aModulesInfo = GetModulesInfo(); + if (array_key_exists($sModuleName, $aModulesInfo)) + { + return $aModulesInfo[$sModuleName]['version']; + } + return null; + } + + /** + * Check if the given path/url is an http(s) URL + * @param string $sPath + * @return boolean + */ + public static function IsURL($sPath) + { + $bRet = false; + if ((substr($sPath, 0, 7) == 'http://') || (substr($sPath, 0, 8) == 'https://') || (substr($sPath, 0, 8) == 'ftp://')) + { + $bRet = true; + } + return $bRet; + } + + /** + * Check if the given URL is a link to download a document/image on the CURRENT iTop + * In such a case we can read the content of the file directly in the database (if the users rights allow) and return the ormDocument + * @param string $sPath + * @return false|ormDocument + * @throws Exception + */ + public static function IsSelfURL($sPath) + { + $result = false; + $sPageUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php'; + if (substr($sPath, 0, strlen($sPageUrl)) == $sPageUrl) + { + // If the URL is an URL pointing to this instance of iTop, then + // extract the "query" part of the URL and analyze it + $sQuery = parse_url($sPath, PHP_URL_QUERY); + if ($sQuery !== null) + { + $aParams = array(); + foreach(explode('&', $sQuery) as $sChunk) + { + $aParts = explode('=', $sChunk); + if (count($aParts) != 2) continue; + $aParams[$aParts[0]] = urldecode($aParts[1]); + } + $result = array_key_exists('operation', $aParams) && array_key_exists('class', $aParams) && array_key_exists('id', $aParams) && array_key_exists('field', $aParams) && ($aParams['operation'] == 'download_document'); + if ($result) + { + // This is a 'download_document' operation, let's retrieve the document directly from the database + $sClass = $aParams['class']; + $iKey = $aParams['id']; + $sAttCode = $aParams['field']; + + $oObj = MetaModel::GetObject($sClass, $iKey, false /* must exist */); // Users rights apply here !! + if ($oObj) + { + /** + * @var ormDocument $result + */ + $result = clone $oObj->Get($sAttCode); + return $result; + } + } + } + throw new Exception('Invalid URL. This iTop URL is not pointing to a valid Document/Image.'); + } + return $result; + } + + /** + * Read the content of a file (and retrieve its MIME type) from either: + * - an URL pointing to a blob (image/document) on the current iTop server + * - an http(s) URL + * - the local file system (but only if you are an administrator) + * @param string $sPath + * @return ormDocument|null + * @throws Exception + */ + public static function FileGetContentsAndMIMEType($sPath) + { + $oUploadedDoc = null; + $aKnownExtensions = array( + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'pdf' => 'application/pdf', + 'doc' => 'application/msword', + 'dot' => 'application/msword', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'vsd' => 'application/x-visio', + 'vdx' => 'application/visio.drawing', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'zip' => 'application/zip', + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'exe' => 'application/octet-stream' + ); + + $sData = null; + $sMimeType = 'text/plain'; // Default MIME Type: treat the file as a bunch a characters... + $sFileName = 'uploaded-file'; // Default name for downloaded-files + $sExtension = '.txt'; // Default file extension in case we don't know the MIME Type + + if(empty($sPath)) + { + // Empty path (NULL or '') means that there is no input, making an empty document. + $oUploadedDoc = new ormDocument('', '', ''); + } + elseif (static::IsURL($sPath)) + { + if ($oUploadedDoc = static::IsSelfURL($sPath)) + { + // Nothing more to do, we've got it !! + } + else + { + // Remote file, let's use the HTTP headers to find the MIME Type + $sData = @file_get_contents($sPath); + if ($sData === false) + { + throw new Exception("Failed to load the file from the URL '$sPath'."); + } + else + { + if (isset($http_response_header)) + { + $aHeaders = static::ParseHeaders($http_response_header); + $sMimeType = array_key_exists('Content-Type', $aHeaders) ? strtolower($aHeaders['Content-Type']) : 'application/x-octet-stream'; + // Compute the file extension from the MIME Type + foreach($aKnownExtensions as $sExtValue => $sMime) + { + if ($sMime === $sMimeType) + { + $sExtension = '.'.$sExtValue; + break; + } + } + } + $sFileName .= $sExtension; + } + $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); + } + } + else if (UserRights::IsAdministrator()) + { + // Only administrators are allowed to read local files + $sData = @file_get_contents($sPath); + if ($sData === false) + { + throw new Exception("Failed to load the file '$sPath'. The file does not exist or the current process is not allowed to access it."); + } + $sExtension = strtolower(pathinfo($sPath, PATHINFO_EXTENSION)); + $sFileName = basename($sPath); + + if (array_key_exists($sExtension, $aKnownExtensions)) + { + $sMimeType = $aKnownExtensions[$sExtension]; + } + else if (extension_loaded('fileinfo')) + { + $finfo = new finfo(FILEINFO_MIME); + $sMimeType = $finfo->file($sPath); + } + $oUploadedDoc = new ormDocument($sData, $sMimeType, $sFileName); + } + return $oUploadedDoc; + } + + protected static function ParseHeaders($aHeaders) + { + $aCleanHeaders = array(); + foreach( $aHeaders as $sKey => $sValue ) + { + $aTokens = explode(':', $sValue, 2); + if(isset($aTokens[1])) + { + $aCleanHeaders[trim($aTokens[0])] = trim($aTokens[1]); + } + else + { + // The header is not in the form Header-Code: Value + $aCleanHeaders[] = $sValue; // Store the value as-is + $aMatches = array(); + // Check if it's not the HTTP response code + if( preg_match("|HTTP/[0-9\.]+\s+([0-9]+)|", $sValue, $aMatches) ) + { + $aCleanHeaders['reponse_code'] = intval($aMatches[1]); + } + } + } + return $aCleanHeaders; + } + + /** + * Return a string based on compilation time or (if not available because the datamodel has not been loaded) + * the version of iTop. This string is useful to prevent browser side caching of content that may vary at each + * (re)installation of iTop (especially during development). + * @return string + */ + public static function GetCacheBusterTimestamp() + { + if(!defined('COMPILATION_TIMESTAMP')) + { + return ITOP_VERSION; + } + return COMPILATION_TIMESTAMP; + } + + /** + * Check if the given class if configured as a high cardinality class. + * + * @param $sClass + * + * @return bool + */ + public static function IsHighCardinality($sClass) + { + if (utils::GetConfig()->Get('search_manual_submit')) + { + return true; + } + $aHugeClasses = MetaModel::GetConfig()->Get('high_cardinality_classes'); + return in_array($sClass, $aHugeClasses); + } + + /** + * Check if iTop is in a development environment (VCS vs build number) + * + * @return bool + */ + public static function IsDevelopmentEnvironment() + { + return ITOP_REVISION === 'svn'; + } +} diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php index 37bcc1382..797dda9e9 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php @@ -1,1531 +1,1531 @@ - - -namespace Combodo\iTop\Portal\Controller; - -use Silex\Application; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Exception; -use FileUploadException; -use utils; -use Dict; -use IssueLog; -use MetaModel; -use DBObject; -use DBSearch; -use DBObjectSearch; -use FalseExpression; -use BinaryExpression; -use FieldExpression; -use VariableExpression; -use ListExpression; -use ScalarExpression; -use DBObjectSet; -use AttributeEnum; -use AttributeImage; -use AttributeFinalClass; -use AttributeFriendlyName; -use UserRights; -use iPopupMenuExtension; -use URLButtonItem; -use JSButtonItem; -use Combodo\iTop\Portal\Helper\ApplicationHelper; -use Combodo\iTop\Portal\Helper\SecurityHelper; -use Combodo\iTop\Portal\Helper\ContextManipulatorHelper; -use Combodo\iTop\Portal\Form\ObjectFormManager; -use Combodo\iTop\Renderer\Bootstrap\BsFormRenderer; - -/** - * Class ObjectController - * - * Controller to handle basic view / edit / create of cmdbAbstractObjectClass ManageBrickController - * - * @package Combodo\iTop\Portal\Controller - * @author Guillaume Lajarige - * @since 2.3.0 - */ -class ObjectController extends AbstractController -{ - - const ENUM_MODE_VIEW = 'view'; - const ENUM_MODE_EDIT = 'edit'; - const ENUM_MODE_CREATE = 'create'; - - const DEFAULT_PAGE_NUMBER = 1; - const DEFAULT_LIST_LENGTH = 10; - - /** - * Displays an cmdbAbstractObject if the connected user is allowed to. - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sObjectClass (Class must be instance of cmdbAbstractObject) - * @param string $sObjectId - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function ViewAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId) - { - // Checking parameters - if ($sObjectClass === '' || $sObjectId === '') - { - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.'); - $oApp->abort(500, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); - } - - // Checking security layers - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass, $sObjectId)) - { - IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . '::' . $sObjectId . ' object.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving object - $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); - if ($oObject === null) - { - // We should never be there as the secuirty helper makes sure that the object exists, but just in case. - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); - - $aData = array('sMode' => 'view'); - $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId); - $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass), $oObject->GetName()); - - // Add an edit button if user is allowed - if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) - { - $oModifyButton = new URLButtonItem( - 'modify_object', - Dict::S('UI:Menu:Modify'), - $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)) - ); - // Putting this one first - $aData['form']['buttons']['links'][] = $oModifyButton->GetMenuItem(); - } - - // Preparing response - if ($oRequest->isXmlHttpRequest()) - { - // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if (empty($sOperation)) - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); - } - else - { - $oResponse = $oApp->json($aData); - } - } - else - { - // Adding brick if it was passed - $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); - if (!empty($sBrickId)) - { - $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); - if ($oBrick !== null) - { - $aData['oBrick'] = $oBrick; - } - } - $aData['sPageTitle'] = $aData['form']['title']; - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); - } - - return $oResponse; - } - - /** - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param $sObjectClass - * @param $sObjectId - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function EditAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId) - { - // Checking parameters - if ($sObjectClass === '' || $sObjectId === '') - { - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.'); - $oApp->abort(500, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); - } - - // Checking security layers - // Warning : This is a dirty quick fix to allow editing its own contact information - $bAllowWrite = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId()); - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite) - { - IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to modify ' . $sObjectClass . '::' . $sObjectId . ' object.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving object - $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); - if ($oObject === null) - { - // We should never be there as the secuirty helper makes sure that the object exists, but just in case. - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); - - $aData = array('sMode' => 'edit'); - $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId); - $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Edit:Title', MetaModel::GetName($sObjectClass), $aData['form']['object_name']); - - // Preparing response - if ($oRequest->isXmlHttpRequest()) - { - // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if (empty($sOperation)) - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); - } - else - { - $oResponse = $oApp->json($aData); - } - } - else - { - // Adding brick if it was passed - $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); - if (!empty($sBrickId)) - { - $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); - if ($oBrick !== null) - { - $aData['oBrick'] = $oBrick; - } - } - $aData['sPageTitle'] = $aData['form']['title']; - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); - } - - return $oResponse; - } - - /** - * Creates an cmdbAbstractObject of the $sObjectClass - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sObjectClass - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function CreateAction(Request $oRequest, Application $oApp, $sObjectClass) - { - // Checking security layers - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_CREATE, $sObjectClass)) - { - IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to create ' . $sObjectClass . ' object.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); - - $aData = array('sMode' => 'create'); - $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass); - $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Create:Title', MetaModel::GetName($sObjectClass)); - - // Preparing response - if ($oRequest->isXmlHttpRequest()) - { - // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if (empty($sOperation)) - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); - } - else - { - $oResponse = $oApp->json($aData); - } - } - else - { - // Adding brick if it was passed - $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); - if (!empty($sBrickId)) - { - $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); - if ($oBrick !== null) - { - $aData['oBrick'] = $oBrick; - } - } - $aData['sPageTitle'] = $aData['form']['title']; - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); - } - - return $oResponse; - } - - /** - * Creates an cmdbAbstractObject of a class determined by the method encoded in $sEncodedMethodName. - * This method use an origin DBObject in order to determine the created cmdbAbstractObject. - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sObjectClass Class of the origin object - * @param string $sObjectId ID of the origin object - * @param string $sEncodedMethodName Base64 encoded factory method name - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function CreateFromFactoryAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId, $sEncodedMethodName) - { - $sMethodName = base64_decode($sEncodedMethodName); - - // Checking that the factory method is valid - if (!is_callable($sMethodName)) - { - IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Invalid factory method "' . $sMethodName . '" used when creating an object.'); - $oApp->abort(500, 'Invalid factory method "' . $sMethodName . '" used when creating an object'); - } - - // Retrieving origin object - // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated - $oOriginObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, true); - - // Retrieving target object (We check if the method is a simple function or if it's part of a class in which case only static function are supported) - if (!strpos($sMethodName, '::')) - { - $oTargetObject = $sMethodName($oOriginObject); - } - else - { - $aMethodNameParts = explode('::', $sMethodName); - $sMethodClass = $aMethodNameParts[0]; - $sMethodName = $aMethodNameParts[1]; - $oTargetObject = $sMethodClass::$sMethodName($oOriginObject); - } - - // Preparing redirection - // - Route - $aRouteParams = array( - 'sObjectClass' => get_class($oTargetObject) - ); - $sRedirectRoute = $oApp['url_generator']->generate('p_object_create', $aRouteParams); - // - Request - $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all()); - - return $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true); - } - - /** - * Applies a stimulus $sStimulus on an cmdbAbstractObject - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sObjectClass - * @param string $sObjectId - * @param string $sStimulusCode - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function ApplyStimulusAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId, $sStimulusCode) - { - // Checking parameters - if ($sObjectClass === '' || $sObjectId === '' || $sStimulusCode === '') - { - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, sObjectId and $sStimulusCode expected, "' . $sObjectClass . '", "' . $sObjectId . '" and "' . $sStimulusCode . '" given.'); - $oApp->abort(500, Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')); - } - - // Checking security layers - if(!SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass)) - { - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving object - $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); - if ($oObject === null) - { - // We should never be there as the secuirty helper makes sure that the object exists, but just in case. - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving request parameters - $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); - - // Retrieving form properties - $aStimuliForms = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, 'apply_stimulus'); - if(array_key_exists($sStimulusCode, $aStimuliForms)) - { - $aFormProperties = $aStimuliForms[$sStimulusCode]; - } - // Or preparing a default form for the stimulus application - else - { - // Preparing default form - $aFormProperties = array( - 'id' => 'apply-stimulus', - 'type' => 'custom_list', - 'fields' => array(), - 'layout' => null - ); - } - - // Adding stimulus code to form - $aFormProperties['stimulus_code'] = $sStimulusCode; - - // Adding target_state to current_values - $oRequest->request->set('apply_stimulus', array('code' => $sStimulusCode)); - - $aData = array('sMode' => 'apply_stimulus'); - $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties); - $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Stimulus:Title'); - $aData['form']['validation']['redirection'] = array( - 'url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)) - ); - - // TODO : This is a ugly patch to avoid showing a modal with a readonly form to the user as it would prevent user from finishing the transition. - // Instead, we apply the stimulus directly here and then go to the edited object. - if (empty($sOperation)) - { - if (isset($aData['form']['editable_fields_count']) && $aData['form']['editable_fields_count'] === 0) - { - $sOperation = 'redirect'; - - $oSubRequest = $oRequest; - $oSubRequest->request->set('operation', 'submit'); - $oSubRequest->request->set('stimulus_code', ''); - - $aData = array('sMode' => 'apply_stimulus'); - $aData['form'] = $this->HandleForm($oSubRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties); - // Redefining the array to be as simple as possible : - $aData = array('redirection' => - array('url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))) - ); - } - } - - // Preparing response - if ($oRequest->isXmlHttpRequest()) - { - // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if (empty($sOperation)) - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); - } - elseif ($sOperation === 'redirect') - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/modal/mode_loader.html.twig', $aData); - } - else - { - $oResponse = $oApp->json($aData); - } - } - else - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); - } - - return $oResponse; - } - - /** - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sMode - * @param string $sObjectClass - * @param string $sObjectId - * @param string $aFormProperties - * - * @return array - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \OQLException - * @throws \Twig_Error_Loader - * @throws \Twig_Error_Runtime - * @throws \Twig_Error_Syntax - */ - public static function HandleForm(Request $oRequest, Application $oApp, $sMode, $sObjectClass, $sObjectId = null, $aFormProperties = null) - { - $aFormData = array(); - $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); - $bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation)); - - // - Retrieve form properties - if ($aFormProperties === null) - { - $aFormProperties = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, $sMode); - } - - // - Create and - if (empty($sOperation)) - { - // Retrieving action rules - // - // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. - // But it would not be a security issue as it only presets values in the form. - $sActionRulesToken = $oApp['request_manipulator']->ReadParam('ar_token', ''); - $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); - - // Preparing object - if ($sObjectId === null) - { - // Create new UserRequest - $oObject = MetaModel::NewObject($sObjectClass); - - // Retrieve action rules information to auto-fill the form if available - // Preparing object - $oApp['context_manipulator']->PrepareObject($aActionRules, $oObject); - $aPrefillFormParam = array( 'user' => $_SESSION["auth_user"], - 'origin' => 'portal'); - $oObject->PrefillForm('creation_from_0', $aPrefillFormParam); - } - else - { - $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); - } - - // Preparing buttons - $aFormData['buttons'] = array( - 'transitions' => array(), - 'actions' => array(), - 'links' => array(), - 'submit' => array( - 'label' => Dict::S('Portal:Button:Submit'), - ), - ); - if ($sMode !== 'apply_stimulus') - { - // Add transition buttons - $oSetToCheckRights = DBObjectSet::FromObject($oObject); - $aStimuli = Metamodel::EnumStimuli($sObjectClass); - foreach ($oObject->EnumTransitions() as $sStimulusCode => $aTransitionDef) - { - if(SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass, $oSetToCheckRights)) - { - $aFormData['buttons']['transitions'][$sStimulusCode] = $aStimuli[$sStimulusCode]->GetLabel(); - } - } - - // Add plugin buttons - foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) - { - foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJDETAILS_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oObject)) as $oMenuItem) - { - if (is_object($oMenuItem)) - { - if($oMenuItem instanceof JSButtonItem) - { - $aFormData['buttons']['actions'][] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts()); - } - elseif($oMenuItem instanceof URLButtonItem) - { - $aFormData['buttons']['links'][] = $oMenuItem->GetMenuItem(); - } - } - } - } - - // Hiding submit button or changing its label if necessary - if(!empty($aFormData['buttons']['transitions']) && isset($aFormProperties['properties']) &&$aFormProperties['properties']['always_show_submit'] === false) - { - unset($aFormData['buttons']['submit']); - } - elseif($sMode === static::ENUM_MODE_EDIT) - { - $aFormData['buttons']['submit']['label'] = Dict::S('Portal:Button:Apply'); - } - } - else - { - $aPrefillFormParam = array( - 'user' => $_SESSION["auth_user"], - 'origin' => 'portal', - 'stimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)['code'], - ); - $oObject->PrefillForm('state_change', $aPrefillFormParam); - } - - // Preparing callback urls - $aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal); - $aFormData['submit_callback'] = $aCallbackUrls['submit']; - $aFormData['cancel_callback'] = $aCallbackUrls['cancel']; - - // Preparing renderer - // Note : We might need to distinguish form & renderer endpoints - if (in_array($sMode, array('create', 'edit', 'view'))) - { - $sFormEndpoint = $oApp['url_generator']->generate('p_object_' . $sMode, array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)); - } - else - { - $sFormEndpoint = $_SERVER['REQUEST_URI']; - } - $oFormRenderer = new BsFormRenderer(); - $oFormRenderer->SetEndpoint($sFormEndpoint); - - $oFormManager = new ObjectFormManager(); - $oFormManager->SetApplication($oApp) - ->SetObject($oObject) - ->SetMode($sMode) - ->SetActionRulesToken($sActionRulesToken) - ->SetRenderer($oFormRenderer) - ->SetFormProperties($aFormProperties); - - $oFormManager->Build(); - - // Check the number of editable fields - $aFormData['editable_fields_count'] = $oFormManager->GetForm()->GetEditableFieldCount(); - } - else - { - // Update / Submit / Cancel - $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); - $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); - if ( empty($sFormManagerClass) || empty($sFormManagerData) ) - { - IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.'); - $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.'); - } - - $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); - $oFormManager->SetApplication($oApp); - - // Applying action rules if present - if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) - { - $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); - $oObj = $oFormManager->GetObject(); - $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj); - $oFormManager->SetObject($oObj); - } - - switch ($sOperation) - { - case 'submit': - // Applying modification to object - $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'attachmentIds' => $oApp['request_manipulator']->ReadParam('attachment_ids', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties, 'applyStimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null))); - if ($aFormData['validation']['valid'] === true) - { - // Note : We don't use $sObjectId there as it can be null if we are creating a new one. Instead we use the id from the created object once it has been seralized - // Check if stimulus has to be applied - $sStimulusCode = $oApp['request_manipulator']->ReadParam('stimulus_code', ''); - if (!empty($sStimulusCode)) - { - $aFormData['validation']['redirection'] = array( - 'url' => $oApp['url_generator']->generate('p_object_apply_stimulus', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey(), 'sStimulusCode' => $sStimulusCode)), - 'ajax' => true - ); - } - // Otherwise, we show the object if there is no default -// else -// { -// $aFormData['validation']['redirection'] = array( -// 'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey())) -// ); -// } - } - break; - - case 'update': - $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties)); - break; - - case 'cancel': - $oFormManager->OnCancel(); - break; - } - } - - // Preparing field_set data - $aFieldSetData = array( - //'fields_list' => $oFormManager->GetRenderer()->Render(), // GLA : This should be done just after in the if statement. - 'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(), - 'form_path' => $oFormManager->GetForm()->GetId() - ); - - // Preparing fields list regarding the operation - if ($sOperation === 'update') - { - $aRequestedFields = $oApp['request_manipulator']->ReadParam('requested_fields', array(), FILTER_UNSAFE_RAW); - $sFormPath = $oApp['request_manipulator']->ReadParam('form_path', ''); - - // Checking if the update was on a subform, if so we need to make the rendering for that part only - if ( !empty($sFormPath) && $sFormPath !== $oFormManager->GetForm()->GetId() ) - { - $oSubForm = $oFormManager->GetForm()->FindSubForm($sFormPath); - $oSubFormRenderer = new BsFormRenderer($oSubForm); - $oSubFormRenderer->SetEndpoint($oFormManager->GetRenderer()->GetEndpoint()); - $aFormData['updated_fields'] = $oSubFormRenderer->Render($aRequestedFields); - } - else - { - $aFormData['updated_fields'] = $oFormManager->GetRenderer()->Render($aRequestedFields); - } - } - else - { - $aFieldSetData['fields_list'] = $oFormManager->GetRenderer()->Render(); - } - - // Preparing form data - $aFormData['id'] = $oFormManager->GetForm()->GetId(); - $aFormData['transaction_id'] = $oFormManager->GetForm()->GetTransactionId(); - $aFormData['formmanager_class'] = $oFormManager->GetClass(); - $aFormData['formmanager_data'] = $oFormManager->ToJSON(); - $aFormData['renderer'] = $oFormManager->GetRenderer(); - $aFormData['object_name'] = $oFormManager->GetObject()->GetName(); - $aFormData['object_state'] = $oFormManager->GetObject()->GetState(); - $aFormData['fieldset'] = $aFieldSetData; - $aFormData['display_mode'] = (isset($aFormProperties['properties'])) ? $aFormProperties['properties']['display_mode'] : ApplicationHelper::FORM_DEFAULT_DISPLAY_MODE; - - return $aFormData; - } - - /** - * Handles the autocomplete search - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sTargetAttCode Attribute code of the host object pointing to the Object class to search - * @param string $sHostObjectClass Class name of the host object - * @param string $sHostObjectId Id of the host object - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \OQLException - */ - public function SearchAutocompleteAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) - { - $aData = array( - 'results' => array( - 'count' => 0, - 'items' => array() - ) - ); - - // Parsing parameters from request payload - parse_str($oRequest->getContent(), $aRequestContent); - - // Checking parameters - if (!isset($aRequestContent['sQuery'])) - { - IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameter sQuery missing.'); - $oApp->abort(500, Dict::Format('UI:Error:ParameterMissing', 'sQuery')); - } - - // Retrieving parameters - $sQuery = $aRequestContent['sQuery']; - $sFieldId = $aRequestContent['sFieldId']; - - // Checking security layers - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) - { - IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sHostObjectClass . '::' . $sHostObjectId . '.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving host object for future DBSearch parameters - if ($sHostObjectId !== null) - { - // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated - $oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true); - } - else - { - $oHostObject = MetaModel::NewObject($sHostObjectClass); - // Retrieving action rules - // - // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. - // But it would not be a security issue as it only presets values in the form. - $sActionRulesToken = $oApp['request_manipulator']->ReadParam('ar_token', ''); - $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); - // Preparing object - $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject); - } - - // Updating host object with form data / values - $sFormManagerClass = $aRequestContent['formmanager_class']; - $sFormManagerData = $aRequestContent['formmanager_data']; - if (!empty($sFormManagerClass) && !empty($sFormManagerData)) - { - $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); - $oFormManager->SetApplication($oApp); - $oFormManager->SetObject($oHostObject); - - // Applying action rules if present - if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) - { - $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); - $oObj = $oFormManager->GetObject(); - $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj); - $oFormManager->SetObject($oObj); - } - - // Updating host object - $oFormManager->OnUpdate(array('currentValues' => $aRequestContent['current_values'])); - $oHostObject = $oFormManager->GetObject(); - } - - // Building search query - // - Retrieving target object class from attcode - $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode); - if ($oTargetAttDef->GetEditClass() === 'CustomFields') - { - $oRequestTemplate = $oHostObject->Get($sTargetAttCode); - $oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch(); - $sTargetObjectClass = $oTemplateFieldSearch->GetClass(); - } - elseif ($oTargetAttDef->IsLinkSet()) - { - throw new Exception('Search autocomplete cannot apply on AttributeLinkedSet objects, ' . get_class($oTargetAttDef) . ' (' . $sHostObjectClass . '->' . $sTargetAttCode . ') given.'); - } - else - { - $sTargetObjectClass = $oTargetAttDef->GetTargetClass(); - } - // - Base query from meta model - if ($oTargetAttDef->GetEditClass() === 'CustomFields') - { - $oSearch = $oTemplateFieldSearch; - } - else - { - $oSearch = DBSearch::FromOQL($oTargetAttDef->GetValuesDef()->GetFilterExpression()); - } - // - Adding query condition - $oSearch->AddConditionExpression(new BinaryExpression(new FieldExpression('friendlyname', $oSearch->GetClassAlias()), 'LIKE', new VariableExpression('ac_query'))); - // - Intersecting with scope constraints - // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. - // It is the responsability of the template designer to write the right query so the user see only what he should. - if ($oTargetAttDef->GetEditClass() !== 'CustomFields') - { - $oScopeSearch = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ); - $oSearch = $oSearch->Intersect($oScopeSearch); - // - Allowing all data if necessary - if ($oScopeSearch->IsAllDataAllowed()) - { - $oSearch->AllowAllData(); - } - } - - // Retrieving results - // - Preparing object set - $oSet = new DBObjectSet($oSearch, array(), array('this' => $oHostObject, 'ac_query' => '%' . $sQuery . '%')); - $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => array('friendlyname'))); - // Note : This limit is also used in the field renderer by typeahead to determine how many suggestions to display - if ($oTargetAttDef->GetEditClass() === 'CustomFields') - { - $oSet->SetLimit(static::DEFAULT_LIST_LENGTH); - } - else - { - $oSet->SetLimit($oTargetAttDef->GetMaximumComboLength()); // TODO : Is this the right limit value ? We might want to use another parameter - } - // - Retrieving objects - while ($oItem = $oSet->Fetch()) - { - $aData['results']['items'][] = array('id' => $oItem->GetKey(), 'name' => html_entity_decode($oItem->GetName(), ENT_QUOTES, 'UTF-8')); - $aData['results']['count'] ++; - } - - // Preparing response - if ($oRequest->isXmlHttpRequest()) - { - $oResponse = $oApp->json($aData); - } - else - { - $oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - return $oResponse; - } - - /** - * Handles the regular (table) search from an attribute - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sTargetAttCode Attribute code of the host object pointing to the Object class to search - * @param string $sHostObjectClass Class name of the host object - * @param string $sHostObjectId Id of the host object - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \OQLException - */ - public function SearchFromAttributeAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) - { - $aData = array( - 'sMode' => 'search_regular', - 'sTargetAttCode' => $sTargetAttCode, - 'sHostObjectClass' => $sHostObjectClass, - 'sHostObjectId' => $sHostObjectId, - 'sActionRulesToken' => $oApp['request_manipulator']->ReadParam('ar_token', ''), - ); - - // Checking security layers - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) - { - IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sHostObjectClass . '::' . $sHostObjectId . ' object.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving host object for future DBSearch parameters - if ($sHostObjectId !== null) - { - // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated - $oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true); - } - else - { - $oHostObject = MetaModel::NewObject($sHostObjectClass); - // Retrieving action rules - // - // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. - // But it would not be a security issue as it only presets values in the form. - $aActionRules = !empty($aData['sActionRulesToken']) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : array(); - // Preparing object - $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject); - } - - // Updating host object with form data / values - $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); - $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); - if ( !empty($sFormManagerClass) && !empty($sFormManagerData) ) - { - $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); - $oFormManager->SetApplication($oApp); - $oFormManager->SetObject($oHostObject); - - // Applying action rules if present - if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) - { - $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); - $oObj = $oFormManager->GetObject(); - $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj); - $oFormManager->SetObject($oObj); - } - - // Updating host object - $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW))); - $oHostObject = $oFormManager->GetObject(); - } - - // Retrieving request parameters - $iPageNumber = $oApp['request_manipulator']->ReadParam('iPageNumber', static::DEFAULT_PAGE_NUMBER, FILTER_SANITIZE_NUMBER_INT); - $iListLength = $oApp['request_manipulator']->ReadParam('iListLength', static::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT); - $bInitalPass = $oApp['request_manipulator']->HasParam('draw') ? false : true; - $sQuery = $oApp['request_manipulator']->ReadParam('sSearchValue', ''); - $sFormPath = $oApp['request_manipulator']->ReadParam('sFormPath', ''); - $sFieldId = $oApp['request_manipulator']->ReadParam('sFieldId', ''); - $aObjectIdsToIgnore = $oApp['request_manipulator']->ReadParam('aObjectIdsToIgnore', null, FILTER_UNSAFE_RAW); - - // Building search query - // - Retrieving target object class from attcode - $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode); - if ($oTargetAttDef->IsExternalKey()) - { - $sTargetObjectClass = $oTargetAttDef->GetTargetClass(); - } - elseif ($oTargetAttDef->IsLinkSet()) - { - if (!$oTargetAttDef->IsIndirect()) - { - $sTargetObjectClass = $oTargetAttDef->GetLinkedClass(); - } - else - { - $oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote()); - $sTargetObjectClass = $oRemoteAttDef->GetTargetClass(); - } - } - elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') - { - $oRequestTemplate = $oHostObject->Get($sTargetAttCode); - $oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch(); - $sTargetObjectClass = $oTemplateFieldSearch->GetClass(); - } - else - { - throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, ' . get_class($oTargetAttDef) . ' given.'); - } - - // - Retrieving class attribute list - $aAttCodes = ApplicationHelper::GetLoadedListFromClass($oApp, $sTargetObjectClass, 'list'); - // - Adding friendlyname attribute to the list is not already in it - $sTitleAttCode = 'friendlyname'; - if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) - { - $aAttCodes = array_merge(array($sTitleAttCode), $aAttCodes); - } - - // - Retrieving scope search - // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. - // It is the responsability of the template designer to write the right query so the user see only what he should. - $oScopeSearch = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ); - $aInternalParams = array(); - if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) - { - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' has no scope query for ' . $sTargetObjectClass . ' class.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // - Base query from meta model - if ($oTargetAttDef->IsExternalKey()) - { - $oSearch = DBSearch::FromOQL($oTargetAttDef->GetValuesDef()->GetFilterExpression()); - } - elseif ($oTargetAttDef->IsLinkSet()) - { - $oSearch = $oScopeSearch; - } - elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') - { - // Note : $oTemplateFieldSearch has been defined in the "Retrieving target object class from attcode" part, it is not available otherwise - $oSearch = $oTemplateFieldSearch; - } - - // - Filtering objects to ignore - if (($aObjectIdsToIgnore !== null) && (is_array($aObjectIdsToIgnore))) - { - //$oSearch->AddConditionExpression('id', $aObjectIdsToIgnore, 'NOT IN'); - $aExpressions = array(); - foreach ($aObjectIdsToIgnore as $sObjectIdToIgnore) - { - $aExpressions[] = new ScalarExpression($sObjectIdToIgnore); - } - $oSearch->AddConditionExpression(new BinaryExpression(new FieldExpression('id', $oSearch->GetClassAlias()), 'NOT IN', new ListExpression($aExpressions))); - } - - // - Adding query condition - $aInternalParams['this'] = $oHostObject; - if (!empty($sQuery)) - { - $oFullExpr = null; - for ($i = 0; $i < count($aAttCodes); $i++) - { - // Checking if the current attcode is an external key in order to search on the friendlyname - $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $aAttCodes[$i]); - $sAttCode = (!$oAttDef->IsExternalKey()) ? $aAttCodes[$i] : $aAttCodes[$i] . '_friendlyname'; - // Building expression for the current attcode - // - For attributes that need conversion from their display value to storage value - // Note : This is dirty hack that will need to be refactored in the OQL core in order to be nicer and to be extended to other types such as dates etc... - if (($oAttDef instanceof AttributeEnum) || ($oAttDef instanceof AttributeFinalClass)) - { - // Looking up storage value - $aMatchedCodes = array(); - foreach ($oAttDef->GetAllowedValues() as $sValueCode => $sValueLabel) - { - if (stripos($sValueLabel, $sQuery) !== false) - { - $aMatchedCodes[] = $sValueCode; - } - } - // Building expression - if (!empty($aMatchedCodes)) - { - $oEnumeratedListExpr = ListExpression::FromScalars($aMatchedCodes); - $oBinExpr = new BinaryExpression(new FieldExpression($sAttCode, $oSearch->GetClassAlias()), 'IN', $oEnumeratedListExpr); - } - else - { - $oBinExpr = new FalseExpression(); - } - } - // - For regular attributs - else - { - $oBinExpr = new BinaryExpression(new FieldExpression($sAttCode, $oSearch->GetClassAlias()), 'LIKE', new VariableExpression('re_query')); - } - // Adding expression to the full expression (all attcodes) - if ($i === 0) - { - $oFullExpr = $oBinExpr; - } - else - { - $oFullExpr = new BinaryExpression($oFullExpr, 'OR', $oBinExpr); - } - } - // Adding full expression to the search object - $oSearch->AddConditionExpression($oFullExpr); - $aInternalParams['re_query'] = '%' . $sQuery . '%'; - } - - // - Intersecting with scope constraints - // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. - // It is the responsability of the template designer to write the right query so the user see only what he should. - if (($oScopeSearch !== null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) - { - $oSearch = $oSearch->Intersect($oScopeSearch); - // - Allowing all data if necessary - if ($oScopeSearch->IsAllDataAllowed()) - { - $oSearch->AllowAllData(); - } - } - - // Retrieving results - // - Preparing object set - $oSet = new DBObjectSet($oSearch, array(), $aInternalParams); - $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => $aAttCodes)); - $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1)); - // - Retrieving columns properties - $aColumnProperties = array(); - foreach ($aAttCodes as $sAttCode) - { - $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode); - $aColumnProperties[$sAttCode] = array( - 'title' => $oAttDef->GetLabel() - ); - } - // - Retrieving objects - $aItems = array(); - while ($oItem = $oSet->Fetch()) - { - $aItems[] = $this->PrepareObjectInformations($oApp, $oItem, $aAttCodes); - } - - // Preparing response - if ($bInitalPass) - { - $aData = $aData + array( - 'form' => array( - 'id' => 'object_search_form_' . time(), - 'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel(), MetaModel::GetName($sTargetObjectClass)) - ), - 'aColumnProperties' => json_encode($aColumnProperties), - 'aResults' => array( - 'aItems' => json_encode($aItems), - 'iCount' => count($aItems) - ), - 'bMultipleSelect' => $oTargetAttDef->IsLinkSet(), - 'aSource' => array( - 'sFormPath' => $sFormPath, - 'sFieldId' => $sFieldId, - 'aObjectIdsToIgnore' => $aObjectIdsToIgnore, - 'sFormManagerClass' => $sFormManagerClass, - 'sFormManagerData' => $sFormManagerData - ) - ); - - if ($oRequest->isXmlHttpRequest()) - { - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); - } - else - { - //$oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); - } - } - else - { - $aData = $aData + array( - 'levelsProperties' => $aColumnProperties, - 'data' => $aItems, - 'recordsTotal' => $oSet->Count(), - 'recordsFiltered' => $oSet->Count() - ); - - $oResponse = $oApp->json($aData); - } - - return $oResponse; - } - - /** - * Handles the hierarchical search from an attribute - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sTargetAttCode Attribute code of the host object pointing to the Object class to search - * @param string $sHostObjectClass Class name of the host object - * @param string $sHostObjectId Id of the host object - * - * @return void - * - */ - public function SearchHierarchyAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) - { - // TODO - } - - /** - * Handles ormDocument display / download from an object - * - * Note: This is inspired from pages/ajax.document.php, but duplicated as there is no secret mecanism for ormDocument yet. - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sOperation - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function DocumentAction(Request $oRequest, Application $oApp, $sOperation = null) - { - // Setting default operation - if($sOperation === null) - { - $sOperation = 'display'; - } - - // Retrieving ormDocument's host object - $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', ''); - $sObjectId = $oApp['request_manipulator']->ReadParam('sObjectId', ''); - $sObjectField = $oApp['request_manipulator']->ReadParam('sObjectField', ''); - - // When reaching to an Attachment, we have to check security on its host object instead of the Attachment itself - if($sObjectClass === 'Attachment') - { - $oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, true, true); - $sHostClass = $oAttachment->Get('item_class'); - $sHostId = $oAttachment->Get('item_id'); - } - else - { - $sHostClass = $sObjectClass; - $sHostId = $sObjectId; - } - - // Checking security layers - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostClass, $sHostId)) - { - IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to retrieve document from attribute ' . $sObjectField . ' as it not allowed to read ' . $sHostClass . '::' . $sHostId . ' object.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Retrieving object - $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* Must not be found */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sHostClass)); - if ($oObject === null) - { - // We should never be there as the secuirty helper makes sure that the object exists, but just in case. - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); - $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); - } - - // Setting cache timeout - // Note: Attachment download should be handle through AttachmentAction() - if($sObjectClass === 'Attachment') - { - // One year ahead: an attachement cannot change - $iCacheSec = 31556926; - } - else - { - $iCacheSec = $oApp['request_manipulator']->ReadParam('cache', 0, FILTER_SANITIZE_NUMBER_INT); - } - - $aHeaders = array(); - if($iCacheSec > 0) - { - $aHeaders['Expires'] = ''; - $aHeaders['Cache-Control'] = 'no-transform, public,max-age='.$iCacheSec.',s-maxage='.$iCacheSec; - // Reset the value set previously - $aHeaders['Pragma'] = 'cache'; - // An arbitrary date in the past is ok - $aHeaders['Last-Modified'] = 'Wed, 15 Jun 2015 13:21:15 GMT'; - } - - /** @var \ormDocument $oDocument */ - $oDocument = $oObject->Get($sObjectField); - $aHeaders['Content-Type'] = $oDocument->GetMimeType(); - $aHeaders['Content-Disposition'] = (($sOperation === 'display') ? 'inline' : 'attachment') . ';filename="'.$oDocument->GetFileName().'"'; - - return new Response($oDocument->GetData(), Response::HTTP_OK, $aHeaders); - } - - /** - * Handles attachment add/remove on an object - * - * Note: This is inspired from itop-attachment/ajax.attachment.php - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sOperation - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreUnexpectedValue - */ - public function AttachmentAction(Request $oRequest, Application $oApp, $sOperation = null) - { - $aData = array( - 'att_id' => 0, - 'preview' => false, - 'msg' => '' - ); - - // Retrieving sOperation from request only if it wasn't forced (determined by the route) - if ($sOperation === null) - { - $sOperation = $oApp['request_manipulator']->ReadParam('operation', null); - } - switch ($sOperation) - { - case 'add': - $sFieldName = $oApp['request_manipulator']->ReadParam('field_name', ''); - $sObjectClass = $oApp['request_manipulator']->ReadParam('object_class', ''); - $sTempId = $oApp['request_manipulator']->ReadParam('temp_id', ''); - - if (empty($sObjectClass) || empty($sTempId)) - { - $aData['error'] = Dict::Format('UI:Error:2ParametersMissing', 'object_class', 'temp_id'); - } - else - { - try - { - $oDocument = utils::ReadPostedDocument($sFieldName); - $oAttachment = MetaModel::NewObject('Attachment'); - $oAttachment->Set('expire', time() + MetaModel::GetConfig()->Get('draft_attachments_lifetime')); // one hour... - $oAttachment->Set('temp_id', $sTempId); - $oAttachment->Set('item_class', $sObjectClass); - $oAttachment->SetDefaultOrgId(); - $oAttachment->Set('contents', $oDocument); - $iAttId = $oAttachment->DBInsert(); - - $aData['msg'] = htmlentities($oDocument->GetFileName(), ENT_QUOTES, 'UTF-8'); - // TODO : Change icon location when itop-attachment is refactored - //$aData['icon'] = utils::GetAbsoluteUrlAppRoot() . AttachmentPlugIn::GetFileIcon($oDoc->GetFileName()); - $aData['icon'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-attachments/icons/image.png'; - $aData['att_id'] = $iAttId; - $aData['preview'] = $oDocument->IsPreviewAvailable() ? 'true' : 'false'; - } - catch (FileUploadException $e) - { - $aData['error'] = $e->GetMessage(); - } - } - - // Note : The Content-Type header is set to 'text/plain' in order to be IE9 compatible. Otherwise ('application/json') IE9 will download the response as a JSON file to the user computer... - $oResponse = $oApp->json($aData, 200, array('Content-Type' => 'text/plain')); - break; - - case 'download': - // Preparing redirection - // - Route - $aRouteParams = array( - 'sObjectClass' => 'Attachment', - 'sObjectId' => $oApp['request_manipulator']->ReadParam('sAttachmentId', null), - 'sObjectField' => 'contents', - ); - $sRedirectRoute = $oApp['url_generator']->generate('p_object_document_download', $aRouteParams); - // - Request - $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all()); - - $oResponse = $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true); - break; - - default: - $oApp->abort(403); - break; - } - - return $oResponse; - } - - /** - * Returns a json response containing an array of objects informations. - * - * The service must be given 3 parameters : - * - sObjectClass : The class of objects to retrieve information from - * - aObjectIds : An array of object ids - * - aObjectAttCodes : An array of attribute codes to retrieve - * - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * - * @return \Symfony\Component\HttpFoundation\Response - * - * @throws \OQLException - * @throws \CoreException - */ - public function GetInformationsAsJsonAction(Request $oRequest, Application $oApp) - { - $aData = array(); - - // Retrieving parameters - $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', ''); - $aObjectIds = $oApp['request_manipulator']->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW); - $aObjectAttCodes = $oApp['request_manipulator']->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW); - if ( empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes) ) - { - IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, aObjectIds and aObjectAttCodes expected, "' . $sObjectClass . '", "' . implode('/', $aObjectIds) . '" given.'); - $oApp->abort(500, 'Invalid request data, some informations are missing'); - } - - // Checking that id is in the AttCodes - if (!in_array('id', $aObjectAttCodes)) - { - $aObjectAttCodes = array_merge(array('id'), $aObjectAttCodes); - } - - // Building the search - $bIgnoreSilos = $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass); - $oSearch = DBObjectSearch::FromOQL("SELECT " . $sObjectClass . " WHERE id IN ('" . implode("','", $aObjectIds) . "')"); - if ($bIgnoreSilos === true) - { - $oSearch->AllowAllData(); - } - $oSet = new DBObjectSet($oSearch); - $oSet->OptimizeColumnLoad($aObjectAttCodes); - - // Retrieving objects - while ($oObject = $oSet->Fetch()) - { - $aData['items'][] = $this->PrepareObjectInformations($oApp, $oObject, $aObjectAttCodes); - } - - return $oApp->json($aData); - } - - /** - * Prepare a DBObject informations as an array for a client side usage (typically, add a row in a table) - * - * @param \Silex\Application $oApp - * @param \DBObject $oObject - * @param array $aAttCodes - * - * @return array - * - * @throws \Exception - * @throws \CoreException - */ - protected function PrepareObjectInformations(Application $oApp, DBObject $oObject, $aAttCodes = array()) - { - $sObjectClass = get_class($oObject); - $aObjectData = array( - 'id' => $oObject->GetKey(), - 'name' => $oObject->GetName(), - 'attributes' => array(), - ); - - // Retrieving attributes definitions - $aAttDefs = array(); - foreach ($aAttCodes as $sAttCode) - { - if ($sAttCode === 'id') - continue; - - $aAttDefs[$sAttCode] = MetaModel::GetAttributeDef($sObjectClass, $sAttCode); - } - - // Preparing attribute data - foreach ($aAttDefs as $oAttDef) - { - $aAttData = array( - 'att_code' => $oAttDef->GetCode() - ); - - if ($oAttDef->IsExternalKey()) - { - $aAttData['value'] = $oObject->GetAsHTML($oAttDef->GetCode() . '_friendlyname'); - - // Checking if user can access object's external key - if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass())) - { - $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $oAttDef->GetTargetClass(), 'sObjectId' => $oObject->Get($oAttDef->GetCode()))); - } - } - elseif ($oAttDef->IsLinkSet()) - { - // We skip it - continue; - } - elseif ($oAttDef instanceof AttributeImage) - { - $oOrmDoc = $oObject->Get($oAttDef->GetCode()); - if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty()) - { - $sUrl = $oApp['url_generator']->generate('p_object_document_display', array('sObjectClass' => get_class($oObject), 'sObjectId' => $oObject->GetKey(), 'sObjectField' => $oAttDef->GetCode(), 'cache' => 86400)); - } - else - { - $sUrl = $oAttDef->Get('default_image'); - } - $aAttData['value'] = ''; - } - else - { - $aAttData['value'] = $oAttDef->GetAsHTML($oObject->Get($oAttDef->GetCode())); - - if ($oAttDef instanceof AttributeFriendlyName) - { - // Checking if user can access object - if(SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass)) - { - $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oObject->GetKey())); - } - } - } - - $aObjectData['attributes'][$oAttDef->GetCode()] = $aAttData; - } - - return $aObjectData; - } - -} + + +namespace Combodo\iTop\Portal\Controller; + +use Silex\Application; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Exception; +use FileUploadException; +use utils; +use Dict; +use IssueLog; +use MetaModel; +use DBObject; +use DBSearch; +use DBObjectSearch; +use FalseExpression; +use BinaryExpression; +use FieldExpression; +use VariableExpression; +use ListExpression; +use ScalarExpression; +use DBObjectSet; +use AttributeEnum; +use AttributeImage; +use AttributeFinalClass; +use AttributeFriendlyName; +use UserRights; +use iPopupMenuExtension; +use URLButtonItem; +use JSButtonItem; +use Combodo\iTop\Portal\Helper\ApplicationHelper; +use Combodo\iTop\Portal\Helper\SecurityHelper; +use Combodo\iTop\Portal\Helper\ContextManipulatorHelper; +use Combodo\iTop\Portal\Form\ObjectFormManager; +use Combodo\iTop\Renderer\Bootstrap\BsFormRenderer; + +/** + * Class ObjectController + * + * Controller to handle basic view / edit / create of cmdbAbstractObjectClass ManageBrickController + * + * @package Combodo\iTop\Portal\Controller + * @author Guillaume Lajarige + * @since 2.3.0 + */ +class ObjectController extends AbstractController +{ + + const ENUM_MODE_VIEW = 'view'; + const ENUM_MODE_EDIT = 'edit'; + const ENUM_MODE_CREATE = 'create'; + + const DEFAULT_PAGE_NUMBER = 1; + const DEFAULT_LIST_LENGTH = 10; + + /** + * Displays an cmdbAbstractObject if the connected user is allowed to. + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sObjectClass (Class must be instance of cmdbAbstractObject) + * @param string $sObjectId + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function ViewAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId) + { + // Checking parameters + if ($sObjectClass === '' || $sObjectId === '') + { + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.'); + $oApp->abort(500, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + } + + // Checking security layers + if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass, $sObjectId)) + { + IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . '::' . $sObjectId . ' object.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving object + $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); + if ($oObject === null) + { + // We should never be there as the secuirty helper makes sure that the object exists, but just in case. + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + + $aData = array('sMode' => 'view'); + $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId); + $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass), $oObject->GetName()); + + // Add an edit button if user is allowed + if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) + { + $oModifyButton = new URLButtonItem( + 'modify_object', + Dict::S('UI:Menu:Modify'), + $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)) + ); + // Putting this one first + $aData['form']['buttons']['links'][] = $oModifyButton->GetMenuItem(); + } + + // Preparing response + if ($oRequest->isXmlHttpRequest()) + { + // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. + if (empty($sOperation)) + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); + } + else + { + $oResponse = $oApp->json($aData); + } + } + else + { + // Adding brick if it was passed + $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); + if (!empty($sBrickId)) + { + $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); + if ($oBrick !== null) + { + $aData['oBrick'] = $oBrick; + } + } + $aData['sPageTitle'] = $aData['form']['title']; + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); + } + + return $oResponse; + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param $sObjectClass + * @param $sObjectId + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function EditAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId) + { + // Checking parameters + if ($sObjectClass === '' || $sObjectId === '') + { + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.'); + $oApp->abort(500, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + } + + // Checking security layers + // Warning : This is a dirty quick fix to allow editing its own contact information + $bAllowWrite = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId()); + if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite) + { + IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to modify ' . $sObjectClass . '::' . $sObjectId . ' object.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving object + $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); + if ($oObject === null) + { + // We should never be there as the secuirty helper makes sure that the object exists, but just in case. + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + + $aData = array('sMode' => 'edit'); + $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId); + $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Edit:Title', MetaModel::GetName($sObjectClass), $aData['form']['object_name']); + + // Preparing response + if ($oRequest->isXmlHttpRequest()) + { + // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. + if (empty($sOperation)) + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); + } + else + { + $oResponse = $oApp->json($aData); + } + } + else + { + // Adding brick if it was passed + $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); + if (!empty($sBrickId)) + { + $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); + if ($oBrick !== null) + { + $aData['oBrick'] = $oBrick; + } + } + $aData['sPageTitle'] = $aData['form']['title']; + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); + } + + return $oResponse; + } + + /** + * Creates an cmdbAbstractObject of the $sObjectClass + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sObjectClass + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + public function CreateAction(Request $oRequest, Application $oApp, $sObjectClass) + { + // Checking security layers + if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_CREATE, $sObjectClass)) + { + IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to create ' . $sObjectClass . ' object.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + + $aData = array('sMode' => 'create'); + $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass); + $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Create:Title', MetaModel::GetName($sObjectClass)); + + // Preparing response + if ($oRequest->isXmlHttpRequest()) + { + // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. + if (empty($sOperation)) + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); + } + else + { + $oResponse = $oApp->json($aData); + } + } + else + { + // Adding brick if it was passed + $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); + if (!empty($sBrickId)) + { + $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); + if ($oBrick !== null) + { + $aData['oBrick'] = $oBrick; + } + } + $aData['sPageTitle'] = $aData['form']['title']; + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); + } + + return $oResponse; + } + + /** + * Creates an cmdbAbstractObject of a class determined by the method encoded in $sEncodedMethodName. + * This method use an origin DBObject in order to determine the created cmdbAbstractObject. + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sObjectClass Class of the origin object + * @param string $sObjectId ID of the origin object + * @param string $sEncodedMethodName Base64 encoded factory method name + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + */ + public function CreateFromFactoryAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId, $sEncodedMethodName) + { + $sMethodName = base64_decode($sEncodedMethodName); + + // Checking that the factory method is valid + if (!is_callable($sMethodName)) + { + IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Invalid factory method "' . $sMethodName . '" used when creating an object.'); + $oApp->abort(500, 'Invalid factory method "' . $sMethodName . '" used when creating an object'); + } + + // Retrieving origin object + // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated + $oOriginObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, true); + + // Retrieving target object (We check if the method is a simple function or if it's part of a class in which case only static function are supported) + if (!strpos($sMethodName, '::')) + { + $oTargetObject = $sMethodName($oOriginObject); + } + else + { + $aMethodNameParts = explode('::', $sMethodName); + $sMethodClass = $aMethodNameParts[0]; + $sMethodName = $aMethodNameParts[1]; + $oTargetObject = $sMethodClass::$sMethodName($oOriginObject); + } + + // Preparing redirection + // - Route + $aRouteParams = array( + 'sObjectClass' => get_class($oTargetObject) + ); + $sRedirectRoute = $oApp['url_generator']->generate('p_object_create', $aRouteParams); + // - Request + $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all()); + + return $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true); + } + + /** + * Applies a stimulus $sStimulus on an cmdbAbstractObject + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sObjectClass + * @param string $sObjectId + * @param string $sStimulusCode + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + */ + public function ApplyStimulusAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId, $sStimulusCode) + { + // Checking parameters + if ($sObjectClass === '' || $sObjectId === '' || $sStimulusCode === '') + { + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, sObjectId and $sStimulusCode expected, "' . $sObjectClass . '", "' . $sObjectId . '" and "' . $sStimulusCode . '" given.'); + $oApp->abort(500, Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')); + } + + // Checking security layers + if(!SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass)) + { + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving object + $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); + if ($oObject === null) + { + // We should never be there as the secuirty helper makes sure that the object exists, but just in case. + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving request parameters + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + + // Retrieving form properties + $aStimuliForms = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, 'apply_stimulus'); + if(array_key_exists($sStimulusCode, $aStimuliForms)) + { + $aFormProperties = $aStimuliForms[$sStimulusCode]; + } + // Or preparing a default form for the stimulus application + else + { + // Preparing default form + $aFormProperties = array( + 'id' => 'apply-stimulus', + 'type' => 'custom_list', + 'fields' => array(), + 'layout' => null + ); + } + + // Adding stimulus code to form + $aFormProperties['stimulus_code'] = $sStimulusCode; + + // Adding target_state to current_values + $oRequest->request->set('apply_stimulus', array('code' => $sStimulusCode)); + + $aData = array('sMode' => 'apply_stimulus'); + $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties); + $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Stimulus:Title'); + $aData['form']['validation']['redirection'] = array( + 'url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)) + ); + + // TODO : This is a ugly patch to avoid showing a modal with a readonly form to the user as it would prevent user from finishing the transition. + // Instead, we apply the stimulus directly here and then go to the edited object. + if (empty($sOperation)) + { + if (isset($aData['form']['editable_fields_count']) && $aData['form']['editable_fields_count'] === 0) + { + $sOperation = 'redirect'; + + $oSubRequest = $oRequest; + $oSubRequest->request->set('operation', 'submit'); + $oSubRequest->request->set('stimulus_code', ''); + + $aData = array('sMode' => 'apply_stimulus'); + $aData['form'] = $this->HandleForm($oSubRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties); + // Redefining the array to be as simple as possible : + $aData = array('redirection' => + array('url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))) + ); + } + } + + // Preparing response + if ($oRequest->isXmlHttpRequest()) + { + // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. + if (empty($sOperation)) + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); + } + elseif ($sOperation === 'redirect') + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/modal/mode_loader.html.twig', $aData); + } + else + { + $oResponse = $oApp->json($aData); + } + } + else + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); + } + + return $oResponse; + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sMode + * @param string $sObjectClass + * @param string $sObjectId + * @param string $aFormProperties + * + * @return array + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \OQLException + * @throws \Twig_Error_Loader + * @throws \Twig_Error_Runtime + * @throws \Twig_Error_Syntax + */ + public static function HandleForm(Request $oRequest, Application $oApp, $sMode, $sObjectClass, $sObjectId = null, $aFormProperties = null) + { + $aFormData = array(); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + $bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation)); + + // - Retrieve form properties + if ($aFormProperties === null) + { + $aFormProperties = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, $sMode); + } + + // - Create and + if (empty($sOperation)) + { + // Retrieving action rules + // + // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. + // But it would not be a security issue as it only presets values in the form. + $sActionRulesToken = $oApp['request_manipulator']->ReadParam('ar_token', ''); + $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); + + // Preparing object + if ($sObjectId === null) + { + // Create new UserRequest + $oObject = MetaModel::NewObject($sObjectClass); + + // Retrieve action rules information to auto-fill the form if available + // Preparing object + $oApp['context_manipulator']->PrepareObject($aActionRules, $oObject); + $aPrefillFormParam = array( 'user' => $_SESSION["auth_user"], + 'origin' => 'portal'); + $oObject->PrefillForm('creation_from_0', $aPrefillFormParam); + } + else + { + $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); + } + + // Preparing buttons + $aFormData['buttons'] = array( + 'transitions' => array(), + 'actions' => array(), + 'links' => array(), + 'submit' => array( + 'label' => Dict::S('Portal:Button:Submit'), + ), + ); + if ($sMode !== 'apply_stimulus') + { + // Add transition buttons + $oSetToCheckRights = DBObjectSet::FromObject($oObject); + $aStimuli = Metamodel::EnumStimuli($sObjectClass); + foreach ($oObject->EnumTransitions() as $sStimulusCode => $aTransitionDef) + { + if(SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass, $oSetToCheckRights)) + { + $aFormData['buttons']['transitions'][$sStimulusCode] = $aStimuli[$sStimulusCode]->GetLabel(); + } + } + + // Add plugin buttons + foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance) + { + foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJDETAILS_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oObject)) as $oMenuItem) + { + if (is_object($oMenuItem)) + { + if($oMenuItem instanceof JSButtonItem) + { + $aFormData['buttons']['actions'][] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts()); + } + elseif($oMenuItem instanceof URLButtonItem) + { + $aFormData['buttons']['links'][] = $oMenuItem->GetMenuItem(); + } + } + } + } + + // Hiding submit button or changing its label if necessary + if(!empty($aFormData['buttons']['transitions']) && isset($aFormProperties['properties']) &&$aFormProperties['properties']['always_show_submit'] === false) + { + unset($aFormData['buttons']['submit']); + } + elseif($sMode === static::ENUM_MODE_EDIT) + { + $aFormData['buttons']['submit']['label'] = Dict::S('Portal:Button:Apply'); + } + } + else + { + $aPrefillFormParam = array( + 'user' => $_SESSION["auth_user"], + 'origin' => 'portal', + 'stimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)['code'], + ); + $oObject->PrefillForm('state_change', $aPrefillFormParam); + } + + // Preparing callback urls + $aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal); + $aFormData['submit_callback'] = $aCallbackUrls['submit']; + $aFormData['cancel_callback'] = $aCallbackUrls['cancel']; + + // Preparing renderer + // Note : We might need to distinguish form & renderer endpoints + if (in_array($sMode, array('create', 'edit', 'view'))) + { + $sFormEndpoint = $oApp['url_generator']->generate('p_object_' . $sMode, array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)); + } + else + { + $sFormEndpoint = $_SERVER['REQUEST_URI']; + } + $oFormRenderer = new BsFormRenderer(); + $oFormRenderer->SetEndpoint($sFormEndpoint); + + $oFormManager = new ObjectFormManager(); + $oFormManager->SetApplication($oApp) + ->SetObject($oObject) + ->SetMode($sMode) + ->SetActionRulesToken($sActionRulesToken) + ->SetRenderer($oFormRenderer) + ->SetFormProperties($aFormProperties); + + $oFormManager->Build(); + + // Check the number of editable fields + $aFormData['editable_fields_count'] = $oFormManager->GetForm()->GetEditableFieldCount(); + } + else + { + // Update / Submit / Cancel + $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); + $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); + if ( empty($sFormManagerClass) || empty($sFormManagerData) ) + { + IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.'); + $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.'); + } + + $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); + $oFormManager->SetApplication($oApp); + + // Applying action rules if present + if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) + { + $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); + $oObj = $oFormManager->GetObject(); + $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj); + $oFormManager->SetObject($oObj); + } + + switch ($sOperation) + { + case 'submit': + // Applying modification to object + $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'attachmentIds' => $oApp['request_manipulator']->ReadParam('attachment_ids', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties, 'applyStimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null))); + if ($aFormData['validation']['valid'] === true) + { + // Note : We don't use $sObjectId there as it can be null if we are creating a new one. Instead we use the id from the created object once it has been seralized + // Check if stimulus has to be applied + $sStimulusCode = $oApp['request_manipulator']->ReadParam('stimulus_code', ''); + if (!empty($sStimulusCode)) + { + $aFormData['validation']['redirection'] = array( + 'url' => $oApp['url_generator']->generate('p_object_apply_stimulus', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey(), 'sStimulusCode' => $sStimulusCode)), + 'ajax' => true + ); + } + // Otherwise, we show the object if there is no default +// else +// { +// $aFormData['validation']['redirection'] = array( +// 'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey())) +// ); +// } + } + break; + + case 'update': + $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties)); + break; + + case 'cancel': + $oFormManager->OnCancel(); + break; + } + } + + // Preparing field_set data + $aFieldSetData = array( + //'fields_list' => $oFormManager->GetRenderer()->Render(), // GLA : This should be done just after in the if statement. + 'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(), + 'form_path' => $oFormManager->GetForm()->GetId() + ); + + // Preparing fields list regarding the operation + if ($sOperation === 'update') + { + $aRequestedFields = $oApp['request_manipulator']->ReadParam('requested_fields', array(), FILTER_UNSAFE_RAW); + $sFormPath = $oApp['request_manipulator']->ReadParam('form_path', ''); + + // Checking if the update was on a subform, if so we need to make the rendering for that part only + if ( !empty($sFormPath) && $sFormPath !== $oFormManager->GetForm()->GetId() ) + { + $oSubForm = $oFormManager->GetForm()->FindSubForm($sFormPath); + $oSubFormRenderer = new BsFormRenderer($oSubForm); + $oSubFormRenderer->SetEndpoint($oFormManager->GetRenderer()->GetEndpoint()); + $aFormData['updated_fields'] = $oSubFormRenderer->Render($aRequestedFields); + } + else + { + $aFormData['updated_fields'] = $oFormManager->GetRenderer()->Render($aRequestedFields); + } + } + else + { + $aFieldSetData['fields_list'] = $oFormManager->GetRenderer()->Render(); + } + + // Preparing form data + $aFormData['id'] = $oFormManager->GetForm()->GetId(); + $aFormData['transaction_id'] = $oFormManager->GetForm()->GetTransactionId(); + $aFormData['formmanager_class'] = $oFormManager->GetClass(); + $aFormData['formmanager_data'] = $oFormManager->ToJSON(); + $aFormData['renderer'] = $oFormManager->GetRenderer(); + $aFormData['object_name'] = $oFormManager->GetObject()->GetName(); + $aFormData['object_state'] = $oFormManager->GetObject()->GetState(); + $aFormData['fieldset'] = $aFieldSetData; + $aFormData['display_mode'] = (isset($aFormProperties['properties'])) ? $aFormProperties['properties']['display_mode'] : ApplicationHelper::FORM_DEFAULT_DISPLAY_MODE; + + return $aFormData; + } + + /** + * Handles the autocomplete search + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sTargetAttCode Attribute code of the host object pointing to the Object class to search + * @param string $sHostObjectClass Class name of the host object + * @param string $sHostObjectId Id of the host object + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \OQLException + */ + public function SearchAutocompleteAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) + { + $aData = array( + 'results' => array( + 'count' => 0, + 'items' => array() + ) + ); + + // Parsing parameters from request payload + parse_str($oRequest->getContent(), $aRequestContent); + + // Checking parameters + if (!isset($aRequestContent['sQuery'])) + { + IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameter sQuery missing.'); + $oApp->abort(500, Dict::Format('UI:Error:ParameterMissing', 'sQuery')); + } + + // Retrieving parameters + $sQuery = $aRequestContent['sQuery']; + $sFieldId = $aRequestContent['sFieldId']; + + // Checking security layers + if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) + { + IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sHostObjectClass . '::' . $sHostObjectId . '.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving host object for future DBSearch parameters + if ($sHostObjectId !== null) + { + // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated + $oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true); + } + else + { + $oHostObject = MetaModel::NewObject($sHostObjectClass); + // Retrieving action rules + // + // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. + // But it would not be a security issue as it only presets values in the form. + $sActionRulesToken = $oApp['request_manipulator']->ReadParam('ar_token', ''); + $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); + // Preparing object + $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject); + } + + // Updating host object with form data / values + $sFormManagerClass = $aRequestContent['formmanager_class']; + $sFormManagerData = $aRequestContent['formmanager_data']; + if (!empty($sFormManagerClass) && !empty($sFormManagerData)) + { + $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); + $oFormManager->SetApplication($oApp); + $oFormManager->SetObject($oHostObject); + + // Applying action rules if present + if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) + { + $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); + $oObj = $oFormManager->GetObject(); + $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj); + $oFormManager->SetObject($oObj); + } + + // Updating host object + $oFormManager->OnUpdate(array('currentValues' => $aRequestContent['current_values'])); + $oHostObject = $oFormManager->GetObject(); + } + + // Building search query + // - Retrieving target object class from attcode + $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode); + if ($oTargetAttDef->GetEditClass() === 'CustomFields') + { + $oRequestTemplate = $oHostObject->Get($sTargetAttCode); + $oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch(); + $sTargetObjectClass = $oTemplateFieldSearch->GetClass(); + } + elseif ($oTargetAttDef->IsLinkSet()) + { + throw new Exception('Search autocomplete cannot apply on AttributeLinkedSet objects, ' . get_class($oTargetAttDef) . ' (' . $sHostObjectClass . '->' . $sTargetAttCode . ') given.'); + } + else + { + $sTargetObjectClass = $oTargetAttDef->GetTargetClass(); + } + // - Base query from meta model + if ($oTargetAttDef->GetEditClass() === 'CustomFields') + { + $oSearch = $oTemplateFieldSearch; + } + else + { + $oSearch = DBSearch::FromOQL($oTargetAttDef->GetValuesDef()->GetFilterExpression()); + } + // - Adding query condition + $oSearch->AddConditionExpression(new BinaryExpression(new FieldExpression('friendlyname', $oSearch->GetClassAlias()), 'LIKE', new VariableExpression('ac_query'))); + // - Intersecting with scope constraints + // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. + // It is the responsability of the template designer to write the right query so the user see only what he should. + if ($oTargetAttDef->GetEditClass() !== 'CustomFields') + { + $oScopeSearch = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ); + $oSearch = $oSearch->Intersect($oScopeSearch); + // - Allowing all data if necessary + if ($oScopeSearch->IsAllDataAllowed()) + { + $oSearch->AllowAllData(); + } + } + + // Retrieving results + // - Preparing object set + $oSet = new DBObjectSet($oSearch, array(), array('this' => $oHostObject, 'ac_query' => '%' . $sQuery . '%')); + $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => array('friendlyname'))); + // Note : This limit is also used in the field renderer by typeahead to determine how many suggestions to display + if ($oTargetAttDef->GetEditClass() === 'CustomFields') + { + $oSet->SetLimit(static::DEFAULT_LIST_LENGTH); + } + else + { + $oSet->SetLimit($oTargetAttDef->GetMaximumComboLength()); // TODO : Is this the right limit value ? We might want to use another parameter + } + // - Retrieving objects + while ($oItem = $oSet->Fetch()) + { + $aData['results']['items'][] = array('id' => $oItem->GetKey(), 'name' => html_entity_decode($oItem->GetName(), ENT_QUOTES, 'UTF-8')); + $aData['results']['count'] ++; + } + + // Preparing response + if ($oRequest->isXmlHttpRequest()) + { + $oResponse = $oApp->json($aData); + } + else + { + $oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + return $oResponse; + } + + /** + * Handles the regular (table) search from an attribute + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sTargetAttCode Attribute code of the host object pointing to the Object class to search + * @param string $sHostObjectClass Class name of the host object + * @param string $sHostObjectId Id of the host object + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \OQLException + */ + public function SearchFromAttributeAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) + { + $aData = array( + 'sMode' => 'search_regular', + 'sTargetAttCode' => $sTargetAttCode, + 'sHostObjectClass' => $sHostObjectClass, + 'sHostObjectId' => $sHostObjectId, + 'sActionRulesToken' => $oApp['request_manipulator']->ReadParam('ar_token', ''), + ); + + // Checking security layers + if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) + { + IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sHostObjectClass . '::' . $sHostObjectId . ' object.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving host object for future DBSearch parameters + if ($sHostObjectId !== null) + { + // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated + $oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true); + } + else + { + $oHostObject = MetaModel::NewObject($sHostObjectClass); + // Retrieving action rules + // + // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. + // But it would not be a security issue as it only presets values in the form. + $aActionRules = !empty($aData['sActionRulesToken']) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : array(); + // Preparing object + $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject); + } + + // Updating host object with form data / values + $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); + $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); + if ( !empty($sFormManagerClass) && !empty($sFormManagerData) ) + { + $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); + $oFormManager->SetApplication($oApp); + $oFormManager->SetObject($oHostObject); + + // Applying action rules if present + if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) + { + $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken()); + $oObj = $oFormManager->GetObject(); + $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj); + $oFormManager->SetObject($oObj); + } + + // Updating host object + $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW))); + $oHostObject = $oFormManager->GetObject(); + } + + // Retrieving request parameters + $iPageNumber = $oApp['request_manipulator']->ReadParam('iPageNumber', static::DEFAULT_PAGE_NUMBER, FILTER_SANITIZE_NUMBER_INT); + $iListLength = $oApp['request_manipulator']->ReadParam('iListLength', static::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT); + $bInitalPass = $oApp['request_manipulator']->HasParam('draw') ? false : true; + $sQuery = $oApp['request_manipulator']->ReadParam('sSearchValue', ''); + $sFormPath = $oApp['request_manipulator']->ReadParam('sFormPath', ''); + $sFieldId = $oApp['request_manipulator']->ReadParam('sFieldId', ''); + $aObjectIdsToIgnore = $oApp['request_manipulator']->ReadParam('aObjectIdsToIgnore', null, FILTER_UNSAFE_RAW); + + // Building search query + // - Retrieving target object class from attcode + $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode); + if ($oTargetAttDef->IsExternalKey()) + { + $sTargetObjectClass = $oTargetAttDef->GetTargetClass(); + } + elseif ($oTargetAttDef->IsLinkSet()) + { + if (!$oTargetAttDef->IsIndirect()) + { + $sTargetObjectClass = $oTargetAttDef->GetLinkedClass(); + } + else + { + $oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote()); + $sTargetObjectClass = $oRemoteAttDef->GetTargetClass(); + } + } + elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') + { + $oRequestTemplate = $oHostObject->Get($sTargetAttCode); + $oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch(); + $sTargetObjectClass = $oTemplateFieldSearch->GetClass(); + } + else + { + throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, ' . get_class($oTargetAttDef) . ' given.'); + } + + // - Retrieving class attribute list + $aAttCodes = ApplicationHelper::GetLoadedListFromClass($oApp, $sTargetObjectClass, 'list'); + // - Adding friendlyname attribute to the list is not already in it + $sTitleAttCode = 'friendlyname'; + if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) + { + $aAttCodes = array_merge(array($sTitleAttCode), $aAttCodes); + } + + // - Retrieving scope search + // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. + // It is the responsability of the template designer to write the right query so the user see only what he should. + $oScopeSearch = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ); + $aInternalParams = array(); + if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) + { + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' has no scope query for ' . $sTargetObjectClass . ' class.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // - Base query from meta model + if ($oTargetAttDef->IsExternalKey()) + { + $oSearch = DBSearch::FromOQL($oTargetAttDef->GetValuesDef()->GetFilterExpression()); + } + elseif ($oTargetAttDef->IsLinkSet()) + { + $oSearch = $oScopeSearch; + } + elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') + { + // Note : $oTemplateFieldSearch has been defined in the "Retrieving target object class from attcode" part, it is not available otherwise + $oSearch = $oTemplateFieldSearch; + } + + // - Filtering objects to ignore + if (($aObjectIdsToIgnore !== null) && (is_array($aObjectIdsToIgnore))) + { + //$oSearch->AddConditionExpression('id', $aObjectIdsToIgnore, 'NOT IN'); + $aExpressions = array(); + foreach ($aObjectIdsToIgnore as $sObjectIdToIgnore) + { + $aExpressions[] = new ScalarExpression($sObjectIdToIgnore); + } + $oSearch->AddConditionExpression(new BinaryExpression(new FieldExpression('id', $oSearch->GetClassAlias()), 'NOT IN', new ListExpression($aExpressions))); + } + + // - Adding query condition + $aInternalParams['this'] = $oHostObject; + if (!empty($sQuery)) + { + $oFullExpr = null; + for ($i = 0; $i < count($aAttCodes); $i++) + { + // Checking if the current attcode is an external key in order to search on the friendlyname + $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $aAttCodes[$i]); + $sAttCode = (!$oAttDef->IsExternalKey()) ? $aAttCodes[$i] : $aAttCodes[$i] . '_friendlyname'; + // Building expression for the current attcode + // - For attributes that need conversion from their display value to storage value + // Note : This is dirty hack that will need to be refactored in the OQL core in order to be nicer and to be extended to other types such as dates etc... + if (($oAttDef instanceof AttributeEnum) || ($oAttDef instanceof AttributeFinalClass)) + { + // Looking up storage value + $aMatchedCodes = array(); + foreach ($oAttDef->GetAllowedValues() as $sValueCode => $sValueLabel) + { + if (stripos($sValueLabel, $sQuery) !== false) + { + $aMatchedCodes[] = $sValueCode; + } + } + // Building expression + if (!empty($aMatchedCodes)) + { + $oEnumeratedListExpr = ListExpression::FromScalars($aMatchedCodes); + $oBinExpr = new BinaryExpression(new FieldExpression($sAttCode, $oSearch->GetClassAlias()), 'IN', $oEnumeratedListExpr); + } + else + { + $oBinExpr = new FalseExpression(); + } + } + // - For regular attributs + else + { + $oBinExpr = new BinaryExpression(new FieldExpression($sAttCode, $oSearch->GetClassAlias()), 'LIKE', new VariableExpression('re_query')); + } + // Adding expression to the full expression (all attcodes) + if ($i === 0) + { + $oFullExpr = $oBinExpr; + } + else + { + $oFullExpr = new BinaryExpression($oFullExpr, 'OR', $oBinExpr); + } + } + // Adding full expression to the search object + $oSearch->AddConditionExpression($oFullExpr); + $aInternalParams['re_query'] = '%' . $sQuery . '%'; + } + + // - Intersecting with scope constraints + // Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates. + // It is the responsability of the template designer to write the right query so the user see only what he should. + if (($oScopeSearch !== null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) + { + $oSearch = $oSearch->Intersect($oScopeSearch); + // - Allowing all data if necessary + if ($oScopeSearch->IsAllDataAllowed()) + { + $oSearch->AllowAllData(); + } + } + + // Retrieving results + // - Preparing object set + $oSet = new DBObjectSet($oSearch, array(), $aInternalParams); + $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => $aAttCodes)); + $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1)); + // - Retrieving columns properties + $aColumnProperties = array(); + foreach ($aAttCodes as $sAttCode) + { + $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode); + $aColumnProperties[$sAttCode] = array( + 'title' => $oAttDef->GetLabel() + ); + } + // - Retrieving objects + $aItems = array(); + while ($oItem = $oSet->Fetch()) + { + $aItems[] = $this->PrepareObjectInformations($oApp, $oItem, $aAttCodes); + } + + // Preparing response + if ($bInitalPass) + { + $aData = $aData + array( + 'form' => array( + 'id' => 'object_search_form_' . time(), + 'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel(), MetaModel::GetName($sTargetObjectClass)) + ), + 'aColumnProperties' => json_encode($aColumnProperties), + 'aResults' => array( + 'aItems' => json_encode($aItems), + 'iCount' => count($aItems) + ), + 'bMultipleSelect' => $oTargetAttDef->IsLinkSet(), + 'aSource' => array( + 'sFormPath' => $sFormPath, + 'sFieldId' => $sFieldId, + 'aObjectIdsToIgnore' => $aObjectIdsToIgnore, + 'sFormManagerClass' => $sFormManagerClass, + 'sFormManagerData' => $sFormManagerData + ) + ); + + if ($oRequest->isXmlHttpRequest()) + { + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); + } + else + { + //$oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData); + } + } + else + { + $aData = $aData + array( + 'levelsProperties' => $aColumnProperties, + 'data' => $aItems, + 'recordsTotal' => $oSet->Count(), + 'recordsFiltered' => $oSet->Count() + ); + + $oResponse = $oApp->json($aData); + } + + return $oResponse; + } + + /** + * Handles the hierarchical search from an attribute + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sTargetAttCode Attribute code of the host object pointing to the Object class to search + * @param string $sHostObjectClass Class name of the host object + * @param string $sHostObjectId Id of the host object + * + * @return void + * + */ + public function SearchHierarchyAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null) + { + // TODO + } + + /** + * Handles ormDocument display / download from an object + * + * Note: This is inspired from pages/ajax.document.php, but duplicated as there is no secret mecanism for ormDocument yet. + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sOperation + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \ArchivedObjectException + * @throws \CoreException + */ + public function DocumentAction(Request $oRequest, Application $oApp, $sOperation = null) + { + // Setting default operation + if($sOperation === null) + { + $sOperation = 'display'; + } + + // Retrieving ormDocument's host object + $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', ''); + $sObjectId = $oApp['request_manipulator']->ReadParam('sObjectId', ''); + $sObjectField = $oApp['request_manipulator']->ReadParam('sObjectField', ''); + + // When reaching to an Attachment, we have to check security on its host object instead of the Attachment itself + if($sObjectClass === 'Attachment') + { + $oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, true, true); + $sHostClass = $oAttachment->Get('item_class'); + $sHostId = $oAttachment->Get('item_id'); + } + else + { + $sHostClass = $sObjectClass; + $sHostId = $sObjectId; + } + + // Checking security layers + if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostClass, $sHostId)) + { + IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to retrieve document from attribute ' . $sObjectField . ' as it not allowed to read ' . $sHostClass . '::' . $sHostId . ' object.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Retrieving object + $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* Must not be found */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sHostClass)); + if ($oObject === null) + { + // We should never be there as the secuirty helper makes sure that the object exists, but just in case. + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sObjectClass . '::' . $sObjectId . '.'); + $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); + } + + // Setting cache timeout + // Note: Attachment download should be handle through AttachmentAction() + if($sObjectClass === 'Attachment') + { + // One year ahead: an attachement cannot change + $iCacheSec = 31556926; + } + else + { + $iCacheSec = $oApp['request_manipulator']->ReadParam('cache', 0, FILTER_SANITIZE_NUMBER_INT); + } + + $aHeaders = array(); + if($iCacheSec > 0) + { + $aHeaders['Expires'] = ''; + $aHeaders['Cache-Control'] = 'no-transform, public,max-age='.$iCacheSec.',s-maxage='.$iCacheSec; + // Reset the value set previously + $aHeaders['Pragma'] = 'cache'; + // An arbitrary date in the past is ok + $aHeaders['Last-Modified'] = 'Wed, 15 Jun 2015 13:21:15 GMT'; + } + + /** @var \ormDocument $oDocument */ + $oDocument = $oObject->Get($sObjectField); + $aHeaders['Content-Type'] = $oDocument->GetMimeType(); + $aHeaders['Content-Disposition'] = (($sOperation === 'display') ? 'inline' : 'attachment') . ';filename="'.$oDocument->GetFileName().'"'; + + return new Response($oDocument->GetData(), Response::HTTP_OK, $aHeaders); + } + + /** + * Handles attachment add/remove on an object + * + * Note: This is inspired from itop-attachment/ajax.attachment.php + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * @param string $sOperation + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Exception + * @throws \CoreException + * @throws \CoreUnexpectedValue + */ + public function AttachmentAction(Request $oRequest, Application $oApp, $sOperation = null) + { + $aData = array( + 'att_id' => 0, + 'preview' => false, + 'msg' => '' + ); + + // Retrieving sOperation from request only if it wasn't forced (determined by the route) + if ($sOperation === null) + { + $sOperation = $oApp['request_manipulator']->ReadParam('operation', null); + } + switch ($sOperation) + { + case 'add': + $sFieldName = $oApp['request_manipulator']->ReadParam('field_name', ''); + $sObjectClass = $oApp['request_manipulator']->ReadParam('object_class', ''); + $sTempId = $oApp['request_manipulator']->ReadParam('temp_id', ''); + + if (empty($sObjectClass) || empty($sTempId)) + { + $aData['error'] = Dict::Format('UI:Error:2ParametersMissing', 'object_class', 'temp_id'); + } + else + { + try + { + $oDocument = utils::ReadPostedDocument($sFieldName); + $oAttachment = MetaModel::NewObject('Attachment'); + $oAttachment->Set('expire', time() + MetaModel::GetConfig()->Get('draft_attachments_lifetime')); // one hour... + $oAttachment->Set('temp_id', $sTempId); + $oAttachment->Set('item_class', $sObjectClass); + $oAttachment->SetDefaultOrgId(); + $oAttachment->Set('contents', $oDocument); + $iAttId = $oAttachment->DBInsert(); + + $aData['msg'] = htmlentities($oDocument->GetFileName(), ENT_QUOTES, 'UTF-8'); + // TODO : Change icon location when itop-attachment is refactored + //$aData['icon'] = utils::GetAbsoluteUrlAppRoot() . AttachmentPlugIn::GetFileIcon($oDoc->GetFileName()); + $aData['icon'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-attachments/icons/image.png'; + $aData['att_id'] = $iAttId; + $aData['preview'] = $oDocument->IsPreviewAvailable() ? 'true' : 'false'; + } + catch (FileUploadException $e) + { + $aData['error'] = $e->GetMessage(); + } + } + + // Note : The Content-Type header is set to 'text/plain' in order to be IE9 compatible. Otherwise ('application/json') IE9 will download the response as a JSON file to the user computer... + $oResponse = $oApp->json($aData, 200, array('Content-Type' => 'text/plain')); + break; + + case 'download': + // Preparing redirection + // - Route + $aRouteParams = array( + 'sObjectClass' => 'Attachment', + 'sObjectId' => $oApp['request_manipulator']->ReadParam('sAttachmentId', null), + 'sObjectField' => 'contents', + ); + $sRedirectRoute = $oApp['url_generator']->generate('p_object_document_download', $aRouteParams); + // - Request + $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all()); + + $oResponse = $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true); + break; + + default: + $oApp->abort(403); + break; + } + + return $oResponse; + } + + /** + * Returns a json response containing an array of objects informations. + * + * The service must be given 3 parameters : + * - sObjectClass : The class of objects to retrieve information from + * - aObjectIds : An array of object ids + * - aObjectAttCodes : An array of attribute codes to retrieve + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \Silex\Application $oApp + * + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \OQLException + * @throws \CoreException + */ + public function GetInformationsAsJsonAction(Request $oRequest, Application $oApp) + { + $aData = array(); + + // Retrieving parameters + $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', ''); + $aObjectIds = $oApp['request_manipulator']->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW); + $aObjectAttCodes = $oApp['request_manipulator']->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW); + if ( empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes) ) + { + IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, aObjectIds and aObjectAttCodes expected, "' . $sObjectClass . '", "' . implode('/', $aObjectIds) . '" given.'); + $oApp->abort(500, 'Invalid request data, some informations are missing'); + } + + // Checking that id is in the AttCodes + if (!in_array('id', $aObjectAttCodes)) + { + $aObjectAttCodes = array_merge(array('id'), $aObjectAttCodes); + } + + // Building the search + $bIgnoreSilos = $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass); + $oSearch = DBObjectSearch::FromOQL("SELECT " . $sObjectClass . " WHERE id IN ('" . implode("','", $aObjectIds) . "')"); + if ($bIgnoreSilos === true) + { + $oSearch->AllowAllData(); + } + $oSet = new DBObjectSet($oSearch); + $oSet->OptimizeColumnLoad($aObjectAttCodes); + + // Retrieving objects + while ($oObject = $oSet->Fetch()) + { + $aData['items'][] = $this->PrepareObjectInformations($oApp, $oObject, $aObjectAttCodes); + } + + return $oApp->json($aData); + } + + /** + * Prepare a DBObject informations as an array for a client side usage (typically, add a row in a table) + * + * @param \Silex\Application $oApp + * @param \DBObject $oObject + * @param array $aAttCodes + * + * @return array + * + * @throws \Exception + * @throws \CoreException + */ + protected function PrepareObjectInformations(Application $oApp, DBObject $oObject, $aAttCodes = array()) + { + $sObjectClass = get_class($oObject); + $aObjectData = array( + 'id' => $oObject->GetKey(), + 'name' => $oObject->GetName(), + 'attributes' => array(), + ); + + // Retrieving attributes definitions + $aAttDefs = array(); + foreach ($aAttCodes as $sAttCode) + { + if ($sAttCode === 'id') + continue; + + $aAttDefs[$sAttCode] = MetaModel::GetAttributeDef($sObjectClass, $sAttCode); + } + + // Preparing attribute data + foreach ($aAttDefs as $oAttDef) + { + $aAttData = array( + 'att_code' => $oAttDef->GetCode() + ); + + if ($oAttDef->IsExternalKey()) + { + $aAttData['value'] = $oObject->GetAsHTML($oAttDef->GetCode() . '_friendlyname'); + + // Checking if user can access object's external key + if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass())) + { + $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $oAttDef->GetTargetClass(), 'sObjectId' => $oObject->Get($oAttDef->GetCode()))); + } + } + elseif ($oAttDef->IsLinkSet()) + { + // We skip it + continue; + } + elseif ($oAttDef instanceof AttributeImage) + { + $oOrmDoc = $oObject->Get($oAttDef->GetCode()); + if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty()) + { + $sUrl = $oApp['url_generator']->generate('p_object_document_display', array('sObjectClass' => get_class($oObject), 'sObjectId' => $oObject->GetKey(), 'sObjectField' => $oAttDef->GetCode(), 'cache' => 86400)); + } + else + { + $sUrl = $oAttDef->Get('default_image'); + } + $aAttData['value'] = ''; + } + else + { + $aAttData['value'] = $oAttDef->GetAsHTML($oObject->Get($oAttDef->GetCode())); + + if ($oAttDef instanceof AttributeFriendlyName) + { + // Checking if user can access object + if(SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass)) + { + $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oObject->GetKey())); + } + } + } + + $aObjectData['attributes'][$oAttDef->GetCode()] = $aAttData; + } + + return $aObjectData; + } + +} diff --git a/js/utils.js b/js/utils.js index 346ebf71a..fd5c971e9 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,718 +1,718 @@ -// Some general purpose JS functions for the iTop application - -//IE 8 compatibility, copied from: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/IndexOf -if (!Array.prototype.indexOf) { - - if (false) // deactivated since it causes troubles: for(k in aData) => returns the indexOf function as first element on empty arrays ! - { - Array.prototype.indexOf = function (searchElement /*, fromIndex */) { - "use strict"; - if (this == null) { - throw new TypeError(); - } - var t = Object(this); - var len = t.length >>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 1) { - n = Number(arguments[1]); - if (n != n) { // shortcut for verifying if it's NaN - n = 0; - } else if (n != 0 && n != Infinity && n != -Infinity) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len-Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - } - } -} -// Polyfill for Array.from for IE -// Copied from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from -if (!Array.from) { - Array.from = (function () { - var toStr = Object.prototype.toString; - var isCallable = function (fn) { - return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; - }; - var toInteger = function (value) { - var number = Number(value); - if (isNaN(number)) { return 0; } - if (number === 0 || !isFinite(number)) { return number; } - return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); - }; - var maxSafeInteger = Math.pow(2, 53) - 1; - var toLength = function (value) { - var len = toInteger(value); - return Math.min(Math.max(len, 0), maxSafeInteger); - }; - - // The length property of the from method is 1. - return function from(arrayLike/*, mapFn, thisArg */) { - // 1. Let C be the this value. - var C = this; - - // 2. Let items be ToObject(arrayLike). - var items = Object(arrayLike); - - // 3. ReturnIfAbrupt(items). - if (arrayLike == null) { - throw new TypeError('Array.from requires an array-like object - not null or undefined'); - } - - // 4. If mapfn is undefined, then let mapping be false. - var mapFn = arguments.length > 1 ? arguments[1] : void undefined; - var T; - if (typeof mapFn !== 'undefined') { - // 5. else - // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. - if (!isCallable(mapFn)) { - throw new TypeError('Array.from: when provided, the second argument must be a function'); - } - - // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (arguments.length > 2) { - T = arguments[2]; - } - } - - // 10. Let lenValue be Get(items, "length"). - // 11. Let len be ToLength(lenValue). - var len = toLength(items.length); - - // 13. If IsConstructor(C) is true, then - // 13. a. Let A be the result of calling the [[Construct]] internal method - // of C with an argument list containing the single item len. - // 14. a. Else, Let A be ArrayCreate(len). - var A = isCallable(C) ? Object(new C(len)) : new Array(len); - - // 16. Let k be 0. - var k = 0; - // 17. Repeat, while k < len… (also steps a - h) - var kValue; - while (k < len) { - kValue = items[k]; - if (mapFn) { - A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); - } else { - A[k] = kValue; - } - k += 1; - } - // 18. Let putStatus be Put(A, "length", len, true). - A.length = len; - // 20. Return A. - return A; - }; - }()); -} - -/** - * Reload a truncated list - */ -aTruncatedLists = {}; // To keep track of the list being loaded, each member is an ajaxRequest object - -function ReloadTruncatedList(divId, sSerializedFilter, sExtraParams) { - $('#'+divId).block(); - //$('#'+divId).blockUI(); - if (aTruncatedLists[divId] != undefined) { - try { - aAjaxRequest = aTruncatedLists[divId]; - aAjaxRequest.abort(); - } - catch (e) { - // Do nothing special, just continue - console.log('Uh,uh, exception !'); - } - } - aTruncatedLists[divId] = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style=list', - {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams}, - function (data) { - aTruncatedLists[divId] = undefined; - if (data.length > 0) { - $('#'+divId).html(data); - $('#'+divId+' .listResults').tableHover(); // hover tables - $('#'+divId+' .listResults').each(function () { - var table = $(this); - var id = $(this).parent(); - aTruncatedLists[divId] = undefined; - var checkbox = (table.find('th:first :checkbox').length > 0); - if (checkbox) { - // There is a checkbox in the first column, don't make it sortable - table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager")}); // sortable and zebra tables - } - else { - // There is NO checkbox in the first column, all columns are considered sortable - table.tablesorter({widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager"), totalRows: 97, filter: sSerializedFilter, extra_params: sExtraParams}); // sortable and zebra tables - } - }); - $('#'+divId).unblock(); - } - } - ); -} - -/** - * Truncate a previously expanded list ! - */ -function TruncateList(divId, iLimit, sNewLabel, sLinkLabel) { - $('#'+divId).block(); - var iCount = 0; - $('#'+divId+' table.listResults tr:gt('+iLimit+')').each(function () { - $(this).remove(); - }); - $('#lbl_'+divId).html(sNewLabel); - $('#'+divId+' table.listResults tr:last td').addClass('truncated'); - $('#'+divId+' table.listResults').addClass('truncated'); - $('#trc_'+divId).html(sLinkLabel); - $('#'+divId+' .listResults').trigger("update"); // Reset the cache - $('#'+divId).unblock(); -} - -/** - * Reload any block -- used for periodic auto-reload - */ -function ReloadBlock(divId, sStyle, sSerializedFilter, sExtraParams) { - // Check if the user is not editing the list properties right now - var bDialogOpen = false; - var oDataTable = $('#'+divId+' :itop-datatable'); - var bIsDataTable = false; - if (oDataTable.length > 0) { - bDialogOpen = oDataTable.datatable('IsDialogOpen'); - bIsDataTable = true; - } - if (!bDialogOpen) { - if (bIsDataTable) { - oDataTable.datatable('DoRefresh'); - } - else { - $('#'+divId).block(); - - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style='+sStyle, - {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams}, - function (data) { - $('#'+divId).empty(); - $('#'+divId).append(data); - $('#'+divId).removeClass('loading'); - } - ); - } - } -} - -function SaveGroupBySortOrder(sTableId, aValues) { - var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id'); - var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId; - if (aValues.length != 0) { - $sValue = JSON.stringify(aValues); - if (GetUserPreference(sPrefKey, null) != $sValue) { - SetUserPreference(sPrefKey, $sValue, true); - } - } -} - -function LoadGroupBySortOrder(sTableId) { - var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id'); - var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId; - var sValues = GetUserPreference(sPrefKey, null); - if (sValues != null) { - aValues = JSON.parse(sValues); - window.setTimeout(function () { - $('#'+sTableId+' table.listResults').trigger('sorton', [aValues]); - }, 50); - } - -} - -/** - * Update the display and value of a file input widget when the user picks a new file - */ -function UpdateFileName(id, sNewFileName) { - var aPath = sNewFileName.split('\\'); - var sNewFileName = aPath[aPath.length-1]; - - $('#'+id).val(sNewFileName); - $('#'+id).trigger('validate'); - $('#name_'+id).text(sNewFileName); - return true; -} - -/** - * Reload a search form for the specified class - */ -function ReloadSearchForm(divId, sClassName, sBaseClass, sContext, sTableId, sExtraParams) { - var oDiv = $('#ds_'+divId); - oDiv.block(); - // deprecated in jQuery 1.8 - //var oFormEvents = $('#ds_'+divId+' form').data('events'); - var oForm = $('#ds_'+divId+' form'); - var oFormEvents = $._data(oForm[0], "events"); - - // Save the submit handlers - aSubmit = new Array(); - if ((oFormEvents != null) && (oFormEvents.submit != undefined)) { - for (var index = 0; index < oFormEvents.submit.length; index++) { - aSubmit [index] = {data: oFormEvents.submit[index].data, namespace: oFormEvents.submit[index].namespace, handler: oFormEvents.submit[index].handler}; - } - } - sAction = $('#ds_'+divId+' form').attr('action'); - - // Save the current values in the form - var oMap = {}; - $('#ds_'+divId+" form :input[name!='']").each(function () { - oMap[this.name] = this.value; - }); - oMap.operation = 'search_form'; - oMap.className = sClassName; - oMap.baseClass = sBaseClass; - oMap.currentId = divId; - oMap._table_id_ = sTableId; - oMap.action = sAction; - if(sExtraParams['selection_mode']) - { - oMap.selection_mode = sExtraParams['selection_mode']; - } - if(sExtraParams['result_list_outer_selector']) - { - oMap.result_list_outer_selector = sExtraParams['result_list_outer_selector']; - } - if(sExtraParams['cssCount']) - { - oMap.css_count = sExtraParams['cssCount']; - $(sExtraParams['cssCount']).val(0).trigger('change'); - } - if(sExtraParams['table_inner_id']) - { - oMap.table_inner_id = sExtraParams['table_inner_id']; - } - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, oMap, - function (data) { - oDiv.empty(); - oDiv.append(data); - oDiv.unblock(); - oDiv.parent().resize(); // Inform the parent that the form has just been (potentially) resized - oDiv.find('form').triggerHandler('itop.search.form.reloaded'); - } - ); -} - -/** - * Stores - in a persistent way - user specific preferences - * depends on a global variable oUserPreferences created/filled by the iTopWebPage - * that acts as a local -write through- cache - */ -function SetUserPreference(sPreferenceCode, sPrefValue, bPersistent) { - sPreviousValue = undefined; - try { - sPreviousValue = oUserPreferences[sPreferenceCode]; - } - catch (err) { - sPreviousValue = undefined; - } - oUserPreferences[sPreferenceCode] = sPrefValue; - if (bPersistent && (sPrefValue != sPreviousValue)) { - ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', - {operation: 'set_pref', code: sPreferenceCode, value: sPrefValue}); // Make it persistent - } -} - -/** - * Get user specific preferences - * depends on a global variable oUserPreferences created/filled by the iTopWebPage - * that acts as a local -write through- cache - */ -function GetUserPreference(sPreferenceCode, sDefaultValue) { - var value = sDefaultValue; - if (oUserPreferences[sPreferenceCode] != undefined) { - value = oUserPreferences[sPreferenceCode]; - } - return value; -} - -/** - * Check/uncheck a whole list of checkboxes - */ -function CheckAll(sSelector, bValue) { - var value = bValue; - $(sSelector).each(function () { - if (this.checked != value) { - this.checked = value; - $(this).trigger('change'); - } - }); -} - - -/** - * Toggle (enabled/disabled) the specified field of a form - */ -function ToggleField(value, field_id) { - if (value) { - $('#'+field_id).prop('disabled', false); - // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings) - $('#'+field_id+' :input').prop('disabled', false); - } - else { - $('#'+field_id).prop('disabled', true); - // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings) - $('#'+field_id+' :input').prop('disabled', true); - } - $('#'+field_id).trigger('update'); - $('#'+field_id).trigger('validate'); -} - -/** - * For the fields that cannot be visually disabled, they can be blocked - * @return - */ -function BlockField(field_id, bBlocked) { - if (bBlocked) { - $('#'+field_id).block({message: ' ** disabled ** '}); - } - else { - $('#'+field_id).unblock(); - } -} - -/** - * Updates (enables/disables) a "duration" field - */ -function ToggleDurationField(field_id) { - // Toggle all the subfields that compose the "duration" input - aSubFields = new Array('d', 'h', 'm', 's'); - - if ($('#'+field_id).prop('disabled')) { - for (var i = 0; i < aSubFields.length; i++) { - $('#'+field_id+'_'+aSubFields[i]).prop('disabled', true); - } - } - else { - for (var i = 0; i < aSubFields.length; i++) { - $('#'+field_id+'_'+aSubFields[i]).prop('disabled', false); - } - } -} - -/** - * PropagateCheckBox - */ -function PropagateCheckBox(bCurrValue, aFieldsList, bCheck) { - if (bCurrValue == bCheck) { - for (var i = 0; i < aFieldsList.length; i++) { - var sFieldId = aFieldsList[i]; - $('#enable_'+sFieldId).prop('checked', bCheck); - ToggleField(bCheck, sFieldId); - - // Cascade propagation - $('#enable_'+sFieldId).trigger('change'); - } - } -} - -function FixTableSorter(table) { - if (table[0].config == undefined) { - // Table is not sort-able, let's fix it - var checkbox = (table.find('th:first :checkbox').length > 0); - if (checkbox) { - // There is a checkbox in the first column, don't make it sort-able - table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables - } - else { - // There is NO checkbox in the first column, all columns are considered sort-able - table.tablesorter({widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables - } - } -} - -function DashletCreationDlg(sOQL, sContext) { - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'dashlet_creation_dlg', oql: sOQL}, function (data) { - $('body').append(data); - }); - return false; -} - -function ShortcutListDlg(sOQL, sDataTableId, sContext) { - var sDataTableName = 'datatable_'+sDataTableId; - var oTableSettings = { - oColumns: $('#'+sDataTableName).datatable('option', 'oColumns'), - iPageSize: $('#'+sDataTableName).datatable('option', 'iPageSize') - }; - var sTableSettings = JSON.stringify(oTableSettings); - - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'shortcut_list_dlg', oql: sOQL, table_settings: sTableSettings}, function (data) { - $('body').append(data); - }); - return false; -} - -function ExportListDlg(sOQL, sDataTableId, sFormat, sDlgTitle) { - var aFields = []; - if (sDataTableId != '') { - var sDataTableName = 'datatable_'+sDataTableId; - var oColumns = $('#'+sDataTableName).datatable('option', 'oColumns'); - for (var j in oColumns) { - for (var k in oColumns[j]) { - if (oColumns[j][k].checked) { - var sCode = oColumns[j][k].code; - if (sCode == '_key_') { - sCode = 'id'; - } - aFields.push(j+'.'+sCode); - } - } - } - } - - var oParams = { - interactive: 1, - mode: 'dialog', - expression: sOQL, - suggested_fields: aFields.join(','), - dialog_title: sDlgTitle - }; - - if (sFormat !== null) { - oParams.format = sFormat; - } - - $.post(GetAbsoluteUrlAppRoot()+'webservices/export-v2.php', oParams, function (data) { - $('body').append(data); - }); - return false; -} - -function ExportToggleFormat(sFormat) { - $('.form_part').hide(); - for (k in window.aFormParts[sFormat]) { - $('#form_part_'+window.aFormParts[sFormat][k]).show().trigger('form-part-activate'); - } -} - -function ExportStartExport() { - var oParams = {}; - $('.form_part:visible :input').each(function () { - if (this.name != '') { - if ((this.type == 'radio') || (this.type == 'checkbox')) { - if (this.checked) { - oParams[this.name] = $(this).val(); - } - } - else { - oParams[this.name] = $(this).val(); - } - } - }); - $(':itop-tabularfieldsselector:visible').tabularfieldsselector('close_all_tooltips'); - $('#export-form').hide(); - $('#export-feedback').show(); - oParams.operation = 'export_build'; - oParams.format = $('#export-form :input[name=format]').val(); - var sQueryMode = $(':input[name=query_mode]:checked').val(); - if ($(':input[name=query_mode]:checked').length > 0) { - if (sQueryMode == 'oql') { - oParams.expression = $('#export-form :input[name=expression]').val(); - } - else { - oParams.query = $('#export-form :input[name=query]').val(); - } - } - else { - oParams.expression = $('#export-form :input[name=expression]').val(); - oParams.query = $('#export-form :input[name=query]').val(); - } - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) { - if (data == null) { - ExportError('Export failed (no data provided), please contact your administrator'); - } - else { - ExportRun(data); - } - }, 'json') - .fail(function () { - ExportError('Export failed, please contact your administrator'); - }); -} - -function ExportError(sMessage) { - $('.export-message').html(sMessage); - $('.export-progress-bar').hide(); - $('#export-btn').hide(); -} - -function ExportRun(data) { - switch (data.code) { - case 'run': - // Continue - $('.export-progress-bar').progressbar({value: data.percentage}); - $('.export-message').html(data.message); - oParams = {}; - oParams.token = data.token; - var sDataState = $('#export-form').attr('data-state'); - if (sDataState == 'cancelled') { - oParams.operation = 'export_cancel'; - } - else { - oParams.operation = 'export_build'; - } - - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) { - ExportRun(data); - }, - 'json'); - break; - - case 'done': - $('#export-btn').hide(); - sMessage = ''+data.message+''; - $('.export-message').html(sMessage); - $('.export-progress-bar').hide(); - $('#export-btn').hide(); - $('#export-form').attr('data-state', 'done'); - if (data.text_result != undefined) { - if (data.mime_type == 'text/html') { - $('#export_content').parent().html(data.text_result); - $('#export_text_result').show(); - $('#export_text_result .listResults').tableHover(); - $('#export_text_result .listResults').tablesorter({widgets: ['myZebra']}); - } - else { - if ($('#export_text_result').closest('ui-dialog').length == 0) { - // not inside a dialog box, adjust the height... approximately - var jPane = $('#export_text_result').closest('.ui-layout-content'); - var iTotalHeight = jPane.height(); - jPane.children(':visible').each(function () { - if ($(this).attr('id') != '') { - iTotalHeight -= $(this).height(); - } - }); - $('#export_content').height(iTotalHeight-80); - } - $('#export_content').val(data.text_result); - $('#export_text_result').show(); - } - } - $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable'); - break; - - case 'error': - $('#export-form').attr('data-state', 'error'); - $('.export-progress-bar').progressbar({value: data.percentage}); - $('.export-message').html(data.message); - $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable'); - $('#export-btn').hide(); - default: - } -} - -function ExportInitButton(sSelector) { - $(sSelector).on('click', function () { - var sDataState = $('#export-form').attr('data-state'); - switch (sDataState) { - case 'not-yet-started': - $('.form_part:visible').each(function () { - $('#export-form').data('validation_messages', []); - var ret = $(this).trigger('validate'); - }); - var aMessages = $('#export-form').data('validation_messages'); - - if (aMessages.length > 0) { - alert(aMessages.join('')); - return; - } - if ($(this).hasClass('ui-button')) { - $(this).button('option', 'label', Dict.S('UI:Button:Cancel')); - } - else { - $(this).html(Dict.S('UI:Button:Cancel')); - } - $('#export-form').attr('data-state', 'running'); - ExportStartExport(); - break; - - case 'running': - if ($(this).hasClass('ui-button')) { - $(this).button('disable'); - } - else { - $(this).prop('disabled', true); - } - $('#export-form').attr('data-state', 'cancelled'); - break; - - case 'done': - case 'error': - $('#interactive_export_dlg').dialog('close'); - break; - - default: - // Do nothing - } - }); -} - -function DisplayHistory(sSelector, sFilter, iCount, iStart) { - $(sSelector).block(); - var oParams = {operation: 'history_from_filter', filter: sFilter, start: iStart, count: iCount}; - $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) { - $(sSelector).html(data).unblock(); - } - ); -} - -// Very simple equivalent to format: placeholders are %1$s %2$d ... -function Format() { - var args = []; - var str = ''; - if (arguments[0] instanceof Array) { - str = arguments[0][0].toString(); - args = arguments[0]; - } - else { - str = arguments[0].toString(); - if (arguments.length > 1) { - var t = typeof arguments[1]; - args = ("string" === t || "number" === t) ? Array.prototype.slice.call(arguments) : arguments[1]; - } - } - var key; - for (key in args) { - str = str.replace(new RegExp("\\%"+key+"\\$.", "gi"), args[key]); - } - - return str; -} - -/** - * Enable to access translation keys client side. - * The called keys needs to be exported using \WebPage::add_dict_entry - */ -var Dict = {}; -if (typeof aDictEntries == 'undefined') { - Dict._entries = {}; // Entries have not been loaded (we are in the setup ?) -} -else { - Dict._entries = aDictEntries; // Entries were loaded asynchronously via their own js files -} -Dict.S = function (sEntry) { - if (sEntry in Dict._entries) { - return Dict._entries[sEntry]; - } - else { - return sEntry; - } -}; -Dict.Format = function () { - var args = Array.from(arguments); - args[0] = Dict.S(arguments[0]); - return Format(args); +// Some general purpose JS functions for the iTop application + +//IE 8 compatibility, copied from: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/IndexOf +if (!Array.prototype.indexOf) { + + if (false) // deactivated since it causes troubles: for(k in aData) => returns the indexOf function as first element on empty arrays ! + { + Array.prototype.indexOf = function (searchElement /*, fromIndex */) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len-Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } + } +} +// Polyfill for Array.from for IE +// Copied from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from +if (!Array.from) { + Array.from = (function () { + var toStr = Object.prototype.toString; + var isCallable = function (fn) { + return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; + }; + var toInteger = function (value) { + var number = Number(value); + if (isNaN(number)) { return 0; } + if (number === 0 || !isFinite(number)) { return number; } + return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); + }; + var maxSafeInteger = Math.pow(2, 53) - 1; + var toLength = function (value) { + var len = toInteger(value); + return Math.min(Math.max(len, 0), maxSafeInteger); + }; + + // The length property of the from method is 1. + return function from(arrayLike/*, mapFn, thisArg */) { + // 1. Let C be the this value. + var C = this; + + // 2. Let items be ToObject(arrayLike). + var items = Object(arrayLike); + + // 3. ReturnIfAbrupt(items). + if (arrayLike == null) { + throw new TypeError('Array.from requires an array-like object - not null or undefined'); + } + + // 4. If mapfn is undefined, then let mapping be false. + var mapFn = arguments.length > 1 ? arguments[1] : void undefined; + var T; + if (typeof mapFn !== 'undefined') { + // 5. else + // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. + if (!isCallable(mapFn)) { + throw new TypeError('Array.from: when provided, the second argument must be a function'); + } + + // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 2) { + T = arguments[2]; + } + } + + // 10. Let lenValue be Get(items, "length"). + // 11. Let len be ToLength(lenValue). + var len = toLength(items.length); + + // 13. If IsConstructor(C) is true, then + // 13. a. Let A be the result of calling the [[Construct]] internal method + // of C with an argument list containing the single item len. + // 14. a. Else, Let A be ArrayCreate(len). + var A = isCallable(C) ? Object(new C(len)) : new Array(len); + + // 16. Let k be 0. + var k = 0; + // 17. Repeat, while k < len… (also steps a - h) + var kValue; + while (k < len) { + kValue = items[k]; + if (mapFn) { + A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); + } else { + A[k] = kValue; + } + k += 1; + } + // 18. Let putStatus be Put(A, "length", len, true). + A.length = len; + // 20. Return A. + return A; + }; + }()); +} + +/** + * Reload a truncated list + */ +aTruncatedLists = {}; // To keep track of the list being loaded, each member is an ajaxRequest object + +function ReloadTruncatedList(divId, sSerializedFilter, sExtraParams) { + $('#'+divId).block(); + //$('#'+divId).blockUI(); + if (aTruncatedLists[divId] != undefined) { + try { + aAjaxRequest = aTruncatedLists[divId]; + aAjaxRequest.abort(); + } + catch (e) { + // Do nothing special, just continue + console.log('Uh,uh, exception !'); + } + } + aTruncatedLists[divId] = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style=list', + {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams}, + function (data) { + aTruncatedLists[divId] = undefined; + if (data.length > 0) { + $('#'+divId).html(data); + $('#'+divId+' .listResults').tableHover(); // hover tables + $('#'+divId+' .listResults').each(function () { + var table = $(this); + var id = $(this).parent(); + aTruncatedLists[divId] = undefined; + var checkbox = (table.find('th:first :checkbox').length > 0); + if (checkbox) { + // There is a checkbox in the first column, don't make it sortable + table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager")}); // sortable and zebra tables + } + else { + // There is NO checkbox in the first column, all columns are considered sortable + table.tablesorter({widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager"), totalRows: 97, filter: sSerializedFilter, extra_params: sExtraParams}); // sortable and zebra tables + } + }); + $('#'+divId).unblock(); + } + } + ); +} + +/** + * Truncate a previously expanded list ! + */ +function TruncateList(divId, iLimit, sNewLabel, sLinkLabel) { + $('#'+divId).block(); + var iCount = 0; + $('#'+divId+' table.listResults tr:gt('+iLimit+')').each(function () { + $(this).remove(); + }); + $('#lbl_'+divId).html(sNewLabel); + $('#'+divId+' table.listResults tr:last td').addClass('truncated'); + $('#'+divId+' table.listResults').addClass('truncated'); + $('#trc_'+divId).html(sLinkLabel); + $('#'+divId+' .listResults').trigger("update"); // Reset the cache + $('#'+divId).unblock(); +} + +/** + * Reload any block -- used for periodic auto-reload + */ +function ReloadBlock(divId, sStyle, sSerializedFilter, sExtraParams) { + // Check if the user is not editing the list properties right now + var bDialogOpen = false; + var oDataTable = $('#'+divId+' :itop-datatable'); + var bIsDataTable = false; + if (oDataTable.length > 0) { + bDialogOpen = oDataTable.datatable('IsDialogOpen'); + bIsDataTable = true; + } + if (!bDialogOpen) { + if (bIsDataTable) { + oDataTable.datatable('DoRefresh'); + } + else { + $('#'+divId).block(); + + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style='+sStyle, + {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams}, + function (data) { + $('#'+divId).empty(); + $('#'+divId).append(data); + $('#'+divId).removeClass('loading'); + } + ); + } + } +} + +function SaveGroupBySortOrder(sTableId, aValues) { + var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id'); + var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId; + if (aValues.length != 0) { + $sValue = JSON.stringify(aValues); + if (GetUserPreference(sPrefKey, null) != $sValue) { + SetUserPreference(sPrefKey, $sValue, true); + } + } +} + +function LoadGroupBySortOrder(sTableId) { + var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id'); + var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId; + var sValues = GetUserPreference(sPrefKey, null); + if (sValues != null) { + aValues = JSON.parse(sValues); + window.setTimeout(function () { + $('#'+sTableId+' table.listResults').trigger('sorton', [aValues]); + }, 50); + } + +} + +/** + * Update the display and value of a file input widget when the user picks a new file + */ +function UpdateFileName(id, sNewFileName) { + var aPath = sNewFileName.split('\\'); + var sNewFileName = aPath[aPath.length-1]; + + $('#'+id).val(sNewFileName); + $('#'+id).trigger('validate'); + $('#name_'+id).text(sNewFileName); + return true; +} + +/** + * Reload a search form for the specified class + */ +function ReloadSearchForm(divId, sClassName, sBaseClass, sContext, sTableId, sExtraParams) { + var oDiv = $('#ds_'+divId); + oDiv.block(); + // deprecated in jQuery 1.8 + //var oFormEvents = $('#ds_'+divId+' form').data('events'); + var oForm = $('#ds_'+divId+' form'); + var oFormEvents = $._data(oForm[0], "events"); + + // Save the submit handlers + aSubmit = new Array(); + if ((oFormEvents != null) && (oFormEvents.submit != undefined)) { + for (var index = 0; index < oFormEvents.submit.length; index++) { + aSubmit [index] = {data: oFormEvents.submit[index].data, namespace: oFormEvents.submit[index].namespace, handler: oFormEvents.submit[index].handler}; + } + } + sAction = $('#ds_'+divId+' form').attr('action'); + + // Save the current values in the form + var oMap = {}; + $('#ds_'+divId+" form :input[name!='']").each(function () { + oMap[this.name] = this.value; + }); + oMap.operation = 'search_form'; + oMap.className = sClassName; + oMap.baseClass = sBaseClass; + oMap.currentId = divId; + oMap._table_id_ = sTableId; + oMap.action = sAction; + if(sExtraParams['selection_mode']) + { + oMap.selection_mode = sExtraParams['selection_mode']; + } + if(sExtraParams['result_list_outer_selector']) + { + oMap.result_list_outer_selector = sExtraParams['result_list_outer_selector']; + } + if(sExtraParams['cssCount']) + { + oMap.css_count = sExtraParams['cssCount']; + $(sExtraParams['cssCount']).val(0).trigger('change'); + } + if(sExtraParams['table_inner_id']) + { + oMap.table_inner_id = sExtraParams['table_inner_id']; + } + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, oMap, + function (data) { + oDiv.empty(); + oDiv.append(data); + oDiv.unblock(); + oDiv.parent().resize(); // Inform the parent that the form has just been (potentially) resized + oDiv.find('form').triggerHandler('itop.search.form.reloaded'); + } + ); +} + +/** + * Stores - in a persistent way - user specific preferences + * depends on a global variable oUserPreferences created/filled by the iTopWebPage + * that acts as a local -write through- cache + */ +function SetUserPreference(sPreferenceCode, sPrefValue, bPersistent) { + sPreviousValue = undefined; + try { + sPreviousValue = oUserPreferences[sPreferenceCode]; + } + catch (err) { + sPreviousValue = undefined; + } + oUserPreferences[sPreferenceCode] = sPrefValue; + if (bPersistent && (sPrefValue != sPreviousValue)) { + ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', + {operation: 'set_pref', code: sPreferenceCode, value: sPrefValue}); // Make it persistent + } +} + +/** + * Get user specific preferences + * depends on a global variable oUserPreferences created/filled by the iTopWebPage + * that acts as a local -write through- cache + */ +function GetUserPreference(sPreferenceCode, sDefaultValue) { + var value = sDefaultValue; + if (oUserPreferences[sPreferenceCode] != undefined) { + value = oUserPreferences[sPreferenceCode]; + } + return value; +} + +/** + * Check/uncheck a whole list of checkboxes + */ +function CheckAll(sSelector, bValue) { + var value = bValue; + $(sSelector).each(function () { + if (this.checked != value) { + this.checked = value; + $(this).trigger('change'); + } + }); +} + + +/** + * Toggle (enabled/disabled) the specified field of a form + */ +function ToggleField(value, field_id) { + if (value) { + $('#'+field_id).prop('disabled', false); + // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings) + $('#'+field_id+' :input').prop('disabled', false); + } + else { + $('#'+field_id).prop('disabled', true); + // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings) + $('#'+field_id+' :input').prop('disabled', true); + } + $('#'+field_id).trigger('update'); + $('#'+field_id).trigger('validate'); +} + +/** + * For the fields that cannot be visually disabled, they can be blocked + * @return + */ +function BlockField(field_id, bBlocked) { + if (bBlocked) { + $('#'+field_id).block({message: ' ** disabled ** '}); + } + else { + $('#'+field_id).unblock(); + } +} + +/** + * Updates (enables/disables) a "duration" field + */ +function ToggleDurationField(field_id) { + // Toggle all the subfields that compose the "duration" input + aSubFields = new Array('d', 'h', 'm', 's'); + + if ($('#'+field_id).prop('disabled')) { + for (var i = 0; i < aSubFields.length; i++) { + $('#'+field_id+'_'+aSubFields[i]).prop('disabled', true); + } + } + else { + for (var i = 0; i < aSubFields.length; i++) { + $('#'+field_id+'_'+aSubFields[i]).prop('disabled', false); + } + } +} + +/** + * PropagateCheckBox + */ +function PropagateCheckBox(bCurrValue, aFieldsList, bCheck) { + if (bCurrValue == bCheck) { + for (var i = 0; i < aFieldsList.length; i++) { + var sFieldId = aFieldsList[i]; + $('#enable_'+sFieldId).prop('checked', bCheck); + ToggleField(bCheck, sFieldId); + + // Cascade propagation + $('#enable_'+sFieldId).trigger('change'); + } + } +} + +function FixTableSorter(table) { + if (table[0].config == undefined) { + // Table is not sort-able, let's fix it + var checkbox = (table.find('th:first :checkbox').length > 0); + if (checkbox) { + // There is a checkbox in the first column, don't make it sort-able + table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables + } + else { + // There is NO checkbox in the first column, all columns are considered sort-able + table.tablesorter({widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables + } + } +} + +function DashletCreationDlg(sOQL, sContext) { + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'dashlet_creation_dlg', oql: sOQL}, function (data) { + $('body').append(data); + }); + return false; +} + +function ShortcutListDlg(sOQL, sDataTableId, sContext) { + var sDataTableName = 'datatable_'+sDataTableId; + var oTableSettings = { + oColumns: $('#'+sDataTableName).datatable('option', 'oColumns'), + iPageSize: $('#'+sDataTableName).datatable('option', 'iPageSize') + }; + var sTableSettings = JSON.stringify(oTableSettings); + + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'shortcut_list_dlg', oql: sOQL, table_settings: sTableSettings}, function (data) { + $('body').append(data); + }); + return false; +} + +function ExportListDlg(sOQL, sDataTableId, sFormat, sDlgTitle) { + var aFields = []; + if (sDataTableId != '') { + var sDataTableName = 'datatable_'+sDataTableId; + var oColumns = $('#'+sDataTableName).datatable('option', 'oColumns'); + for (var j in oColumns) { + for (var k in oColumns[j]) { + if (oColumns[j][k].checked) { + var sCode = oColumns[j][k].code; + if (sCode == '_key_') { + sCode = 'id'; + } + aFields.push(j+'.'+sCode); + } + } + } + } + + var oParams = { + interactive: 1, + mode: 'dialog', + expression: sOQL, + suggested_fields: aFields.join(','), + dialog_title: sDlgTitle + }; + + if (sFormat !== null) { + oParams.format = sFormat; + } + + $.post(GetAbsoluteUrlAppRoot()+'webservices/export-v2.php', oParams, function (data) { + $('body').append(data); + }); + return false; +} + +function ExportToggleFormat(sFormat) { + $('.form_part').hide(); + for (k in window.aFormParts[sFormat]) { + $('#form_part_'+window.aFormParts[sFormat][k]).show().trigger('form-part-activate'); + } +} + +function ExportStartExport() { + var oParams = {}; + $('.form_part:visible :input').each(function () { + if (this.name != '') { + if ((this.type == 'radio') || (this.type == 'checkbox')) { + if (this.checked) { + oParams[this.name] = $(this).val(); + } + } + else { + oParams[this.name] = $(this).val(); + } + } + }); + $(':itop-tabularfieldsselector:visible').tabularfieldsselector('close_all_tooltips'); + $('#export-form').hide(); + $('#export-feedback').show(); + oParams.operation = 'export_build'; + oParams.format = $('#export-form :input[name=format]').val(); + var sQueryMode = $(':input[name=query_mode]:checked').val(); + if ($(':input[name=query_mode]:checked').length > 0) { + if (sQueryMode == 'oql') { + oParams.expression = $('#export-form :input[name=expression]').val(); + } + else { + oParams.query = $('#export-form :input[name=query]').val(); + } + } + else { + oParams.expression = $('#export-form :input[name=expression]').val(); + oParams.query = $('#export-form :input[name=query]').val(); + } + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) { + if (data == null) { + ExportError('Export failed (no data provided), please contact your administrator'); + } + else { + ExportRun(data); + } + }, 'json') + .fail(function () { + ExportError('Export failed, please contact your administrator'); + }); +} + +function ExportError(sMessage) { + $('.export-message').html(sMessage); + $('.export-progress-bar').hide(); + $('#export-btn').hide(); +} + +function ExportRun(data) { + switch (data.code) { + case 'run': + // Continue + $('.export-progress-bar').progressbar({value: data.percentage}); + $('.export-message').html(data.message); + oParams = {}; + oParams.token = data.token; + var sDataState = $('#export-form').attr('data-state'); + if (sDataState == 'cancelled') { + oParams.operation = 'export_cancel'; + } + else { + oParams.operation = 'export_build'; + } + + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) { + ExportRun(data); + }, + 'json'); + break; + + case 'done': + $('#export-btn').hide(); + sMessage = ''+data.message+''; + $('.export-message').html(sMessage); + $('.export-progress-bar').hide(); + $('#export-btn').hide(); + $('#export-form').attr('data-state', 'done'); + if (data.text_result != undefined) { + if (data.mime_type == 'text/html') { + $('#export_content').parent().html(data.text_result); + $('#export_text_result').show(); + $('#export_text_result .listResults').tableHover(); + $('#export_text_result .listResults').tablesorter({widgets: ['myZebra']}); + } + else { + if ($('#export_text_result').closest('ui-dialog').length == 0) { + // not inside a dialog box, adjust the height... approximately + var jPane = $('#export_text_result').closest('.ui-layout-content'); + var iTotalHeight = jPane.height(); + jPane.children(':visible').each(function () { + if ($(this).attr('id') != '') { + iTotalHeight -= $(this).height(); + } + }); + $('#export_content').height(iTotalHeight-80); + } + $('#export_content').val(data.text_result); + $('#export_text_result').show(); + } + } + $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable'); + break; + + case 'error': + $('#export-form').attr('data-state', 'error'); + $('.export-progress-bar').progressbar({value: data.percentage}); + $('.export-message').html(data.message); + $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable'); + $('#export-btn').hide(); + default: + } +} + +function ExportInitButton(sSelector) { + $(sSelector).on('click', function () { + var sDataState = $('#export-form').attr('data-state'); + switch (sDataState) { + case 'not-yet-started': + $('.form_part:visible').each(function () { + $('#export-form').data('validation_messages', []); + var ret = $(this).trigger('validate'); + }); + var aMessages = $('#export-form').data('validation_messages'); + + if (aMessages.length > 0) { + alert(aMessages.join('')); + return; + } + if ($(this).hasClass('ui-button')) { + $(this).button('option', 'label', Dict.S('UI:Button:Cancel')); + } + else { + $(this).html(Dict.S('UI:Button:Cancel')); + } + $('#export-form').attr('data-state', 'running'); + ExportStartExport(); + break; + + case 'running': + if ($(this).hasClass('ui-button')) { + $(this).button('disable'); + } + else { + $(this).prop('disabled', true); + } + $('#export-form').attr('data-state', 'cancelled'); + break; + + case 'done': + case 'error': + $('#interactive_export_dlg').dialog('close'); + break; + + default: + // Do nothing + } + }); +} + +function DisplayHistory(sSelector, sFilter, iCount, iStart) { + $(sSelector).block(); + var oParams = {operation: 'history_from_filter', filter: sFilter, start: iStart, count: iCount}; + $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) { + $(sSelector).html(data).unblock(); + } + ); +} + +// Very simple equivalent to format: placeholders are %1$s %2$d ... +function Format() { + var args = []; + var str = ''; + if (arguments[0] instanceof Array) { + str = arguments[0][0].toString(); + args = arguments[0]; + } + else { + str = arguments[0].toString(); + if (arguments.length > 1) { + var t = typeof arguments[1]; + args = ("string" === t || "number" === t) ? Array.prototype.slice.call(arguments) : arguments[1]; + } + } + var key; + for (key in args) { + str = str.replace(new RegExp("\\%"+key+"\\$.", "gi"), args[key]); + } + + return str; +} + +/** + * Enable to access translation keys client side. + * The called keys needs to be exported using \WebPage::add_dict_entry + */ +var Dict = {}; +if (typeof aDictEntries == 'undefined') { + Dict._entries = {}; // Entries have not been loaded (we are in the setup ?) +} +else { + Dict._entries = aDictEntries; // Entries were loaded asynchronously via their own js files +} +Dict.S = function (sEntry) { + if (sEntry in Dict._entries) { + return Dict._entries[sEntry]; + } + else { + return sEntry; + } +}; +Dict.Format = function () { + var args = Array.from(arguments); + args[0] = Dict.S(arguments[0]); + return Format(args); } \ No newline at end of file diff --git a/setup/ajax.dataloader.php b/setup/ajax.dataloader.php index 4195c74bd..dd53a85f0 100644 --- a/setup/ajax.dataloader.php +++ b/setup/ajax.dataloader.php @@ -1,208 +1,208 @@ - - - -/** - * Does load data from XML files (currently used in the setup only) - * - * @copyright Copyright (C) 2010-2018 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * This page is called to perform "asynchronously" the setup actions - * parameters - * 'operation': one of 'compile_data_model', 'update_db_schema', 'after_db_creation', 'file' - * - * if 'operation' == 'update_db_schema': - * 'mode': install | upgrade - * - * if 'operation' == 'after_db_creation': - * 'mode': install | upgrade - * - * if 'operation' == 'file': - * 'file': string Name of the file to load - * 'session_status': string 'start', 'continue' or 'end' - * 'percent': integer 0..100 the percentage of completion once the file has been loaded - */ -define('SAFE_MINIMUM_MEMORY', 64*1024*1024); -require_once('../approot.inc.php'); -require_once(APPROOT.'/application/utils.inc.php'); -require_once(APPROOT.'/setup/setuppage.class.inc.php'); -require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); - -ini_set('max_execution_time', max(3600, ini_get('max_execution_time'))); // Under Windows SQL/backup operations are part of the timeout and require extra time -date_default_timezone_set('Europe/Paris'); // Just to avoid a warning if the timezone is not set in php.ini - -$sMemoryLimit = trim(ini_get('memory_limit')); -if (empty($sMemoryLimit)) -{ - // On some PHP installations, memory_limit does not exist as a PHP setting! - // (encountered on a 5.2.0 under Windows) - // In that case, ini_set will not work, let's keep track of this and proceed with the data load - SetupPage::log_info("No memory limit has been defined in this instance of PHP"); -} -else -{ - // Check that the limit will allow us to load the data - // - $iMemoryLimit = utils::ConvertToBytes($sMemoryLimit); - if (!utils::IsMemoryLimitOk($iMemoryLimit, SAFE_MINIMUM_MEMORY)) - { - if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE) - { - SetupPage::log_error("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself."); - } - else - { - SetupPage::log_info("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY."."); - } - } -} - - -define('PHP_FATAL_ERROR_TAG', 'phpfatalerror'); - - -/** - * Handler for register_shutdown_function, to catch PHP errors - */ -function ShutdownCallback() -{ - $error = error_get_last(); - $bIsErrorToReport = (($error !== null) && ($error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR))); - if (!$bIsErrorToReport) - { - return; - } - - $errno = $error["type"]; - $errfile = $error["file"]; - $errline = $error["line"]; - $errstr = $error["message"]; - $sLogMessage = "PHP error occured : msg=$errstr, no=$errno, file=$errfile, line=$errline"; - SetupPage::log_error("Setup error: $sLogMessage"); - echo '<'.PHP_FATAL_ERROR_TAG.'>'.$sLogMessage.''; -} - - -function FatalErrorCatcher($sOutput) -{ - if (preg_match('|<'.PHP_FATAL_ERROR_TAG.'>.*|s', $sOutput, $aMatches)) - { - header("HTTP/1.0 500 Internal server error."); - $errors = ''; - foreach ($aMatches as $sMatch) - { - $errors .= strip_tags($sMatch)."\n"; - } - $sOutput = "$errors\n"; - // Logging to a file does not work if the whole memory is exhausted... - //SetupPage::log_error("Fatal error - in $__FILE__ , $errors"); - } - return $sOutput; -} - -//Define some bogus, invalid HTML tags that no sane -//person would ever put in an actual document and tell -//PHP to delimit fatal error warnings with them. -ini_set('error_prepend_string', '<'.PHP_FATAL_ERROR_TAG.'>'); -ini_set('error_append_string', ''); - -// callback on errors to log -register_shutdown_function('ShutdownCallback'); -// Starts the capture of the ouput, and sets a filter to capture the fatal errors. -ob_start('FatalErrorCatcher'); // Start capturing the output, and pass it through the fatal error catcher - -require_once(APPROOT.'/core/config.class.inc.php'); -require_once(APPROOT.'/core/log.class.inc.php'); -require_once(APPROOT.'/core/kpi.class.inc.php'); -require_once(APPROOT.'/core/cmdbsource.class.inc.php'); -require_once('./xmldataloader.class.inc.php'); -require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); - - -// Never cache this page -header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 -header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past - -/** - * Main program - */ -$sOperation = Utils::ReadParam('operation', ''); -try -{ - switch($sOperation) - { - case 'async_action': - ini_set('max_execution_time', max(240, ini_get('max_execution_time'))); - // While running the setup it is desirable to see any error that may happen - ini_set('display_errors', true); - ini_set('display_startup_errors', true); - - require_once(APPROOT.'/setup/wizardcontroller.class.inc.php'); - require_once(APPROOT.'/setup/wizardsteps.class.inc.php'); - - $sClass = utils::ReadParam('step_class', ''); - $sState = utils::ReadParam('step_state', ''); - $sActionCode = utils::ReadParam('code', ''); - $aParams = utils::ReadParam('params', array(), false, 'raw_data'); - $oPage = new ajax_page(''); - $oDummyController = new WizardController(''); - if (is_subclass_of($sClass, 'WizardStep')) - { - /** @var WizardStep $oStep */ - $oStep = new $sClass($oDummyController, $sState); - $sConfigFile = utils::GetConfigFilePath(); - if (file_exists($sConfigFile) && !is_writable($sConfigFile) && $oStep->RequiresWritableConfig()) - { - $oPage->error("Error: the configuration file '".$sConfigFile."' already exists and cannot be overwritten."); - $oPage->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '".realpath($sConfigFile)."' can be modified by the web server."); - $oPage->output(); - } - else - { - $oStep->AsyncAction($oPage, $sActionCode, $aParams); - } - } - $oPage->output(); - break; - - default: - throw(new Exception("Error unsupported operation '$sOperation'")); - } -} -catch(Exception $e) -{ - header("HTTP/1.0 500 Internal server error."); - echo "

An error happened while processing the installation:

\n"; - echo '

'.$e."

\n"; - SetupPage::log_error("An error happened while processing the installation: ".$e); -} - -if (function_exists('memory_get_peak_usage')) -{ - if ($sOperation == 'file') - { - SetupPage::log_info("loading file '$sFileName', peak memory usage. ".memory_get_peak_usage()); - } - else - { - SetupPage::log_info("operation '$sOperation', peak memory usage. ".memory_get_peak_usage()); - } + + + +/** + * Does load data from XML files (currently used in the setup only) + * + * @copyright Copyright (C) 2010-2018 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * This page is called to perform "asynchronously" the setup actions + * parameters + * 'operation': one of 'compile_data_model', 'update_db_schema', 'after_db_creation', 'file' + * + * if 'operation' == 'update_db_schema': + * 'mode': install | upgrade + * + * if 'operation' == 'after_db_creation': + * 'mode': install | upgrade + * + * if 'operation' == 'file': + * 'file': string Name of the file to load + * 'session_status': string 'start', 'continue' or 'end' + * 'percent': integer 0..100 the percentage of completion once the file has been loaded + */ +define('SAFE_MINIMUM_MEMORY', 64*1024*1024); +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/utils.inc.php'); +require_once(APPROOT.'/setup/setuppage.class.inc.php'); +require_once(APPROOT.'/setup/moduleinstaller.class.inc.php'); + +ini_set('max_execution_time', max(3600, ini_get('max_execution_time'))); // Under Windows SQL/backup operations are part of the timeout and require extra time +date_default_timezone_set('Europe/Paris'); // Just to avoid a warning if the timezone is not set in php.ini + +$sMemoryLimit = trim(ini_get('memory_limit')); +if (empty($sMemoryLimit)) +{ + // On some PHP installations, memory_limit does not exist as a PHP setting! + // (encountered on a 5.2.0 under Windows) + // In that case, ini_set will not work, let's keep track of this and proceed with the data load + SetupPage::log_info("No memory limit has been defined in this instance of PHP"); +} +else +{ + // Check that the limit will allow us to load the data + // + $iMemoryLimit = utils::ConvertToBytes($sMemoryLimit); + if (!utils::IsMemoryLimitOk($iMemoryLimit, SAFE_MINIMUM_MEMORY)) + { + if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE) + { + SetupPage::log_error("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself."); + } + else + { + SetupPage::log_info("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY."."); + } + } +} + + +define('PHP_FATAL_ERROR_TAG', 'phpfatalerror'); + + +/** + * Handler for register_shutdown_function, to catch PHP errors + */ +function ShutdownCallback() +{ + $error = error_get_last(); + $bIsErrorToReport = (($error !== null) && ($error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR))); + if (!$bIsErrorToReport) + { + return; + } + + $errno = $error["type"]; + $errfile = $error["file"]; + $errline = $error["line"]; + $errstr = $error["message"]; + $sLogMessage = "PHP error occured : msg=$errstr, no=$errno, file=$errfile, line=$errline"; + SetupPage::log_error("Setup error: $sLogMessage"); + echo '<'.PHP_FATAL_ERROR_TAG.'>'.$sLogMessage.''; +} + + +function FatalErrorCatcher($sOutput) +{ + if (preg_match('|<'.PHP_FATAL_ERROR_TAG.'>.*|s', $sOutput, $aMatches)) + { + header("HTTP/1.0 500 Internal server error."); + $errors = ''; + foreach ($aMatches as $sMatch) + { + $errors .= strip_tags($sMatch)."\n"; + } + $sOutput = "$errors\n"; + // Logging to a file does not work if the whole memory is exhausted... + //SetupPage::log_error("Fatal error - in $__FILE__ , $errors"); + } + return $sOutput; +} + +//Define some bogus, invalid HTML tags that no sane +//person would ever put in an actual document and tell +//PHP to delimit fatal error warnings with them. +ini_set('error_prepend_string', '<'.PHP_FATAL_ERROR_TAG.'>'); +ini_set('error_append_string', ''); + +// callback on errors to log +register_shutdown_function('ShutdownCallback'); +// Starts the capture of the ouput, and sets a filter to capture the fatal errors. +ob_start('FatalErrorCatcher'); // Start capturing the output, and pass it through the fatal error catcher + +require_once(APPROOT.'/core/config.class.inc.php'); +require_once(APPROOT.'/core/log.class.inc.php'); +require_once(APPROOT.'/core/kpi.class.inc.php'); +require_once(APPROOT.'/core/cmdbsource.class.inc.php'); +require_once('./xmldataloader.class.inc.php'); +require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); + + +// Never cache this page +header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 +header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past + +/** + * Main program + */ +$sOperation = Utils::ReadParam('operation', ''); +try +{ + switch($sOperation) + { + case 'async_action': + ini_set('max_execution_time', max(240, ini_get('max_execution_time'))); + // While running the setup it is desirable to see any error that may happen + ini_set('display_errors', true); + ini_set('display_startup_errors', true); + + require_once(APPROOT.'/setup/wizardcontroller.class.inc.php'); + require_once(APPROOT.'/setup/wizardsteps.class.inc.php'); + + $sClass = utils::ReadParam('step_class', ''); + $sState = utils::ReadParam('step_state', ''); + $sActionCode = utils::ReadParam('code', ''); + $aParams = utils::ReadParam('params', array(), false, 'raw_data'); + $oPage = new ajax_page(''); + $oDummyController = new WizardController(''); + if (is_subclass_of($sClass, 'WizardStep')) + { + /** @var WizardStep $oStep */ + $oStep = new $sClass($oDummyController, $sState); + $sConfigFile = utils::GetConfigFilePath(); + if (file_exists($sConfigFile) && !is_writable($sConfigFile) && $oStep->RequiresWritableConfig()) + { + $oPage->error("Error: the configuration file '".$sConfigFile."' already exists and cannot be overwritten."); + $oPage->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '".realpath($sConfigFile)."' can be modified by the web server."); + $oPage->output(); + } + else + { + $oStep->AsyncAction($oPage, $sActionCode, $aParams); + } + } + $oPage->output(); + break; + + default: + throw(new Exception("Error unsupported operation '$sOperation'")); + } +} +catch(Exception $e) +{ + header("HTTP/1.0 500 Internal server error."); + echo "

An error happened while processing the installation:

\n"; + echo '

'.$e."

\n"; + SetupPage::log_error("An error happened while processing the installation: ".$e); +} + +if (function_exists('memory_get_peak_usage')) +{ + if ($sOperation == 'file') + { + SetupPage::log_info("loading file '$sFileName', peak memory usage. ".memory_get_peak_usage()); + } + else + { + SetupPage::log_info("operation '$sOperation', peak memory usage. ".memory_get_peak_usage()); + } } \ No newline at end of file diff --git a/webservices/backoffice.dataloader.php b/webservices/backoffice.dataloader.php index 691c347a9..2ba07f0bd 100644 --- a/webservices/backoffice.dataloader.php +++ b/webservices/backoffice.dataloader.php @@ -1,170 +1,170 @@ - - - -/** - * Does load data from XML files (currently used in the setup and the backoffice data loader utility) - * - * @copyright Copyright (C) 2010-2012 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - */ - -/** - * This page is called to load an XML file into the database - * parameters - * 'file' string Name of the file to load - */ -define('SAFE_MINIMUM_MEMORY', 256*1024*1024); - -require_once('../approot.inc.php'); -require_once(APPROOT.'/application/application.inc.php'); - -require_once(APPROOT.'/application/startup.inc.php'); - -require_once(APPROOT.'/application/loginwebpage.class.inc.php'); -LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin) - -// required because the class xmldataloader is reporting errors in the setup.log file -require_once(APPROOT.'/setup/setuppage.class.inc.php'); -require_once(APPROOT.'/setup/xmldataloader.class.inc.php'); - - -function SetMemoryLimit($oP) -{ - $sMemoryLimit = trim(ini_get('memory_limit')); - if (empty($sMemoryLimit)) - { - // On some PHP installations, memory_limit does not exist as a PHP setting! - // (encountered on a 5.2.0 under Windows) - // In that case, ini_set will not work, let's keep track of this and proceed with the data load - $oP->p("No memory limit has been defined in this instance of PHP"); - } - else - { - // Check that the limit will allow us to load the data - // - $iMemoryLimit = utils::ConvertToBytes($sMemoryLimit); - if (!utils::IsMemoryLimitOk($iMemoryLimit, SAFE_MINIMUM_MEMORY)) - { - if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE) - { - $oP->p("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself."); - } - else - { - $oP->p("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY."."); - } - } - } -} - - -//////////////////////////////////////////////////////////////////////////////// -// -// Main -// -//////////////////////////////////////////////////////////////////////////////// - -// Never cache this page -header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 -header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past - -/** - * Main program - */ -$sFileName = Utils::ReadParam('file', '', false, 'raw_data'); - -$oP = new WebPage("iTop - Backoffice data loader"); - - -try -{ - // Note: the data model must be loaded first - $oDataLoader = new XMLDataLoader(); - - if (empty($sFileName)) - { - throw(new Exception("Missing argument 'file'")); - } - if (!file_exists($sFileName)) - { - throw(new Exception("File $sFileName does not exist")); - } - - SetMemoryLimit($oP); - - - // The XMLDataLoader constructor has initialized the DB, let's start a transaction - CMDBSource::Query('START TRANSACTION'); - - $oChange = MetaModel::NewObject("CMDBChange"); - $oChange->Set("date", time()); - $oChange->Set("userinfo", "Initialization"); - $iChangeId = $oChange->DBInsert(); - $oP->p("Starting data load."); - $oDataLoader->StartSession($oChange); - - $oDataLoader->LoadFile($sFileName); - - $oP->p("Ending data load session"); - if ($oDataLoader->EndSession(true /* strict */)) - { - $iCountCreated = $oDataLoader->GetCountCreated(); - CMDBSource::Query('COMMIT'); - - $oP->p("Data successfully written into the DB: $iCountCreated objects created"); - } - else - { - CMDBSource::Query('ROLLBACK'); - $oP->p("Some issues have been encountered, changes will not be recorded, please review the source data"); - $aErrors = $oDataLoader->GetErrors(); - if (count($aErrors) > 0) - { - $oP->p('Errors ('.count($aErrors).')'); - foreach ($aErrors as $sMsg) - { - $oP->p(' * '.$sMsg); - } - } - $aWarnings = $oDataLoader->GetWarnings(); - if (count($aWarnings) > 0) - { - $oP->p('Warnings ('.count($aWarnings).')'); - foreach ($aWarnings as $sMsg) - { - $oP->p(' * '.$sMsg); - } - } - } - -} -catch(Exception $e) -{ - $oP->p("An error happened while loading the data: ".$e->getMessage()); - $oP->p("Aborting (no data written)..."); - CMDBSource::Query('ROLLBACK'); -} - -if (function_exists('memory_get_peak_usage')) -{ - $oP->p("Information: memory peak usage: ".memory_get_peak_usage()); -} - -$oP->Output(); -?> + + + +/** + * Does load data from XML files (currently used in the setup and the backoffice data loader utility) + * + * @copyright Copyright (C) 2010-2012 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/** + * This page is called to load an XML file into the database + * parameters + * 'file' string Name of the file to load + */ +define('SAFE_MINIMUM_MEMORY', 256*1024*1024); + +require_once('../approot.inc.php'); +require_once(APPROOT.'/application/application.inc.php'); + +require_once(APPROOT.'/application/startup.inc.php'); + +require_once(APPROOT.'/application/loginwebpage.class.inc.php'); +LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin) + +// required because the class xmldataloader is reporting errors in the setup.log file +require_once(APPROOT.'/setup/setuppage.class.inc.php'); +require_once(APPROOT.'/setup/xmldataloader.class.inc.php'); + + +function SetMemoryLimit($oP) +{ + $sMemoryLimit = trim(ini_get('memory_limit')); + if (empty($sMemoryLimit)) + { + // On some PHP installations, memory_limit does not exist as a PHP setting! + // (encountered on a 5.2.0 under Windows) + // In that case, ini_set will not work, let's keep track of this and proceed with the data load + $oP->p("No memory limit has been defined in this instance of PHP"); + } + else + { + // Check that the limit will allow us to load the data + // + $iMemoryLimit = utils::ConvertToBytes($sMemoryLimit); + if (!utils::IsMemoryLimitOk($iMemoryLimit, SAFE_MINIMUM_MEMORY)) + { + if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE) + { + $oP->p("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself."); + } + else + { + $oP->p("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY."."); + } + } + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// +// Main +// +//////////////////////////////////////////////////////////////////////////////// + +// Never cache this page +header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 +header("Expires: Fri, 17 Jul 1970 05:00:00 GMT"); // Date in the past + +/** + * Main program + */ +$sFileName = Utils::ReadParam('file', '', false, 'raw_data'); + +$oP = new WebPage("iTop - Backoffice data loader"); + + +try +{ + // Note: the data model must be loaded first + $oDataLoader = new XMLDataLoader(); + + if (empty($sFileName)) + { + throw(new Exception("Missing argument 'file'")); + } + if (!file_exists($sFileName)) + { + throw(new Exception("File $sFileName does not exist")); + } + + SetMemoryLimit($oP); + + + // The XMLDataLoader constructor has initialized the DB, let's start a transaction + CMDBSource::Query('START TRANSACTION'); + + $oChange = MetaModel::NewObject("CMDBChange"); + $oChange->Set("date", time()); + $oChange->Set("userinfo", "Initialization"); + $iChangeId = $oChange->DBInsert(); + $oP->p("Starting data load."); + $oDataLoader->StartSession($oChange); + + $oDataLoader->LoadFile($sFileName); + + $oP->p("Ending data load session"); + if ($oDataLoader->EndSession(true /* strict */)) + { + $iCountCreated = $oDataLoader->GetCountCreated(); + CMDBSource::Query('COMMIT'); + + $oP->p("Data successfully written into the DB: $iCountCreated objects created"); + } + else + { + CMDBSource::Query('ROLLBACK'); + $oP->p("Some issues have been encountered, changes will not be recorded, please review the source data"); + $aErrors = $oDataLoader->GetErrors(); + if (count($aErrors) > 0) + { + $oP->p('Errors ('.count($aErrors).')'); + foreach ($aErrors as $sMsg) + { + $oP->p(' * '.$sMsg); + } + } + $aWarnings = $oDataLoader->GetWarnings(); + if (count($aWarnings) > 0) + { + $oP->p('Warnings ('.count($aWarnings).')'); + foreach ($aWarnings as $sMsg) + { + $oP->p(' * '.$sMsg); + } + } + } + +} +catch(Exception $e) +{ + $oP->p("An error happened while loading the data: ".$e->getMessage()); + $oP->p("Aborting (no data written)..."); + CMDBSource::Query('ROLLBACK'); +} + +if (function_exists('memory_get_peak_usage')) +{ + $oP->p("Information: memory peak usage: ".memory_get_peak_usage()); +} + +$oP->Output(); +?>