From dc4dfcb999ba78773475c48c6db3c6938636dec0 Mon Sep 17 00:00:00 2001 From: i-just Date: Wed, 26 Nov 2025 15:05:30 +0100 Subject: [PATCH 1/4] return asset url after upload --- src/controllers/AssetsController.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/controllers/AssetsController.php b/src/controllers/AssetsController.php index b492827d2cf..3270eaff80f 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 (Exception $e) { + // 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, ]); } From 8610b4be0f415651ecf0a650f531a51a7ad8e29d Mon Sep 17 00:00:00 2001 From: i-just Date: Wed, 26 Nov 2025 15:06:14 +0100 Subject: [PATCH 2/4] move find folder logic around so it can be reused --- src/fields/Assets.php | 52 ++----------------------------- src/helpers/Assets.php | 69 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/src/fields/Assets.php b/src/fields/Assets.php index ad36f6e04e7..de50ed95312 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::findFolderBySubpath($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..e0672487602 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,68 @@ public static function isTempUploadFs(FsInterface $fs): bool $handle = App::parseEnv(Craft::$app->getConfig()->getGeneral()->tempAssetUploadFs); return $fs->handle === $handle; } + + /** + * Finds a volume folder by a volume and (dynamic?) subpath. + * Returns the folder and rendered subpath. + * + * @param Volume $volume + * @param string $subpath + * @param ElementInterface|null $element + * @return array + * @throws Exception + * @throws InvalidSubpathException + * @throws \Throwable + */ + public static function findFolderBySubpath(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]; + } + + $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 . '/', + ]); + + return [$subpath, $folder]; + } } From 7e62ee0a99a3f5f1126d4d481c66a3df4614b7ae Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Sun, 30 Nov 2025 09:22:11 -0800 Subject: [PATCH 3/4] Cleanup --- src/controllers/AssetsController.php | 2 +- src/fields/Assets.php | 2 +- src/helpers/Assets.php | 16 +++++++--------- src/services/Assets.php | 4 +++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/controllers/AssetsController.php b/src/controllers/AssetsController.php index 3270eaff80f..0f48569c647 100644 --- a/src/controllers/AssetsController.php +++ b/src/controllers/AssetsController.php @@ -352,7 +352,7 @@ public function actionUpload(): Response $url = null; try { $url = $asset->getUrl(); - } catch (Exception $e) { + } catch (Throwable) { // do nothing } diff --git a/src/fields/Assets.php b/src/fields/Assets.php index de50ed95312..37473d40e3b 100644 --- a/src/fields/Assets.php +++ b/src/fields/Assets.php @@ -937,7 +937,7 @@ private function _findFolder(string $sourceKey, ?string $subpath, ?ElementInterf throw new InvalidFsException("Invalid source key: $sourceKey"); } - [$subpath, $folder] = AssetsHelper::findFolderBySubpath($volume, $subpath, $element); + [$subpath, $folder] = AssetsHelper::resolveSubpath($volume, $subpath, $element); // Ensure that the folder exists if (!$folder) { diff --git a/src/helpers/Assets.php b/src/helpers/Assets.php index e0672487602..127890ce3f8 100644 --- a/src/helpers/Assets.php +++ b/src/helpers/Assets.php @@ -980,18 +980,18 @@ public static function isTempUploadFs(FsInterface $fs): bool } /** - * Finds a volume folder by a volume and (dynamic?) subpath. - * Returns the folder and rendered subpath. + * 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 $subpath + * @param string|null $subpath * @param ElementInterface|null $element - * @return array + * @return array{0:string,1:VolumeFolder|null} * @throws Exception * @throws InvalidSubpathException - * @throws \Throwable + * @since 5.9.0 */ - public static function findFolderBySubpath(Volume $volume, ?string $subpath, ?ElementInterface $element = null): array + public static function resolveSubpath(Volume $volume, ?string $subpath, ?ElementInterface $element = null): array { $assetsService = Craft::$app->getAssets(); $rootFolder = $assetsService->getRootFolderByVolumeId($volume->id); @@ -1002,9 +1002,7 @@ public static function findFolderBySubpath(Volume $volume, ?string $subpath, ?El return [$subpath, $rootFolder]; } - $isDynamic = preg_match('/\{|\}/', $subpath); - - if ($isDynamic) { + if (str_contains($subpath, '{')) { // Prepare the path by parsing tokens and normalizing slashes. try { if ($element?->duplicateOf) { 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 = ''; From e1fefd0720aa7bc3b29cacb99c86bb0de790bd69 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Sun, 30 Nov 2025 09:23:36 -0800 Subject: [PATCH 4/4] Release note --- CHANGELOG-WIP.md | 1 + 1 file changed, 1 insertion(+) 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()`.