Skip to content
Open
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
203 changes: 171 additions & 32 deletions src/Tags/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@

namespace Statamic\Tags;

use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Facades\Log;
use Statamic\Assets\AssetCollection;
use Statamic\Contracts\Query\Builder;
use Statamic\Facades\Asset;
use Statamic\Facades\AssetContainer;
use Statamic\Facades\Entry;
use Statamic\Facades\Pattern;
use Statamic\Facades\Site;
use Statamic\Fields\Value;
use Statamic\Support\Arr;
use Statamic\Support\Str;

class Assets extends Tags
{
use Concerns\GetsQueryResults,
Concerns\OutputsItems,
Concerns\QueriesConditions,
Concerns\QueriesOrderBys,
Concerns\QueriesScopes;

/**
* @var AssetCollection
*/
Expand Down Expand Up @@ -40,7 +51,7 @@ public function wildcard($method)

$this->assets = (new AssetCollection([$value]))->flatten();

return $this->output();
return $this->outputCollection($this->assets);
}

if ($value instanceof Value) {
Expand All @@ -66,21 +77,24 @@ public function index()
$path = $this->params->get('path');
$collection = $this->params->get('collection');

$this->assets = $collection
? $this->assetsFromCollection($collection)
: $this->assetsFromContainer($id, $path);
if ($collection) {
return $this->outputCollection($this->assetsFromCollection($collection));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double filterByType applied on collection path

Low Severity

When the index() method takes the collection path, filterByType is applied twice. assetsFromCollection already calls filterByType on each field's value internally. Then outputCollection calls applyPostCollectionFilters, which calls filterByType again on the entire resulting collection. While functionally idempotent for now, this is redundant and risks inconsistency if filterByType behavior ever changes.

Additional Locations (1)

Fix in Cursor Fix in Web

}

if ($this->assets->isEmpty()) {
$results = $this->assetsFromContainer($id, $path);
$results = $this->applyPostQueryFilters($results);

if ($results instanceof \Illuminate\Support\Collection && $results->isEmpty()) {
return $this->parseNoResults();
}

return $this->output();
return $this->output($results);
}

protected function assetsFromContainer($id, $path)
{
if (! $id && ! $path) {
\Log::debug('No asset container ID or path was specified.');
Log::debug('No asset container ID or path was specified.');

return collect();
}
Expand All @@ -95,9 +109,78 @@ protected function assetsFromContainer($id, $path)
return collect();
}

$assets = $container->assets($this->params->get('folder'), $this->params->get('recursive', false));
$query = $container->queryAssets();

$this->queryFolder($query);
$this->queryConditions($query);
$this->queryScopes($query);
$this->queryOrderBys($query);

return $this->results($query);
}

protected function queryConditions($query)
{
$this->queryableConditionParams()->each(function ($value, $param) use ($query) {
$field = explode(':', $param)[0];
$condition = explode(':', $param)[1] ?? false;
$value = $this->getQueryConditionValue($value, $field);
$fields = $this->queryConditionFields($field);

if (count($fields) === 1) {
$this->queryCondition($query, $fields[0], $condition, $value);

return $this->filterByType($assets);
return;
}

$query->where(function ($query) use ($fields, $condition, $value) {
$this->queryCondition($query, Arr::pull($fields, 0), $condition, $value);

foreach ($fields as $field) {
$query->orWhere(function ($query) use ($field, $condition, $value) {
$this->queryCondition($query, $field, $condition, $value);
});
}
});
});
}

protected function queryConditionFields($field): array
{
if (Str::contains($field, ['.', '->']) || $this->isNativeAssetConditionField($field)) {
return [$field];
}

if (! Site::hasMultiple()) {
return [$field, "data->{$field}"];
}

$site = Site::current()->handle();

return [$field, "data->{$field}", "data->{$site}->{$field}"];
}

protected function isNativeAssetConditionField(string $field): bool
{
return in_array($field, [
'id',
'container',
'path',
'basename',
'folder',
'filename',
'extension',
'is_image',
'is_video',
'is_audio',
'size',
'last_modified',
'height',
'width',
'mime_type',
'duration',
'ratio',
]);
}

protected function assetsFromCollection($collection)
Expand Down Expand Up @@ -159,22 +242,6 @@ protected function filterByType($value)
});
}

/**
* Filter out assets from a requested folder.
*
* @return void
*/
private function filterNotIn()
{
if ($not_in = $this->params->get('not_in')) {
$regex = '#^('.$not_in.')#';

$this->assets = $this->assets->reject(function ($path) use ($regex) {
return preg_match($regex, $path);
});
}
}

/**
* Perform the asset lookups.
*
Expand Down Expand Up @@ -204,20 +271,24 @@ protected function assets($urls)
];
});

return $this->output();
return $this->outputCollection($this->assets);
}

private function output()
private function outputCollection($assets)
{
$this->filterNotIn();
$this->assets = $this->applyPostCollectionFilters($assets);

$this->sortCollection();
$this->limitCollection();

$this->sort();
$this->limit();
if ($this->assets->isEmpty()) {
return $this->parseNoResults();
}

return $this->assets;
}

private function sort()
private function sortCollection()
{
if ($sort = $this->params->get('sort')) {
$this->assets = $this->assets->multisort($sort);
Expand All @@ -227,7 +298,7 @@ private function sort()
/**
* Limit and offset the asset collection.
*/
private function limit()
private function limitCollection()
{
$limit = $this->params->int('limit');
$limit = ($limit == 0) ? $this->assets->count() : $limit;
Expand All @@ -236,9 +307,77 @@ private function limit()
$this->assets = $this->assets->splice($offset, $limit);
}

protected function queryFolder($query)
{
$folder = $this->params->get('folder');
$recursive = $this->params->get('recursive', false);

if ($folder === '/' && $recursive) {
$folder = null;
}

if ($folder === null) {
return;
}

if ($recursive) {
$query->where('path', 'like', Pattern::sqlLikeQuote($folder).'/%');

return;
}

$query->where('folder', $folder);
}

protected function applyPostQueryFilters($results)
{
if ($results instanceof AbstractPaginator) {
$results->setCollection($this->applyPostCollectionFilters($results->getCollection())->values());

return $results;
}

if ($results instanceof Chunks) {
return $results->map(function ($chunk) {
return $this->applyPostCollectionFilters($chunk)->values();
});
}

if ($results instanceof \Illuminate\Support\Collection) {
return $this->applyPostCollectionFilters($results);
}

return $results;
}

protected function applyPostCollectionFilters($assets)
{
return $this->filterNotIn($this->filterByType($assets));
}

private function isAssetsFieldValue($value)
{
return $value instanceof Value
&& optional($value->fieldtype())->handle() === 'assets';
}

/**
* Filter out assets from a requested folder.
*/
private function filterNotIn($assets)
{
if (! $not_in = $this->params->get('not_in')) {
return $assets;
}

$regex = '#^('.$not_in.')#';

return $assets->reject(function ($asset) use ($regex) {
$path = method_exists($asset, 'path')
? $asset->path()
: (string) $asset;

return preg_match($regex, $path);
});
}
}
Loading