# fluent-cart/1.6.2/app/Services/FileSystem/Drivers/S3/S3FileUploader.php

FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler, version 1.6.2. 276 lines.

- Page: https://pluginprobe.com/plugins/fluent-cart/1.6.2/code/app/Services/FileSystem/Drivers/S3/S3FileUploader.php
- Raw: https://pluginprobe.com/plugins/fluent-cart/1.6.2/raw/app/Services/FileSystem/Drivers/S3/S3FileUploader.php
- Modified: 2026-08-20T12:34:14+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-cart/1.6.2/code/app/Services/FileSystem/Drivers/S3/S3FileUploader.php#L10-L20`.

```php
<?php

namespace FluentCart\App\Services\FileSystem\Drivers\S3;

use Exception;
use FluentCart\App\Modules\StorageDrivers\S3\S3;
use FluentCart\Framework\Support\Str;
use WP_Error;

class S3FileUploader
{
    /**
     * Value of the If-None-Match header. "*" means "only if no object exists
     * under this key", which S3 answers with 412 when the key is taken.
     */
    private const IF_NONE_MATCH = '*';

    private string $accessKey;
    private string $secretKey;
    private string $bucket;
    private string $region;
    private string $hashAlgorithm = 'sha256';
    private string $httpMethod;
    private string $localFilePath;
    private string $s3FilePath;
    private string $signature;
    private string $requestUrl;
    private string $timeStamp;
    private string $date;
    private bool $preventOverwrite;

    /**
     * @throws Exception
     */
    public function __construct(string $secret, string $accessKey, string $bucket, string $region, string $localFilePath, string $s3FilePath)
    {
        $this->accessKey = $accessKey;
        $this->secretKey = $secret;
        $this->bucket = $bucket;
        $this->region = S3::getBucketRegion($bucket);
        $this->localFilePath = $localFilePath;
        $this->s3FilePath = $s3FilePath;
        $this->httpMethod = "PUT";

        $this->timeStamp = gmdate('Ymd\THis\Z');
        $this->date = substr($this->timeStamp, 0, 8);

        // ✅ Correct Regional Endpoint
        // $this->requestUrl = "https://{$this->bucket}.s3.{$this->region}.amazonaws.com/{$this->s3FilePath}";

        $hasDot = strpos($this->bucket, '.') !== false;

        if ($hasDot) {
            // Path-style URL
            $this->requestUrl = "https://s3.{$this->region}.amazonaws.com/{$this->bucket}/{$this->s3FilePath}";
        } else {
            // Virtual-hosted style
            $this->requestUrl = "https://{$this->bucket}.s3.{$this->region}.amazonaws.com/{$this->s3FilePath}";
        }

        // Opt-in: when enabled the upload is refused instead of replacing an
        // existing object. Resolved before signing because the header is part
        // of the canonical request.
        $this->preventOverwrite = (bool)apply_filters('fluent_cart/storage/s3_prevent_overwrite', false, [
            'bucket'       => $this->bucket,
            's3_file_path' => $this->s3FilePath,
        ]);

        $this->signature = $this->generateSignature();
    }

    /**
     * @throws Exception
     */
    public static function upload(string $secret, string $accessKey, string $bucket, string $region, string $localFilePath, string $s3FilePath)
    {
        return (new static($secret, $accessKey, $bucket, $region, $localFilePath, $s3FilePath))->uploadFile();
    }

    /**
     * @throws Exception
     */
    public function uploadFile()
    {
        add_filter('http_request_timeout', fn() => 30);

        $args = [
            'method'  => 'PUT',
            'headers' => $this->getHeaders(),
            'body'    => file_get_contents($this->localFilePath),
        ];

        $response = wp_remote_request($this->requestUrl, $args);
        $responseCode = wp_remote_retrieve_response_code($response);

        if ($responseCode === 200) {
            return [
                'message' => __('File Uploaded Successfully', 'fluent-cart'),
                'driver'  => 's3',
                'path'    => $this->s3FilePath
            ];
        }

        // S3 refused because the key is already taken. Gated on the opt-in so
        // a 412 raised for any other precondition is not misreported as an
        // overwrite conflict.
        if ($responseCode === 412 && $this->preventOverwrite) {
            return new WP_Error(
                $responseCode,
                sprintf(
                    /* translators: %s is the file name that already exists in the bucket */
                    __('A file named "%s" already exists in this bucket and existing files cannot be replaced. Upload it under a new name, for example by increasing the version number.', 'fluent-cart'),
                    basename($this->s3FilePath)
                )
            );
        }

        return new WP_Error($responseCode, __('Failed To Upload File', 'fluent-cart'));
    }

    public function getSignature(): string
    {
        return $this->signature;
    }

    /**
     * @throws Exception
     */
    public function generateSignature()
    {
        return hash_hmac(
            $this->hashAlgorithm,
            $this->createStringToSign(),
            $this->getSigningKey()
        );
    }

    private function createScope(): string
    {
        return "{$this->date}/{$this->region}/s3/aws4_request";
    }

    /**
     * @throws Exception
     */
    private function getContentHash(): string
    {
        if (!file_exists($this->localFilePath)) {
            throw new \Exception(esc_html__('File not found', 'fluent-cart'));
        }
        return hash($this->hashAlgorithm, file_get_contents($this->localFilePath));
    }

    /**
     * @throws Exception
     */
    private function createCanonicalUrl(): string
    {
        // Ensure file path begins with /
        $s3FilePath = Str::startsWith($this->s3FilePath, '/')
            ? $this->s3FilePath
            : "/{$this->s3FilePath}";

        $contentHash = $this->getContentHash();

        // If bucket has dot, use path-style URL in canonical request
        if (strpos($this->bucket, '.') !== false) {
            $canonicalUri = "/{$this->bucket}{$s3FilePath}";
        } else {
            $canonicalUri = $s3FilePath;
        }

        $canonicalHeaders = '';
        foreach ($this->getCanonicalHeaders($contentHash) as $name => $value) {
            $canonicalHeaders .= "{$name}:{$value}\n";
        }

        return "{$this->httpMethod}\n"
            . "{$canonicalUri}\n\n"
            . $canonicalHeaders
            . "\n"
            . "{$this->getSignedHeaders()}\n"
            . "{$contentHash}";
    }

    /**
     * Headers covered by the signature, keyed by lowercase name and sorted as
     * SigV4 requires. Single source for both the canonical request and the
     * SignedHeaders list in the Authorization header — if the two ever
     * disagree, S3 rejects every upload with 403.
     *
     * @return array<string, string>
     */
    private function getCanonicalHeaders(string $contentHash): array
    {
        $headers = [
            'host'                 => $this->getUploadHost(),
            'x-amz-content-sha256' => $contentHash,
            'x-amz-date'           => $this->timeStamp,
        ];

        if ($this->preventOverwrite) {
            $headers['if-none-match'] = self::IF_NONE_MATCH;
        }

        ksort($headers);

        return $headers;
    }

    /**
     * The semicolon-separated SignedHeaders value. Built from the same names
     * as the canonical request, without re-reading the file to hash it.
     */
    private function getSignedHeaders(): string
    {
        return implode(';', array_keys($this->getCanonicalHeaders('')));
    }

    private function getUploadHostOld(): string
    {
        // ✅ Regional host
        return "{$this->bucket}.s3.{$this->region}.amazonaws.com";
    }

    private function getUploadHost(): string
    {
        if ($this->bucket === '') {
            return "s3.{$this->region}.amazonaws.com";
        }

        // If bucket contains dot, use path-style host
        if (strpos($this->bucket, '.') !== false) {
            return "s3.{$this->region}.amazonaws.com";
        }

        return "{$this->bucket}.s3.{$this->region}.amazonaws.com";
    }

    /**
     * @throws Exception
     */
    private function createStringToSign(): string
    {
        $hash = hash($this->hashAlgorithm, $this->createCanonicalUrl());
        return "AWS4-HMAC-SHA256\n{$this->timeStamp}\n{$this->createScope()}\n{$hash}";
    }

    private function getSigningKey()
    {
        $dateKey = hash_hmac($this->hashAlgorithm, $this->date, "AWS4{$this->secretKey}", true);
        $regionKey = hash_hmac($this->hashAlgorithm, $this->region, $dateKey, true);
        $serviceKey = hash_hmac($this->hashAlgorithm, 's3', $regionKey, true);

        return hash_hmac($this->hashAlgorithm, 'aws4_request', $serviceKey, true);
    }

    /**
     * @throws Exception
     */
    public function getHeaders(): array
    {
        $headers = [
            'x-amz-content-sha256' => $this->getContentHash(),
            'x-amz-date'           => $this->timeStamp,
            'Authorization'        => "AWS4-HMAC-SHA256 Credential={$this->accessKey}/{$this->date}/{$this->region}/s3/aws4_request, SignedHeaders={$this->getSignedHeaders()}, Signature={$this->getSignature()}"
        ];

        if ($this->preventOverwrite) {
            $headers['If-None-Match'] = self::IF_NONE_MATCH;
        }

        return $headers;
    }
}

```
