Example #1
0
 /**
  * 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;
 }