From 22e973c2c9a9cd0311f2087ec84371424a6bbf2a Mon Sep 17 00:00:00 2001 From: Jade Date: Thu, 23 Jul 2026 09:35:15 +0200 Subject: [PATCH 1/3] Allow file uploads (WIP) --- resources/js/components/QuoteData.vue | 41 +++++++++----- src/Fieldtypes/Products.php | 81 ++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/resources/js/components/QuoteData.vue b/resources/js/components/QuoteData.vue index e9f9a32..16252a1 100644 --- a/resources/js/components/QuoteData.vue +++ b/resources/js/components/QuoteData.vue @@ -1,5 +1,6 @@ \ No newline at end of file + diff --git a/resources/js/components/fieldtypes/Products.vue b/resources/js/components/fieldtypes/Products.vue index 296a974..5c63831 100644 --- a/resources/js/components/fieldtypes/Products.vue +++ b/resources/js/components/fieldtypes/Products.vue @@ -21,6 +21,11 @@ +{{ price(option.price) }} +
{{ price(item.totalPrice) }} diff --git a/src/Fieldtypes/Products.php b/src/Fieldtypes/Products.php index 9c70a11..57079f4 100644 --- a/src/Fieldtypes/Products.php +++ b/src/Fieldtypes/Products.php @@ -3,9 +3,10 @@ namespace Rapidez\StatamicQuote\Fieldtypes; use Illuminate\Support\Arr; -use Illuminate\Support\Str; use Rapidez\Core\Facades\Rapidez; +use Rapidez\Core\Models\QuoteItemOption; use Statamic\Exceptions\AssetContainerNotFoundException; +use Statamic\Facades\Asset; use Statamic\Facades\AssetContainer; use Statamic\Fieldtypes\Assets\UndefinedContainerException; use Statamic\Fields\Fieldtype; @@ -49,40 +50,110 @@ public function process($value) $customOptions = $products->pluck('customOptions', 'id'); $savedProducts = $products->map(fn($product) => Arr::except($product, 'customOptions')); - //if ($this->config('allow_uploads') && $this->config('container')) { + if ($this->config('allow_uploads') && $this->config('container')) { $uploaded = $customOptions->map(function ($options, $id) { - collect($options)->map(function ($value, $key) { - $data = json_decode($value)->base64_encoded_data ?? null; - if (!$data) { - return null; - } + return collect($options)->mapWithKeys(function ($value, $key) { + $file = collect(is_string($value) ? json_decode($value) : $value); - return $this->valueToId($data); + $isCartData = $file->has('customizable_option_uid'); + return $isCartData ? $this->handleCartDataUpload($file) : $this->handleFileUpload($file, $key); }); }); - //} - // - dd($uploaded); + } return [ 'store' => config('rapidez.store'), 'products' => $savedProducts->toJson(), + 'uploaded' => $uploaded, ]; } + protected function handleFileUpload($file, $optionId) + { + $name = basename($file['name'] ?? ''); + if (! $name) { + return null; + } + + $data = $file['base64_encoded_data'] ?? null; + if (! $data) { + return null; + } + + // 1365 here is 1024 / 6 * 8. This is to account for base64 being larger than the actual file size. + if (strlen($data) / 1365 > config('rapidez.quote.file_upload_max_size', 51200)) { + throw new \Exception('File exceeds the maximum upload size'); + } + + return [$optionId => $this->valueToId($name, $data)]; + } + + protected function handleCartDataUpload($file) + { + $optionUid = base64_decode($file['customizable_option_uid']); + $optionId = explode('/', $optionUid)[1] ?? null; + $optionId = filter_var($optionId, FILTER_VALIDATE_INT); + if ($optionId === false) { + return null; + } + + // Get first file data from the option and check if it's really there + $fileData = $file['values'][0] ?? null; + if (! $fileData) { + return null; + } + + // Check name and valueId from said data + $name = $fileData['value'] ?? null; + $valueId = $fileData['id'] ?? null; + if (! $name || ! $valueId) { + return null; + } + + // Check if the quote item exists with the correct option id + $itemOption = QuoteItemOption::find($valueId); + if (! $itemOption || ($itemOption->code !== 'option_' . $optionId)) { + return null; + } + + // Also double check whether the given name checks out + $optionData = $itemOption->value; + if (! $optionData->fullpath || $optionData->title !== $name) { + return null; + }; + + if ($optionData->size / 1024 > config('rapidez.quote.file_upload_max_size', 51200)) { + throw new \Exception('File exceeds the maximum upload size'); + } + + // Once we get here we can be certain the data is legitimate + $data = fopen($optionData->fullpath, 'r'); + + return [$optionId => $this->valueToId($name, $data)]; + } + public function preProcess($products) { - dd($products); return $this->augment($products); } - protected function valueToId($value) + protected function valueToId($name, $value) { - if (Str::contains($value, '::')) { - return $value; + $folder = now()->format('YmdHis'); + $path = "$folder/$name"; + + $asset = $this->container()->makeAsset($path); + if ($asset->exists()) { + return $asset->id(); } - return optional($this->container()->asset($value))->id(); + $fileData = is_resource($value) ? stream_get_contents($value) : base64_decode($value, true); + + $this->container()->disk()->put($path, $fileData); + $asset = $this->container()->makeAsset($path); + $asset->save(); + + return $asset->id(); } protected function container() @@ -105,8 +176,9 @@ protected function container() public function augment($data) { $store = $data['store'] ?? 1; + $uploaded = $data['uploaded'] ?? []; - return Rapidez::withStore($store, function() use ($data, $store) { + return Rapidez::withStore($store, function() use ($data, $store, $uploaded) { $products = collect(json_decode($data['products'] ?? $data, true)); $productModel = config('rapidez.models.product'); /** @var \Rapidez\Core\Models\Product $productInstance */ @@ -116,7 +188,7 @@ public function augment($data) ->get() ->keyBy('sku'); - return $products->map(function($product) use ($dbProducts, $store) { + return $products->map(function($product) use ($dbProducts, $store, $uploaded) { $dbProduct = $dbProducts[$product['sku']] ?? null; $productOptions = $dbProduct @@ -136,8 +208,26 @@ public function augment($data) ? ($productOptions->sum('price.price') + $dbProduct->price) * $product['qty'] : null; + $currentUploaded = collect($uploaded[$product['id'] ?? ''] ?? []) + ->map(function ($id, $optionId) use ($dbProduct) { + $asset = Asset::findById($id); + if (!$asset) { + return null; + } + + $option = collect($dbProduct->options)->first(fn ($productOption) => $productOption->option_id == $optionId); + + return [ + 'path' => $asset->url(), + 'filename' => basename($asset->url()), + 'option' => $option->title ?? $optionId, + ]; + }) + ->whereNotNull(); + return [ ...$product, + 'uploaded' => $currentUploaded, 'store' => $store, 'product' => $dbProduct, 'options' => $productOptions, diff --git a/src/Listeners/QuoteRequestListener.php b/src/Listeners/QuoteRequestListener.php index fc07e14..e82aece 100644 --- a/src/Listeners/QuoteRequestListener.php +++ b/src/Listeners/QuoteRequestListener.php @@ -23,6 +23,7 @@ public function handle(FormSubmitted $event) $quoteData = Eventy::filter('quote.data', [ 'store' => Rapidez::getStore(config('rapidez.store')), + 'customer' => auth('magento-customer')->user(), 'products' => $event->submission->augmentedValue('products')->value(), 'formData' => $event->submission->toArray(), ]); From 038aab5fa1f47dfb447533e7909818a92f12cb30 Mon Sep 17 00:00:00 2001 From: Jade Date: Fri, 21 Aug 2026 14:11:58 +0200 Subject: [PATCH 3/3] Improve restrictions --- config/rapidez/quote.php | 3 --- src/Fieldtypes/Products.php | 28 ++++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/config/rapidez/quote.php b/config/rapidez/quote.php index 68644d3..c7b7251 100644 --- a/config/rapidez/quote.php +++ b/config/rapidez/quote.php @@ -7,7 +7,4 @@ ], 'email_markdown' => true, 'auto_send_quote' => true, - - // Maximum file size in KB - 'file_upload_max_size' => 51200, ]; diff --git a/src/Fieldtypes/Products.php b/src/Fieldtypes/Products.php index 57079f4..b88e081 100644 --- a/src/Fieldtypes/Products.php +++ b/src/Fieldtypes/Products.php @@ -28,6 +28,17 @@ protected function configFieldItems(): array 'type' => 'toggle', 'default' => true, ], + 'max_upload_size' => [ + 'display' => __('Maximum File Size (KB)'), + 'type' => 'integer', + 'default' => '10240', + ], + 'allowed_filetypes' => [ + 'display' => __('Allowed Filetypes'), + 'instructions' => __('The file extensions that are allowed to be uploaded, separated by a comma'), + 'type' => 'text', + 'required' => true, + ], 'container' => [ 'display' => __('Container'), 'instructions' => __('statamic::fieldtypes.assets.config.container'), @@ -71,7 +82,7 @@ public function process($value) protected function handleFileUpload($file, $optionId) { $name = basename($file['name'] ?? ''); - if (! $name) { + if (! $name || ! $this->isAllowedFileType($name)) { return null; } @@ -81,7 +92,7 @@ protected function handleFileUpload($file, $optionId) } // 1365 here is 1024 / 6 * 8. This is to account for base64 being larger than the actual file size. - if (strlen($data) / 1365 > config('rapidez.quote.file_upload_max_size', 51200)) { + if (strlen($data) / 1365 > $this->config('max_upload_size')) { throw new \Exception('File exceeds the maximum upload size'); } @@ -106,7 +117,7 @@ protected function handleCartDataUpload($file) // Check name and valueId from said data $name = $fileData['value'] ?? null; $valueId = $fileData['id'] ?? null; - if (! $name || ! $valueId) { + if (! $name || ! $valueId || ! $this->isAllowedFileType($name)) { return null; } @@ -122,16 +133,25 @@ protected function handleCartDataUpload($file) return null; }; - if ($optionData->size / 1024 > config('rapidez.quote.file_upload_max_size', 51200)) { + if ($optionData->size / 1024 > $this->config('max_upload_size')) { throw new \Exception('File exceeds the maximum upload size'); } // Once we get here we can be certain the data is legitimate + // TODO: This assumes that your Magento installation is on the same server as your Rapidez installation + // Might be worthwhile to just get the file from its url instead (but this would require allow_url_fopen to be enabled) $data = fopen($optionData->fullpath, 'r'); return [$optionId => $this->valueToId($name, $data)]; } + protected function isAllowedFileType($name): bool + { + $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + $allowed = explode(',', $this->config('allowed_filetypes')); + return in_array($extension, $allowed); + } + public function preProcess($products) { return $this->augment($products);