1<?php 2 3namespace ILIAS\FileDelivery\FileDeliveryTypes; 4 5use ILIAS\FileDelivery\ilFileDeliveryType; 6use ILIAS\HTTP\GlobalHttpState; 7use ILIAS\HTTP\Response\ResponseHeader; 8 9require_once('./Services/FileDelivery/interfaces/int.ilFileDeliveryType.php'); 10 11/** 12 * Class PHPChunked 13 * 14 * @author Fabian Schmid <fs@studer-raimann.ch> 15 * @since 5.3 16 * @version 1.0 17 */ 18final class PHPChunked implements ilFileDeliveryType 19{ 20 21 /** 22 * @var GlobalHttpState $httpService 23 */ 24 private $httpService; 25 26 27 /** 28 * PHP constructor. 29 * 30 * @param GlobalHttpState $httpState 31 */ 32 public function __construct(GlobalHttpState $httpState) 33 { 34 $this->httpService = $httpState; 35 } 36 37 38 /** 39 * @inheritDoc 40 */ 41 public function doesFileExists($path_to_file) 42 { 43 return is_readable($path_to_file); 44 } 45 46 47 /** 48 * @inheritdoc 49 */ 50 public function prepare($path_to_file) 51 { 52 return true; 53 } 54 55 56 /** 57 * @inheritdoc 58 */ 59 public function deliver($path_to_file, $file_marked_to_delete) 60 { 61 $file = $path_to_file; 62 $fp = @fopen($file, 'rb'); 63 64 $size = filesize($file); // File size 65 $length = $size; // Content length 66 $start = 0; // Start byte 67 $end = $size - 1; // End byte 68 // Now that we've gotten so far without errors we send the accept range header 69 /* At the moment we only support single ranges. 70 * Multiple ranges requires some more work to ensure it works correctly 71 * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 72 * 73 * Multirange support annouces itself with: 74 * header('Accept-Ranges: bytes'); 75 * 76 * Multirange content must be sent with multipart/byteranges mediatype, 77 * (mediatype = mimetype) 78 * as well as a boundry header to indicate the various chunks of data. 79 */ 80 $response = $this->httpService->response()->withHeader("Accept-Ranges", "0-$length"); 81 $this->httpService->saveResponse($response); 82 $server = $this->httpService->request()->getServerParams(); 83 // header('Accept-Ranges: bytes'); 84 // multipart/byteranges 85 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 86 if (isset($server['HTTP_RANGE'])) { 87 $c_start = $start; 88 $c_end = $end; 89 90 // Extract the range string 91 list(, $range) = explode('=', $server['HTTP_RANGE'], 2); 92 // Make sure the client hasn't sent us a multibyte range 93 if (strpos($range, ',') !== false) { 94 // (?) Shoud this be issued here, or should the first 95 // range be used? Or should the header be ignored and 96 // we output the whole content? 97 $response = $this->httpService->response()->withStatus(416)->withHeader(ResponseHeader::CONTENT_RANGE, "bytes $start-$end/$size"); 98 $this->httpService->saveResponse($response); 99 100 //header("Content-Range: bytes $start-$end/$size"); 101 // (?) Echo some info to the client? 102 $this->close(); 103 } // fim do if 104 // If the range starts with an '-' we start from the beginning 105 // If not, we forward the file pointer 106 // And make sure to get the end byte if spesified 107 if ($range[0] == '-') { 108 // The n-number of the last bytes is requested 109 $c_start = $size - substr($range, 1); 110 } else { 111 $range = explode('-', $range); 112 $c_start = $range[0]; 113 $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; 114 } // fim do if 115 /* Check the range and make sure it's treated according to the specs. 116 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html 117 */ 118 // End bytes can not be larger than $end. 119 $c_end = ($c_end > $end) ? $end : $c_end; 120 // Validate the requested range and return an error if it's not correct. 121 if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) { 122 $response = $this->httpService->response()->withStatus(416)->withHeader(ResponseHeader::CONTENT_RANGE, "bytes $start-$end/$size"); 123 124 $this->httpService->saveResponse($response); 125 // (?) Echo some info to the client? 126 $this->close(); 127 } // fim do if 128 129 $start = $c_start; 130 $end = $c_end; 131 $length = $end - $start + 1; // Calculate new content length 132 fseek($fp, $start); 133 134 $response = $this->httpService->response()->withStatus(206); 135 136 $this->httpService->saveResponse($response); 137 } // fim do if 138 139 // Notify the client the byte range we'll be outputting 140 $response = $this->httpService->response()->withHeader(ResponseHeader::CONTENT_RANGE, "bytes $start-$end/$size")->withHeader(ResponseHeader::CONTENT_LENGTH, $length); 141 142 $this->httpService->saveResponse($response); 143 144 //render response and start buffered download 145 $this->httpService->sendResponse(); 146 147 // Start buffered download 148 $buffer = 1024 * 8; 149 while (!feof($fp) && ($p = ftell($fp)) <= $end) { 150 if ($p + $buffer > $end) { 151 // In case we're only outputtin a chunk, make sure we don't 152 // read past the length 153 $buffer = $end - $p + 1; 154 } // fim do if 155 156 set_time_limit(0); // Reset time limit for big files 157 echo fread($fp, $buffer); 158 flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. 159 } // fim do while 160 161 fclose($fp); 162 163 return true; 164 } 165 166 167 /** 168 * @inheritdoc 169 */ 170 public function supportsInlineDelivery() 171 { 172 return true; 173 } 174 175 176 /** 177 * @inheritdoc 178 */ 179 public function supportsAttachmentDelivery() 180 { 181 return true; 182 } 183 184 185 /** 186 * @inheritdoc 187 */ 188 public function supportsStreaming() 189 { 190 return true; 191 } 192 193 194 private function close() 195 { 196 //render response 197 $this->httpService->sendResponse(); 198 exit; 199 } 200 201 202 /** 203 * @inheritdoc 204 */ 205 public function handleFileDeletion($path_to_file) 206 { 207 return unlink($path_to_file); 208 } 209} 210