Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: encode URLs correctly (fix #15298) #15311

Merged
merged 7 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions docs/guide/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,5 @@ experimental: {
}
}
```

Note that the `filename` passed is a decoded URL, and if the function returns a URL string, it should also be decoded. Vite will handle the encoding automatically when rendering the URLs. If an object with `runtime` is returned, encoding should be handled yourself where needed as the runtime code will be rendered as is.
15 changes: 11 additions & 4 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
emptyDir,
joinUrlSegments,
normalizePath,
partialEncodeURI,
requireResolveFromRootWithFallback,
} from './utils'
import { manifestPlugin } from './plugins/manifest'
Expand Down Expand Up @@ -1092,7 +1093,7 @@ const getResolveUrl = (path: string, URL = 'URL') => `new ${URL}(${path}).href`

const getRelativeUrlFromDocument = (relativePath: string, umd = false) =>
getResolveUrl(
`'${escapeId(relativePath)}', ${
`'${escapeId(partialEncodeURI(relativePath))}', ${
umd ? `typeof document === 'undefined' ? location.href : ` : ''
}document.currentScript && document.currentScript.src || document.baseURI`,
)
Expand All @@ -1118,11 +1119,15 @@ const relativeUrlMechanisms: Record<
relativePath,
)} : ${getRelativeUrlFromDocument(relativePath)})`,
es: (relativePath) =>
getResolveUrl(`'${escapeId(relativePath)}', import.meta.url`),
getResolveUrl(
`'${escapeId(partialEncodeURI(relativePath))}', import.meta.url`,
),
iife: (relativePath) => getRelativeUrlFromDocument(relativePath),
// NOTE: make sure rollup generate `module` params
system: (relativePath) =>
getResolveUrl(`'${escapeId(relativePath)}', module.meta.url`),
getResolveUrl(
`'${escapeId(partialEncodeURI(relativePath))}', module.meta.url`,
),
umd: (relativePath) =>
`(typeof document === 'undefined' && typeof location === 'undefined' ? ${getFileUrlFromRelativePath(
relativePath,
Expand All @@ -1133,7 +1138,9 @@ const relativeUrlMechanisms: Record<
const customRelativeUrlMechanisms = {
...relativeUrlMechanisms,
'worker-iife': (relativePath) =>
getResolveUrl(`'${escapeId(relativePath)}', self.location.href`),
getResolveUrl(
`'${escapeId(partialEncodeURI(relativePath))}', self.location.href`,
),
} as const satisfies Record<string, (relativePath: string) => string>

export type RenderBuiltAssetUrl = (
Expand Down
8 changes: 5 additions & 3 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function renderAssetUrlInJS(
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(match.index, match.index + full.length, replacementString)
}
Expand All @@ -123,7 +123,7 @@ export function renderAssetUrlInJS(
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(match.index, match.index + full.length, replacementString)
}
Expand Down Expand Up @@ -205,7 +205,9 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
}
}

return `export default ${JSON.stringify(url)}`
return `export default ${JSON.stringify(
url.startsWith('data:') ? url : encodeURI(url),
)}`
},

renderChunk(code, chunk, opts) {
Expand Down
34 changes: 19 additions & 15 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,13 +593,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, postfix = '') => {
const filename = this.getFileName(fileHash) + postfix
chunk.viteMetadata!.importedAssets.add(cleanUrl(filename))
return toOutputFilePathInCss(
filename,
'asset',
cssAssetName,
'css',
config,
toRelative,
return encodeURI(
toOutputFilePathInCss(
filename,
'asset',
cssAssetName,
'css',
config,
toRelative,
),
)
})
// resolve public URL from CSS paths
Expand All @@ -610,13 +612,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)
chunkCSS = chunkCSS.replace(publicAssetUrlRE, (_, hash) => {
const publicUrl = publicAssetUrlMap.get(hash)!.slice(1)
return toOutputFilePathInCss(
publicUrl,
'public',
cssAssetName,
'css',
config,
() => `${relativePathToPublicFromCSS}/${publicUrl}`,
return encodeURI(
toOutputFilePathInCss(
publicUrl,
'public',
cssAssetName,
'css',
config,
() => `${relativePathToPublicFromCSS}/${publicUrl}`,
),
)
})
}
Expand Down Expand Up @@ -711,7 +715,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(start, end, replacementString)
}
Expand Down
35 changes: 22 additions & 13 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isDataUrl,
isExternalUrl,
normalizePath,
partialEncodeURI,
processSrcSet,
removeLeadingSlash,
urlCanParse,
Expand Down Expand Up @@ -436,7 +437,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
overwriteAttrValue(
s,
sourceCodeLocation!,
toOutputPublicFilePath(url),
partialEncodeURI(toOutputPublicFilePath(url)),
)
}

