Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand Down
10 changes: 10 additions & 0 deletions src/controllers/AssetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand All @@ -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,
]);
}

Expand Down
52 changes: 2 additions & 50 deletions src/fields/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -940,60 +937,15 @@ 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) {
if (!$createDynamicFolders) {
throw new InvalidSubpathException($subpath);
}

$folder = $assetsService->ensureFolderByFullPathAndVolume($subpath, $volume);
$folder = Craft::$app->getAssets()->ensureFolderByFullPathAndVolume($subpath, $volume);
}

return $folder;
Expand Down
67 changes: 67 additions & 0 deletions src/helpers/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
}
}
4 changes: 3 additions & 1 deletion src/services/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
Loading