Snowboard's Url utility mangles protocol-relative asset URLs, hanging AJAX handlers
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 1.5k
- Forks
- 246
- Avg merge
- 19h 2m
- Merged PRs (30d)
- 7
Description
Winter CMS Build
dev-develop
PHP Version
8.4
Database engine
MySQL/MariaDB
Plugins installed
Winter.Location, LukeTowers.EasyAudit
Issue description
Snowboard's Url utility does not recognise protocol-relative URLs (//example.com/script.js). Both to() and asset() test the URL against a regex that requires an explicit scheme, and when it doesn't match they strip all leading slashes and resolve the remainder against the site:
const urlRegex = /^(?:[^:]+:\/\/)[-a-z0-9@:%._+~#=]{1,256}\b([-a-z0-9()@:%_+.~#?&//=]*)/i;
if (url.match(urlRegex)) {
return url;
}
const theUrl = url.replace(/^\/+/, ''); // <- strips both slashes of "//host/path"
return `${this.assetUrl()}${theUrl}`;
So //maps.googleapis.com/maps/api/js becomes https://mysite.test/maps.googleapis.com/maps/api/js.
This is mostly invisible until an asset is injected over AJAX, because AssetMaker::getAssetPath() and getAssetScheme() on the PHP side both pass protocol-relative URLs through untouched, so a full page render emits a working <script src="//host/...">. It's only when the same asset arrives via X_WINTER_ASSETS and goes through AssetLoader.loadScript() → Url.asset() that it gets rewritten.
The failure is worse than a 404 on an unused asset: AssetLoader.loadScript() rejects its promise when the script errors, and handleUpdateResponse awaits it, so the AJAX handler never completes. In my case that left a popup spinning forever with no visible error other than the 404 in the console.
This affects Winter.Location's AddressFinder form widget out of the box, which registers the Maps API this way:
https://github.com/wintercms/wn-location-plugin/blob/main/formwidgets/AddressFinder.php#L92
$this->addJs('//maps.googleapis.com/maps/api/js?libraries=places&key='.$apiKey);
So any backend form containing an AddressFinder breaks on its first AJAX request. I hit it via LukeTowers.EasyAudit, whose audit log tab opens a popup over AJAX, but any handler on such a form would do it. I'm submitting a PR against that plugin to switch the widget to https://, but the underlying Url behaviour seems worth fixing regardless — a protocol-relative URL is perfectly valid and is being silently turned into a same-origin path.
Observed console output:
GET https://mysite.test/maps.googleapis.com/maps/api/js?libraries=places&key=… net::ERR_ABORTED 404 (Not Found)
at loadScript (system.js)
at load (system.js)
at handleUpdateResponse (framework.js:398)
Uncaught (in promise) Error: Unable to load script file:
"https://mysite.test/maps.googleapis.com/maps/api/js?libraries=places&key=…"
Steps to replicate
- In a backend controller, register a protocol-relative asset, e.g.
$this->addJs('//code.jquery.com/jquery-3.7.1.min.js'); - Trigger any AJAX handler on that page.
- Observe the request for
https://<site>/code.jquery.com/jquery-3.7.1.min.jsreturning 404, and the handler's response never being applied.
The same result can be reproduced against the regex alone:
const urlRegex = /^(?:[^:]+:\/\/)[-a-z0-9@:%._+~#=]{1,256}\b([-a-z0-9()@:%_+.~#?&//=]*)/i;
'//code.jquery.com/jquery-3.7.1.min.js'.match(urlRegex); // null
Workaround
Registering the asset with an explicit scheme (https://…) avoids it, since the URL then matches the regex and is returned unchanged.
Suggested fix
Return protocol-relative URLs unchanged in both to() and asset(), alongside the existing check:
if (url.startsWith('//') || url.match(urlRegex)) {
return url;
}
Happy to put a PR together for this if the approach looks right — I wasn't sure whether you'd prefer returning it as-is or expanding it to the page's current protocol.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in modules/system/assets/js/snowboard/utilities/Url.js, focusing on the to() and asset() methods and their URL checks. Reproduce the protocol-relative URL case from the issue, then verify that both methods preserve it and that AJAX-loaded assets no longer become same-origin paths or leave the handler hanging.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, php
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100