PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Http/Controllers/ProductDownloadablesController.php +71 -5 1.3.19 → 1.6.5 View file →
@@ -8,8 +8,10 @@
8 8 use FluentCart\App\Http\Requests\ProductDownloadable\ProductDownloadableFileRequest;
9 9 use FluentCart\App\Models\Product;
10 10 use FluentCart\App\Models\ProductDetail;
11 11 use FluentCart\App\Models\ProductDownload;
12 +use FluentCart\App\Services\FileSystem\StorageBucketResolver;
13 +use FluentCart\App\Services\FileSystem\StoragePath;
12 14 use FluentCart\App\Services\URL;
13 15 use FluentCart\Framework\Http\Request\Request;
14 16 use FluentCart\Framework\Http\URL as BaseUrl;
15 17 use FluentCart\Framework\Support\Arr;
@@ -20,9 +22,13 @@
20 22 {
21 23 public function syncDownloadableFiles(Request $request): \WP_REST_Response
22 24 {
23 25 $productDownloadableBulkFileRequest = new ProductDownloadableBulkFileRequest();
24 - $validator = Validator::make($request->all(), $productDownloadableBulkFileRequest->rules());
26 + $validator = Validator::make(
27 + $request->all(),
28 + $productDownloadableBulkFileRequest->rules(),
29 + $productDownloadableBulkFileRequest->messages()
30 + );
25 31 $validationErrors = [];
26 32
27 33
28 34 if ($validator->fails()) {
@@ -76,8 +82,18 @@
76 82 $file['download_identifier'] = Str::uuid();
77 83 $file['post_id'] = $productId;
78 84 $file['file_path'] = !empty($file['file_path']) ? $file['file_path'] : $file['file_name'];
79 85 $file['file_url'] = !empty($file['file_url']) ? $file['file_url'] : $file['file_name'];
86 +
87 + // file_path is later composed onto the storage directory to read the
88 + // file back, so a relative segment stored here would read outside it.
89 + // Checked after the file_name fallback, which feeds the same column.
90 + if (!StoragePath::isSafe($file['file_path'])) {
91 + return $this->sendError([
92 + 'message' => __('Invalid file path', 'fluent-cart')
93 + ], 422);
94 + }
95 +
80 96 $file['product_variation_id'] = json_encode(
81 97 Arr::get($file, 'product_variation_id', [])
82 98 );
83 99
@@ -85,12 +101,21 @@
85 101 $fileName = explode('_____fluent-cart_____', $fileName)[0];
86 102 $fileName = explode('__fluent-cart__', $fileName)[0];
87 103 $file['file_name'] = $fileName;
88 104
89 - $file['settings'] = json_encode(
90 - Arr::get($file, 'settings', [])
91 - );
105 + // settings.bucket decides which cloud bucket the download is later signed
106 + // against, so it is resolved from the driver's own settings rather than
107 + // taken from the request — otherwise any editable product could point at
108 + // a sibling bucket the store credentials happen to read.
109 + $settings = $this->withResolvedBucket(Arr::get($file, 'settings', []), $file['driver']);
110 + if (is_wp_error($settings)) {
111 + return $this->sendError([
112 + 'message' => $settings->get_error_message()
113 + ], 422);
114 + }
92 115
116 + $file['settings'] = json_encode($settings);
117 +
93 118 unset($file['id']);
94 119 unset($file['bucket']);
95 120 }
96 121
@@ -111,8 +136,31 @@
111 136 ]);
112 137 }
113 138 }
114 139
140 + /**
141 + * Replace the caller-supplied settings.bucket with the bucket the selected
142 + * driver is actually configured to use.
143 + *
144 + * @param mixed $settings the submitted settings blob
145 + * @param string $driver the submitted driver slug
146 + * @return array|\WP_Error WP_Error when the driver is unknown, disabled, or
147 + * bucket-backed with no configured bucket
148 + */
149 + private function withResolvedBucket($settings, $driver)
150 + {
151 + $settings = is_array($settings) ? $settings : [];
152 +
153 + $bucket = StorageBucketResolver::resolve($driver);
154 + if (is_wp_error($bucket)) {
155 + return $bucket;
156 + }
157 +
158 + $settings['bucket'] = $bucket;
159 +
160 + return $settings;
161 + }
162 +
115 163 public function getDownloadableUrl($downloadableId)
116 164 {
117 165 $productDownload = ProductDownload::query()->findOrFail($downloadableId);
118 166
@@ -140,8 +188,17 @@
140 188 $fileName = Arr::get($data, 'file_name');
141 189
142 190 $filePath = Arr::get($data, 'file_path') ?: $fileName;
143 191 $fileUrl = Arr::get($data, 'file_url') ?: $fileName;
192 +
193 + // Same containment as the sync path: file_path is composed onto the
194 + // storage directory when the file is read back.
195 + if (!StoragePath::isSafe($filePath)) {
196 + return $this->sendError([
197 + 'message' => __('Invalid file path', 'fluent-cart')
198 + ], 422);
199 + }
200 +
144 201 $productVariationId = Arr::get($data, 'product_variation_id', []);
145 202 $fileName = explode('_____fluent-cart_____', $fileName)[0];
146 203 $fileName = explode('__fluent-cart__', $fileName)[0];
147 204
@@ -152,8 +209,17 @@
152 209 } else {
153 210 $productVariationId = [];
154 211 }
155 212
213 + // Same server-side bucket resolution as the sync path — editing a file must
214 + // not be a second way to substitute an unconfigured bucket.
215 + $settings = $this->withResolvedBucket(Arr::get($data, 'settings', []), Arr::get($data, 'driver'));
216 + if (is_wp_error($settings)) {
217 + return $this->sendError([
218 + 'message' => $settings->get_error_message()
219 + ], 422);
220 + }
221 +
156 222 $productDownload->product_variation_id = $productVariationId;
157 223 $productDownload->title = Arr::get($data, 'title');
158 224 $productDownload->type = Arr::get($data, 'type');
159 225 $productDownload->driver = Arr::get($data, 'driver');
@@ -159,9 +225,9 @@
159 225 $productDownload->driver = Arr::get($data, 'driver');
160 226 $productDownload->file_name = $fileName;
161 227 $productDownload->file_path = $filePath;
162 228 $productDownload->file_url = $fileUrl;
163 - $productDownload->settings = Arr::get($data, 'settings');
229 + $productDownload->settings = $settings;
164 230 $productDownload->serial = Arr::get($data, 'serial');
165 231
166 232 if ($productDownload->save()) {
167 233 return $this->sendSuccess([