5 * @package akeebaengine
6 * @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
7 * @license GNU General Public License version 3, or later
10 namespace Akeeba\Engine\Postproc\Connector\S3v4;
12 // Protection against direct access
13 defined('AKEEBAENGINE') or die();
16 * Defines an input source for PUT/POST requests to Amazon S3
21 * Input type: resource
23 const INPUT_RESOURCE = 1;
31 * Input type: raw data
36 * File pointer, in case we have a resource
43 * Absolute filename to the file
50 * Data to upload, as a string
57 * Length of the data to upload
64 * Content type (MIME type)
71 * MD5 sum of the data to upload, as base64 encoded string. If it's false no MD5 sum will be returned.
75 private $md5sum = null;
78 * SHA-256 sum of the data to upload, as lowercase hex string.
82 private $sha256 = null;
85 * The Upload Session ID used for multipart uploads
89 private $UploadID = null;
92 * The part number used in multipart uploads
96 private $PartNumber = null;
99 * The list of ETags used when finalising a multipart upload
106 * Create an input object from a file (also: any valid URL wrapper)
108 * @param string $file Absolute file path or any valid URL fopen() wrapper
109 * @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate.
110 * @param null|string $sha256sum The SHA256 sum. null to auto calculate.
114 public static function createFromFile(string $file, ?string $md5sum = null, ?string $sha256sum = null): self
116 $input = new Input();
118 $input->setFile($file);
119 $input->setMd5sum($md5sum);
120 $input->setSha256($sha256sum);
126 * Create an input object from a stream resource / file pointer.
128 * Please note that the contentLength cannot be calculated automatically unless you have a seekable stream resource.
130 * @param resource $resource The file pointer or stream resource
131 * @param int $contentLength The length of the content in bytes. Set to -1 for auto calculation.
132 * @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate.
133 * @param null|string $sha256sum The SHA256 sum. null to auto calculate.
137 public static function createFromResource(&$resource, int $contentLength, ?string $md5sum = null, ?string $sha256sum = null): self
139 $input = new Input();
141 $input->setFp($resource);
142 $input->setSize($contentLength);
143 $input->setMd5sum($md5sum);
144 $input->setSha256($sha256sum);
150 * Create an input object from raw data.
152 * Please bear in mind that the data is being duplicated in memory. Therefore you'll need at least 2xstrlen($data)
153 * of free memory when you are using this method. You can instantiate an object and use assignData to work around
154 * this limitation when handling large amounts of data which may cause memory outages (typically: over 10Mb).
156 * @param string $data The data to use.
157 * @param null|string $md5sum The MD5 sum. null to auto calculate, empty string to never calculate.
158 * @param null|string $sha256sum The SHA256 sum. null to auto calculate.
162 public static function createFromData(string &$data, ?string $md5sum = null, ?string $sha256sum = null): self
164 $input = new Input();
166 $input->setData($data);
167 $input->setMd5sum($md5sum);
168 $input->setSha256($sha256sum);
176 function __destruct()
178 if (is_resource($this->fp))
185 * Returns the input type (resource, file or data)
189 public function getInputType(): int
191 if (!empty($this->file))
193 return self::INPUT_FILE;
196 if (!empty($this->fp))
198 return self::INPUT_RESOURCE;
201 return self::INPUT_DATA;
205 * Return the file pointer to the data, or null if this is not a resource input
207 * @return resource|null
209 public function getFp()
211 if (!is_resource($this->fp))
220 * Set the file pointer (or, generally, stream resource)
222 * @param resource $fp
224 public function setFp($fp): void
226 if (!is_resource($fp))
228 throw new Exception\InvalidFilePointer('$fp is not a file resource');
235 * Get the absolute path to the input file, or null if this is not a file input
237 * @return string|null
239 public function getFile(): ?string
241 if (empty($this->file))
250 * Set the absolute path to the input file
252 * @param string $file
254 public function setFile(string $file): void
259 if (is_resource($this->fp))
264 $this->fp = @fopen($file, 'rb');
266 if ($this->fp === false)
268 throw new Exception\CannotOpenFileForRead($file);
273 * Return the raw input data, or null if this is a file or stream input
275 * @return string|null
277 public function getData(): ?string
279 if (empty($this->data) && ($this->getInputType() != self::INPUT_DATA))
288 * Set the raw input data
290 * @param string $data
292 public function setData(string $data): void
296 if (is_resource($this->fp))
306 * Return a reference to the raw input data
308 * @return string|null
310 public function &getDataReference(): ?string
312 if (empty($this->data) && ($this->getInputType() != self::INPUT_DATA))
321 * Set the raw input data by doing an assignment instead of memory copy. While this conserves memory you cannot use
322 * this with hardcoded strings, method results etc without going through a variable first.
324 * @param string $data
326 public function assignData(string &$data): void
330 if (is_resource($this->fp))
340 * Returns the size of the data to be uploaded, in bytes. If it's not already specified it will try to guess.
344 public function getSize(): int
348 $this->size = $this->getInputSize();
355 * Set the size of the data to be uploaded.
359 public function setSize(int $size)
365 * Get the MIME type of the data
367 * @return string|null
369 public function getType(): ?string
371 if (empty($this->type))
373 $this->type = 'application/octet-stream';
375 if ($this->getInputType() == self::INPUT_FILE)
377 $this->type = $this->getMimeType($this->file);
385 * Set the MIME type of the data
387 * @param string|null $type
389 public function setType(?string $type)
395 * Get the MD5 sum of the content
397 * @return null|string
399 public function getMd5sum(): ?string
401 if ($this->md5sum === '')
406 if (is_null($this->md5sum))
408 $this->md5sum = $this->calculateMd5();
411 return $this->md5sum;
415 * Set the MD5 sum of the content as a base64 encoded string of the raw MD5 binary value.
417 * WARNING: Do not set a binary MD5 sum or a hex-encoded MD5 sum, it will result in an invalid signature error!
419 * Set to null to automatically calculate it from the raw data. Set to an empty string to force it to never be
420 * calculated and no value for it set either.
422 * @param string|null $md5sum
424 public function setMd5sum(?string $md5sum): void
426 $this->md5sum = $md5sum;
430 * Get the SHA-256 hash of the content
434 public function getSha256(): string
436 if (empty($this->sha256))
438 $this->sha256 = $this->calculateSha256();
441 return $this->sha256;
445 * Set the SHA-256 sum of the content. It must be a lowercase hexadecimal encoded string.
447 * Set to null to automatically calculate it from the raw data.
449 * @param string|null $sha256
451 public function setSha256(?string $sha256): void
453 $this->sha256 = strtolower($sha256);
457 * Get the Upload Session ID for multipart uploads
459 * @return string|null
461 public function getUploadID(): ?string
463 return $this->UploadID;
467 * Set the Upload Session ID for multipart uploads
469 * @param string|null $UploadID
471 public function setUploadID(?string $UploadID): void
473 $this->UploadID = $UploadID;
477 * Get the part number for multipart uploads.
479 * Returns null if the part number has not been set yet.
483 public function getPartNumber(): ?int
485 return $this->PartNumber;
489 * Set the part number for multipart uploads
491 * @param int $PartNumber
493 public function setPartNumber(int $PartNumber): void
495 // Clamp the part number to integers greater than zero.
496 $this->PartNumber = max(1, (int) $PartNumber);
500 * Get the list of ETags for multipart uploads
504 public function getEtags(): array
510 * Set the list of ETags for multipart uploads
512 * @param string[] $etags
514 public function setEtags(array $etags): void
516 $this->etags = $etags;
520 * Calculates the upload size from the input source. For data it's the entire raw string length. For a file resource
521 * it's the entire file's length. For seekable stream resources it's the remaining data from the current seek
524 * WARNING: You should never try to specify files or resources over 2Gb minus 1 byte otherwise 32-bit versions of
525 * PHP (anything except Linux x64 builds) will fail in unpredictable ways: the internal int representation in PHP
526 * depends on the target platform and is typically a signed 32-bit integer.
530 private function getInputSize(): int
532 switch ($this->getInputType())
534 case self::INPUT_DATA:
535 return function_exists('mb_strlen') ? mb_strlen($this->data, '8bit') : strlen($this->data);
538 case self::INPUT_FILE:
539 clearstatcache(true, $this->file);
541 $filesize = @filesize($this->file);
543 return ($filesize === false) ? 0 : $filesize;
546 case self::INPUT_RESOURCE:
547 $meta = stream_get_meta_data($this->fp);
549 if ($meta['seekable'])
551 $pos = ftell($this->fp);
552 $endPos = fseek($this->fp, 0, SEEK_END);
553 fseek($this->fp, $pos, SEEK_SET);
555 return $endPos - $pos + 1;
565 * Get the MIME type of a file
567 * @param string $file The absolute path to the file for which we want to get the MIME type
569 * @return string The MIME type of the file
571 private function getMimeType(string $file): string
575 // Fileinfo documentation says fileinfo_open() will use the
576 // MAGIC env var for the magic file
577 if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
578 ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false
581 if (($type = finfo_file($finfo, $file)) !== false)
583 // Remove the charset and grab the last content-type
584 $type = explode(' ', str_replace('; charset=', ';charset=', $type));
585 $type = array_pop($type);
586 $type = explode(';', $type);
587 $type = trim(array_shift($type));
592 elseif (function_exists('mime_content_type'))
594 $type = trim(mime_content_type($file));
597 if ($type !== false && strlen($type) > 0)
602 // Otherwise do it the old fashioned way
604 'jpg' => 'image/jpeg',
605 'gif' => 'image/gif',
606 'png' => 'image/png',
607 'tif' => 'image/tiff',
608 'tiff' => 'image/tiff',
609 'ico' => 'image/x-icon',
610 'swf' => 'application/x-shockwave-flash',
611 'pdf' => 'application/pdf',
612 'zip' => 'application/zip',
613 'gz' => 'application/x-gzip',
614 'tar' => 'application/x-tar',
615 'bz' => 'application/x-bzip',
616 'bz2' => 'application/x-bzip2',
617 'txt' => 'text/plain',
618 'asc' => 'text/plain',
619 'htm' => 'text/html',
620 'html' => 'text/html',
622 'js' => 'text/javascript',
624 'xsl' => 'application/xsl+xml',
625 'ogg' => 'application/ogg',
626 'mp3' => 'audio/mpeg',
627 'wav' => 'audio/x-wav',
628 'avi' => 'video/x-msvideo',
629 'mpg' => 'video/mpeg',
630 'mpeg' => 'video/mpeg',
631 'mov' => 'video/quicktime',
632 'flv' => 'video/x-flv',
633 'php' => 'text/x-php',
636 $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
638 return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream';
642 * Calculate the MD5 sum of the input data
644 * @return string Base-64 encoded MD5 sum
646 private function calculateMd5(): string
648 switch ($this->getInputType())
650 case self::INPUT_DATA:
651 return base64_encode(md5($this->data, true));
654 case self::INPUT_FILE:
655 return base64_encode(md5_file($this->file, true));
658 case self::INPUT_RESOURCE:
659 $ctx = hash_init('md5');
660 $pos = ftell($this->fp);
661 $size = $this->getSize();
663 $batch = min(1048576, $size);
665 while ($done < $size)
667 $toRead = min($batch, $done - $size);
668 $data = @fread($this->fp, $toRead);
669 hash_update($ctx, $data);
673 fseek($this->fp, $pos, SEEK_SET);
675 return base64_encode(hash_final($ctx, true));
684 * Calcualte the SHA256 data of the input data
686 * @return string Lowercase hex representation of the SHA-256 sum
688 private function calculateSha256(): string
690 $inputType = $this->getInputType();
693 case self::INPUT_DATA:
694 return hash('sha256', $this->data, false);
697 case self::INPUT_FILE:
698 case self::INPUT_RESOURCE:
699 if ($inputType == self::INPUT_FILE)
701 $filesize = @filesize($this->file);
702 $fPos = @ftell($this->fp);
704 if (($filesize == $this->getSize()) && ($fPos === 0))
706 return hash_file('sha256', $this->file, false);
710 $ctx = hash_init('sha256');
711 $pos = ftell($this->fp);
712 $size = $this->getSize();
714 $batch = min(1048576, $size);
716 while ($done < $size)
718 $toRead = min($batch, $size - $done);
719 $data = @fread($this->fp, $toRead);
721 hash_update($ctx, $data);
725 fseek($this->fp, $pos, SEEK_SET);
727 return hash_final($ctx, false);