Expand Down Expand Up @@ -488,22 +489,24 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
if (attrKey === 'srcset') {
assetUrlsPromises.push(
(async () => {
const processedUrl = await processSrcSet(
const processedEncodedUrl = await processSrcSet(
p.value,
async ({ url }) => {
const decodedUrl = decodeURI(url)
if (!isExcludedUrl(decodedUrl)) {
const result = await processAssetUrl(url)
return result !== decodedUrl ? result : url
return result !== decodedUrl
? encodeURI(result)
: url
}
return url
},
)
if (processedUrl !== p.value) {
if (processedEncodedUrl !== p.value) {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
processedUrl,
processedEncodedUrl,
)
}
})(),
Expand All @@ -514,7 +517,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
toOutputPublicFilePath(url),
partialEncodeURI(toOutputPublicFilePath(url)),
)
} else if (!isExcludedUrl(url)) {
if (
Expand Down Expand Up @@ -560,7 +563,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
overwriteAttrValue(
s,
getAttrSourceCodeLocation(node, attrKey),
processedUrl,
partialEncodeURI(processedUrl),
)
}
})(),
Expand Down Expand Up @@ -633,9 +636,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
// emit <script>import("./aaa")</script> asset
for (const { start, end, url } of scriptUrls) {
if (checkPublicFile(url, config)) {
s.update(start, end, toOutputPublicFilePath(url))
s.update(start, end, partialEncodeURI(toOutputPublicFilePath(url)))
} else if (!isExcludedUrl(url)) {
s.update(start, end, await urlToBuiltUrl(url, id, config, this))
s.update(
start,
end,
partialEncodeURI(await urlToBuiltUrl(url, id, config, this)),
)
}
}

Expand Down Expand Up @@ -897,17 +904,19 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
if (chunk) {
chunk.viteMetadata!.importedAssets.add(cleanUrl(file))
}
return toOutputAssetFilePath(file) + postfix
return encodeURI(toOutputAssetFilePath(file)) + postfix
})

result = result.replace(publicAssetUrlRE, (_, fileHash) => {
const publicAssetPath = toOutputPublicAssetFilePath(
getPublicAssetFilename(fileHash, config)!,
)

return urlCanParse(publicAssetPath)
? publicAssetPath
: normalizePath(publicAssetPath)
return encodeURI(
urlCanParse(publicAssetPath)
? publicAssetPath
: normalizePath(publicAssetPath),
)
})

if (chunk && canInlineEntry) {
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
joinUrlSegments,
moduleListContains,
normalizePath,
partialEncodeURI,
prettifyUrl,
removeImportQuery,
removeTimestampQuery,
Expand Down Expand Up @@ -591,7 +592,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
rewriteDone = true
}
if (!rewriteDone) {
const rewrittenUrl = JSON.stringify(url)
const rewrittenUrl = JSON.stringify(partialEncodeURI(url))
const s = isDynamicImport ? start : start - 1
const e = isDynamicImport ? end : end + 1
str().overwrite(s, e, rewrittenUrl, {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement).slice(1, -1)
? JSON.stringify(encodeURI(replacement)).slice(1, -1)
: `"+${replacement.runtime}+"`
s.update(match.index, match.index + full.length, replacementString)
}
Expand Down
10 changes: 10 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ function joinSrcset(ret: ImageCandidate[]) {
.join(', ')
}

// NOTE: The returned `url` should perhaps be decoded so all handled URLs within Vite are consistently decoded.
// However, this may also require a refactor for `cssReplacer` to accept decoded URLs instead.
function splitSrcSetDescriptor(srcs: string): ImageCandidate[] {
return splitSrcSet(srcs)
.map((s) => {
Expand Down Expand Up @@ -1407,3 +1409,11 @@ export function displayTime(time: number): string {
// display: {X}m {Y}s
return `${mins}m${seconds < 1 ? '' : ` ${seconds.toFixed(0)}s`}`
}

/**
* Like `encodeURI`, but only replacing `%` as `%25`. This is useful for environments
* that can handle un-encoded URIs, where `%` is the only ambiguous character.
*/
export function partialEncodeURI(uri: string): string {
return uri.replaceAll('%', '%25')
}
4 changes: 2 additions & 2 deletions playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,11 @@ test('?url import on css', async () => {

describe('unicode url', () => {
test('from js import', async () => {
const src = readFile('テスト-測試-white space.js')
const src = readFile('テスト-測試-white space%.js')
expect(await page.textContent('.unicode-url')).toMatch(
isBuild
? `data:text/javascript;base64,${Buffer.from(src).toString('base64')}`
: `/foo/bar/テスト-測試-white space.js`,
: encodeURI(`/foo/bar/テスト-測試-white space%.js`),
)
})
})
Expand Down
6 changes: 3 additions & 3 deletions playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ <h2>CSS url references</h2>
<h2>Unicode URL</h2>
<div>
<code class="unicode-url"></code>
<img src="./nested/テスト-測試-white space.png" />
<img src="./nested/テスト-測試-white space%25.png" />
</div>

<h2>Filename including single quote</h2>
Expand All @@ -147,7 +147,7 @@ <h2>encodeURI for the address</h2>
<div>
<img
class="encodeURI"
src="./nested/%E3%83%86%E3%82%B9%E3%83%88-%E6%B8%AC%E8%A9%A6-white%20space.png"
src="./nested/%E3%83%86%E3%82%B9%E3%83%88-%E6%B8%AC%E8%A9%A6-white%20space%25.png"
/>
</div>

Expand Down Expand Up @@ -442,7 +442,7 @@ <h3>assets in noscript</h3>
import fooUrl from './foo.js?url'
text('.url', fooUrl)

import unicodeUrl from './テスト-測試-white space.js?url'
import unicodeUrl from './テスト-測試-white space%.js?url'
text('.unicode-url', unicodeUrl)

import filenameIncludingSingleQuoteUrl from "./nested/with-single'quote.png"
Expand Down