setFile($file); $input->setMd5sum($md5sum); $input->setSha256($sha256sum); return $input; } /** * Create an input object from a stream resource / file pointer. * * Please note that the contentLength cannot be calculated automatically unless you have a seekable stream resource. * * @param resource $resource The file pointer or stream resource * @param int $contentLength The length of the content in bytes. Set to -1 for auto calculation. * @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate. * @param null|string $sha256sum The SHA256 sum. null to auto calculate. * * @return Input */ public static function createFromResource(&$resource, int $contentLength, ?string $md5sum = null, ?string $sha256sum = null): self { $input = new Input(); $input->setFp($resource); $input->setSize($contentLength); $input->setMd5sum($md5sum); $input->setSha256($sha256sum); return $input; } /** * Create an input object from raw data. * * Please bear in mind that the data is being duplicated in memory. Therefore you'll need at least 2xstrlen($data) * of free memory when you are using this method. You can instantiate an object and use assignData to work around * this limitation when handling large amounts of data which may cause memory outages (typically: over 10Mb). * * @param string $data The data to use. * @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate. * @param null|string $sha256sum The SHA256 sum. null to auto calculate. * * @return Input */ public static function createFromData(string &$data, ?string $md5sum = null, ?string $sha256sum = null): self { $input = new Input(); $input->setData($data); $input->setMd5sum($md5sum); $input->setSha256($sha256sum); return $input; } /** * Destructor. */ function __destruct() { if (is_resource($this->fp)) { @fclose($this->fp); } } /** * Returns the input type (resource, file or data) * * @return int */ public function getInputType(): int { if (!empty($this->file)) { return self::INPUT_FILE; } if (!empty($this->fp)) { return self::INPUT_RESOURCE; } return self::INPUT_DATA; } /** * Return the file pointer to the data, or null if this is not a resource input * * @return resource|null */ public function getFp() { if (!is_resource($this->fp)) { return null; } return $this->fp; } /** * Set the file pointer (or, generally, stream resource) * * @param resource $fp */ public function setFp($fp): void { if (!is_resource($fp)) { throw new Exception\InvalidFilePointer('$fp is not a file resource'); } $this->fp = $fp; } /** * Get the absolute path to the input file, or null if this is not a file input * * @return string|null */ public function getFile(): ?string { if (empty($this->file)) { return null; } return $this->file; } /** * Set the absolute path to the input file * * @param string $file */ public function setFile(string $file): void { $this->file = $file; $this->data = null; if (is_resource($this->fp)) { @fclose($this->fp); } $this->fp = @fopen($file, 'rb'); if ($this->fp === false) { throw new Exception\CannotOpenFileForRead($file); } } /** * Return the raw input data, or null if this is a file or stream input * * @return string|null */ public function getData(): ?string { if (empty($this->data) && ($this->getInputType() != self::INPUT_DATA)) { return null; } return $this->data; } /** * Set the raw input data * * @param string $data */ public function setData(string $data): void { $this->data = $data; if (is_resource($this->fp)) { @fclose($this->fp); } $this->file = null; $this->fp = null; } /** * Return a reference to the raw input data * * @return string|null */ public function &getDataReference(): ?string { if (empty($this->data) && ($this->getInputType() != self::INPUT_DATA)) { $this->data = null; } return $this->data; } /** * Set the raw input data by doing an assignment instead of memory copy. While this conserves memory you cannot use * this with hardcoded strings, method results etc without going through a variable first. * * @param string $data */ public function assignData(string &$data): void { $this->data = $data; if (is_resource($this->fp)) { @fclose($this->fp); } $this->file = null; $this->fp = null; } /** * Returns the size of the data to be uploaded, in bytes. If it's not already specified it will try to guess. * * @return int */ public function getSize(): int { if ($this->size < 0) { $this->size = $this->getInputSize(); } return $this->size; } /** * Set the size of the data to be uploaded. * * @param int $size */ public function setSize(int $size) { $this->size = $size; } /** * Get the MIME type of the data * * @return string|null */ public function getType(): ?string { if (empty($this->type)) { $this->type = 'application/octet-stream'; if ($this->getInputType() == self::INPUT_FILE) { $this->type = $this->getMimeType($this->file); } } return $this->type; } /** * Set the MIME type of the data * * @param string|null $type */ public function setType(?string $type) { $this->type = $type; } /** * Get the MD5 sum of the content * * @return null|string */ public function getMd5sum(): ?string { if ($this->md5sum === '') { return null; } if (is_null($this->md5sum)) { $this->md5sum = $this->calculateMd5(); } return $this->md5sum; } /** * Set the MD5 sum of the content as a base64 encoded string of the raw MD5 binary value. * * WARNING: Do not set a binary MD5 sum or a hex-encoded MD5 sum, it will result in an invalid signature error! * * Set to null to automatically calculate it from the raw data. Set to an empty string to force it to never be * calculated and no value for it set either. * * @param string|null $md5sum */ public function setMd5sum(?string $md5sum): void { $this->md5sum = $md5sum; } /** * Get the SHA-256 hash of the content * * @return string */ public function getSha256(): string { if (empty($this->sha256)) { $this->sha256 = $this->calculateSha256(); } return $this->sha256; } /** * Set the SHA-256 sum of the content. It must be a lowercase hexadecimal encoded string. * * Set to null to automatically calculate it from the raw data. * * @param string|null $sha256 */ public function setSha256(?string $sha256): void { $this->sha256 = strtolower($sha256); } /** * Get the Upload Session ID for multipart uploads * * @return string|null */ public function getUploadID(): ?string { return $this->UploadID; } /** * Set the Upload Session ID for multipart uploads * * @param string|null $UploadID */ public function setUploadID(?string $UploadID): void { $this->UploadID = $UploadID; } /** * Get the part number for multipart uploads. * * Returns null if the part number has not been set yet. * * @return int|null */ public function getPartNumber(): ?int { return $this->PartNumber; } /** * Set the part number for multipart uploads * * @param int $PartNumber */ public function setPartNumber(int $PartNumber): void { // Clamp the part number to integers greater than zero. $this->PartNumber = max(1, (int) $PartNumber); } /** * Get the list of ETags for multipart uploads * * @return string[] */ public function getEtags(): array { return $this->etags; } /** * Set the list of ETags for multipart uploads * * @param string[] $etags */ public function setEtags(array $etags): void { $this->etags = $etags; } /** * Calculates the upload size from the input source. For data it's the entire raw string length. For a file resource * it's the entire file's length. For seekable stream resources it's the remaining data from the current seek * position to EOF. * * WARNING: You should never try to specify files or resources over 2Gb minus 1 byte otherwise 32-bit versions of * PHP (anything except Linux x64 builds) will fail in unpredictable ways: the internal int representation in PHP * depends on the target platform and is typically a signed 32-bit integer. * * @return int */ private function getInputSize(): int { switch ($this->getInputType()) { case self::INPUT_DATA: return function_exists('mb_strlen') ? mb_strlen($this->data, '8bit') : strlen($this->data); break; case self::INPUT_FILE: clearstatcache(true, $this->file); $filesize = @filesize($this->file); return ($filesize === false) ? 0 : $filesize; break; case self::INPUT_RESOURCE: $meta = stream_get_meta_data($this->fp); if ($meta['seekable']) { $pos = ftell($this->fp); $endPos = fseek($this->fp, 0, SEEK_END); fseek($this->fp, $pos, SEEK_SET); return $endPos - $pos + 1; } break; } return 0; } /** * Get the MIME type of a file * * @param string $file The absolute path to the file for which we want to get the MIME type * * @return string The MIME type of the file */ private function getMimeType(string $file): string { $type = false; // Fileinfo documentation says fileinfo_open() will use the // MAGIC env var for the magic file if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false ) { if (($type = finfo_file($finfo, $file)) !== false) { // Remove the charset and grab the last content-type $type = explode(' ', str_replace('; charset=', ';charset=', $type)); $type = array_pop($type); $type = explode(';', $type); $type = trim(array_shift($type)); } finfo_close($finfo); } elseif (function_exists('mime_content_type')) { $type = trim(mime_content_type($file)); } if ($type !== false && strlen($type) > 0) { return $type; } // Otherwise do it the old fashioned way static $exts = [ 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', 'zip' => 'application/zip', 'gz' => 'application/x-gzip', 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', 'css' => 'text/css', 'js' => 'text/javascript', 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php', ]; $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; } /** * Calculate the MD5 sum of the input data * * @return string Base-64 encoded MD5 sum */ private function calculateMd5(): string { switch ($this->getInputType()) { case self::INPUT_DATA: return base64_encode(md5($this->data, true)); break; case self::INPUT_FILE: return base64_encode(md5_file($this->file, true)); break; case self::INPUT_RESOURCE: $ctx = hash_init('md5'); $pos = ftell($this->fp); $size = $this->getSize(); $done = 0; $batch = min(1048576, $size); while ($done < $size) { $toRead = min($batch, $done - $size); $data = @fread($this->fp, $toRead); hash_update($ctx, $data); unset($data); } fseek($this->fp, $pos, SEEK_SET); return base64_encode(hash_final($ctx, true)); break; } return ''; } /** * Calcualte the SHA256 data of the input data * * @return string Lowercase hex representation of the SHA-256 sum */ private function calculateSha256(): string { $inputType = $this->getInputType(); switch ($inputType) { case self::INPUT_DATA: return hash('sha256', $this->data, false); break; case self::INPUT_FILE: case self::INPUT_RESOURCE: if ($inputType == self::INPUT_FILE) { $filesize = @filesize($this->file); $fPos = @ftell($this->fp); if (($filesize == $this->getSize()) && ($fPos === 0)) { return hash_file('sha256', $this->file, false); } } $ctx = hash_init('sha256'); $pos = ftell($this->fp); $size = $this->getSize(); $done = 0; $batch = min(1048576, $size); while ($done < $size) { $toRead = min($batch, $size - $done); $data = @fread($this->fp, $toRead); $done += $toRead; hash_update($ctx, $data); unset($data); } fseek($this->fp, $pos, SEEK_SET); return hash_final($ctx, false); break; } return ''; } }