all(), $productDownloadableBulkFileRequest->rules(), $productDownloadableBulkFileRequest->messages() ); $validationErrors = []; if ($validator->fails()) { $validationErrors = $validator->errors(); } if (count($validationErrors) !== 0) { return $this->sendError($validationErrors); } $data = $request->get('downloadable_files'); $sanitizedData = []; foreach ($data as $downloadableFile) { $variationIds = Arr::get($downloadableFile, 'product_variation_id'); if (is_array($variationIds)) { $variationIds = array_map('intval', $variationIds); } else { $variationIds = []; } $sanitizedData[] = [ 'id' => intval(Arr::get($downloadableFile, 'id')), 'post_id' => intval(Arr::get($downloadableFile, 'post_id')), 'product_variation_id' => $variationIds, 'title' => sanitize_text_field(Arr::get($downloadableFile, 'title')), 'type' => sanitize_text_field(Arr::get($downloadableFile, 'type')), 'driver' => sanitize_text_field(Arr::get($downloadableFile, 'driver')), 'bucket' => sanitize_text_field(Arr::get($downloadableFile, 'bucket')), 'file_name' => sanitize_text_field(Arr::get($downloadableFile, 'file_name')), 'file_path' => sanitize_text_field(Arr::get($downloadableFile, 'file_path')), 'file_url' => sanitize_text_field(Arr::get($downloadableFile, 'file_url')), 'settings' => Arr::get($downloadableFile, 'settings'), 'serial' => sanitize_text_field(Arr::get($downloadableFile, 'serial')), 'file_size' => sanitize_text_field(Arr::get($downloadableFile, 'file_size')), ]; } $fileData['downloadable_files'] = $sanitizedData; unset($fileData['downloadable_files']['*']); $productId = $request->getSafe('postId', 'intval'); foreach ($fileData['downloadable_files'] as &$file) { $file['download_identifier'] = Str::uuid(); $file['post_id'] = $productId; $file['file_path'] = !empty($file['file_path']) ? $file['file_path'] : $file['file_name']; $file['file_url'] = !empty($file['file_url']) ? $file['file_url'] : $file['file_name']; // file_path is later composed onto the storage directory to read the // file back, so a relative segment stored here would read outside it. // Checked after the file_name fallback, which feeds the same column. if (!StoragePath::isSafe($file['file_path'])) { return $this->sendError([ 'message' => __('Invalid file path', 'fluent-cart') ], 422); } $file['product_variation_id'] = json_encode( Arr::get($file, 'product_variation_id', []) ); $fileName = $file['file_name']; $fileName = explode('_____fluent-cart_____', $fileName)[0]; $fileName = explode('__fluent-cart__', $fileName)[0]; $file['file_name'] = $fileName; // settings.bucket decides which cloud bucket the download is later signed // against, so it is resolved from the driver's own settings rather than // taken from the request — otherwise any editable product could point at // a sibling bucket the store credentials happen to read. $settings = $this->withResolvedBucket(Arr::get($file, 'settings', []), $file['driver']); if (is_wp_error($settings)) { return $this->sendError([ 'message' => $settings->get_error_message() ], 422); } $file['settings'] = json_encode($settings); unset($file['id']); unset($file['bucket']); } ProductDetail::query()->where('post_id', $productId)->update([ 'manage_downloadable' => 1 ]); $isCreated = ProductDownload::query()->insert($fileData['downloadable_files']); if ($isCreated) { return $this->sendSuccess([ 'downloadable_files' => ProductDownloadResource::search(['post_id' => $productId]) ]); } else { return $this->sendError([ 'message' => __('Failed to attach downloadable files', 'fluent-cart') ]); } } /** * Replace the caller-supplied settings.bucket with the bucket the selected * driver is actually configured to use. * * @param mixed $settings the submitted settings blob * @param string $driver the submitted driver slug * @return array|\WP_Error WP_Error when the driver is unknown, disabled, or * bucket-backed with no configured bucket */ private function withResolvedBucket($settings, $driver) { $settings = is_array($settings) ? $settings : []; $bucket = StorageBucketResolver::resolve($driver); if (is_wp_error($bucket)) { return $bucket; } $settings['bucket'] = $bucket; return $settings; } public function getDownloadableUrl($downloadableId) { $productDownload = ProductDownload::query()->findOrFail($downloadableId); if (empty($productDownload)) { return $this->sendError([ 'message' => __('Failed to generate downloadable link', 'fluent-cart') ]); } return $this->sendSuccess([ 'url' => Helper::generateDownloadFileLink($productDownload, null, 60 * 60 * 24 * 7, true) ]); } public function update(ProductDownloadableFileRequest $request, $downloadId) { $productDownload = ProductDownload::query()->findOrFail($downloadId); $data = $request->getSafe($request->sanitize()); $fileName = Arr::get($data, 'file_name'); $filePath = Arr::get($data, 'file_path') ?: $fileName; $fileUrl = Arr::get($data, 'file_url') ?: $fileName; // Same containment as the sync path: file_path is composed onto the // storage directory when the file is read back. if (!StoragePath::isSafe($filePath)) { return $this->sendError([ 'message' => __('Invalid file path', 'fluent-cart') ], 422); } $productVariationId = Arr::get($data, 'product_variation_id', []); $fileName = explode('_____fluent-cart_____', $fileName)[0]; $fileName = explode('__fluent-cart__', $fileName)[0]; if (is_array($productVariationId)) { $productVariationId = array_values(array_filter(array_map('intval', $productVariationId))); } else { $productVariationId = []; } // Same server-side bucket resolution as the sync path — editing a file must // not be a second way to substitute an unconfigured bucket. $settings = $this->withResolvedBucket(Arr::get($data, 'settings', []), Arr::get($data, 'driver')); if (is_wp_error($settings)) { return $this->sendError([ 'message' => $settings->get_error_message() ], 422); } $productDownload->product_variation_id = $productVariationId; $productDownload->title = Arr::get($data, 'title'); $productDownload->type = Arr::get($data, 'type'); $productDownload->driver = Arr::get($data, 'driver'); $productDownload->file_name = $fileName; $productDownload->file_path = $filePath; $productDownload->file_url = $fileUrl; $productDownload->settings = $settings; $productDownload->serial = Arr::get($data, 'serial'); if ($productDownload->save()) { return $this->sendSuccess([ 'message' => __('Product downloadable files updated successfully', 'fluent-cart') ]); } else { return $this->sendError([ 'message' => __('Failed to update product downloadable files', 'fluent-cart') ]); } } public function delete($downloadableFileId) { $response = ProductDownloadResource::delete($downloadableFileId); if (is_wp_error($response)) { return $this->sendError([ 'message' => $response->get_error_message() ]); } return $this->sendSuccess([ 'message' => $response['message'] ]); } public function updateDownloadableFile(Request $request, Product $product, ProductDownload $productDownload) { } }