From 0aa0229170d5e5df0c3a40836d49083036417448 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Wed, 21 Dec 2022 22:58:04 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B02889=20-=20Add=20counter=20on=20file=20a?= =?UTF-8?q?ttributes=20/=20attachments=20downloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/attributedef.class.inc.php | 26 +++-- core/ormdocument.class.inc.php | 110 +++++++++++++----- .../dictionaries/en.dict.itop-attachments.php | 1 + .../dictionaries/fr.dict.itop-attachments.php | 1 + .../renderers.itop-attachments.php | 11 +- .../src/Controller/ObjectController.php | 1 + .../BsFileUploadFieldRenderer.php | 17 ++- 7 files changed, 126 insertions(+), 41 deletions(-) diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 09b33f65f..530264a23 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -8095,34 +8095,38 @@ class AttributeBlob extends AttributeDefinition $aColumns[''] = $sPrefix.'_mimetype'; $aColumns['_data'] = $sPrefix.'_data'; $aColumns['_filename'] = $sPrefix.'_filename'; + $aColumns['_downloads_count'] = $sPrefix.'_downloads_count'; return $aColumns; } public function FromSQLToValue($aCols, $sPrefix = '') { - if (!array_key_exists($sPrefix, $aCols)) - { + if (!array_key_exists($sPrefix, $aCols)) { $sAvailable = implode(', ', array_keys($aCols)); throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}"); } $sMimeType = isset($aCols[$sPrefix]) ? $aCols[$sPrefix] : ''; - if (!array_key_exists($sPrefix.'_data', $aCols)) - { + if (!array_key_exists($sPrefix.'_data', $aCols)) { $sAvailable = implode(', ', array_keys($aCols)); throw new MissingColumnException("Missing column '".$sPrefix."_data' from {$sAvailable}"); } $data = isset($aCols[$sPrefix.'_data']) ? $aCols[$sPrefix.'_data'] : null; - if (!array_key_exists($sPrefix.'_filename', $aCols)) - { + if (!array_key_exists($sPrefix.'_filename', $aCols)) { $sAvailable = implode(', ', array_keys($aCols)); throw new MissingColumnException("Missing column '".$sPrefix."_filename' from {$sAvailable}"); } $sFileName = isset($aCols[$sPrefix.'_filename']) ? $aCols[$sPrefix.'_filename'] : ''; - $value = new ormDocument($data, $sMimeType, $sFileName); + if (!array_key_exists($sPrefix.'_downloads_count', $aCols)) { + $sAvailable = implode(', ', array_keys($aCols)); + throw new MissingColumnException("Missing column '".$sPrefix."_downloads_count' from {$sAvailable}"); + } + $iDownloadsCount = isset($aCols[$sPrefix.'_downloads_count']) ? $aCols[$sPrefix.'_downloads_count'] : ormDocument::DEFAULT_DOWNLOADS_COUNT; + + $value = new ormDocument($data, $sMimeType, $sFileName, $iDownloadsCount); return $value; } @@ -8148,6 +8152,7 @@ class AttributeBlob extends AttributeDefinition } $aValues[$this->GetCode().'_mimetype'] = $value->GetMimeType(); $aValues[$this->GetCode().'_filename'] = $value->GetFileName(); + $aValues[$this->GetCode().'_downloads_count'] = $value->GetDownloadsCount(); } else { @@ -8155,6 +8160,7 @@ class AttributeBlob extends AttributeDefinition $aValues[$this->GetCode().'_data'] = ''; $aValues[$this->GetCode().'_mimetype'] = ''; $aValues[$this->GetCode().'_filename'] = ''; + $aValues[$this->GetCode().'_downloads_count'] = ''; // Note: Should this be set to \ormDocument::DEFAULT_DOWNLOADS_COUNT ? } return $aValues; @@ -8166,6 +8172,7 @@ class AttributeBlob extends AttributeDefinition $aColumns[$this->GetCode().'_data'] = 'LONGBLOB'; // 2^32 (4 Gb) $aColumns[$this->GetCode().'_mimetype'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); $aColumns[$this->GetCode().'_filename'] = 'VARCHAR(255)'.CMDBSource::GetSqlStringColumnDefinition(); + $aColumns[$this->GetCode().'_downloads_count'] = 'INT(11) UNSIGNED'; return $aColumns; } @@ -8235,11 +8242,13 @@ class AttributeBlob extends AttributeDefinition $sRet = ''; if (is_object($value)) { + /** @var \ormDocument $value */ if (!$value->IsEmpty()) { $sRet = ''.$value->GetMimeType().''; $sRet .= ''.$value->GetFileName().''; $sRet .= ''.base64_encode($value->GetData()).''; + $sRet .= ''.$value->GetDownloadsCount().''; } } @@ -8258,6 +8267,7 @@ class AttributeBlob extends AttributeDefinition $aValues['data'] = base64_encode($value->GetData()); $aValues['mimetype'] = $value->GetMimeType(); $aValues['filename'] = $value->GetFileName(); + $aValues['downloads_count'] = $value->GetDownloadsCount(); } else { @@ -8276,7 +8286,7 @@ class AttributeBlob extends AttributeDefinition if (isset($json->data)) { $data = base64_decode($json->data); - $value = new ormDocument($data, $json->mimetype, $json->filename); + $value = new ormDocument($data, $json->mimetype, $json->filename, $json->downloads_count); } else { diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php index 0bce486e8..11c010944 100644 --- a/core/ormdocument.class.inc.php +++ b/core/ormdocument.class.inc.php @@ -1,28 +1,20 @@ - - /** - * ormDocument - * encapsulate the behavior of a binary data set that will be stored an attribute of class AttributeBlob + * Copyright (C) 2013-2022 Combodo SARL * - * @copyright Copyright (C) 2010-2021 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License */ use Combodo\iTop\Service\Events\EventData; @@ -35,21 +27,52 @@ use Combodo\iTop\Service\Events\EventService; * * @package itopORM */ - class ormDocument { + /** + * @var string For content that should be displayed in the browser + * @link https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Content-Disposition#syntaxe + * @since 3.1.0 + */ + public const ENUM_CONTENT_DISPOSITION_INLINE = 'inline'; + /** + * @var string For content that should be downloaded on the device. Mind that "attachment" Content-Disposition has nothing to do with the "Attachment" class from the DataModel. + * @link https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Content-Disposition#syntaxe + * @since 3.1.0 + */ + public const ENUM_CONTENT_DISPOSITION_ATTACHMENT = 'attachment'; + + /** + * @var int Default downloads count of the document, should always be 0. + * @since 3.1.0 + */ + public const DEFAULT_DOWNLOADS_COUNT = 0; + protected $m_data; protected $m_sMimeType; protected $m_sFileName; - + /** + * @var int $m_iDownloadsCount Number of times the document has been downloaded (through the standard API!). Note that download from the browser's cache won't appear. + * @since 3.1.0 + */ + private $m_iDownloadsCount; + /** * Constructor + * + * @param null $data + * @param string $sMimeType + * @param string $sFileName + * @param int $iDownloadsCount + * + * @since 3.1.0 N°2889 Add $iDownloadsCount parameter */ - public function __construct($data = null, $sMimeType = 'text/plain', $sFileName = '') + public function __construct($data = null, $sMimeType = 'text/plain', $sFileName = '', $iDownloadsCount = self::DEFAULT_DOWNLOADS_COUNT) { $this->m_data = $data; $this->m_sMimeType = $sMimeType; $this->m_sFileName = $sFileName; + $this->m_iDownloadsCount = $iDownloadsCount; } public function __toString() @@ -109,6 +132,30 @@ class ormDocument return $this->m_sFileName; } + /** + * @see static::DownloadDocument() + * @see static::$m_iDownloadsCount + * @return int Number of times the document has been downloaded (through the standard API!) + * @since 3.1.0 + */ + public function GetDownloadsCount(): int + { + // Force cast to get 0 instead of null on fields prior to the features that have never been downloaded. + return (int) $this->m_iDownloadsCount; + } + + /** + * Increase the number of downloads of the document by $iNumber + * + * @param int $iNumber Step to increase the counter with, default is 1. + * @return void + * @since 3.1.0 + */ + public function IncreaseDownloadsCount($iNumber = 1): void + { + $this->m_iDownloadsCount += $iNumber; + } + public function GetAsHTML() { $sResult = ''; @@ -119,7 +166,8 @@ class ormDocument } else { $data = $this->GetData(); $sSize = utils::BytesToFriendlyFormat(strlen($data)); - $sResult = utils::EscapeHtml($this->GetFileName()).' ('.$sSize.')
'; + $iDownloadsCount = $this->GetDownloadsCount(); + $sResult = utils::EscapeHtml($this->GetFileName()).' ('.$sSize.' / '.$iDownloadsCount.' )
'; } return $sResult; } @@ -196,6 +244,8 @@ class ormDocument * @param string $sContentDisposition Either 'inline' or 'attachment' * @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file * @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField + * + * @return void */ public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) { @@ -211,6 +261,7 @@ class ormDocument usleep(200); throw new Exception("Invalid secret for class '$sClass' - the object does not exist or you are not allowed to view it"); } + /** @var \ormDocument $oDocument */ $oDocument = $oObj->Get($sAttCode); if (is_object($oDocument)) { @@ -224,6 +275,13 @@ class ormDocument $oPage->SetContentType($oDocument->GetMimeType()); $oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName()); $oPage->add($oDocument->GetData()); + + // Update downloads count only when content disposition is set to "attachment" as other disposition are to display the document within the page + if($sContentDisposition === static::ENUM_CONTENT_DISPOSITION_ATTACHMENT) { + $oDocument->IncreaseDownloadsCount(); + $oObj->Set($sAttCode, $oDocument); + $oObj->DBUpdate(); + } } } catch(Exception $e) diff --git a/datamodels/2.x/itop-attachments/dictionaries/en.dict.itop-attachments.php b/datamodels/2.x/itop-attachments/dictionaries/en.dict.itop-attachments.php index 579aa5e65..05949676c 100644 --- a/datamodels/2.x/itop-attachments/dictionaries/en.dict.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/dictionaries/en.dict.itop-attachments.php @@ -69,6 +69,7 @@ Dict::Add('EN US', 'English', 'English', array( 'Attachments:File:Uploader' => 'Uploaded by', 'Attachments:File:Size' => 'Size', 'Attachments:File:MimeType' => 'Type', + 'Attachments:File:DownloadsCount' => 'Downloads', )); // // Class: Attachment diff --git a/datamodels/2.x/itop-attachments/dictionaries/fr.dict.itop-attachments.php b/datamodels/2.x/itop-attachments/dictionaries/fr.dict.itop-attachments.php index d38bf1291..dde721987 100644 --- a/datamodels/2.x/itop-attachments/dictionaries/fr.dict.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/dictionaries/fr.dict.itop-attachments.php @@ -68,6 +68,7 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Attachments:File:Uploader' => 'Chargé par', 'Attachments:File:Size' => 'Taille', 'Attachments:File:MimeType' => 'Type', + 'Attachments:File:DownloadsCount' => 'Téléchargements', )); // // Class: Attachment diff --git a/datamodels/2.x/itop-attachments/renderers.itop-attachments.php b/datamodels/2.x/itop-attachments/renderers.itop-attachments.php index 8f45a18c8..b01804e2f 100644 --- a/datamodels/2.x/itop-attachments/renderers.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/renderers.itop-attachments.php @@ -31,6 +31,7 @@ use Combodo\iTop\Application\UI\Base\Component\Input\FileSelect\FileSelectUIBloc use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; use Combodo\iTop\Renderer\BlockRenderer; +define('ATTACHMENT_DISPLAY_URL', 'pages/ajax.render.php?operation=display_document&class=Attachment&field=contents&id='); define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.document.php?operation=download_document&class=Attachment&field=contents&id='); define('ATTACHMENTS_RENDERER', 'TableDetailsAttachmentsRenderer'); @@ -416,6 +417,7 @@ class TableDetailsAttachmentsRenderer extends AbstractAttachmentsRenderer $sFileDate = Dict::S('Attachments:File:Date'); $sFileUploader = Dict::S('Attachments:File:Uploader'); $sFileType = Dict::S('Attachments:File:MimeType'); + $sFileDownloadsCount = Dict::S('Attachments:File:DownloadsCount'); if ($bWithDeleteButton) { @@ -443,6 +445,7 @@ class TableDetailsAttachmentsRenderer extends AbstractAttachmentsRenderer 'upload-date' => array('label' => $sFileDate, 'description' => $sFileDate), 'uploader' => array('label' => $sFileUploader, 'description' => $sFileUploader), 'type' => array('label' => $sFileType, 'description' => $sFileType), + 'downloads-count' => array('label' => $sFileDownloadsCount, 'description' => $sFileDownloadsCount), ); if ($bWithDeleteButton) { @@ -496,6 +499,7 @@ JS /** @var \ormDocument $oDoc */ $oDoc = $oAttachment->Get('contents'); + $sDocDisplayUrl = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DISPLAY_URL.$iAttachmentId; $sDocDownloadUrl = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttachmentId; $sFileName = utils::HtmlEntities($oDoc->GetFileName()); $sTrId = $this->GetAttachmentContainerId($iAttachmentId); @@ -521,6 +525,7 @@ JS $sFileType = $oDoc->GetMimeType(); $sAttachmentThumbUrl = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); + $sAttachmentPreviewUrl = ''; $sIconClass = ''; $iMaxWidth = MetaModel::GetModuleSetting('itop-attachments', 'preview_max_width', 290); $iMaxSizeForPreview = MetaModel::GetModuleSetting('itop-attachments', 'icon_preview_max_size', self::DEFAULT_MAX_SIZE_FOR_PREVIEW); @@ -530,9 +535,10 @@ JS if ($oDoc->IsPreviewAvailable()) { $sIconClass = ' preview'; + $sAttachmentPreviewUrl = $sDocDisplayUrl; if ($oDoc->GetSize() <= $iMaxSizeForPreview) { - $sAttachmentThumbUrl = $sDocDownloadUrl; + $sAttachmentThumbUrl = $sDocDisplayUrl; } $sPreviewMarkup = utils::HtmlEntities(''); } @@ -541,12 +547,13 @@ JS $aAttachmentLine = array( '@id' => $sTrId, '@meta' => 'data-file-type="'.utils::HtmlEntities($sFileType).'" data-file-size-raw="'.utils::HtmlEntities($iFileSize).'" data-file-size-formatted="'.utils::HtmlEntities($sFileFormattedSize).'" data-file-uploader="'.utils::HtmlEntities($sAttachmentUploader).'"', - 'icon' => '', + 'icon' => '', 'filename' => ''.$sFileName.''.$sAttachmentMeta, 'formatted-size' => $sFileFormattedSize, 'upload-date' => $sAttachmentDateFormatted, 'uploader' => $sAttachmentUploaderForHtml, 'type' => $sFileType, + 'downloads-count' => $oDoc->GetDownloadsCount(), 'js' => '', ); diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php index db6c935b8..e290bc232 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php @@ -1245,6 +1245,7 @@ class ObjectController extends BrickController $aData['att_id'] = $iAttId; $aData['preview'] = $oDocument->IsPreviewAvailable(); $aData['file_size'] = $oDocument->GetFormattedSize(); + $aData['downloads_count'] = $oDocument->GetDownloadsCount(); $aData['creation_date'] = $oAttachment->Get('creation_date'); $aData['user_id_friendlyname'] = $oAttachment->Get('user_id_friendlyname'); $aData['file_type'] = $oDocument->GetMimeType(); diff --git a/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php b/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php index a7b1c902b..27a9178af 100644 --- a/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php +++ b/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php @@ -189,6 +189,7 @@ JS '{{sAttachmentMeta}}', '{{sFileSize}}', '{{iFileSizeRaw}}', + '{{iFileDownloadsCount}}', '{{sAttachmentDate}}', '{{iAttachmentDateRaw}}', $bIsDeleteAllowed @@ -250,6 +251,7 @@ JS {search: "{{sFileName}}", replace: data.result.msg }, {search: "{{sAttachmentMeta}}", replace:sAttachmentMeta }, {search: "{{sFileSize}}", replace:data.result.file_size }, + {search: "{{iFileDownloadsCount}}", replace:data.result.downloads_count }, {search: "{{sAttachmentDate}}", replace:data.result.creation_date }, ]; var sAttachmentRow = attachmentRowTemplate ; @@ -414,6 +416,7 @@ HTML $iFileSizeRaw = $oDoc->GetSize(); $sFileSize = $oDoc->GetFormattedSize(); + $iFileDownloadsCount = $oDoc->GetDownloadsCount(); $bIsTempAttachment = ($oAttachment->Get('item_id') === 0); $sAttachmentDate = ''; @@ -434,6 +437,7 @@ HTML $sAttachmentMeta, $sFileSize, $iFileSizeRaw, + $iFileDownloadsCount, $sAttachmentDate, $iAttachmentDateRaw, $bIsDeleteAllowed @@ -460,6 +464,7 @@ HTML $sTitleFileName = Dict::S('Attachments:File:Name'); $sTitleFileSize = Dict::S('Attachments:File:Size'); $sTitleFileDate = Dict::S('Attachments:File:Date'); + $sTitleFileDownloadsCount = Dict::S('Attachments:File:DownloadsCount'); // Optional column $sDeleteHeaderAsHtml = ($bIsDeleteAllowed) ? '' : ''; @@ -470,6 +475,7 @@ HTML $sTitleFileName $sTitleFileSize $sTitleFileDate + $sTitleFileDownloadsCount $sDeleteHeaderAsHtml HTML; @@ -494,7 +500,7 @@ HTML; */ protected static function GetAttachmentTableRow( $iAttId, $sLineStyle, $sDocDownloadUrl, $bHasPreview, $sAttachmentThumbUrl, $sFileName, $sAttachmentMeta, $sFileSize, - $iFileSizeRaw, $sAttachmentDate, $iAttachmentDateRaw, $bIsDeleteAllowed + $iFileSizeRaw, $iFileDownloadsCount, $sAttachmentDate, $iAttachmentDateRaw, $bIsDeleteAllowed ) { $sDeleteCell = ''; if ($bIsDeleteAllowed) @@ -511,10 +517,11 @@ HTML; } $sHtml .= <<$sFileName$sAttachmentMeta - $sFileSize - $sAttachmentDate - $sDeleteCell + $sFileName$sAttachmentMeta + $sFileSize + $sAttachmentDate + $iFileDownloadsCount + $sDeleteCell HTML; return $sHtml;