From 52309bb1e5155433c38ea7ef37b58f790e127ab0 Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Thu, 16 Jun 2016 08:23:15 +0000 Subject: [PATCH] Improved images caching: since 2.3.0-beta, iTop handles inline images (case logs, ticket description) and a picture for a person (AttributeImage). This code refactoring handles a case where the browser checks for the validity of the image and the 304 response code can then be returned without checking anything because we assume that the URL of the image contains a signature of it (or the data cannot change -attachement and inline images are in this case). SVN:trunk[4217] --- core/inlineimage.class.inc.php | 2 +- core/ormdocument.class.inc.php | 48 +++++++- .../1.x/itop-attachments/main.attachments.php | 8 +- .../2.x/itop-attachments/main.attachments.php | 2 +- js/simple_graph.js | 2 +- pages/ajax.document.php | 106 ++++++++++++++++++ pages/ajax.render.php | 76 +------------ 7 files changed, 158 insertions(+), 86 deletions(-) create mode 100644 pages/ajax.document.php diff --git a/core/inlineimage.class.inc.php b/core/inlineimage.class.inc.php index a204ca493..4782efff4 100644 --- a/core/inlineimage.class.inc.php +++ b/core/inlineimage.class.inc.php @@ -16,7 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see -define('INLINEIMAGE_DOWNLOAD_URL', 'pages/ajax.render.php?operation=download_inlineimage&id='); +define('INLINEIMAGE_DOWNLOAD_URL', 'pages/ajax.document.php?operation=download_inlineimage&id='); /** * Persistent classes (internal): store images referenced inside HTML formatted text fields diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php index d447fc0fe..f33c00ae3 100644 --- a/core/ormdocument.class.inc.php +++ b/core/ormdocument.class.inc.php @@ -1,5 +1,5 @@ GetData()); - return utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=download_document&class=$sClass&id=$Id&field=$sAttCode&s=$sSignature&cache=86400"; + return utils::GetAbsoluteUrlAppRoot()."pages/ajax.document.php?operation=download_document&class=$sClass&id=$Id&field=$sAttCode&s=$sSignature&cache=86400"; } @@ -144,5 +144,45 @@ class ormDocument } return $bRet; } + + /** + * Downloads a document to the browser, either as 'inline' or 'attachment' + * + * @param WebPage $oPage The web page for the output + * @param string $sClass Class name of the object + * @param mixed $id Identifier of the object + * @param string $sAttCode Name of the attribute containing the document to download + * @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 none + */ + public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) + { + try + { + $oObj = MetaModel::GetObject($sClass, $id, false, false); + if (!is_object($oObj)) + { + throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it"); + } + if (($sSecretField != null) && ($oObj->Get($sSecretField) != $sSecretValue)) + { + usleep(200); + throw new Exception("Invalid secret for class '$sClass' - the object does not exist or you are not allowed to view it"); + } + $oDocument = $oObj->Get($sAttCode); + if (is_object($oDocument)) + { + $oPage->TrashUnexpectedOutput(); + $oPage->SetContentType($oDocument->GetMimeType()); + //$oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName()); + $oPage->add($oDocument->GetData()); + } + } + catch(Exception $e) + { + $oPage->p($e->getMessage()); + } + } } -?> diff --git a/datamodels/1.x/itop-attachments/main.attachments.php b/datamodels/1.x/itop-attachments/main.attachments.php index 75442a217..d5fef0837 100644 --- a/datamodels/1.x/itop-attachments/main.attachments.php +++ b/datamodels/1.x/itop-attachments/main.attachments.php @@ -282,7 +282,7 @@ EOF } else { - var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=download_document&class=Attachment&id='+data.att_id+'&field=contents'; + var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.document.php?operation=download_document&class=Attachment&id='+data.att_id+'&field=contents'; $('#attachments').append(''); if($sIsDeleteEnabled) { @@ -313,7 +313,7 @@ EOF $oDoc = $oAttachment->Get('contents'); $sFileName = $oDoc->GetFileName(); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); - $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; $oPage->add(''); } @@ -340,7 +340,7 @@ EOF $oDoc = $oAttachment->Get('contents'); $sFileName = $oDoc->GetFileName(); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); - $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; $oPage->add(''); $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."', false /* not an line image */]);"); } @@ -374,7 +374,7 @@ EOF $oDoc = $oAttachment->Get('contents'); $sFileName = $oDoc->GetFileName(); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); - $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; $oPage->add(''); } } diff --git a/datamodels/2.x/itop-attachments/main.attachments.php b/datamodels/2.x/itop-attachments/main.attachments.php index 1458bddd5..394febd32 100755 --- a/datamodels/2.x/itop-attachments/main.attachments.php +++ b/datamodels/2.x/itop-attachments/main.attachments.php @@ -16,7 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see -define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.render.php?operation=download_document&class=Attachment&field=contents&id='); +define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.document.php?operation=download_document&class=Attachment&field=contents&id='); class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExtension { diff --git a/js/simple_graph.js b/js/simple_graph.js index 447bf5e9f..8d520b2e6 100644 --- a/js/simple_graph.js +++ b/js/simple_graph.js @@ -857,7 +857,7 @@ $(function() jTab.find('span').html(sTabText+' '); } $.post(sUrl, oParams, function(data) { - var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=download_document&class=Attachment&field=contents&id='+data.att_id; + var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.document.php?operation=download_document&class=Attachment&field=contents&id='+data.att_id; var sIcon = GetAbsoluteUrlModulesRoot()+'itop-attachments/icons/pdf.png'; if (jTab != null) { diff --git a/pages/ajax.document.php b/pages/ajax.document.php new file mode 100644 index 000000000..64358aac1 --- /dev/null +++ b/pages/ajax.document.php @@ -0,0 +1,106 @@ + + + +/** + * Handles various ajax requests + * + * @copyright Copyright (C) 2010-2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +require_once('../approot.inc.php'); +require_once(APPROOT.'application/utils.inc.php'); + + +if (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER) && (strlen($_SERVER['HTTP_IF_MODIFIED_SINCE']) > 0)) +{ + // The content is garanteed to be unmodified since the URL includes a signature based on the contents of the document + header('not modified', true, 304); + exit; +} + +try +{ + require_once(APPROOT.'/application/application.inc.php'); + require_once(APPROOT.'/application/webpage.class.inc.php'); + require_once(APPROOT.'/application/ajaxwebpage.class.inc.php'); + 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(); + + $operation = utils::ReadParam('operation', ''); + $sClass = utils::ReadParam('class', 'MissingAjaxParam', false, 'class'); + + switch($operation) + { + case 'download_document': + $id = utils::ReadParam('id', ''); + $sField = utils::ReadParam('field', ''); + if ($sClass == 'Attachment') + { + $iCacheSec = 31556926; // One year ahead: an attachment cannot change + } + else + { + $iCacheSec = (int)utils::ReadParam('cache', 0); + } + if (!empty($sClass) && ($sClass != 'InlineImage') && !empty($id) && !empty($sField)) + { + ormDocument::DownloadDocument($oPage, $sClass, $id, $sField, 'attachment'); + if ($iCacheSec > 0) + { + $oPage->add_header("Expires: "); // Reset the value set in ajax_page + $oPage->add_header("Cache-Control: no-transform,public,max-age=$iCacheSec,s-maxage=$iCacheSec"); + $oPage->add_header("Pragma: cache"); // Reset the value set .... where ? + $oPage->add_header("Last-Modified: Wed, 15 Jun 2015 13:21:15 GMT"); // An arbitrary date in the past is ok + } + } + break; + + case 'download_inlineimage': + $id = utils::ReadParam('id', ''); + $sSecret = utils::ReadParam('s', ''); + $iCacheSec = 31556926; // One year ahead: an inline image cannot change + if (!empty($id) && !empty($sSecret)) + { + ormDocument::DownloadDocument($oPage, 'InlineImage', $id, 'contents', 'attachment', 'secret', $sSecret); + $oPage->add_header("Expires: "); // Reset the value set in ajax_page + $oPage->add_header("Cache-Control: no-transform,public,max-age=$iCacheSec,s-maxage=$iCacheSec"); + $oPage->add_header("Pragma: cache"); // Reset the value set .... where ? + $oPage->add_header("Last-Modified: Wed, 15 Jun 2016 13:21:15 GMT"); // An arbitrary date in the past is ok + } + break; + + default: + $oPage->p("Invalid query."); + } + + $oPage->output(); +} +catch (Exception $e) +{ + // note: transform to cope with XSS attacks + echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8'); + IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString()); +} + diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 94caa8be0..76debff93 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -772,22 +772,7 @@ try $sField = utils::ReadParam('field', ''); if (!empty($sClass) && ($sClass != 'InlineImage') && !empty($id) && !empty($sField)) { - DownloadDocument($oPage, $sClass, $id, $sField, 'inline'); - } - break; - - case 'download_document': - $id = utils::ReadParam('id', ''); - $sField = utils::ReadParam('field', ''); - $iCacheSec = (int) utils::ReadParam('cache', 0); - if (!empty($sClass) && ($sClass != 'InlineImage') && !empty($id) && !empty($sField)) - { - DownloadDocument($oPage, $sClass, $id, $sField, 'attachment'); - if ($iCacheSec > 0) - { - $oPage->add_header("Expires: "); // Reset the value set in ajax_page - $oPage->add_header("Cache-Control: no-transform,public,max-age=$iCacheSec,s-maxage=$iCacheSec"); - } + ormDocument::DownloadDocument($oPage, $sClass, $id, $sField, 'inline'); } break; @@ -2433,21 +2418,6 @@ EOF $oPage->add(""); break; - case 'download_inlineimage': - $id = utils::ReadParam('id', ''); - $sSecret = utils::ReadParam('s', ''); - $iCacheSec = (int) utils::ReadParam('cache', 0); - if (!empty($id) && !empty($sSecret)) - { - DownloadDocument($oPage, 'InlineImage', $id, 'contents', 'attachment', 'secret', $sSecret); - if ($iCacheSec > 0) - { - $oPage->add_header("Expires: "); // Reset the value set in ajax_page - $oPage->add_header("Cache-Control: no-transform,public,max-age=$iCacheSec,s-maxage=$iCacheSec"); - } - } - break; - case 'custom_fields_update': $oPage->SetContentType('application/json'); $sAttCode = utils::ReadParam('attcode', ''); @@ -2489,47 +2459,3 @@ catch (Exception $e) echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8'); IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString()); } - - - -/** - * Downloads a document to the browser, either as 'inline' or 'attachment' - * - * @param WebPage $oPage The web page for the output - * @param string $sClass Class name of the object - * @param mixed $id Identifier of the object - * @param string $sAttCode Name of the attribute containing the document to download - * @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 none - */ -function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) -{ - try - { - $oObj = MetaModel::GetObject($sClass, $id, false, false); - if (!is_object($oObj)) - { - throw new Exception("Invalid id ($id) for class '$sClass' - the object does not exist or you are not allowed to view it"); - } - if (($sSecretField != null) && ($oObj->Get($sSecretField) != $sSecretValue)) - { - usleep(200); - throw new Exception("Invalid secret for class '$sClass' - the object does not exist or you are not allowed to view it"); - } - $oDocument = $oObj->Get($sAttCode); - if (is_object($oDocument)) - { - $oPage->TrashUnexpectedOutput(); - $oPage->SetContentType($oDocument->GetMimeType()); - $oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName()); - $oPage->add($oDocument->GetData()); - } - } - catch(Exception $e) - { - $oPage->p($e->getMessage()); - } -} -?>