Files
DomFramework/outputdl.php

232 lines
6.9 KiB
PHP

<?php
/** DomFramework
* @package domframework
* @author Dominique Fournier <dominique@fournier38.fr>
*/
/** This class allow a program to download a specific file from the filesystem,
* without using too much memory. In also allow to manage the resuming of a
* paused transfert by getting the range of download.
*/
class outputdl
{
/** Allow or deny the resuming of the HTTP transferts
*/
private $resumeAllow = true;
/** The base dir used as root
*/
private $base = "/";
/** Store the headers to allow the unit tests to get them
*/
private $headers = array ();
/** Headers sent
*/
private $headersSent = false;
/** Get/Set Allow/Deny the resuming of transferts
* @param boolean|null $resumeAllow True : resume OK, false : resume forbid
* @return $this|boolean
*/
public function resumeAllow ($resumeAllow = null)
// {{{
{
if ($resumeAllow === null)
return $this->resumeAllow;
$this->resumeAllow = !! $resumeAllow;
return $this;
}
// }}}
/** Get/Set Base of filesystem
* The base directory is use to secure the download. A user can not request
* a file outside the base. Example : if the base is /var/lib/files, the
* user can not request /etc/passwd file (out of scope). If not defined,
* all the filesystem is allowed
* @param string|null $base The filesystem base
* @return $this|string
*/
public function base ($base = null)
// {{{
{
if ($base === null)
return $this->base;
$this->base = $base;
return $this;
}
// }}}
/** Get headers from headers list
* The headers can be tested too
* @return array
*/
public function headers ()
// {{{
{
return $this->headers;
}
// }}}
/** Add a new header and send it if possible
* @param string $header The header to send
* @return $this
*/
private function header ($header)
// {{{
{
if (! headers_sent())
header ($header);
$this->headers[] = $header;
return $this;
}
// }}}
/** Download a file with management of Partial Download (like resume)
* Manage the HTTP headers to allow to resume the download if it is allowed
* Do not go throw the renderer, exit at end of transfert
* @param string $path The path to download
* @param string|null $filename The filename to send to the browser
*/
public function downloadFile ($path, $filename = null)
// {{{
{
if (!file_exists ($path))
throw new \Exception (dgettext ("domframework",
"Invalid file to download : file doesn't exists"), 404);
if (is_link ($path) || is_dir ($path))
throw new \Exception (dgettext ("domframework",
"Invalid file to download : not a file"), 406);
$path = realpath ($path);
if (substr ($path, 0, strlen ($this->base)) !== $this->base)
throw new \Exception (dgettext ("domframework",
"Invalid file to download : out of base"), 406);
if (! is_file ($path))
throw new \Exception (dgettext ("domframework",
"Invalid file to download : not a file"), 406);
if (! is_readable ($path))
throw new \Exception (dgettext ("domframework",
"Invalid file to download : file not readable"), 406);
ini_set ('display_errors', 0);
ini_set ('display_startup_errors', 0);
if (! defined ("PHPUNIT") && ob_get_level ())
ob_end_clean ();
if ($filename === null)
$filename = basename ($path);
$this->header ('Content-Description: File Transfer');
$this->header ('Content-Type: application/octet-stream');
$this->header ('Content-Disposition: attachment; filename="'.$filename.'"');
$this->header ('Content-Transfer-Encoding: binary');
//header ('Expires: 0');
//header ('Cache-Control: must-revalidate, post-check=0, pre-check=0');
//header ('Pragma: public');
$filesize = filesize ($path);
if (! $this->resumeAllow)
{
$this->header ('Accept-Ranges: none');
}
else
{
$this->header ('Accept-Ranges: bytes');
if (isset ($_SERVER["HTTP_RANGE"]) &&
strtolower (substr ($_SERVER["HTTP_RANGE"], 0, 6)) === "bytes=")
{
$boundary = "Qm91bmRhcnk=";
$rangeTxt = trim (substr ($_SERVER["HTTP_RANGE"], 6));
$ranges = explode (",", $rangeTxt);
if (count ($ranges) > 1)
$this->header (
"Content-Type: multipart/byteranges; boundary=$boundary");
foreach ($ranges as $nb => $range)
{
if (trim ($range) === "-")
throw new \Exception ("Invalid range provided", 416);
@list ($start, $stop) = explode ("-", trim ($range));
if ($stop === null || $stop === "")
$stop = $filesize;
if ($stop && ($start === null || $start === ""))
{
$start = $filesize - $stop;
$stop = $filesize;
}
$start = intval ($start);
$stop = intval ($stop);
if ($start > $stop)
throw new \Exception ("Invalid range provided", 416);
if ($start < 0 || $stop > $filesize || $start > $filesize)
throw new \Exception ("Invalid range provided", 416);
if (count ($ranges) > 1)
{
if ($nb > 0)
echo "\r\n";
echo "--".$boundary."\r\n";
echo "Content-Range: bytes $start-$stop/$filesize\r\n";
echo "Content-Type: application/octet-stream\r\n";
echo "\r\n";
}
else
{
$this->header ("Content-Range: bytes $start-$stop/$filesize");
}
http_response_code (206);
$this->downloadFileRange ($path, $start, $stop);
}
if (count ($ranges) > 1)
{
if ($nb > 0)
echo "\r\n";
echo "--".$boundary."--\r\n";
}
if (! defined ("PHPUNIT"))
exit;
return;
}
}
// No range or error : send all the file
$this->header ('Content-Length: '. $filesize);
$this->downloadFileRange ($path, 0, $filesize);
if (! defined ("PHPUNIT"))
exit;
return;
}
// }}}
/** Download the file. Do not go through the renderer
* @param string $path The path to download
* @param integer $start The start range
* @param integer $stop The stop range
*/
private function downloadFileRange ($path, $start, $stop)
// {{{
{
$file = realpath ($path);
$chunksize = 10*1024*1024; // how many bytes per chunk
$start = intval ($start);
$stop = intval ($stop);
$handle = fopen ($file, 'rb');
if ($handle === false)
{
die ("error");
}
if (fseek ($handle, $start) === false)
die ("Can not seek");
$size = $start;
while (! feof ($handle))
{
$block = fread ($handle, $chunksize);
if ($size + $chunksize > $stop)
$block = substr ($block, 0, $stop - $size + 1);
$size += strlen ($block);
echo $block;
flush ();
if ($size >= $stop)
break;
}
fclose($handle);
}
// }}}
}