diff --git a/application/utils.inc.php b/application/utils.inc.php index 0e2ada515..f4bda8adb 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1590,4 +1590,211 @@ class utils } 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 string[]|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.xlsx', + '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 (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; + } } diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index a07012250..d0a1fa0f9 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -5243,75 +5243,34 @@ class AttributeBlob extends AttributeDefinition { return ''; } - - - // Facilitate things: allow administrators to upload a document - // from a CSV by specifying its path/URL + + /** + * Users can provide the document from an URL (including an URL on iTop itself) + * for CSV import. Administrators can even provide the path to a local file + * {@inheritDoc} + * @see AttributeDefinition::MakeRealValue() + */ public function MakeRealValue($proposedValue, $oHostObj) { + if ($proposedValue === null) return null; + if (is_object($proposedValue)) { $proposedValue = clone $proposedValue; } else { - if (file_exists($proposedValue) && UserRights::IsAdministrator()) + try { - $sContent = file_get_contents($proposedValue); - $sExtension = strtolower(pathinfo($proposedValue, PATHINFO_EXTENSION)); - $sMimeType = "application/x-octet-stream"; - $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.xlsx', - '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' - ); - - if (!array_key_exists($sExtension, $aKnownExtensions) && extension_loaded('fileinfo')) - { - $finfo = new finfo(FILEINFO_MIME); - $sMimeType = $finfo->file($proposedValue); - } - return new ormDocument($sContent, $sMimeType); + // Read the file from iTop, an URL (or the local file system - for admins only) + $proposedValue = Utils::FileGetContentsAndMIMEType($proposedValue); } - else + catch(Exception $e) { - return new ormDocument($proposedValue, 'text/plain'); - } + IssueLog::Warning(get_class($this)."::MakeRealValue - ".$e->getMessage()); + // Not a real document !! store is as text !!! (This was the default behavior before) + $proposedValue = new ormDocument($e->getMessage()." \n".$proposedValue, 'text/plain'); + } } return $proposedValue; } @@ -5426,6 +5385,11 @@ class AttributeBlob extends AttributeDefinition public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { + $sAttCode = $this->GetCode(); + if ($sValue instanceof ormDocument) + { + return $sValue->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $sAttCode); + } return ''; // Not exportable in CSV ! } @@ -5529,39 +5493,31 @@ class AttributeImage extends AttributeBlob { public function GetEditClass() {return "Image";} - // Facilitate things: allow administrators to upload a document - // from a CSV by specifying its path/URL + /** + * {@inheritDoc} + * @see AttributeBlob::MakeRealValue() + */ public function MakeRealValue($proposedValue, $oHostObj) { - if (is_object($proposedValue)) - { - $proposedValue = clone $proposedValue; - } - else - { - if (file_exists($proposedValue) && UserRights::IsAdministrator()) - { - $sContent = file_get_contents($proposedValue); - $sExtension = strtolower(pathinfo($proposedValue, PATHINFO_EXTENSION)); - $sMimeType = "application/x-octet-stream"; - $aKnownExtensions = array( - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png' - ); - - if (!array_key_exists($sExtension, $aKnownExtensions) && extension_loaded('fileinfo')) - { - $finfo = new finfo(FILEINFO_MIME); - $sMimeType = $finfo->file($proposedValue); - } - return new ormDocument($sContent, $sMimeType); - } - } - return $proposedValue; + $oDoc = parent::MakeRealValue($proposedValue, $oHostObj); + // The validation of the MIME Type is done by CheckFormat below + return $oDoc; } - + + /** + * Check that the supplied ormDocument actually contains an image + * {@inheritDoc} + * @see AttributeDefinition::CheckFormat() + */ + public function CheckFormat($value) + { + if ($value instanceof ormDocument) + { + return ($value->GetMainMimeType() == 'image'); + } + return true; + } + public function GetAsHTML($value, $oHostObject = null, $bLocalize = true) { $iMaxWidthPx = $this->Get('display_max_width'); @@ -5570,7 +5526,16 @@ class AttributeImage extends AttributeBlob $sRet = ''; if (is_object($value) && !$value->IsEmpty()) { - $sUrl = $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode()); + if ($oHostObject->IsNew() || ($oHostObject->IsModified() && (array_key_exists($this->GetCode(), $oHostObject->ListChanges())))) + { + // If the object is modified (or not yet stored in the database) we must serve the content of the image directly inline + // otherwise (if we just give an URL) the browser will be given the wrong content... and may cache it + $sUrl = 'data:'.$value->GetMimeType().';base64,'.base64_encode($value->GetData()); + } + else + { + $sUrl = $value->GetDownloadURL(get_class($oHostObject), $oHostObject->GetKey(), $this->GetCode()); + } $sRet = ''; } return '
'.$sRet.'
'; diff --git a/core/excelbulkexport.class.inc.php b/core/excelbulkexport.class.inc.php index fc03918b9..ed15c9465 100644 --- a/core/excelbulkexport.class.inc.php +++ b/core/excelbulkexport.class.inc.php @@ -188,6 +188,11 @@ EOF $oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode); $sRet = $oAttDef->GetAsCSV($value, '', '', $oObj); } + else if ($value instanceOf ormDocument) + { + $oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode); + $sRet = $oAttDef->GetAsCSV($value, '', '', $oObj); + } else { $oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode);