/** * Outputs a file that can be paused and resumed by the client (by following the partial content spec) * * Additionally, this function will also set up correct file headers (Content-Type, Content-Disposition, etc) * or can return them as an array if $returnheader is set to TRUE. * In this case no fileoutput will be generated. You can use Loops\Misc::readpartialfile to actually send the file. * * The Content-Disposition header will always be set to 'attachment'. 'servefiles' purpose is to provide * a resumeable download. If you want to send inline content, use PHPs readfile and header functions instead. * * $filename or $mimetype can be also set to a boolean. If boolean TRUE, values will be infered from the $file. * If set to FALSE, values will NOT be included in the headers. * i.e. no filename="..." in Content-Disposition and/or no Content-Type header. * * If the file and headers have been sent, -1 is returned, thus not genarating additional output. * In case of an error, a http status code is generated (4xx, 5xx series) and returned. * You can conveniently use this funcion in the action method of loops elements: * <code> * use Loops\Misc; * * public function action($parameter) { * return Misc::servefile("example.png"); * } * </code> * * This function will raise an exeption if there is already data in an outbut buffer or headers were already sent. * * @todo Do not rely on $_SERVER variable for the partial spec * * @param string $file The physical location of the file (must be understood by fopen) * @param bool|string $filename An alternate filename that is sent in Content-Disposition (must be an UTF-8 string or bool) * @param bool|string $mimetype The mimetype that is used for the Content-Type header. * @param bool $returnheader If set to TRUE, headers will be returned as an array (no output is generated) * @param resource $context The context passed to fopen. * @return int|array An http status code or an array of headers */ public static function servefile($file, $filename = TRUE, $mimetype = TRUE, $returnheader = FALSE, $force = FALSE, $context = NULL) { if (headers_sent()) { throw new Exception("Can't serve file because output headers have already been sent."); } if (!$force) { if (!@file_exists($file)) { return 404; } if (!@is_readable($file)) { return 403; } } //detect mimetype - note that spl fileobject does not support this over data urls for some reason if ($mimetype === TRUE) { $mimetype = "application/octet-stream"; if ($finfo = finfo_open(FILEINFO_MIME_TYPE)) { if ($type = finfo_file($finfo, $file)) { $mimetype = $type; } finfo_close($finfo); } } //open file if (!$file instanceof SplFileObject) { $file = new SplFileObject($file); } //get size $filesize = $file->getSize(); //get requested range from header $range = FALSE; if (isset($_SERVER["HTTP_RANGE"])) { $range = $_SERVER["HTTP_RANGE"]; } else { if (isset($_SERVER["HTTP_CONTENT_RANGE"])) { $range = $_SERVER["HTTP_CONTENT_RANGE"]; } else { if (function_exists('apache_request_headers')) { $headers = apache_request_headers(); foreach ($headers as $key => $value) { if (strtolower($key) == "range") { $range = $value; break; } } } } } //partial information $partial = FALSE; if ($range) { if (strpos($range, '=') === FALSE) { return 400; } list($param, $range) = explode('=', $range, 2); if (strtolower(trim($param)) != 'bytes') { // Bad request - range unit is not 'bytes' return 400; } $range = explode(',', $range); $range = explode('-', $range[0]); // We only deal with the first requested range if (count($range) != 2) { // Bad request - 'bytes' parameter is not valid return 400; } if ($range[0] === '') { // First number missing, return last $range[1] bytes $offset = $filesize - intval($range[1]); if ($offset && $offset < $filesize) { $partial = [$offset, $filesize - 1]; } else { return 416; } } else { if ($range[1] === '') { // Second number missing, return from byte $range[0] to end $offset = intval($range[0]); if ($offset < $filesize) { $partial = [$offset, $filesize - 1]; } else { return 416; } } else { // Both numbers present, return specific range $offset1 = intval($range[0]); $offset2 = intval($range[1]); if ($offset1 < $filesize && $offset2 < $filesize) { $partial = [$offset1, $offset2]; } else { return 416; } } } } if ($filename) { if ($filename === TRUE) { $filename = basename($file); } // get user-agent from header $useragent = FALSE; if (isset($_SERVER["HTTP_USER_AGENT"])) { $useragent = $_SERVER["HTTP_USER_AGENT"]; } else { if (function_exists('apache_request_headers')) { $headers = apache_request_headers(); foreach ($headers as $key => $value) { if (strtolower($key) == "user-agent") { $useragent = $value; break; } } } } //we need to get a cross browser compatible filename for the header. oh boy.... //lets trust the following stack overflow question: // http://stackoverflow.com/questions/93551/how-to-encode-the-filename-parameter-of-content-disposition-header-in-http if ($useragent && preg_match("/IE [5678]\\./", $useragent)) { $filename = " filename=" . urlencode($filename); } else { if ($useragent && preg_match("/Safari/", $useragent)) { $filename = " filename={$filename}"; } else { if ($useragent && preg_match("/[^a-zA-Z0-9_\\.]/", $filename)) { $filename = " filename=\"{$filename}\"; filename*=UTF-8''" . urlencode($filename); } else { $filename = " filename=\"{$filename}\";"; } } } } //create output headers $length = $partial ? $partial[1] - $partial[0] + 1 : $filesize; $headers = array(); $headers[] = "Content-Type: {$mimetype}"; $headers[] = "Content-Disposition: attachment;{$filename}"; $headers[] = "Content-Length: {$length}"; $headers[] = "Accept-Ranges: bytes"; if ($partial) { $headers[] = "Content-Range: bytes {$partial['0']}-{$partial['1']}/{$filesize}"; } if ($returnheader) { return $headers; } //actual output happens from now on //set status code and header if (!$context) { $context = Loops::getCurrentLoops(); } if ($partial) { $context->response->setStatusCode(206); } foreach ($headers as $line) { $context->response->addHeader($line); } $context->response->setHeader(); if ($filesize) { if ($partial) { $bytes = Misc::readPartialFile($file, $partial[0], $partial[1], $context); } else { $bytes = $file->fpassthru(); } //if no output was generated, the above functions failed and we have to/can recover if (!$bytes) { //remove previously set headers foreach ($headers as $line) { $context->response->removeHeader($line); } return 500; } } //do not generate output return -1; }