From 4aeb78ccac618618e9f8d58cf37b7788b4f8b56d Mon Sep 17 00:00:00 2001 From: Pierre Goiffon Date: Thu, 12 Dec 2019 10:56:19 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B0330=20Attachments=20display=20as=20table?= =?UTF-8?q?=20:=20console?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/ormdocument.class.inc.php | 23 + .../2.x/itop-attachments/ajax.attachment.php | 106 --- .../itop-attachments/ajax.itop-attachment.php | 140 +++ .../de.dict.itop-attachments.php | 2 + .../en.dict.itop-attachments.php | 11 + .../fr.dict.itop-attachments.php | 2 + .../2.x/itop-attachments/main.attachments.php | 900 ------------------ .../main.itop-attachments.php | 702 ++++++++++++++ ...hments.php => module.itop-attachments.php} | 9 +- .../renderers.itop-attachments.php | 478 ++++++++++ 10 files changed, 1362 insertions(+), 1011 deletions(-) delete mode 100755 datamodels/2.x/itop-attachments/ajax.attachment.php create mode 100644 datamodels/2.x/itop-attachments/ajax.itop-attachment.php delete mode 100755 datamodels/2.x/itop-attachments/main.attachments.php create mode 100644 datamodels/2.x/itop-attachments/main.itop-attachments.php rename datamodels/2.x/itop-attachments/{module.attachments.php => module.itop-attachments.php} (97%) mode change 100755 => 100644 create mode 100644 datamodels/2.x/itop-attachments/renderers.itop-attachments.php diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php index 68d8d0368..0a3322542 100644 --- a/core/ormdocument.class.inc.php +++ b/core/ormdocument.class.inc.php @@ -75,6 +75,29 @@ class ormDocument return $this->m_sMimeType; } + /** + * @return int size in bits + * @uses strlen which returns the no of bits used + * @since 2.7.0 + */ + public function GetSize() + { + return strlen($this->m_data); + } + + public function GetFormatedSize($precision = 2) + { + $bytes = $this->GetSize(); + $units = array('B', 'KB', 'MB', 'GB', 'TB'); + + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= pow(1024, $pow); + + return round($bytes, $precision).' '.$units[$pow]; + } public function GetData() { return $this->m_data; diff --git a/datamodels/2.x/itop-attachments/ajax.attachment.php b/datamodels/2.x/itop-attachments/ajax.attachment.php deleted file mode 100755 index cfcedd6c6..000000000 --- a/datamodels/2.x/itop-attachments/ajax.attachment.php +++ /dev/null @@ -1,106 +0,0 @@ -no_cache(); - - $sOperation = utils::ReadParam('operation', ''); - - switch($sOperation) - { - case 'add': - $aResult = array( - 'error' => '', - 'att_id' => 0, - 'preview' => 'false', - 'msg' => '' - ); - $sObjClass = stripslashes(utils::ReadParam('obj_class', '', false, 'class')); - $sTempId = utils::ReadParam('temp_id', '', false, 'transaction_id'); - if (empty($sObjClass)) - { - $aResult['error'] = "Missing argument 'obj_class'"; - } - elseif (empty($sTempId)) - { - $aResult['error'] = "Missing argument 'temp_id'"; - } - else - { - try - { - $oDoc = utils::ReadPostedDocument('file'); - /** @var Attachment $oAttachment */ - $oAttachment = MetaModel::NewObject('Attachment'); - $oAttachment->Set('expire', time() + MetaModel::GetConfig()->Get('draft_attachments_lifetime')); - $oAttachment->Set('temp_id', $sTempId); - $oAttachment->Set('item_class', $sObjClass); - $oAttachment->SetDefaultOrgId(); - $oAttachment->Set('contents', $oDoc); - $iAttId = $oAttachment->DBInsert(); - - $aResult['msg'] = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); - $aResult['icon'] = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($oDoc->GetFileName()); - $aResult['att_id'] = $iAttId; - $aResult['preview'] = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; - } - catch (FileUploadException $e) - { - $aResult['error'] = $e->GetMessage(); - } - } - $oPage->add(json_encode($aResult)); - break; - - case 'remove': - $iAttachmentId = utils::ReadParam('att_id', ''); - $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE id = :id"); - $oSet = new DBObjectSet($oSearch, array(), array('id' => $iAttachmentId)); - while ($oAttachment = $oSet->Fetch()) - { - $oAttachment->DBDelete(); - } - break; - - default: - $oPage->p("Missing argument 'operation'"); - } - - $oPage->output(); -} -catch (Exception $e) -{ - // note: transform to cope with XSS attacks - echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8'); - IssueLog::Error($e->getMessage()); -} -?> diff --git a/datamodels/2.x/itop-attachments/ajax.itop-attachment.php b/datamodels/2.x/itop-attachments/ajax.itop-attachment.php new file mode 100644 index 000000000..e32d252d7 --- /dev/null +++ b/datamodels/2.x/itop-attachments/ajax.itop-attachment.php @@ -0,0 +1,140 @@ +SetContentType('text/html'); + $oAttachmentsRenderer = AttachmentsRendererFactory::GetInstance($oPage, $sClass, $sId, $iTransactionId); + + $bIsReadOnlyState = (is_null($oObject)) + ? false + : AttachmentPlugIn::IsReadonlyState($oObject, $oObject->GetState(), AttachmentPlugIn::ENUM_GUI_BACKOFFICE); + if ($bEditMode && !$bIsReadOnlyState) + { + $oAttachmentsRenderer->RenderEditAttachmentsList($aAttachmentsDeleted); + } + else + { + $oAttachmentsRenderer->RenderViewAttachmentsList(); + } +} + +try +{ + require_once APPROOT.'/application/startup.inc.php'; + require_once APPROOT.'/application/loginwebpage.class.inc.php'; + LoginWebPage::DoLoginEx(null /* any portal */, false); + + $oPage = new ajax_page(""); + $oPage->no_cache(); + + $sOperation = utils::ReadParam('operation', ''); + + switch ($sOperation) + { + case 'add': + $aResult = array( + 'error' => '', + 'att_id' => 0, + 'preview' => 'false', + 'msg' => '', + ); + $sClass = stripslashes(utils::ReadParam('obj_class', '', false, 'class')); + $sTempId = utils::ReadParam('temp_id', '', false, 'transaction_id'); + if (empty($sClass)) + { + $aResult['error'] = "Missing argument 'obj_class'"; + } + elseif (empty($sTempId)) + { + $aResult['error'] = "Missing argument 'temp_id'"; + } + else + { + try + { + $oDoc = utils::ReadPostedDocument('file'); + /** @var Attachment $oAttachment */ + $oAttachment = MetaModel::NewObject('Attachment'); + $oAttachment->Set('expire', time() + MetaModel::GetConfig()->Get('draft_attachments_lifetime')); + $oAttachment->Set('temp_id', $sTempId); + $oAttachment->Set('item_class', $sClass); + $oAttachment->SetDefaultOrgId(); + $oAttachment->Set('contents', $oDoc); + $iAttId = $oAttachment->DBInsert(); + + $aResult['msg'] = htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8'); + $aResult['icon'] = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($oDoc->GetFileName()); + $aResult['att_id'] = $iAttId; + $aResult['preview'] = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; + } + catch (FileUploadException $e) + { + $aResult['error'] = $e->GetMessage(); + } + } + $oPage->add(json_encode($aResult)); + break; + + case 'remove': + $iAttachmentId = utils::ReadParam('att_id', ''); + $oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE id = :id"); + $oSet = new DBObjectSet($oSearch, array(), array('id' => $iAttachmentId)); + while ($oAttachment = $oSet->Fetch()) + { + $oAttachment->DBDelete(); + } + break; + + case 'refresh_attachments_render': + $sTempId = utils::ReadParam('temp_id', '', false, 'transaction_id'); + RenderAttachments($oPage, $sTempId); + break; + + default: + $oPage->p("Missing argument 'operation'"); + } + + $oPage->output(); +} +catch (Exception $e) +{ + // note: transform to cope with XSS attacks + echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8'); + IssueLog::Error($e->getMessage()); +} diff --git a/datamodels/2.x/itop-attachments/de.dict.itop-attachments.php b/datamodels/2.x/itop-attachments/de.dict.itop-attachments.php index a4a583eee..3c1adde81 100644 --- a/datamodels/2.x/itop-attachments/de.dict.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/de.dict.itop-attachments.php @@ -37,6 +37,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Attachments:NoAttachment' => 'Kein Attachment. ', 'Attachments:PreviewNotAvailable' => 'Vorschau für diesen Attachment-Typ nicht verfügbar.', 'Attachments:Error:FileTooLarge' => 'File is too large to be uploaded. %1$s~~', + 'Attachments:Render:Icons' => 'Display as icons~~', + 'Attachments:Render:Table' => 'Display as list~~', )); // diff --git a/datamodels/2.x/itop-attachments/en.dict.itop-attachments.php b/datamodels/2.x/itop-attachments/en.dict.itop-attachments.php index 0908826b2..2ee120c56 100755 --- a/datamodels/2.x/itop-attachments/en.dict.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/en.dict.itop-attachments.php @@ -36,6 +36,8 @@ Dict::Add('EN US', 'English', 'English', array( 'Attachments:NoAttachment' => 'No attachment. ', 'Attachments:PreviewNotAvailable' => 'Preview not available for this type of attachment.', 'Attachments:Error:FileTooLarge' => 'File is too large to be uploaded. %1$s', + 'Attachments:Render:Icons' => 'Display as icons', + 'Attachments:Render:Table' => 'Display as list', )); // @@ -58,3 +60,12 @@ Dict::Add('EN US', 'English', 'English', array( 'Class:Attachment/Attribute:contents' => 'Contents', 'Class:Attachment/Attribute:contents+' => '', )); + + +Dict::Add('EN US', 'English', 'English', array( + 'Attachments:File:Thumbnail' => 'Icon', + 'Attachments:File:Name' => 'File name', + 'Attachments:File:Date' => 'Date added', + 'Attachments:File:Size' => 'Size', + 'Attachments:File:MimeType' => 'Type', +)); \ No newline at end of file diff --git a/datamodels/2.x/itop-attachments/fr.dict.itop-attachments.php b/datamodels/2.x/itop-attachments/fr.dict.itop-attachments.php index 1ec402e00..cfbe47c1d 100755 --- a/datamodels/2.x/itop-attachments/fr.dict.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/fr.dict.itop-attachments.php @@ -36,6 +36,8 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Attachments:NoAttachment' => 'Aucune pièce jointe.', 'Attachments:PreviewNotAvailable' => 'Pas d\'aperçu pour ce type de pièce jointe.', 'Attachments:Error:FileTooLarge' => 'Le fichier est trop gros pour être chargé. %1$s', + 'Attachments:Render:Icons' => 'Affichage en icônes', + 'Attachments:Render:Table' => 'Affichage en liste', )); // diff --git a/datamodels/2.x/itop-attachments/main.attachments.php b/datamodels/2.x/itop-attachments/main.attachments.php deleted file mode 100755 index df994e368..000000000 --- a/datamodels/2.x/itop-attachments/main.attachments.php +++ /dev/null @@ -1,900 +0,0 @@ - - -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); - } - } - } - - /** - * Returns the value of "upload_max_filesize" in bytes if upload allowed, false otherwise. - * - * @since 2.6.1 - * - * @return number|boolean - */ - public static function GetMaxUploadSize() - { - $sMaxUpload = ini_get('upload_max_filesize'); - if (!$sMaxUpload) - { - $result = false; - } - else - { - $result = utils::ConvertToBytes($sMaxUpload); - } - - return $result; - } - - /** - * Returns the max. file upload size allowed as a dictionary entry - * - * @return string - */ - public static function GetMaxUpload() - { - $iMaxUpload = static::GetMaxUploadSize(); - if (!$iMaxUpload) - { - $sRet = Dict::S('Attachments:UploadNotAllowedOnThisSystem'); - } - else - { - 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; - } - - /** - * @param \DBObject $oObject - * @param \WebPage $oPage - * @param bool $bEditMode - * - * @throws \CoreCannotSaveObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * @throws \OQLException - */ - public function DisplayAttachments(DBObject $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())); - - $iTransactionId = $oPage->GetTransactionId(); - $sTempId = utils::GetUploadTempId($iTransactionId); - $oSearchTemp = DBObjectSearch::FromOQL("SELECT Attachment WHERE temp_id = :temp_id"); - $oSetTemp = new DBObjectSet($oSearchTemp, array(), array('temp_id' => $sTempId)); - - if ($this->GetAttachmentsPosition() == 'relations') - { - $iCount = $oSet->Count() + $oSetTemp->Count(); - $sTitle = ($iCount > 0)? Dict::Format('Attachments:TabTitle_Count', $iCount) : Dict::S('Attachments:EmptyTabTitle'); - $oPage->SetCurrentTab($sTitle); - } - $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'; - $sClass = get_class($oObject); - $sDeleteBtn = Dict::S('Attachments:DeleteBtn'); - $oPage->add_script( -<<add(''); - while ($oAttachment = $oSet->Fetch()) - { - $this->DisplayOneAttachment($oPage, $oAttachment); - } - - // Display Temporary attachments - while ($oAttachment = $oSetTemp->Fetch()) - { - $this->DisplayOneAttachment($oPage, $oAttachment, true); - } - - - // 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 - $this->DisplayOneAttachment($oPage, $oAttachment, true); - } - } - } - - $oPage->add(''); - $oPage->add('
'); - $iMaxUploadInBytes = $this->GetMaxUploadSize(); - $sMaxUploadLabel = $this->GetMaxUpload(); - $sFileTooBigLabel = Dict::Format('Attachments:Error:FileTooLarge', $sMaxUploadLabel); - $sFileTooBigLabelForJS = addslashes($sFileTooBigLabel); - $oPage->p(Dict::S('Attachments:AddAttachment').' '.$sMaxUploadLabel); - - $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 */]); - } - } - }, - send: function(e, data){ - // Don't send attachment if size is greater than PHP post_max_size, otherwise it will break the request and all its parameters (\$_REQUEST, \$_POST, ...) - // Note: We loop on the files as the data structures is an array but in this case, we only upload 1 file at a time. - var iTotalSizeInBytes = 0; - for(var i = 0; i < data.files.length; i++) - { - iTotalSizeInBytes += data.files[i].size; - } - - if(iTotalSizeInBytes > $iMaxUploadInBytes) - { - alert('$sFileTooBigLabelForJS'); - return false; - } - }, - 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; - } - $sTransactionId = utils::ReadParam('transaction_id', null, false, 'transaction_id'); - if (!is_null($sTransactionId)) - { - $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) attachments - $sTempId = utils::GetUploadTempId($sTransactionId); - // 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 CopyAttachments($oObject, $sTransactionId) - { - $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())); - // Attach new (temporary) attachments - $sTempId = utils::GetUploadTempId($sTransactionId); - while ($oAttachment = $oSet->Fetch()) - { - $oTempAttachment = clone $oAttachment; - $oTempAttachment->Set('item_id', null); - $oTempAttachment->Set('temp_id', $sTempId); - $oTempAttachment->DBInsert(); - } - } - - ///////////////////////////////////////////////////////////////////////////////////////// - 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()); - $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 - * @throws \CoreException - */ - 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; - } - - /** - * @param \WebPage $oPage - * @param $oAttachment - * @param bool $bIsTemporary - * - * @throws \Exception - */ - protected function DisplayOneAttachment(WebPage $oPage, $oAttachment, $bIsTemporary = false) - { - $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(''); - if ($bIsTemporary) - { - $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."', false /* not an line image */]);"); - } - } -} - -/** - * 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 - $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 - $sResult = Dict::Format('Attachments:History_File_Removed', ''.htmlentities($this->Get('filename'), ENT_QUOTES, 'UTF-8').''); - return $sResult; - } -} - diff --git a/datamodels/2.x/itop-attachments/main.itop-attachments.php b/datamodels/2.x/itop-attachments/main.itop-attachments.php new file mode 100644 index 000000000..a3a8e7c16 --- /dev/null +++ b/datamodels/2.x/itop-attachments/main.itop-attachments.php @@ -0,0 +1,702 @@ + + +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); + } + } + } + + /** + * Returns the value of "upload_max_filesize" in bytes if upload allowed, false otherwise. + * + * @return number|boolean + * @since 2.6.1 + * + */ + public static function GetMaxUploadSize() + { + $sMaxUpload = ini_get('upload_max_filesize'); + if (!$sMaxUpload) + { + $result = false; + } + else + { + $result = utils::ConvertToBytes($sMaxUpload); + } + + return $result; + } + + /** + * Returns the max. file upload size allowed as a dictionary entry + * + * @return string + */ + public static function GetMaxUpload() + { + $iMaxUpload = static::GetMaxUploadSize(); + if (!$iMaxUpload) + { + $sRet = Dict::S('Attachments:UploadNotAllowedOnThisSystem'); + } + else + { + 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; + } + + /** + * @param \DBObject $oObject + * @param \WebPage $oPage + * @param bool $bEditMode + * + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \InvalidParameterException + */ + public function DisplayAttachments(DBObject $oObject, WebPage $oPage, $bEditMode = false) + { + // Exit here if the class is not allowed + if (!$this->IsTargetObject($oObject)) + { + return; + } + + $sObjClass = get_class($oObject); + $iObjKey = $oObject->GetKey(); + $sTransactionId = $oPage->GetTransactionId(); + if ($bEditMode && empty($sTransactionId)) + { + throw new InvalidParameterException('Attachments renderer : invalid transaction id'); + } + $oAttachmentsRenderer = AttachmentsRendererFactory::GetInstance($oPage, $sObjClass, $iObjKey, $sTransactionId); + + if ($this->GetAttachmentsPosition() === 'relations') + { + $iCount = $oAttachmentsRenderer->GetAttachmentsSet()->Count() + $oAttachmentsRenderer->GetTempAttachmentsSet()->Count(); + $sTitle = ($iCount > 0) ? Dict::Format('Attachments:TabTitle_Count', $iCount) : Dict::S('Attachments:EmptyTabTitle'); + $oPage->SetCurrentTab($sTitle); + } + + $oPage->add('
'); + $oPage->add(''.Dict::S('Attachments:FieldsetTitle').''); + + $oPage->add('
'); + $bIsReadOnlyState = self::IsReadonlyState($oObject, $oObject->GetState(), AttachmentPlugIn::ENUM_GUI_BACKOFFICE); + if ($bEditMode && !$bIsReadOnlyState) + { + $oAttachmentsRenderer->RenderEditAttachmentsList(); + } + else + { + $oAttachmentsRenderer->RenderViewAttachmentsList(); + } + $oPage->add('
'); + + $oPage->add('
'); + } + + 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; + } + $sTransactionId = utils::ReadParam('transaction_id', null, false, 'transaction_id'); + if (!is_null($sTransactionId)) + { + $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) attachments + $sTempId = utils::GetUploadTempId($sTransactionId); + // 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 CopyAttachments($oObject, $sTransactionId) + { + $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())); + // Attach new (temporary) attachments + $sTempId = utils::GetUploadTempId($sTransactionId); + while ($oAttachment = $oSet->Fetch()) + { + $oTempAttachment = clone $oAttachment; + $oTempAttachment->Set('item_id', null); + $oTempAttachment->Set('temp_id', $sTempId); + $oTempAttachment->DBInsert(); + } + } + + ///////////////////////////////////////////////////////////////////////////////////////// + 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()); + $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 + * @throws \CoreException + */ + 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 + $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 + $sResult = Dict::Format('Attachments:History_File_Removed', + ''.htmlentities($this->Get('filename'), ENT_QUOTES, 'UTF-8').''); + + return $sResult; + } +} + + +class AttachmentsHelper +{ + /** + * @param string $sObjClass class name of the objects holding the attachments + * @param int $iObjKey key of the objects holding the attachments + * + * @return array containing attachment_id as key and date as value + */ + public static function GetAttachmentsDateAddedFromDb($sObjClass, $iObjKey) + { + $sQuery = "SELECT CMDBChangeOpAttachmentAdded WHERE objclass='$sObjClass' AND objkey=$iObjKey"; + try + { + $oSearch = DBObjectSearch::FromOQL($sQuery); + } + catch (OQLException $e) + { + return array(); + } + $oSet = new DBObjectSet($oSearch); + + try + { + $aAttachmentDates = array(); + while ($oChangeOpAttAdded = $oSet->Fetch()) + { + $iAttachmentId = $oChangeOpAttAdded->Get('attachment_id'); + $sAttachmentDate = $oChangeOpAttAdded->Get('date'); + $aAttachmentDates[$iAttachmentId] = $sAttachmentDate; + } + } + catch (Exception $e) + { + return array(); + } + + return $aAttachmentDates; + } +} diff --git a/datamodels/2.x/itop-attachments/module.attachments.php b/datamodels/2.x/itop-attachments/module.itop-attachments.php old mode 100755 new mode 100644 similarity index 97% rename from datamodels/2.x/itop-attachments/module.attachments.php rename to datamodels/2.x/itop-attachments/module.itop-attachments.php index b59bdfd34..abbfdeb42 --- a/datamodels/2.x/itop-attachments/module.attachments.php +++ b/datamodels/2.x/itop-attachments/module.itop-attachments.php @@ -19,7 +19,7 @@ SetupWebPage::AddModule( __FILE__, // Path to the current file, all other file names are relative to the directory containing this file - 'itop-attachments/2.6.2', + 'itop-attachments/2.7.0', array( // Identification // @@ -28,9 +28,7 @@ SetupWebPage::AddModule( // Setup // - 'dependencies' => array( - - ), + 'dependencies' => array(), 'mandatory' => false, 'visible' => true, 'installer' => 'AttachmentInstaller', @@ -39,7 +37,8 @@ SetupWebPage::AddModule( // 'datamodel' => array( 'model.itop-attachments.php', - 'main.attachments.php', + 'main.itop-attachments.php', + 'renderers.itop-attachments.php', ), 'webservice' => array( diff --git a/datamodels/2.x/itop-attachments/renderers.itop-attachments.php b/datamodels/2.x/itop-attachments/renderers.itop-attachments.php new file mode 100644 index 000000000..827337852 --- /dev/null +++ b/datamodels/2.x/itop-attachments/renderers.itop-attachments.php @@ -0,0 +1,478 @@ + + +define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.document.php?operation=download_document&class=Attachment&field=contents&id='); +define('ATTACHMENTS_RENDERER', 'TableDetailsAttachmentsRenderer'); + + +class AttachmentsRendererFactory +{ + /** + * @param \WebPage $oPage + * @param string $sObjClass class name of the objects holding the attachments + * @param int $iObjKey key of the objects holding the attachments + * @param string $sTransactionId CSRF token + * + * @return \AbstractAttachmentsRenderer rendering impl + */ + public static function GetInstance($oPage, $sObjClass, $iObjKey, $sTransactionId) + { + $sRendererClass = ATTACHMENTS_RENDERER; + /** @var \AbstractAttachmentsRenderer $oAttachmentsRenderer */ + $oAttachmentsRenderer = new $sRendererClass($oPage, $sObjClass, $iObjKey, $sTransactionId); + + return $oAttachmentsRenderer; + } +} + + +/** + * Common code for attachment rendering + * + * On each attachment you'll need to have : + * + * * an id on the attachment container (see GetAttachmentContainerId) + * * an input hidden inside the container (see GetAttachmentHiddenInput) + * + * @see \AttachmentPlugIn::DisplayAttachments() + */ +abstract class AbstractAttachmentsRenderer +{ + /** + * If size (in bits) is above this, then we will display a file icon instead of preview + */ + const MAX_SIZE_FOR_PREVIEW = 500000; + + /** @var \WebPage */ + protected $oPage; + /** + * @var string CSRF token, must be provided cause when getting content from AJAX we need the one from the original page, not the + * ajaxpage + */ + private $sTransactionId; + /** @var string */ + protected $sObjClass; + /** @var int */ + protected $iObjKey; + /** @var \DBObjectSet */ + protected $oTempAttachmentsSet; + /** @var \DBObjectSet */ + protected $oAttachmentsSet; + + /** + * @param \WebPage $oPage + * @param string $sObjClass class name of the objects holding the attachments + * @param int $iObjKey key of the objects holding the attachments + * @param string $sTransactionId CSRF token + * + * @throws \OQLException + */ + public function __construct(\WebPage $oPage, $sObjClass, $iObjKey, $sTransactionId) + { + $this->oPage = $oPage; + $this->sObjClass = $sObjClass; + $this->iObjKey = $iObjKey; + $this->sTransactionId = $sTransactionId; + + $oSearch = DBObjectSearch::FromOQL('SELECT Attachment WHERE item_class = :class AND item_id = :item_id'); + $this->oAttachmentsSet = new DBObjectSet($oSearch, array(), array('class' => $sObjClass, 'item_id' => $iObjKey)); + + $oSearchTemp = DBObjectSearch::FromOQL('SELECT Attachment WHERE temp_id = :temp_id'); + $this->oTempAttachmentsSet = new DBObjectSet($oSearchTemp, array(), array('temp_id' => $this->sTransactionId)); + } + + /** + * @return \DBObjectSet + */ + public function GetTempAttachmentsSet() + { + return $this->oTempAttachmentsSet; + } + + /** + * @return \DBObjectSet + */ + public function GetAttachmentsSet() + { + return $this->oAttachmentsSet; + } + + public function GetAttachmentsCount() + { + return $this->GetAttachmentsSet()->Count() + $this->GetTempAttachmentsSet()->Count(); + } + + /** + * @param int[] $aAttachmentsDeleted Attachments id that should be deleted after form submission + * + * @return string + */ + abstract public function RenderEditAttachmentsList($aAttachmentsDeleted = array()); + + abstract public function RenderViewAttachmentsList(); + + protected function AddUploadButton() + { + $sClass = $this->sObjClass; + $sId = $this->iObjKey; + + $this->oPage->add('
'); + $iMaxUploadInBytes = AttachmentPlugIn::GetMaxUploadSize(); + $sMaxUploadLabel = AttachmentPlugIn::GetMaxUpload(); + $sFileTooBigLabel = Dict::Format('Attachments:Error:FileTooLarge', $sMaxUploadLabel); + $sFileTooBigLabelForJS = addslashes($sFileTooBigLabel); + $this->oPage->p(Dict::S('Attachments:AddAttachment').' '.$sMaxUploadLabel); + + $this->oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.iframe-transport.js'); + $this->oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.fileupload.js'); + + $this->oPage->add_ready_script( + <<tbody>tr[id^="display_attachment_"]>td input[name="removed_attachments[]"]'), + aAttachmentsDeletedIds = aAttachmentsDeletedHiddenInputs.map(function() { return $(this).val() }).toArray(); + $(sContentNode).block(); + $.post(GetAbsoluteUrlModulesRoot()+'itop-attachments/ajax.itop-attachment.php', + { + operation: 'refresh_attachments_render', + objclass: '$sClass', + objkey: $sId, + temp_id: '$this->sTransactionId', + edit_mode: 1, + attachments_deleted: aAttachmentsDeletedIds + }, + function(data) { + $(sContentNode).html(data); + $(sContentNode).unblock(); + } + ); + } + + $('#file').fileupload({ + url: GetAbsoluteUrlModulesRoot()+'itop-attachments/ajax.itop-attachment.php', + formData: { operation: 'add', temp_id: '$this->sTransactionId', obj_class: '$sClass' }, + dataType: 'json', + pasteZone: null, // Don't accept files via Chrome's copy/paste + done: RefreshAttachmentsDisplay, + send: function(e, data){ + // Don't send attachment if size is greater than PHP post_max_size, otherwise it will break the request and all its parameters (\$_REQUEST, \$_POST, ...) + // Note: We loop on the files as the data structures is an array but in this case, we only upload 1 file at a time. + var iTotalSizeInBytes = 0; + for(var i = 0; i < data.files.length; i++) + { + iTotalSizeInBytes += data.files[i].size; + } + + if(iTotalSizeInBytes > $iMaxUploadInBytes) + { + alert('$sFileTooBigLabelForJS'); + return false; + } + }, + 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 ); +JS + ); + $this->oPage->p(''); + $this->oPage->p(''); + + $this->oPage->add_style(<<'; + } + + protected function GetDeleteAttachmentButton($iAttId) + { + return ''; + } + + protected function GetDeleteAttachmentJs() + { + return <<GetAttachmentsCount() === 0) + { + $this->oPage->add(Dict::S('Attachments:NoAttachment')); + + return; + } + + $this->oPage->add(''.PHP_EOL); + $this->oPage->add(''.PHP_EOL); + $this->oPage->add(' '.PHP_EOL); + $this->oPage->add(' '.PHP_EOL); + $this->oPage->add(' '.PHP_EOL); + $this->oPage->add(' '.PHP_EOL); + $this->oPage->add(' '.PHP_EOL); + if ($bWithDeleteButton) + { + $this->oPage->add(' '.PHP_EOL); + } + $this->oPage->add(''.PHP_EOL); + $this->oPage->add(''.PHP_EOL); + + + $iMaxWidth = MetaModel::GetModuleSetting('itop-attachments', 'preview_max_width', 290); + $sPreviewNotAvailable = addslashes(Dict::S('Attachments:PreviewNotAvailable')); + $this->oPage->add_ready_script( + <<tbody>tr>td a.trigger-preview', + position: { + my: 'left top', at: 'right top', using: function (position, feedback) { + $(this).css(position); + } + }, + content: function () { + if ($(this).hasClass("preview")) + { + return (''); + } + else + { + return '$sPreviewNotAvailable'; + } + } +}); +JS + ); + if ($bWithDeleteButton) + { + $this->oPage->add_script($this->GetDeleteAttachmentJs()); + } + $this->oPage->add_style( + <<tbody>tr>td:first-child { + text-align: center; +} +CSS + ); + + $bIsEven = false; + $aAttachmentsDate = AttachmentsHelper::GetAttachmentsDateAddedFromDb($this->sObjClass, $this->iObjKey); + while ($oAttachment = $this->oAttachmentsSet->Fetch()) + { + $bIsEven = ($bIsEven) ? false : true; + $this->AddAttachmentsTableLine($bWithDeleteButton, $bIsEven, $oAttachment, $aAttachmentsDate, $aAttachmentsDeleted); + } + while ($oTempAttachment = $this->oTempAttachmentsSet->Fetch()) + { + $bIsEven = ($bIsEven) ? false : true; + $this->AddAttachmentsTableLine($bWithDeleteButton, $bIsEven, $oTempAttachment, $aAttachmentsDate, $aAttachmentsDeleted); + } + + $this->oPage->add(''.PHP_EOL); + $this->oPage->add('
'.Dict::S('Attachments:File:Thumbnail').''.Dict::S('Attachments:File:Name').''.Dict::S('Attachments:File:Size').''.Dict::S('Attachments:File:Date').''.Dict::S('Attachments:File:MimeType').'
'.PHP_EOL); + } + + /** + * @param $bWithDeleteButton + * @param $bIsEven + * @param \DBObject $oAttachment + * @param array $aAttachmentsDate + * @param int[] $aAttachmentsDeleted + * + * @throws \ArchivedObjectException + * @throws \CoreException + */ + private function AddAttachmentsTableLine($bWithDeleteButton, $bIsEven, $oAttachment, $aAttachmentsDate, $aAttachmentsDeleted) + { + $iAttachmentId = $oAttachment->GetKey(); + + $sLineClass = ''; + if ($bIsEven) + { + $sLineClass = 'class="even"'; + } + + $sLineStyle = ''; + $bIsDeletedAttachment = false; + if (in_array($iAttachmentId, $aAttachmentsDeleted, true)) + { + $sLineStyle = 'style="display: none;"'; + $bIsDeletedAttachment = true; + } + + /** @var \ormDocument $oDoc */ + $oDoc = $oAttachment->Get('contents'); + + $sDocDownloadUrl = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttachmentId; + $sFileName = utils::HtmlEntities($oDoc->GetFileName()); + $sTrId = $this->GetAttachmentContainerId($iAttachmentId); + $sAttachmentMeta = $this->GetAttachmentHiddenInput($iAttachmentId, $bIsDeletedAttachment); + $sFileSize = $oDoc->GetFormatedSize(); + $sAttachmentDate = array_key_exists($iAttachmentId, $aAttachmentsDate) ? $aAttachmentsDate[$iAttachmentId] : 'N/A'; + $sFileType = $oDoc->GetMimeType(); + + $sAttachmentThumbUrl = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); + $sIconClass = ''; + if ($oDoc->IsPreviewAvailable()) + { + $sIconClass = ' preview'; + if ($oDoc->GetSize() <= self::MAX_SIZE_FOR_PREVIEW) + { + $sAttachmentThumbUrl = $sDocDownloadUrl; + } + } + + $sDeleteColumn = ''; + if ($bWithDeleteButton) + { + $sDeleteButton = $this->GetDeleteAttachmentButton($iAttachmentId); + $sDeleteColumn = "$sDeleteButton"; + } + + $this->oPage->add(<< + + $sFileName$sAttachmentMeta + $sFileSize + $sAttachmentDate + $sFileType + $sDeleteColumn + +HTML + ); + } + + /** + * @inheritDoc + */ + public function RenderEditAttachmentsList($aAttachmentsDeleted = array()) + { + $this->AddUploadButton(); + + $this->AddAttachmentsTable(true, $aAttachmentsDeleted); + } + + /** + * @inheritDoc + */ + public function RenderViewAttachmentsList() + { + $this->AddAttachmentsTable(false); + } +}