diff --git a/core/src/core/src/pydio/Core/Http/Response/FileReaderResponse.php b/core/src/core/src/pydio/Core/Http/Response/FileReaderResponse.php new file mode 100644 index 0000000000..5916d2be42 --- /dev/null +++ b/core/src/core/src/pydio/Core/Http/Response/FileReaderResponse.php @@ -0,0 +1,370 @@ + + * This file is part of Pydio. + * + * Pydio is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pydio is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pydio. If not, see . + * + * The latest code can be found at . + */ +namespace Pydio\Core\Http\Response; + +use Pydio\Access\Core\AJXP_MetaStreamWrapper; +use Pydio\Access\Core\Model\AJXP_Node; +use Pydio\Access\Driver\StreamProvider\FS\fsAccessWrapper; +use Pydio\Core\Controller\HTMLWriter; +use Pydio\Core\Services\ConfService; +use Pydio\Core\Utils\TextEncoder; +use Pydio\Core\Utils\Utils; +use Pydio\Log\Core\AJXP_Logger; +use Zend\Diactoros\ServerRequestFactory; + +defined('AJXP_EXEC') or die('Access not allowed'); + + +class FileReaderResponse extends AsyncResponseStream +{ + /** @var AJXP_Node */ + private $node; + + /** @var string */ + private $file; + + /** @var string */ + private $data; + + /** @var string */ + private $localName = ""; + + /** @var string */ + private $headerType = "force-download"; + + /** @var integer */ + private $offset = -1; + + /** @var integer */ + private $length = -1; + + /** @var callable */ + private $preRead; + + /** @var callable */ + private $postRead; + + /** + * FileReaderResponse constructor. + * @param AJXP_Node|string $nodeOrFile + * @param string $data + */ + public function __construct($nodeOrFile = null, $data = null) + { + $callable = array($this, "readData"); + if($nodeOrFile instanceof AJXP_Node){ + $this->node = $nodeOrFile; + }else{ + $this->file = $nodeOrFile; + } + if($data !== null){ + $this->data = $data; + } + parent::__construct($callable); + } + + protected function logDebug($message, $params = []){ + AJXP_Logger::getInstance()->logDebug("FileReader", $message, $params); + } + + public function setHeaderType($headerType){ + $this->headerType = $headerType; + } + + public function setLocalName($localName){ + $this->localName = $localName; + } + + /** + * @param integer $offset + * @param integer $length + */ + public function setPartial($offset, $length){ + $this->offset = $offset; + $this->length = $length; + } + + public function setPreReadCallback(callable $pre){ + $this->preRead = $pre; + } + + public function setPostReadCallback(callable $post){ + $this->postRead = $post; + } + + protected function readData(){ + if($this->preRead){ + call_user_func($this->preRead); + } + + $this->readFile($this->node, $this->file, $this->data, $this->headerType, $this->localName, $this->offset, $this->length); + + if($this->postRead){ + call_user_func($this->postRead); + } + } + + /** + * @param AJXP_Node|null $node + * @param string $filePath + * @param string|bool $data + * @param string $headerType + * @param string $localName + * @param int $byteOffset + * @param int $byteLength + * @throws \Exception + */ + public function readFile($node = null, $filePath = null, $data = null, $headerType="plain", $localName="", $byteOffset=-1, $byteLength=-1) + { + if($node !== null){ + $filePathOrData = $node->getUrl(); + }else{ + $filePathOrData = $filePath; + } + + if(!$data === null && !file_exists($filePathOrData)){ + throw new \Exception("File $filePathOrData not found!"); + } + $confGzip = ConfService::getCoreConf("GZIP_COMPRESSION"); + $confGzipLimit = ConfService::getCoreConf("GZIP_LIMIT"); + $confUseXSendFile = ConfService::getCoreConf("USE_XSENDFILE"); + $confUseXAccelRedirect = ConfService::getCoreConf("USE_XACCELREDIRECT"); + $fakeReq = ServerRequestFactory::fromGlobals(); + $serverParams = $fakeReq->getServerParams(); + + if ($node !== null && !$node->wrapperIsRemote()) { + $originalFilePath = $filePathOrData; + $filePathOrData = fsAccessWrapper::patchPathForBaseDir($filePathOrData); + } + session_write_close(); + + restore_error_handler(); + restore_exception_handler(); + + set_exception_handler('Pydio\Access\Driver\StreamProvider\FS\download_exception_handler'); + set_error_handler('Pydio\Access\Driver\StreamProvider\FS\download_exception_handler'); + // required for IE, otherwise Content-disposition is ignored + if (ini_get('zlib.output_compression')) { + Utils::safeIniSet('zlib.output_compression', 'Off'); + } + + $isFile = ($data !== null) && !$confGzip; + if ($byteLength == -1) { + if ($data !== null) { + $size = strlen($data); + } else if ($node === null) { + $size = sprintf("%u", filesize($filePathOrData)); + } else { + $size = filesize($filePathOrData); + } + } else { + $size = $byteLength; + } + if ($confGzip && ($size > $confGzipLimit || !function_exists("gzencode") || @strpos($serverParams['HTTP_ACCEPT_ENCODING'], 'gzip') === FALSE)) { + $confGzip = false; + } + + $localName = ($localName =="" ? basename((isSet($originalFilePath)?$originalFilePath:$filePathOrData)) : $localName); + + if ($headerType == "plain") { + + header("Content-type:text/plain"); + + } else if ($headerType == "image") { + + header("Content-Type: ".Utils::getImageMimeType(basename($filePathOrData))."; name=\"".$localName."\""); + header("Content-Length: ".$size); + header('Cache-Control: public'); + + } else { + + if ($isFile) { + header("Accept-Ranges: 0-$size"); + $this->logDebug("Sending accept range 0-$size"); + } + + // Check if we have a range header (we are resuming a transfer) + if ( isset($serverParams['HTTP_RANGE']) && $isFile && $size != 0 ) { + + if ($headerType == "stream_content") { + + if (extension_loaded('fileinfo') && (( $node !== null && !$node->wrapperIsRemote()) || $filePath !== null)) { + $fInfo = new \fInfo( FILEINFO_MIME ); + if($node !== null){ + $realfile = $node->getRealFile(); + }else{ + $realfile = $filePathOrData; + } + $mimeType = $fInfo->file($realfile); + $splitChar = explode(";", $mimeType); + $mimeType = trim($splitChar[0]); + $this->logDebug("Detected mime $mimeType for $realfile"); + } else { + $mimeType = Utils::getStreamingMimeType(basename($filePathOrData)); + } + header('Content-type: '.$mimeType); + } + // multiple ranges, which can become pretty complex, so ignore it for now + $ranges = explode('=', $_SERVER['HTTP_RANGE']); + $offsets = explode('-', $ranges[1]); + $offset = floatval($offsets[0]); + + $length = floatval($offsets[1]) - $offset; + if (!$length) $length = $size - $offset; + if ($length + $offset > $size || $length < 0) $length = $size - $offset; + $this->logDebug('Content-Range: bytes ' . $offset . '-' . $length . '/' . $size); + header('HTTP/1.1 206 Partial Content'); + header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $size); + + header("Content-Length: ". $length); + $file = fopen($filePathOrData, 'rb'); + if(!is_resource($file)){ + throw new \Exception("Failed opening file ".$filePathOrData); + } + fseek($file, 0); + $relOffset = $offset; + while ($relOffset > 2.0E9) { + // seek to the requested offset, this is 0 if it's not a partial content request + fseek($file, 2000000000, SEEK_CUR); + $relOffset -= 2000000000; + // This works because we never overcome the PHP 32 bit limit + } + fseek($file, $relOffset, SEEK_CUR); + + while(ob_get_level()) ob_end_flush(); + $readSize = 0.0; + $bufferSize = 1024 * 8; + while (!feof($file) && $readSize < $length && connection_status() == 0) { + $this->logDebug("dl reading $readSize to $length", ["httpRange" => $serverParams["HTTP_RANGE"]]); + echo fread($file, $bufferSize); + $readSize += $bufferSize; + flush(); + } + + fclose($file); + return; + + } else { + + if ($confGzip) { + + $gzippedData = ($data?gzencode($filePathOrData,9):gzencode(file_get_contents($filePathOrData), 9)); + $size = strlen($gzippedData); + + } + + HTMLWriter::generateAttachmentsHeader($localName, $size, $isFile, $confGzip); + + if ($confGzip && isSet($gzippedData)) { + print $gzippedData; + return; + } + } + } + + if ($data !== null) { + + print($data); + + } else { + + if(($node !== null && !$node->wrapperIsRemote()) || $filePath !== null){ + + if ($confUseXSendFile) { + if($node != null) { + $filePathOrData = $node->getRealFile(); + } + $filePathOrData = str_replace("\\", "/", $filePathOrData); + $server_name = $serverParams["SERVER_SOFTWARE"]; + $regex = '/^(lighttpd\/1.4).([0-9]{2}$|[0-9]{3}$|[0-9]{4}$)+/'; + if(preg_match($regex, $server_name)) + $header_sendfile = "X-LIGHTTPD-send-file"; + else + $header_sendfile = "X-Sendfile"; + + + header($header_sendfile.": ".TextEncoder::toUTF8($filePathOrData)); + header("Content-type: application/octet-stream"); + header('Content-Disposition: attachment; filename="' . basename($filePathOrData) . '"'); + return; + } + if ($confUseXAccelRedirect && array_key_exists("X-Accel-Mapping", $serverParams)) { + if($node !== null) { + $filePathOrData = $node->getRealFile(); + } + $filePathOrData = str_replace("\\", "/", $filePathOrData); + $filePathOrData = TextEncoder::toUTF8($filePathOrData); + $mapping = explode('=',$serverParams['X-Accel-Mapping']); + $replacecount = 0; + $accelfile = str_replace($mapping[0],$mapping[1],$filePathOrData,$replacecount); + if ($replacecount == 1) { + header("X-Accel-Redirect: $accelfile"); + header("Content-type: application/octet-stream"); + header('Content-Disposition: attachment; filename="' . basename($accelfile) . '"'); + } else { + $this->logDebug("X-Accel-Redirect: Problem with X-Accel-Mapping for file $filePathOrData"); + } + return; + } + } + + $stream = fopen("php://output", "a"); + + if ($node == null) { + + $this->logDebug("realFS!", array("file"=>$filePathOrData)); + $fp = fopen($filePathOrData, "rb"); + if(!is_resource($fp)){ + throw new \Exception("Failed opening file ".$filePathOrData); + } + if ($byteOffset != -1) { + fseek($fp, $byteOffset); + } + $sentSize = 0; + $readChunk = 4096; + while (!feof($fp)) { + if ( $byteLength != -1 && ($sentSize + $readChunk) >= $byteLength) { + // compute last chunk and break after + $readChunk = $byteLength - $sentSize; + $break = true; + } + $data = fread($fp, $readChunk); + $dataSize = strlen($data); + fwrite($stream, $data, $dataSize); + $sentSize += $dataSize; + if (isSet($break)) { + break; + } + } + fclose($fp); + } else { + + AJXP_MetaStreamWrapper::copyFileInStream($filePathOrData, $stream); + + } + fflush($stream); + fclose($stream); + + } + } + + +} \ No newline at end of file