* @license BSD */ //namespace Domframework; /** 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); } // }}} }