diff --git a/attachment.php b/attachment.php deleted file mode 100644 index 05e256a42a980c0c15e5469167af2a047952718c..0000000000000000000000000000000000000000 --- a/attachment.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/********************************************************************* - attachment.php - - Attachments interface for clients. - Clients should never see the dir paths. - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ -require('secure.inc.php'); -require_once(INCLUDE_DIR.'class.attachment.php'); -//Basic checks -if(!$thisclient - || !$_GET['id'] - || !$_GET['h'] - || !($attachment=Attachment::lookup($_GET['id'])) - || !($file=$attachment->getFile())) - Http::response(404, __('Unknown or invalid file')); - -//Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!! -$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey())); -if(strcasecmp(trim($_GET['h']),$vhash) - || !($ticket=$attachment->getTicket()) - || !$ticket->checkUserAccess($thisclient)) - Http::response(404, __('Unknown or invalid file')); -//Download the file.. -$file->download(); - -?> diff --git a/file.php b/file.php new file mode 100644 index 0000000000000000000000000000000000000000..e5dd71ed8069048ce1de64a95515027bf6243233 --- /dev/null +++ b/file.php @@ -0,0 +1,39 @@ +<?php +/********************************************************************* + file.php + + File download facilitator for clients + + Peter Rotich <peter@osticket.com> + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('client.inc.php'); +require_once(INCLUDE_DIR.'class.file.php'); + +//Basic checks +if (!$_GET['key'] + || !$_GET['signature'] + || !$_GET['expires'] + || !($file = AttachmentFile::lookup($_GET['key'])) +) { + Http::response(404, __('Unknown or invalid file')); +} + +// Validate session access hash - we want to make sure the link is FRESH! +// and the user has access to the parent ticket!! +if ($file->verifySignature($_GET['signature'], $_GET['expires'])) { + if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0) + $file->display($s); + + // Download the file.. + $file->download(@$_GET['disposition'] ?: false, $_GET['expires']); +} +// else +Http::response(404, __('Unknown or invalid file')); diff --git a/image.php b/image.php deleted file mode 100644 index 5b7f27283ba23175ff12772f4212a41859ea3e18..0000000000000000000000000000000000000000 --- a/image.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/********************************************************************* - image.php - - Simply downloads the file...on hash validation as follows; - - * Hash must be 64 chars long. - * First 32 chars is the perm. file hash - * Next 32 chars is md5(file_id.session_id().file_hash) - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ - -require('client.inc.php'); -require_once(INCLUDE_DIR.'class.file.php'); -$h=trim($_GET['h']); -//basic checks -if(!$h || strlen($h)!=64 //32*2 - || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. - || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash. - Http::response(404, __('Unknown or invalid file')); - -$file->display(); -?> diff --git a/include/ajax.draft.php b/include/ajax.draft.php index f727e2cc9a10040786f6abb3502a8a21f7e04806..091632891e0d5d065048bdd8859095f867b1880e 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -129,7 +129,7 @@ class DraftAjaxAPI extends AjaxController { echo JsonDataEncoder::encode(array( 'content_id' => 'cid:'.$f->getKey(), - 'filelink' => sprintf('image.php?h=%s', $f->getDownloadHash()) + 'filelink' => $f->getDownloadUrl(false, 'inline'), )); } @@ -309,7 +309,7 @@ class DraftAjaxAPI extends AjaxController { ); while (list($id, $type) = db_fetch_row($res)) { $f = AttachmentFile::lookup($id); - $url = 'image.php?h='.$f->getDownloadHash(); + $url = $f->getDownloadUrl(); $files[] = array( 'thumb'=>$url.'&s=128', 'image'=>$url, diff --git a/include/class.attachment.php b/include/class.attachment.php index d562b31a095fa69251f62af687d14f40dfda0243..3f2101e18be4a18695f8f67a3eb5289006c8f8fc 100644 --- a/include/class.attachment.php +++ b/include/class.attachment.php @@ -171,15 +171,15 @@ class GenericAttachments { function _getList($separate=false, $inlines=false) { if(!isset($this->attachments)) { $this->attachments = array(); - $sql='SELECT f.id, f.size, f.`key`, f.name, a.inline ' + $sql='SELECT f.id, f.size, f.`key`, f.signature, f.name, a.inline ' .' FROM '.FILE_TABLE.' f ' .' INNER JOIN '.ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' .' WHERE a.`type`='.db_input($this->getType()) .' AND a.object_id='.db_input($this->getId()); if(($res=db_query($sql)) && db_num_rows($res)) { while($rec=db_fetch_array($res)) { - $rec['download'] = AttachmentFile::getDownloadForIdAndKey( - $rec['id'], $rec['key']); + $rec['download_url'] = AttachmentFile::generateDownloadUrl( + $rec['id'], $rec['key'], $rec['signature']); $this->attachments[] = $rec; } } diff --git a/include/class.draft.php b/include/class.draft.php index cdb5d43f440081d3e047993dd0df006cf23c39d6..3235a9ff92a5e7f967a73d78811568951975ebec 100644 --- a/include/class.draft.php +++ b/include/class.draft.php @@ -61,7 +61,7 @@ class Draft { } function setBody($body) { - // Change image.php urls back to content-id's + // Change file.php urls back to content-id's $body = Format::sanitize($body, false); $this->ht['body'] = $body; diff --git a/include/class.faq.php b/include/class.faq.php index 0395b4968671f9a487167443d66ef242d7f9da04..5df6655d54f90e7eb50a5f5fb22648dedc92ed1e 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -61,7 +61,7 @@ class FAQ { function getQuestion() { return $this->ht['question']; } function getAnswer() { return $this->ht['answer']; } function getAnswerWithImages() { - return Format::viewableImages($this->ht['answer'], ROOT_PATH.'image.php'); + return Format::viewableImages($this->ht['answer']); } function getSearchableAnswer() { return ThreadBody::fromFormattedText($this->ht['answer'], 'html') @@ -198,12 +198,11 @@ class FAQ { if(($attachments=$this->attachments->getSeparates())) { foreach($attachments as $attachment ) { /* The h key must match validation in file.php */ - $hash=$attachment['key'].md5($attachment['id'].session_id().strtolower($attachment['key'])); if($attachment['size']) $size=sprintf(' <small>(<i>%s</i>)</small>',Format::file_size($attachment['size'])); - $str.=sprintf('<a class="Icon file no-pjax" href="file.php?h=%s" target="%s">%s</a>%s %s', - $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + $str.=sprintf('<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s %s', + $attachment['download_url'], $target, Format::htmlchars($attachment['name']), $size, $separator); } } diff --git a/include/class.file.php b/include/class.file.php index b38ddb2dc6e895642403a394a21d632223b454cc..ce662085ae0b62af1163ce451bca4b08bd31e8ca 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -107,19 +107,6 @@ class AttachmentFile { return $this->ht['created']; } - static function getDownloadForIdAndKey($id, $key) { - return strtolower($key . md5($id.session_id().strtolower($key))); - } - - - /** - * Retrieve a signature that can be sent to scp/file.php?h= in order to - * download this file - */ - function getDownloadHash() { - return self::getDownloadForIdAndKey($this->getId(), $this->getKey()); - } - function open() { return FileStorageBackend::getInstance($this); } @@ -200,13 +187,70 @@ class AttachmentFile { exit(); } - function download() { + function getDownloadUrl($minage=false, $disposition=false, $handler=false) { + // XXX: Drop this when AttachmentFile goes to ORM + return static::generateDownloadUrl($this->getId(), + strtolower($this->getKey()), $this->getSignature(), $minage, + $disposition, $handler); + } + + static function generateDownloadUrl($id, $key, $hash, $minage=false, + $disposition=false, $handler=false + ) { + // Expire at the nearest midnight, allowing at least 12 hours access + $minage = $minage ?: 43200; + $gmnow = Misc::gmtime() + $minage; + $expires = $gmnow + 86400 - ($gmnow % 86400); + + // Generate a signature based on secret content + $signature = static::_genUrlSignature($id, $key, $hash, $expires); + + $handler = $handler ?: ROOT_PATH . 'file.php'; + + // Return sanitized query string + $args = array( + 'key' => $key, + 'expires' => $expires, + 'signature' => $signature, + ); + + if ($disposition) + $args['disposition'] = $disposition; + + return $handler . '?' . http_build_query($args); + } + + function verifySignature($signature, $expires) { + $gmnow = Misc::gmtime(); + if ($expires < $gmnow) + return false; + + $check = static::_genUrlSignature($this->getId(), $this->getKey(), + $this->getSignature(), $expires); + return $signature == $check; + } + + static function _genUrlSignature($id, $key, $signature, $expires) { + $pieces = array( + 'Host='.$_SERVER['HTTP_HOST'], + 'Path='.ROOT_PATH, + 'Id='.$id, + 'Key='.strtolower($key), + 'Hash='.$signature, + 'Expires='.$expires, + ); + return hash_hmac('sha1', implode("\n", $pieces), SECRET_SALT); + } + + function download($disposition=false, $expires=false) { + $disposition = $disposition ?: 'attachment'; $bk = $this->open(); - if ($bk->sendRedirectUrl('inline')) + if ($bk->sendRedirectUrl($disposition)) return; - $this->makeCacheable(); + $ttl = ($expires) ? $expires - Misc::gmtime() : false; + $this->makeCacheable($ttl); Http::download($this->getName(), $this->getType() ?: 'application/octet-stream', - null, 'inline'); + null, $disposition); header('Content-Length: '.$this->getSize()); $this->sendData(false); exit(); diff --git a/include/class.format.php b/include/class.format.php index 7ac9f1e297dd7022a1ee9e81545a71e9b04bc646..a124a577f60a0cc88beff06dce0c3953fa8d96d6 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -259,9 +259,10 @@ class Format { } function localizeInlineImages($text) { - // Change image.php urls back to content-id's - return preg_replace('/image\\.php\\?h=([\\w.-]{32})\\w{32}/', - 'cid:$1', $text); + // Change file.php urls back to content-id's + return preg_replace( + '/src="(?:\/[^"]+?)?\/file\\.php\\?(?:\w+=[^&]+&(?:amp;)?)*?key=([^&]+)[^"]*/', + 'src="cid:$1', $text); } function sanitize($text, $striptags=false) { @@ -408,14 +409,14 @@ class Format { } - function viewableImages($html, $script='image.php') { + function viewableImages($html, $script=false) { return preg_replace_callback('/"cid:([\w._-]{32})"/', function($match) use ($script) { $hash = $match[1]; if (!($file = AttachmentFile::lookup($hash))) return $match[0]; - return sprintf('"%s?h=%s" data-cid="%s"', - $script, $file->getDownloadHash(), $match[1]); + return sprintf('"%s" data-cid="%s"', + $file->getDownloadUrl(false, 'inline', $script), $match[1]); }, $html); } diff --git a/include/class.forms.php b/include/class.forms.php index f21f5f7974f6a64c6883cdeffbd1651e98bde261..e242a44ba40c77807e454be6f9b2313cabc6bb5f 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1647,10 +1647,8 @@ class FileUploadField extends FormField { function display($value) { $links = array(); foreach ($this->getFiles() as $f) { - $hash = strtolower($f['key'] - . md5($f['id'].session_id().strtolower($f['key']))); - $links[] = sprintf('<a class="no-pjax" href="file.php?h=%s">%s</a>', - $hash, Format::htmlchars($f['name'])); + $links[] = sprintf('<a class="no-pjax" href="%s">%s</a>', + Format::htmlchars($f['download_url']), Format::htmlchars($f['name'])); } return implode('<br/>', $links); } @@ -2096,6 +2094,7 @@ class FileUploadWidget extends Widget { 'name' => $file->getName(), 'type' => $file->getType(), 'size' => $file->getSize(), + 'download_url' => $file->getDownloadUrl(), ); } } diff --git a/include/class.http.php b/include/class.http.php index d45218051a12e3c9d5bbf1ddf24ade23d2760c2c..463387df54e148e0f164321159c203bd59c53540 100644 --- a/include/class.http.php +++ b/include/class.http.php @@ -63,14 +63,16 @@ class Http { function cacheable($etag, $modified, $ttl=3600) { // Thanks, http://stackoverflow.com/a/1583753/1025836 - $last_modified = Misc::db2gmtime($modified); - header("Last-Modified: ".date('D, d M y H:i:s', $last_modified)." GMT", false); + // Timezone doesn't matter here — but the time needs to be + // consistent round trip to the browser and back. + $last_modified = strtotime($modified." GMT"); + header("Last-Modified: ".date('D, d M Y H:i:s', $last_modified)." GMT", false); header('ETag: "'.$etag.'"'); header("Cache-Control: private, max-age=$ttl"); - header('Expires: ' . gmdate(DATE_RFC822, time() + $ttl)." GMT"); + header('Expires: ' . gmdate('D, d M Y H:i:s', Misc::gmtime() + $ttl)." GMT"); header('Pragma: private'); if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified || - @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) { + @trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') == $etag) { header("HTTP/1.1 304 Not Modified"); exit(); } @@ -97,9 +99,8 @@ class Http { function download($filename, $type, $data=null, $disposition='attachment') { header('Pragma: private'); - header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Cache-Control: private'); + header('Cache-Control: private', false); header('Content-Type: '.$type); header(sprintf('Content-Disposition: %s; %s', $disposition, diff --git a/include/class.page.php b/include/class.page.php index c533617d6126b04668d5927bf43cacf875c34cb8..a6faf5f019762ef075005a1e48f2d911c03e5613 100644 --- a/include/class.page.php +++ b/include/class.page.php @@ -71,7 +71,7 @@ class Page { return $this->ht['body']; } function getBodyWithImages() { - return Format::viewableImages($this->getBody(), ROOT_PATH.'image.php'); + return Format::viewableImages($this->getBody()); } function getNotes() { diff --git a/include/class.thread.php b/include/class.thread.php index b6fcf78c304d5c466269213b594f4baa16dae1e4..6a7e498a1f34901727152ba48335f1f79aeddfe1 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -579,7 +579,7 @@ Class ThreadEntry { return $this->attachments; //XXX: inner join the file table instead? - $sql='SELECT a.attach_id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.name, a.inline ' + $sql='SELECT a.attach_id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.`signature` as file_sig, f.name, a.inline ' .' FROM '.FILE_TABLE.' f ' .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' .' WHERE a.ticket_id='.db_input($this->getTicketId()) @@ -587,19 +587,21 @@ Class ThreadEntry { $this->attachments = array(); if(($res=db_query($sql)) && db_num_rows($res)) { - while($rec=db_fetch_array($res)) + while($rec=db_fetch_array($res)) { + $rec['download_url'] = AttachmentFile::generateDownloadUrl( + $rec['file_id'], $rec['file_hash'], $rec['file_sig']); $this->attachments[] = $rec; + } } return $this->attachments; } - function getAttachmentUrls($script='image.php') { + function getAttachmentUrls() { $json = array(); foreach ($this->getAttachments() as $att) { $json[$att['file_hash']] = array( - 'download_url' => sprintf('attachment.php?id=%d&h=%s', $att['attach_id'], - strtolower(md5($att['file_id'].session_id().$att['file_hash']))), + 'download_url' => $att['download_url'], 'filename' => $att['name'], ); } @@ -612,14 +614,12 @@ Class ThreadEntry { foreach($this->getAttachments() as $attachment ) { if ($attachment['inline']) continue; - /* The hash can be changed but must match validation in @file */ - $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); $size = ''; if($attachment['size']) $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); - $str.=sprintf('<a class="Icon file no-pjax" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', - $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + $str.=sprintf('<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s %s', + $attachment['download_url'], $target, Format::htmlchars($attachment['name']), $size, $separator); } return $str; diff --git a/include/staff/settings-pages.inc.php b/include/staff/settings-pages.inc.php index 9727bde4060beeaacce6169a375b87b0b5382aa9..3bf1c78776adc2bc90b9604175591bd3ff969202 100644 --- a/include/staff/settings-pages.inc.php +++ b/include/staff/settings-pages.inc.php @@ -135,7 +135,7 @@ $pages = Page::getPages(); echo $logo->getId(); ?>" <?php if ($logo->getId() == $current) echo 'checked="checked"'; ?>/> - <img src="image.php?h=<?php echo $logo->getDownloadHash(); ?>" + <img src="<?php echo $logo->getDownloadUrl(); ?>" alt="Custom Logo" valign="middle" style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5); margin: 0.5em; height: 5em; diff --git a/js/filedrop.field.js b/js/filedrop.field.js index 4eb5d27e91ebdb78033b0e6cdab7e4d20b57f0f8..63b4b456ab1cd2cbe93bdf5454c909c79c295ed0 100644 --- a/js/filedrop.field.js +++ b/js/filedrop.field.js @@ -180,11 +180,12 @@ } if (file.id) filenode.data('fileId', file.id); - if (file.download) + if (file.download_url) { filenode.find('.filename').prepend( $('<a class="no-pjax" target="_blank"></a>').text(file.name) - .attr('href', 'file.php?h='+escape(file.download)) + .attr('href', file.download_url) ); + } else filenode.find('.filename').prepend($('<span>').text(file.name)); this.$element.parent().find('.files').append(filenode); diff --git a/kb/file.php b/kb/file.php deleted file mode 100644 index 99664708a567a08d80eaca73ece27a7f9973f87a..0000000000000000000000000000000000000000 --- a/kb/file.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/********************************************************************* - file.php - - Simply downloads the file...on hash validation as follows; - - * Hash must be 64 chars long. - * First 32 chars is the perm. file hash - * Next 32 chars is md5(file_id.session_id().file_hash) - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ -require('kb.inc.php'); -require_once(INCLUDE_DIR.'class.file.php'); -$h=trim($_GET['h']); -//basic checks -if(!$h || strlen($h)!=64 //32*2 - || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. - || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash. - Http::response(404, __('Unknown or invalid file')); - -$file->download(); -?> diff --git a/scp/attachment.php b/scp/attachment.php deleted file mode 100644 index 07f20981a19a982c3014033eab67465b9f5587a8..0000000000000000000000000000000000000000 --- a/scp/attachment.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/********************************************************************* - attachment.php - - Handles attachment downloads & access validation. - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ -require('staff.inc.php'); -require_once(INCLUDE_DIR.'class.attachment.php'); - -//Basic checks -if(!$thisstaff || !$_GET['id'] || !$_GET['h'] - || !($attachment=Attachment::lookup($_GET['id'])) - || !($file=$attachment->getFile())) - Http::response(404, __('Unknown or invalid file')); - -//Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!! -$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey())); -if(strcasecmp(trim($_GET['h']),$vhash) || !($ticket=$attachment->getTicket()) || !$ticket->checkStaffAccess($thisstaff)) die(__('Access Denied')); - -//Download the file.. -$file->download(); -?> diff --git a/scp/file.php b/scp/file.php deleted file mode 100644 index 97687de0f0bb0a341ce895298f587891a9c179c0..0000000000000000000000000000000000000000 --- a/scp/file.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/********************************************************************* - file.php - - Simply downloads the file...on hash validation as follows; - - * Hash must be 64 chars long. - * First 32 chars is the perm. file hash - * Next 32 chars is md5(file_id.session_id().file_hash) - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ -require('staff.inc.php'); -require_once(INCLUDE_DIR.'class.file.php'); -$h=trim($_GET['h']); -//basic checks -if(!$h || strlen($h)!=64 //32*2 - || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. - || strcasecmp($file->getDownloadHash(), $h)) //next 32 is file id + session hash. - Http::response(404, __('Unknown or invalid file')); - -$file->download(); -?> diff --git a/scp/image.php b/scp/image.php deleted file mode 100644 index 4c4ddbfe6da36e21588c0359b96924c365fba64b..0000000000000000000000000000000000000000 --- a/scp/image.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/********************************************************************* - image.php - - Simply downloads the file...on hash validation as follows; - - * Hash must be 64 chars long. - * First 32 chars is the perm. file hash - * Next 32 chars is md5(file_id.session_id().file_hash) - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ - -require('staff.inc.php'); -require_once(INCLUDE_DIR.'class.file.php'); -$h=trim($_GET['h']); -//basic checks -if(!$h || strlen($h)!=64 //32*2 - || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash. - || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash. - Http::response(404, __('Unknown or invalid file')); - -if ($_GET['s'] && is_numeric($_GET['s'])) - $file->display($_GET['s']); -else - $file->display(); -?>