Skip to content

findByUrl() does not decode percent-encoded paths, returning null for filenames with spaces, parentheses or @ #609

Description

@DonnyOexman

Bug description

Statamic\Eloquent\Assets\AssetRepository::findByUrl() looks up the asset path without decoding it first, unlike the core repository it replaces. Asset URLs are percent-encoded, so any asset whose filename contains a character that gets encoded (spaces, parentheses, @, accented characters, commas) cannot be resolved from its own URL and returns null.

Expected: Asset::find($asset->url()) returns the asset it was given.
Actual: it returns null for any filename that needs URL encoding.

The cause is a single missing call. Core (statamic/cms, src/Assets/AssetRepository.php:62):

$path = rawurldecode(Str::after($url, $containerUrl));

Eloquent driver (src/Assets/AssetRepository.php:56):

$path = Str::after($url, $containerUrl);

Without the decode, findById() is handed my%20file%20%281%29.jpg and queries the database for that literal basename, which matches no row.

In our production install this made every page carrying such an asset as its Open Graph image return a 500. See "Additional details" for why a failed lookup escalates that far.

How to reproduce

  1. Enable the eloquent driver for assets.
  2. Upload an asset with a space, parenthesis or @ in its filename, e.g. my file (1).jpg.
  3. Resolve that asset from its own URL:
$asset = Asset::find('assets::my file (1).jpg');

$asset->url();                      // "/assets/my%20file%20%281%29.jpg"
Asset::find($asset->url());         // null  <- expected the same asset back

Any filename that survives a round trip through url() unencoded works fine, which is why this stays hidden until someone uploads a file with a space in it.

To see it take down a page, put such an asset in SEO Pro's Open Graph image field and request any page that uses it.

Logs

Call to a member function isVideo() on null
Error: Call to a member function isVideo() on null at /var/www/html/vendor/statamic/cms/src/Imaging/ImageGenerator.php:158

#0 /var/www/html/vendor/statamic/cms/src/Tags/Glide.php(180): Statamic\Imaging\ImageGenerator->generateByAsset()
#1 /var/www/html/vendor/statamic/cms/src/Tags/Glide.php(139): Statamic\Tags\Glide->generateImage()
#2 [internal function]: Statamic\Tags\Glide->{closure:Statamic\Tags\Glide::generate():134}()
#5 /var/www/html/vendor/statamic/cms/src/Tags/Glide.php(134): Illuminate\Support\Collection->map()
#6 [internal function]: Statamic\Tags\Glide->generate()
#17 /var/www/html/vendor/statamic/seo-pro/src/RendersMetaHtml.php(17): Illuminate\View\View->render()
#18 /var/www/html/vendor/statamic/seo-pro/src/Tags/SeoProTags.php(31): Statamic\SeoPro\Tags\SeoProTags->renderMetaHtml()
#30 /var/www/html/vendor/statamic/cms/src/Http/Responses/DataResponse.php(155): Statamic\View\View->render()
#32 /var/www/html/vendor/statamic/cms/src/Entries/Entry.php(614): Statamic\Http\Responses\DataResponse->toResponse()

Environment

Environment
Laravel Version: 12.68.0
PHP Version: 8.5.2
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: nl

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: file
Database: mysql
Logs: stack / single
Mail: log
Queue: sync
Session: file

Storage
public/storage: LINKED

Statamic
Addons: 7
License Key: Not set
Sites: 19
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.29.0 PRO

Statamic Addons
appswithlove/statamic-one-click-content-translation: 6.0.6
eminos/statamic-tabs: 2.0.1
keyagency/content-importer: dev-main
keyagency/kai-personalize: 1.2.12
keyagency/statamic-asset-usage: 1.0.1
statamic/eloquent-driver: 5.11.0
statamic/seo-pro: 7.13.3

Statamic Eloquent Driver
Addon Settings: eloquent
Asset Containers: file
Assets: eloquent
Blueprints: eloquent
Collection Trees: eloquent
Collections: eloquent
Entries: eloquent
Fieldsets: file
Form Submissions: eloquent
Forms: eloquent
Global Sets: eloquent
Global Variables: eloquent
Navigation Trees: eloquent
Navigations: eloquent
Revisions: eloquent
Sites: eloquent
Taxonomies: eloquent
Terms: eloquent
Tokens: eloquent

Additional details

Statamic\Tags\Glide::generateImage() passes the result straight through:

return $this->getGenerator()->generateByAsset(Asset::find($item), $params);

ImageGenerator::generateByAsset() has no null check, so line 158 throws Error: Call to a member function isVideo() on null. Because that is an \Error and Glide::generate() only catches \Exception, it escapes the tag's own error handling and takes down the entire response instead of just skipping the image.

The path that reaches this in practice is an Asset instance passed to the glide tag: normalizeItem() returns it unchanged, and Asset::find() then casts it to a string via Asset::__toString(), which yields the encoded url(). SEO Pro's meta.antlers.html does exactly this for og:image, so a single asset with a space in its name 500s every page that uses it. Static caching never stores the error response, so it recurs on every request.

The missing null check in ImageGenerator is arguably a second bug, but it lives in statamic/cms rather than in this repository.

Suggested fix

Restore the decode to match core:

-        $path = Str::after($url, $containerUrl);
+        $path = rawurldecode(Str::after($url, $containerUrl));

We are running that change as a local subclass of the repository and it resolves all affected assets, with no other behaviour changing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    assetsbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions