diff --git a/include/class.file.php b/include/class.file.php index a8e10eaf2181e7e092b3d63b822367e96183ee39..d68a3b11b1f403a4a0aacc1a58db37c61151eb68 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -196,24 +196,13 @@ class AttachmentFile { } function download() { + $bk = $this->open(); + if ($bk->sendRedirectUrl('inline')) + return; $this->makeCacheable(); - - header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream')); - - $filename=basename($this->getName()); - $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']); - if (false !== strpos($user_agent,'msie') && false !== strpos($user_agent,'win')) - header('Content-Disposition: filename='.rawurlencode($filename).';'); - elseif (false !== strpos($user_agent, 'safari') && false === strpos($user_agent, 'chrome')) - // Safari and Safari only can handle the filename as is - header('Content-Disposition: filename='.str_replace(',', '', $filename).';'); - else - // Use RFC5987 - header("Content-Disposition: filename*=UTF-8''".rawurlencode($filename).';' ); - - header('Content-Transfer-Encoding: binary'); + Http::download($this->getName(), $this->getType() ?: 'application/octet-stream'); header('Content-Length: '.$this->getSize()); - $this->sendData(true, 'attachment'); + $this->sendData(false); exit(); } @@ -354,7 +343,7 @@ class AttachmentFile { if (!$bk->upload($file['tmp_name'])) return false; } - elseif (!$bk->write($file['data'])) { + elseif (!$bk->write($file['data']) || !$bk->flush()) { // XXX: Fallthrough to default backend if different? return false; } @@ -384,27 +373,45 @@ class AttachmentFile { * True if the migration was successful and false otherwise. */ function migrate($bk) { - $before = hash_init('sha1'); - $after = hash_init('sha1'); // Copy the file to the new backend and hash the contents $target = FileStorageBackend::lookup($bk, $this); $source = $this->open(); + + // Initialize hashing algorithm to verify uploaded contents + $algos = $target->getNativeHashAlgos(); + $common_algo = 'sha1'; + if ($algos && is_array($algos)) { + $supported = hash_algos(); + foreach ($algos as $a) { + if (in_array(strtolower($a), $supported)) { + $common_algo = strtolower($a); + break; + } + } + } + $before = hash_init($common_algo); // TODO: Make this resumable so that if the file cannot be migrated // in the max_execution_time, the migration can be continued // the next time the cron runs - while ($block = $source->read()) { + while ($block = $source->read($target->getBlockSize())) { hash_update($before, $block); $target->write($block); } + $target->flush(); + + // Ask the backend to generate its own hash if at all possible + if (!($target_hash = $target->getHashDigest($common_algo))) { + $after = hash_init($common_algo); + // Verify that the hash of the target file matches the hash of + // the source file + $target = FileStorageBackend::lookup($bk, $this); + while ($block = $target->read()) + hash_update($after, $block); + $target_hash = hash_final($after); + } - // Verify that the hash of the target file matches the hash of the - // source file - $target = FileStorageBackend::lookup($bk, $this); - while ($block = $target->read()) - hash_update($after, $block); - - if (hash_final($before) != hash_final($after)) { + if (hash_final($before) != $target_hash) { $target->unlink(); return false; } @@ -554,6 +561,7 @@ class FileStorageBackend { var $meta; static $desc = false; static $registry; + static $blocksize = 131072; /** * All storage backends should call this function during the request @@ -597,6 +605,14 @@ class FileStorageBackend { return new $class($file); } + /** + * Returns the optimal block size for the backend. When migrating, this + * size blocks would be best for sending to the ::write() method + */ + function getBlockSize() { + return static::$blocksize; + } + /** * Create an instance of the storage backend linking the related file. * Information about the file metadata is accessible via the received @@ -618,6 +634,15 @@ class FileStorageBackend { return false; } + /** + * Called after all the blocks are sent to the ::write() method. This + * method should return boolean FALSE if flushing the data was + * somehow inhibited. + */ + function flush() { + return true; + } + /** * Upload a file to the backend. This method is preferred over ::write() * for files which are uploaded or are otherwise available out of @@ -665,6 +690,26 @@ class FileStorageBackend { function unlink() { return false; } + + /** + * Fetches a list of hash algorithms that are supported transparently + * through the ::write() and ::upload() methods. After writing or + * uploading file content, the ::getHashDigest($algo) method can be + * called to get a hash of the remote content without fetching the + * entire data stream to verify the content locally. + */ + function getNativeHashAlgos() { + return array(); + } + + /** + * Returns a hash of the content calculated remotely by the storage + * backend. If this method fails, the hash chould be calculated by + * downloading the content and hashing locally + */ + function getHashDigest($algo) { + return false; + } } @@ -676,10 +721,12 @@ class FileStorageBackend { define('CHUNK_SIZE', 500*1024); # Beware if you change this... class AttachmentChunkedData extends FileStorageBackend { static $desc = "In the database"; + static $blocksize = CHUNK_SIZE; function __construct($file) { $this->file = $file; - $this->_pos = 0; + $this->_chunk = 0; + $this->_buffer = false; } function length() { @@ -689,12 +736,19 @@ class AttachmentChunkedData extends FileStorageBackend { return $length; } - function read() { + function read($amount=CHUNK_SIZE, $offset=0) { # Read requested length of data from attachment chunks - list($buffer) = @db_fetch_row(db_query( - 'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id=' - .db_input($this->file->getId()).' AND chunk_id='.$this->_pos++)); - return $buffer; + while (strlen($this->_buffer) < $amount + $offset) { + list($buf) = @db_fetch_row(db_query( + 'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id=' + .db_input($this->file->getId()).' AND chunk_id='.$this->_chunk++)); + if (!$buf) + break; + $this->_buffer .= $buf; + } + $chunk = substr($this->_buffer, $offset, $amount); + $this->_buffer = substr($this->_buffer, $offset + $amount); + return $chunk; } function write($what, $chunk_size=CHUNK_SIZE) { @@ -704,12 +758,12 @@ class AttachmentChunkedData extends FileStorageBackend { if (!$block) break; if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE .' SET filedata=0x'.bin2hex($block).', file_id=' - .db_input($this->file->getId()).', chunk_id='.db_input($this->_pos++))) + .db_input($this->file->getId()).', chunk_id='.db_input($this->_chunk++))) return false; $offset += strlen($block); } - return $this->_pos; + return $this->_chunk; } function unlink() { diff --git a/include/class.http.php b/include/class.http.php index ef917845d76a1f943e022167304eb7a2b0d131c5..a96ab84ab2f124f190c178d554c8d3aaaaaf44ce 100644 --- a/include/class.http.php +++ b/include/class.http.php @@ -55,6 +55,9 @@ class Http { }else{ header("Location: $url"); } + print('<html></html>'); + flush(); + ob_flush(); exit; } @@ -73,20 +76,33 @@ class Http { } } + /** + * Creates the filename=... part of the Content-Disposition header. This + * is browser dependent, so the user agent is inspected to determine the + * best encoding format for the filename + */ + function getDispositionFilename($filename) { + $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']); + if (false !== strpos($user_agent,'msie') + && false !== strpos($user_agent,'win')) + return 'filename='.rawurlencode($filename); + elseif (false !== strpos($user_agent, 'safari') + && false === strpos($user_agent, 'chrome')) + // Safari and Safari only can handle the filename as is + return 'filename='.str_replace(',', '', $filename); + else + // Use RFC5987 + return "filename*=UTF-8''".rawurlencode($filename); + } + function download($filename, $type, $data=null) { - header('Pragma: public'); + header('Pragma: private'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Cache-Control: public'); + header('Cache-Control: private'); header('Content-Type: '.$type); - $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']); - if (strpos($user_agent,'msie') !== false - && strpos($user_agent,'win') !== false) { - header('Content-Disposition: filename="'.basename($filename).'";'); - } else { - header('Content-Disposition: attachment; filename="' - .basename($filename).'"'); - } + header('Content-Disposition: attachment; %s;', + self::getDispositionFilename(basename($filename))); header('Content-Transfer-Encoding: binary'); if ($data !== null) { header('Content-Length: '.strlen($data));