define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.document.php?operation=download_document&class=Attachment&field=contents&id='); class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExtension { const ENUM_GUI_ALL = 'all'; const ENUM_GUI_BACKOFFICE = 'backoffice'; const ENUM_GUI_PORTALS = 'portals'; protected static $m_bIsModified = false; public function OnDisplayProperties($oObject, WebPage $oPage, $bEditMode = false) { if ($this->GetAttachmentsPosition() == 'properties') { $this->DisplayAttachments($oObject, $oPage, $bEditMode); } } public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false) { if ($this->GetAttachmentsPosition() == 'relations') { $this->DisplayAttachments($oObject, $oPage, $bEditMode); } } public function OnFormSubmit($oObject, $sFormPrefix = '') { if ($this->IsTargetObject($oObject)) { // For new objects attachments are processed in OnDBInsert if (!$oObject->IsNew()) { self::UpdateAttachments($oObject); } } } protected function GetMaxUpload() { $iMaxUpload = ini_get('upload_max_filesize'); if (!$iMaxUpload) { $sRet = Dict::S('Attachments:UploadNotAllowedOnThisSystem'); } else { $iMaxUpload = utils::ConvertToBytes($iMaxUpload); if ($iMaxUpload > 1024*1024*1024) { $sRet = Dict::Format('Attachment:Max_Go', sprintf('%0.2f', $iMaxUpload/(1024*1024*1024))); } else if ($iMaxUpload > 1024*1024) { $sRet = Dict::Format('Attachment:Max_Mo', sprintf('%0.2f', $iMaxUpload/(1024*1024))); } else { $sRet = Dict::Format('Attachment:Max_Ko', sprintf('%0.2f', $iMaxUpload/(1024))); } } return $sRet; } public function OnFormCancel($sTempId) { // Delete all "pending" attachments for this form $sOQL = 'SELECT Attachment WHERE temp_id = :temp_id'; $oSearch = DBObjectSearch::FromOQL($sOQL); $oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId)); while($oAttachment = $oSet->Fetch()) { $oAttachment->DBDelete(); // Pending attachment, don't mention it in the history } } public function EnumUsedAttributes($oObject) { return array(); } public function GetIcon($oObject) { return ''; } public function GetHilightClass($oObject) { // Possible return values are: // HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE return HILIGHT_CLASS_NONE; } public function EnumAllowedActions(DBObjectSet $oSet) { // No action return array(); } public function OnIsModified($oObject) { return self::$m_bIsModified; } public function OnCheckToWrite($oObject) { return array(); } public function OnCheckToDelete($oObject) { return array(); } public function OnDBUpdate($oObject, $oChange = null) { if ($this->IsTargetObject($oObject)) { // Get all current attachments $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id"); $oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey())); while ($oAttachment = $oSet->Fetch()) { $oAttachment->SetItem($oObject, true /*updateonchange*/); } } } public function OnDBInsert($oObject, $oChange = null) { if ($this->IsTargetObject($oObject)) { self::UpdateAttachments($oObject, $oChange); } } public function OnDBDelete($oObject, $oChange = null) { if ($this->IsTargetObject($oObject)) { $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id"); $oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey())); while ($oAttachment = $oSet->Fetch()) { $oAttachment->DBDelete(); } } } /////////////////////////////////////////////////////////////////////////////////////////////////////// // // Plug-ins specific functions // /////////////////////////////////////////////////////////////////////////////////////////////////////// protected function IsTargetObject($oObject) { $aAllowedClasses = MetaModel::GetModuleSetting('itop-attachments', 'allowed_classes', array('Ticket')); foreach($aAllowedClasses as $sAllowedClass) { if ($oObject instanceof $sAllowedClass) { return true; } } return false; } protected function GetAttachmentsPosition() { return MetaModel::GetModuleSetting('itop-attachments', 'position', 'relations'); } var $m_bDeleteEnabled = true; public function EnableDelete($bEnabled) { $this->m_bDeleteEnabled = $bEnabled; } public function DisplayAttachments($oObject, WebPage $oPage, $bEditMode = false) { // Exit here if the class is not allowed if (!$this->IsTargetObject($oObject)) return; $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id"); $oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey())); if ($this->GetAttachmentsPosition() == 'relations') { $sTitle = ($oSet->Count() > 0)? Dict::Format('Attachments:TabTitle_Count', $oSet->Count()) : Dict::S('Attachments:EmptyTabTitle'); $oPage->SetCurrentTab($sTitle); } $sMaxWidth = MetaModel::GetModuleSetting('itop-attachment', 'inline_image_max_width', '450px'); $oPage->add_style( <<add('
'); $oPage->add(''.Dict::S('Attachments:FieldsetTitle').''); if ($bEditMode && !static::IsReadonlyState($oObject, $oObject->GetState(), static::ENUM_GUI_BACKOFFICE) ) { $sIsDeleteEnabled = $this->m_bDeleteEnabled ? 'true' : 'false'; $iTransactionId = $oPage->GetTransactionId(); $sClass = get_class($oObject); $iObjectId = $oObject->Getkey(); $sTempId = session_id().'_'.$iTransactionId; $sDeleteBtn = Dict::S('Attachments:DeleteBtn'); $oPage->add_script( <<add(''); while ($oAttachment = $oSet->Fetch()) { $iAttId = $oAttachment->GetKey(); $oDoc = $oAttachment->Get('contents'); $sFileName = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; $oPage->add(''); } // Suggested attachments are listed here but treated as temporary $aDefault = utils::ReadParam('default', array(), false, 'raw_data'); if (array_key_exists('suggested_attachments', $aDefault)) { $sSuggestedAttachements = $aDefault['suggested_attachments']; if (is_array($sSuggestedAttachements)) { $sSuggestedAttachements = implode(',', $sSuggestedAttachements); } $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE id IN($sSuggestedAttachements)"); $oSet = new DBObjectSet($oSearch, array()); if ($oSet->Count() > 0) { while ($oAttachment = $oSet->Fetch()) { // Mark the attachments as temporary attachments for the current object/form $oAttachment->Set('temp_id', $sTempId); $oAttachment->DBUpdate(); // Display them $iAttId = $oAttachment->GetKey(); $oDoc = $oAttachment->Get('contents'); $sFileName = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; $oPage->add(''); $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."', false /* not an line image */]);"); } } } $oPage->add(''); $oPage->add('
'); $sMaxUpload = $this->GetMaxUpload(); $oPage->p(Dict::S('Attachments:AddAttachment').' '.$sMaxUpload); $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.iframe-transport.js'); $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.fileupload.js'); $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL; $oPage->add_ready_script( <<< EOF $('#file').fileupload({ url: GetAbsoluteUrlModulesRoot()+'itop-attachments/ajax.attachment.php', formData: { operation: 'add', temp_id: '$sTempId', obj_class: '$sClass' }, dataType: 'json', pasteZone: null, // Don't accept files via Chrome's copy/paste done: function (e, data) { if(typeof(data.result.error) != 'undefined') { if(data.result.error != '') { alert(data.result.error); } else { var sDownloadLink = '$sDownloadLink'+data.result.att_id; $('#attachments').append(''); if($sIsDeleteEnabled) { $('#display_attachment_'+data.result.att_id).hover( function() { $(this).children(':button').toggleClass('btn_hidden'); } ); } $('#attachment_plugin').trigger('add_attachment', [data.result.att_id, data.result.msg, false /* inline image */]); } } }, start: function() { $('#attachment_loading').show(); }, stop: function() { $('#attachment_loading').hide(); } }); $(document).bind('dragover', function (e) { var bFiles = false; if (e.dataTransfer && e.dataTransfer.types) { for (var i = 0; i < e.dataTransfer.types.length; i++) { if (e.dataTransfer.types[i] == "application/x-moz-nativeimage") { bFiles = false; // mozilla contains "Files" in the types list when dragging images inside the page, but it also contains "application/x-moz-nativeimage" before break; } if (e.dataTransfer.types[i] == "Files") { bFiles = true; break; } } } if (!bFiles) return; // Not dragging files var dropZone = $('#file').closest('fieldset'); if (!dropZone.is(':visible')) { // Hidden, but inside an inactive tab? Higlight the tab var sTabId = dropZone.closest('.ui-tabs-panel').attr('aria-labelledby'); dropZone = $('#'+sTabId).closest('li'); } timeout = window.dropZoneTimeout; if (!timeout) { dropZone.addClass('drag_in'); } else { clearTimeout(timeout); } window.dropZoneTimeout = setTimeout(function () { window.dropZoneTimeout = null; dropZone.removeClass('drag_in'); }, 300); }); // check if the attachments are used by inline images window.setTimeout( function() { $('.attachment a').each(function() { var sUrl = $(this).attr('href'); if($('img[src="'+sUrl+'"]').length > 0) { $(this).addClass('image-in-use').find('img').wrap('
'); } }); $('.htmlEditor').each(function() { var oEditor = $(this).ckeditorGet(); var sHtml = oEditor.getData(); var jElement = $('
').html(sHtml).contents(); jElement.find('img').each(function() { var sSrc = $(this).attr('src'); $('.attachment a[href="'+sSrc+'"]').parent().addClass('image-in-use').find('img').wrap('
'); }); }); $('.image-in-use-wrapper').append('
'); }, 200 ); EOF ); $oPage->p(''); $oPage->p(''); if ($this->m_bDeleteEnabled) { $oPage->add_ready_script('$(".attachment").hover( function() {$(this).children(":button").toggleClass("btn_hidden"); } );'); } } else { $oPage->add(''); if ($oSet->Count() == 0) { $oPage->add(Dict::S('Attachments:NoAttachment')); } else { while ($oAttachment = $oSet->Fetch()) { $iAttId = $oAttachment->GetKey(); $oDoc = $oAttachment->Get('contents'); $sFileName = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; $oPage->add(''); } } $oPage->add(''); } $oPage->add('
'); $sPreviewNotAvailable = addslashes(Dict::S('Attachments:PreviewNotAvailable')); $iMaxWidth = MetaModel::GetModuleSetting('itop-attachments', 'preview_max_width', 290); $oPage->add_ready_script( <<');} else { return '$sPreviewNotAvailable'; }} }); EOF ); } protected static function UpdateAttachments($oObject, $oChange = null) { self::$m_bIsModified = false; if (utils::ReadParam('attachment_plugin', 'not-in-form') == 'not-in-form') { // Workaround to an issue in iTop < 2.0 // Leave silently if there is no trace of the attachment form return; } $iTransactionId = utils::ReadParam('transaction_id', null); if (!is_null($iTransactionId)) { $aActions = array(); $aAttachmentIds = utils::ReadParam('attachments', array()); $aRemovedAttachmentIds = utils::ReadParam('removed_attachments', array()); // Get all current attachments $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id"); $oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey())); while ($oAttachment = $oSet->Fetch()) { // Remove attachments that are no longer attached to the current object if (in_array($oAttachment->GetKey(), $aRemovedAttachmentIds)) { $oAttachment->DBDelete(); $aActions[] = self::GetActionChangeOp($oAttachment, false /* false => deletion */); } } // Attach new (temporary) attachements $sTempId = session_id().'_'.$iTransactionId; // The object is being created from a form, check if there are pending attachments // for this object, but deleting the "new" ones that were already removed from the form $sOQL = 'SELECT Attachment WHERE temp_id = :temp_id'; $oSearch = DBObjectSearch::FromOQL($sOQL); foreach($aAttachmentIds as $iAttachmentId) { $oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId)); while($oAttachment = $oSet->Fetch()) { if (in_array($oAttachment->GetKey(),$aRemovedAttachmentIds)) { $oAttachment->DBDelete(); // temporary attachment removed, don't even mention it in the history } else { $oAttachment->SetItem($oObject); $oAttachment->Set('temp_id', ''); $oAttachment->DBUpdate(); // temporary attachment confirmed, list it in the history $aActions[] = self::GetActionChangeOp($oAttachment, true /* true => creation */); } } } if (count($aActions) > 0) { foreach($aActions as $oChangeOp) { self::RecordHistory($oChange, $oObject, $oChangeOp); } self::$m_bIsModified = true; } } } ///////////////////////////////////////////////////////////////////////////////////////// public static function GetFileIcon($sFileName) { $aPathParts = pathinfo($sFileName); if (!array_key_exists('extension', $aPathParts)) { // No extension: use the default icon $sIcon = 'document.png'; } else { switch($aPathParts['extension']) { case 'doc': case 'docx': $sIcon = 'doc.png'; break; case 'xls': case 'xlsx': $sIcon = 'xls.png'; break; case 'ppt': case 'pptx': $sIcon = 'ppt.png'; break; case 'pdf': $sIcon = 'pdf.png'; break; case 'txt': case 'text': $sIcon = 'txt.png'; break; case 'rtf': $sIcon = 'rtf.png'; break; case 'odt': $sIcon = 'odt.png'; break; case 'ods': $sIcon = 'ods.png'; break; case 'odp': $sIcon = 'odp.png'; break; case 'html': case 'htm': $sIcon = 'html.png'; break; case 'png': case 'gif': case 'jpg': case 'jpeg': case 'tiff': case 'tif': case 'bmp': $sIcon = 'image.png'; break; case 'zip': case 'gz': case 'tgz': case 'rar': $sIcon = 'zip.png'; break; default: $sIcon = 'document.png'; break; } } return 'env-'.utils::GetCurrentEnvironment()."/itop-attachments/icons/$sIcon"; } ///////////////////////////////////////////////////////////////////////// private static function RecordHistory($oChange, $oTargetObject, $oMyChangeOp) { if (!is_null($oChange)) { $oMyChangeOp->Set("change", $oChange->GetKey()); } $oMyChangeOp->Set("objclass", get_class($oTargetObject)); $oMyChangeOp->Set("objkey", $oTargetObject->GetKey()); $iId = $oMyChangeOp->DBInsertNoReload(); } ///////////////////////////////////////////////////////////////////////// private static function GetActionChangeOp($oAttachment, $bCreate = true) { $oBlob = $oAttachment->Get('contents'); $sFileName = $oBlob->GetFileName(); if ($bCreate) { $oChangeOp = new CMDBChangeOpAttachmentAdded(); $oChangeOp->Set('attachment_id', $oAttachment->GetKey()); $oChangeOp->Set('filename', $sFileName); } else { $oChangeOp = new CMDBChangeOpAttachmentRemoved(); $oChangeOp->Set('filename', $sFileName); } return $oChangeOp; } ///////////////////////////////////////////////////////////////////////// /** * Returns if Attachments should be readonly for $oObject in the $sState state for the $sGUI GUI * * @param DBObject $oObject * @param string $sState * @param string $sGUI * @return bool */ public static function IsReadonlyState(DBObject $oObject, $sState, $sGUI = self::ENUM_GUI_ALL) { $aParamDefaultValue = array( static::ENUM_GUI_ALL => array( 'Ticket' => array('closed') ) ); $bReadonly = false; $sClass = get_class($oObject); $aReadonlyStatus = MetaModel::GetModuleSetting('itop-attachments', 'readonly_states', $aParamDefaultValue); if(!empty($aReadonlyStatus)) { // Merging GUIs entries $aEntries = array(); // - All if( array_key_exists(static::ENUM_GUI_ALL, $aReadonlyStatus) ) { $aEntries = array_merge_recursive($aEntries, $aReadonlyStatus[static::ENUM_GUI_ALL]); } // - Backoffice & Portals foreach( array(static::ENUM_GUI_BACKOFFICE, static::ENUM_GUI_PORTALS) as $sEnumGUI) { if( in_array($sGUI, array(static::ENUM_GUI_ALL, $sEnumGUI)) ) { if( array_key_exists($sEnumGUI, $aReadonlyStatus) ) { $aEntries = array_merge_recursive($aEntries, $aReadonlyStatus[$sEnumGUI]); } } } $aParentClasses = array_reverse( MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL) ); foreach($aParentClasses as $sParentClass) { if( array_key_exists($sParentClass, $aEntries) ) { // If we found an ancestor of the object's class, we stop looking event if the current state is not specified if( in_array($oObject->GetState(), $aEntries[$sParentClass]) ) { $bReadonly = true; } break; } } } return $bReadonly; } } /** * Record the modification of a caselog (text) * since the caselog itself stores the history * of its entries, there is no need to duplicate * the text here * * @package iTopORM */ class CMDBChangeOpAttachmentAdded extends CMDBChangeOp { public static function Init() { $aParams = array ( "category" => "core/cmdb", "key_type" => "", "name_attcode" => "change", "state_attcode" => "", "reconc_keys" => array(), "db_table" => "priv_changeop_attachment_added", "db_key_field" => "id", "db_finalclass_field" => "", ); MetaModel::Init_Params($aParams); MetaModel::Init_InheritAttributes(); MetaModel::Init_AddAttribute(new AttributeExternalKey("attachment_id", array("targetclass"=>"Attachment", "allowed_values"=>null, "sql"=>"attachment_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("filename", array("allowed_values"=>null, "sql"=>"filename", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); // Display lists MetaModel::Init_SetZListItems('details', array('attachment_id')); // Attributes to be displayed for the complete details MetaModel::Init_SetZListItems('list', array('attachment_id')); // Attributes to be displayed for a list } /** * Describe (as a text string) the modifications corresponding to this change */ public function GetDescription() { // Temporary, until we change the options of GetDescription() -needs a more global revision $bIsHtml = true; $sResult = ''; $sTargetObjectClass = 'Attachment'; $iTargetObjectKey = $this->Get('attachment_id'); $sFilename = htmlentities($this->Get('filename'), ENT_QUOTES, 'UTF-8'); $oTargetSearch = new DBObjectSearch($sTargetObjectClass); $oTargetSearch->AddCondition('id', $iTargetObjectKey, '='); $oMonoObjectSet = new DBObjectSet($oTargetSearch); if ($oMonoObjectSet->Count() > 0) { $oAttachment = $oMonoObjectSet->Fetch(); $oDoc = $oAttachment->Get('contents'); $sPreview = $oDoc->IsPreviewAvailable() ? 'data-preview="true"' : ''; $sResult = Dict::Format('Attachments:History_File_Added', ''.$sFilename.''); } else { $sResult = Dict::Format('Attachments:History_File_Added', ''.$sFilename.''); } return $sResult; } } class CMDBChangeOpAttachmentRemoved extends CMDBChangeOp { public static function Init() { $aParams = array ( "category" => "core/cmdb", "key_type" => "", "name_attcode" => "change", "state_attcode" => "", "reconc_keys" => array(), "db_table" => "priv_changeop_attachment_removed", "db_key_field" => "id", "db_finalclass_field" => "", ); MetaModel::Init_Params($aParams); MetaModel::Init_InheritAttributes(); MetaModel::Init_AddAttribute(new AttributeString("filename", array("allowed_values"=>null, "sql"=>"filename", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); // Display lists MetaModel::Init_SetZListItems('details', array('filename')); // Attributes to be displayed for the complete details MetaModel::Init_SetZListItems('list', array('filename')); // Attributes to be displayed for a list } /** * Describe (as a text string) the modifications corresponding to this change */ public function GetDescription() { // Temporary, until we change the options of GetDescription() -needs a more global revision $bIsHtml = true; $sResult = Dict::Format('Attachments:History_File_Removed', ''.htmlentities($this->Get('filename'), ENT_QUOTES, 'UTF-8').''); return $sResult; } }