diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 8110690887c..49942892f5d 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -89,6 +89,7 @@ - Added `craft\web\Request::getHasInvalidToken()`. - Added `craft\web\Response::FORMAT_GQL`. - Added `craft\web\twig\nodes\BaseNode`. +- Added `craft\helpers\Assets::resolveSubpath()`. ([#18103](https://github.com/craftcms/cms/pull/18103)) - Added `Craft.BaseElementIndex::asyncSelectDefaultSource()`. - Added `Craft.BaseElementIndex::asyncSelectSource()`. - Added `Craft.BaseElementIndex::asyncSelectSourceByKey()`. diff --git a/src/controllers/AssetsController.php b/src/controllers/AssetsController.php index b492827d2cf..0f48569c647 100644 --- a/src/controllers/AssetsController.php +++ b/src/controllers/AssetsController.php @@ -348,6 +348,14 @@ public function actionUpload(): Response } } + // try to get uploaded asset's URL + $url = null; + try { + $url = $asset->getUrl(); + } catch (Throwable) { + // do nothing + } + if ($asset->conflictingFilename !== null) { $conflictingAsset = Asset::findOne(['folderId' => $folder->id, 'filename' => $asset->conflictingFilename]); @@ -358,12 +366,14 @@ public function actionUpload(): Response 'conflictingAssetId' => $conflictingAsset->id ?? null, 'suggestedFilename' => $asset->suggestedFilename, 'conflictingAssetUrl' => ($conflictingAsset && $conflictingAsset->getVolume()->getFs()->hasUrls) ? $conflictingAsset->getUrl() : null, + 'url' => $url, ]); } return $this->asSuccess(data: [ 'filename' => $asset->getFilename(), 'assetId' => $asset->id, + 'url' => $url, ]); } diff --git a/src/fields/Assets.php b/src/fields/Assets.php index ad36f6e04e7..37473d40e3b 100644 --- a/src/fields/Assets.php +++ b/src/fields/Assets.php @@ -37,9 +37,6 @@ use craft\services\Gql as GqlService; use craft\web\UploadedFile; use GraphQL\Type\Definition\Type; -use Illuminate\Support\Collection; -use Twig\Error\RuntimeError; -use yii\base\InvalidConfigException; /** * Assets represents an Assets field. @@ -940,52 +937,7 @@ private function _findFolder(string $sourceKey, ?string $subpath, ?ElementInterf throw new InvalidFsException("Invalid source key: $sourceKey"); } - $assetsService = Craft::$app->getAssets(); - $rootFolder = $assetsService->getRootFolderByVolumeId($volume->id); - - // Are we looking for the root folder? - $subpath = trim($subpath ?? '', '/'); - if ($subpath === '') { - return $rootFolder; - } - - $isDynamic = preg_match('/\{|\}/', $subpath); - - if ($isDynamic) { - // Prepare the path by parsing tokens and normalizing slashes. - try { - if ($element?->duplicateOf) { - $element = $element->duplicateOf->getCanonical(); - } - $renderedSubpath = Craft::$app->getView()->renderObjectTemplate($subpath, $element); - } catch (InvalidConfigException|RuntimeError $e) { - throw new InvalidSubpathException($subpath, null, 0, $e); - } - - // Did any of the tokens return null? - if ( - $renderedSubpath === '' || - trim($renderedSubpath, '/') != $renderedSubpath || - str_contains($renderedSubpath, '//') || - Collection::make(explode('/', $renderedSubpath)) - ->contains(fn(string $segment) => ElementHelper::isTempSlug($segment)) - ) { - throw new InvalidSubpathException($subpath); - } - - // Sanitize the subpath - $segments = array_filter(explode('/', $renderedSubpath), fn(string $segment): bool => $segment !== ':ignore:'); - $generalConfig = Craft::$app->getConfig()->getGeneral(); - $segments = array_map(fn(string $segment): string => FileHelper::sanitizeFilename($segment, [ - 'asciiOnly' => $generalConfig->convertFilenamesToAscii, - ]), $segments); - $subpath = implode('/', $segments); - } - - $folder = $assetsService->findFolder([ - 'volumeId' => $volume->id, - 'path' => $subpath . '/', - ]); + [$subpath, $folder] = AssetsHelper::resolveSubpath($volume, $subpath, $element); // Ensure that the folder exists if (!$folder) { @@ -993,7 +945,7 @@ private function _findFolder(string $sourceKey, ?string $subpath, ?ElementInterf throw new InvalidSubpathException($subpath); } - $folder = $assetsService->ensureFolderByFullPathAndVolume($subpath, $volume); + $folder = Craft::$app->getAssets()->ensureFolderByFullPathAndVolume($subpath, $volume); } return $folder; diff --git a/src/helpers/Assets.php b/src/helpers/Assets.php index 022e2d0f5fa..127890ce3f8 100644 --- a/src/helpers/Assets.php +++ b/src/helpers/Assets.php @@ -9,17 +9,22 @@ use Craft; use craft\base\BaseFsInterface; +use craft\base\ElementInterface; use craft\base\FsInterface; use craft\base\LocalFsInterface; use craft\elements\Asset; use craft\enums\TimePeriod; use craft\errors\FsException; +use craft\errors\InvalidSubpathException; use craft\events\RegisterAssetFileKindsEvent; use craft\events\SetAssetFilenameEvent; use craft\fs\Temp; use craft\helpers\ImageTransforms as TransformHelper; +use craft\models\Volume; use craft\models\VolumeFolder; use DateTime; +use Illuminate\Support\Collection; +use Twig\Error\RuntimeError; use yii\base\Event; use yii\base\Exception; use yii\base\InvalidArgumentException; @@ -973,4 +978,66 @@ public static function isTempUploadFs(FsInterface $fs): bool $handle = App::parseEnv(Craft::$app->getConfig()->getGeneral()->tempAssetUploadFs); return $fs->handle === $handle; } + + /** + * Resolves a possibly dynamic subpath for a given element, and returns the rendered subpath and + * matching volume folder (if one exists). + * + * @param Volume $volume + * @param string|null $subpath + * @param ElementInterface|null $element + * @return array{0:string,1:VolumeFolder|null} + * @throws Exception + * @throws InvalidSubpathException + * @since 5.9.0 + */ + public static function resolveSubpath(Volume $volume, ?string $subpath, ?ElementInterface $element = null): array + { + $assetsService = Craft::$app->getAssets(); + $rootFolder = $assetsService->getRootFolderByVolumeId($volume->id); + + // Are we looking for the root folder? + $subpath = trim($subpath ?? '', '/'); + if ($subpath === '') { + return [$subpath, $rootFolder]; + } + + if (str_contains($subpath, '{')) { + // Prepare the path by parsing tokens and normalizing slashes. + try { + if ($element?->duplicateOf) { + $element = $element->duplicateOf->getCanonical(); + } + $renderedSubpath = Craft::$app->getView()->renderObjectTemplate($subpath, $element); + } catch (InvalidConfigException|RuntimeError $e) { + throw new InvalidSubpathException($subpath, null, 0, $e); + } + + // Did any of the tokens return null? + if ( + $renderedSubpath === '' || + trim($renderedSubpath, '/') != $renderedSubpath || + str_contains($renderedSubpath, '//') || + Collection::make(explode('/', $renderedSubpath)) + ->contains(fn(string $segment) => ElementHelper::isTempSlug($segment)) + ) { + throw new InvalidSubpathException($subpath); + } + + // Sanitize the subpath + $segments = array_filter(explode('/', $renderedSubpath), fn(string $segment): bool => $segment !== ':ignore:'); + $generalConfig = Craft::$app->getConfig()->getGeneral(); + $segments = array_map(fn(string $segment): string => FileHelper::sanitizeFilename($segment, [ + 'asciiOnly' => $generalConfig->convertFilenamesToAscii, + ]), $segments); + $subpath = implode('/', $segments); + } + + $folder = $assetsService->findFolder([ + 'volumeId' => $volume->id, + 'path' => $subpath . '/', + ]); + + return [$subpath, $folder]; + } } diff --git a/src/services/Assets.php b/src/services/Assets.php index 54b77c251be..4189b8cbd3a 100644 --- a/src/services/Assets.php +++ b/src/services/Assets.php @@ -870,9 +870,11 @@ public function ensureFolderByFullPathAndVolume(string $fullPath, Volume $volume $folderModel = $parentFolder; $parentId = $parentFolder->id; + $fullPath = trim($fullPath, '/\\'); + if ($fullPath !== '') { // If we don't have a folder matching these, create a new one - $parts = preg_split('/\\\\|\//', trim($fullPath, '/\\')); + $parts = preg_split('/\\\\|\//', $fullPath); // creep up the folder path $path = '';