Skip to content

Commit afbddcb

Browse files
committed
🐛 Fix: url encode bug
ISSUES CLOSED: Molunerfinn/PicGo#1351
1 parent 7868866 commit afbddcb

5 files changed

Lines changed: 42 additions & 16 deletions

File tree

src/plugins/uploader/aliyun.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import crypto from 'crypto'
33
import mime from 'mime-types'
44
import { IBuildInEvent } from '../../utils/enum'
55
import { ILocalesKey } from '../../i18n/zh-CN'
6+
import { handleUrlPathSafeEncode } from '../../utils/common'
67

78
// generate OSS signature
89
const generateSignature = (options: IAliyunConfig, fileName: string): string => {
@@ -17,9 +18,10 @@ const generateSignature = (options: IAliyunConfig, fileName: string): string =>
1718
}
1819

1920
const postOptions = (options: IAliyunConfig, fileName: string, signature: string, image: Buffer): IOldReqOptionsWithFullResponse => {
21+
const path = options.path || ''
2022
return {
2123
method: 'PUT',
22-
url: `https://${options.bucket}.${options.area}.aliyuncs.com/${encodeURI(options.path)}${encodeURIComponent(fileName)}`,
24+
url: `https://${options.bucket}.${options.area}.aliyuncs.com/${handleUrlPathSafeEncode(path, fileName)}`,
2325
headers: {
2426
Host: `${options.bucket}.${options.area}.aliyuncs.com`,
2527
Authorization: signature,
@@ -39,7 +41,7 @@ const handle = async (ctx: IPicGo): Promise<IPicGo> => {
3941
try {
4042
const imgList = ctx.output
4143
const customUrl = aliYunOptions.customUrl
42-
const path = aliYunOptions.path
44+
const path = aliYunOptions.path || ''
4345
for (const img of imgList) {
4446
if (img.fileName && img.buffer) {
4547
const signature = generateSignature(aliYunOptions, img.fileName)
@@ -54,9 +56,9 @@ const handle = async (ctx: IPicGo): Promise<IPicGo> => {
5456
delete img.buffer
5557
const optionUrl = aliYunOptions.options || ''
5658
if (customUrl) {
57-
img.imgUrl = `${customUrl}/${encodeURI(path)}${encodeURIComponent(img.fileName)}${optionUrl}`
59+
img.imgUrl = `${customUrl}/${handleUrlPathSafeEncode(path, img.fileName)}${optionUrl}`
5860
} else {
59-
img.imgUrl = `https://${aliYunOptions.bucket}.${aliYunOptions.area}.aliyuncs.com/${encodeURI(path)}${encodeURIComponent(img.fileName)}${optionUrl}`
61+
img.imgUrl = `https://${aliYunOptions.bucket}.${aliYunOptions.area}.aliyuncs.com/${handleUrlPathSafeEncode(path, img.fileName)}${optionUrl}`
6062
}
6163
} else {
6264
throw new Error('Upload failed')

src/plugins/uploader/github.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { IPicGo, IPluginConfig, IGithubConfig, IOldReqOptionsWithJSON } from '..
22
import { IBuildInEvent } from '../../utils/enum'
33
import { ILocalesKey } from '../../i18n/zh-CN'
44
import mime from 'mime-types'
5+
import { handleUrlPathSafeEncode } from '../../utils/common'
56

67
const postOptions = (fileName: string, options: IGithubConfig, data: any): IOldReqOptionsWithJSON => {
78
const path = options.path || ''
89
const { token, repo } = options
910
return {
1011
method: 'PUT',
11-
url: `https://api.github.com/repos/${repo}/contents/${encodeURI(path)}${encodeURIComponent(fileName)}`,
12+
url: `https://api.github.com/repos/${repo}/contents/${handleUrlPathSafeEncode(path, fileName)}`,
1213
headers: {
1314
Authorization: `token ${token}`,
1415
'User-Agent': 'PicGo',
@@ -46,7 +47,7 @@ const handle = async (ctx: IPicGo): Promise<IPicGo> => {
4647
delete img.base64Image
4748
delete img.buffer
4849
if (githubOptions.customUrl) {
49-
img.imgUrl = `${githubOptions.customUrl}/${encodeURI(githubOptions.path)}${encodeURIComponent(img.fileName)}`
50+
img.imgUrl = `${githubOptions.customUrl}/${handleUrlPathSafeEncode(githubOptions.path || '', img.fileName)}`
5051
} else {
5152
img.imgUrl = body.content.download_url
5253
}
@@ -59,9 +60,9 @@ const handle = async (ctx: IPicGo): Promise<IPicGo> => {
5960
delete img.base64Image
6061
delete img.buffer
6162
if (githubOptions.customUrl) {
62-
img.imgUrl = `${githubOptions.customUrl}/${encodeURI(githubOptions.path)}${encodeURIComponent(img.fileName)}`
63+
img.imgUrl = `${githubOptions.customUrl}/${handleUrlPathSafeEncode(githubOptions.path || '', img.fileName)}`
6364
} else {
64-
img.imgUrl = `https://raw.githubusercontent.com/${githubOptions.repo}/${githubOptions.branch}/${encodeURI(githubOptions.path)}${encodeURIComponent(img.fileName)}`
65+
img.imgUrl = `https://raw.githubusercontent.com/${githubOptions.repo}/${githubOptions.branch}/${handleUrlPathSafeEncode(githubOptions.path || '', img.fileName)}`
6566
}
6667
} else {
6768
throw e

src/plugins/uploader/tcyun.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import mime from 'mime-types'
33
import { IPicGo, IPluginConfig, ITcyunConfig, IOldReqOptionsWithFullResponse } from '../../types'
44
import { IBuildInEvent } from '../../utils/enum'
55
import { ILocalesKey } from '../../i18n/zh-CN'
6+
import { handleUrlPathSafeEncode } from '../../utils/common'
67

78
// generate COS signature string
89

@@ -14,6 +15,9 @@ export interface ISignature {
1415
}
1516

1617
const cosSafeUrlEncode = (str: string): string => {
18+
// this must be encodeURIComponent instead of handleUrlEncode
19+
// cause this url encode is for content type
20+
// for example, application/json -> application%2Fjson
1721
return encodeURIComponent(str)
1822
.replace(/!/g, '%21')
1923
.replace(/'/g, '%27')
@@ -69,7 +73,7 @@ const postOptions = (options: ITcyunConfig, fileName: string, signature: ISignat
6973
if (!options.version || options.version === 'v4') {
7074
return {
7175
method: 'POST',
72-
url: `http://${area}.file.myqcloud.com/files/v2/${signature.appId}/${signature.bucket}/${encodeURI(path)}${fileName}`,
76+
url: `http://${area}.file.myqcloud.com/files/v2/${signature.appId}/${signature.bucket}/${handleUrlPathSafeEncode(path, fileName)}`,
7377
headers: {
7478
Host: `${area}.file.myqcloud.com`,
7579
Authorization: signature.signature,
@@ -88,7 +92,7 @@ const postOptions = (options: ITcyunConfig, fileName: string, signature: ISignat
8892

8993
return {
9094
method: 'PUT',
91-
url: `http://${options.bucket}.${endpoint}/${encodeURI(path)}${encodeURIComponent(fileName)}`,
95+
url: `http://${options.bucket}.${endpoint}/${handleUrlPathSafeEncode(path, fileName)}`,
9296
headers: {
9397
Host: `${options.bucket}.${endpoint}`,
9498
Authorization: `q-sign-algorithm=sha1&q-ak=${options.secretId}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=content-length;content-type;host&q-url-param-list=&q-signature=${signature.signature}`,
@@ -161,10 +165,10 @@ const handle = async (ctx: IPicGo): Promise<IPicGo | boolean> => {
161165
delete img.base64Image
162166
delete img.buffer
163167
if (customUrl) {
164-
img.imgUrl = `${customUrl}/${encodeURI(path)}${encodeURIComponent(img.fileName)}${optionUrl}`
168+
img.imgUrl = `${customUrl}/${encodeURI(path)}${handleUrlPathSafeEncode(img.fileName)}${optionUrl}`
165169
} else {
166170
const endpoint = tcYunOptions.endpoint ? tcYunOptions.endpoint : `cos.${tcYunOptions.area}.myqcloud.com`
167-
img.imgUrl = `https://${tcYunOptions.bucket}.${endpoint}/${encodeURI(path)}${encodeURIComponent(img.fileName)}${optionUrl}`
171+
img.imgUrl = `https://${tcYunOptions.bucket}.${endpoint}/${encodeURI(path)}${handleUrlPathSafeEncode(img.fileName)}${optionUrl}`
168172
}
169173
} else {
170174
throw new Error(res.body.msg)

src/plugins/uploader/upyun.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import crypto from 'crypto'
33
import MD5 from 'md5'
44
import { IBuildInEvent } from '../../utils/enum'
55
import { ILocalesKey } from '../../i18n/zh-CN'
6-
import { safeParse } from '../../utils/common'
6+
import { safeParse, handleUrlPathSafeEncode } from '../../utils/common'
77
import mime from 'mime-types'
88

99
// generate COS signature string
@@ -13,7 +13,7 @@ const generateSignature = (options: IUpyunConfig, fileName: string): string => {
1313
const password = options.password
1414
const md5Password = MD5(password)
1515
const date = new Date().toUTCString()
16-
const uri = `/${options.bucket}/${encodeURI(path)}${encodeURIComponent(fileName)}`
16+
const uri = `/${options.bucket}/${handleUrlPathSafeEncode(path, fileName)}`
1717
const value = `PUT&${uri}&${date}`
1818
const sign = crypto.createHmac('sha1', md5Password).update(value).digest('base64')
1919
return `UPYUN ${operator}:${sign}`
@@ -24,7 +24,7 @@ const postOptions = (options: IUpyunConfig, fileName: string, signature: string,
2424
const path = options.path || ''
2525
return {
2626
method: 'PUT',
27-
url: `https://v0.api.upyun.com/${bucket}/${encodeURI(path)}${encodeURIComponent(fileName)}`,
27+
url: `https://v0.api.upyun.com/${bucket}/${handleUrlPathSafeEncode(path, fileName)}`,
2828
headers: {
2929
Authorization: signature,
3030
Date: new Date().toUTCString(),
@@ -55,7 +55,7 @@ const handle = async (ctx: IPicGo): Promise<IPicGo> => {
5555
if (body.statusCode === 200) {
5656
delete img.base64Image
5757
delete img.buffer
58-
img.imgUrl = `${upyunOptions.url}/${encodeURI(path)}${encodeURIComponent(img.fileName)}${upyunOptions.options}`
58+
img.imgUrl = `${upyunOptions.url}/${handleUrlPathSafeEncode(path, img.fileName)}${upyunOptions.options}`
5959
} else {
6060
throw new Error('Upload failed')
6161
}

src/utils/common.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,32 @@ export const isUrlEncode = (url: string): boolean => {
2121
return false
2222
}
2323
}
24+
25+
/**
26+
* just encode the url with encodeURI
27+
*/
2428
export const handleUrlEncode = (url: string): string => {
2529
if (!isUrlEncode(url)) {
2630
url = encodeURI(url)
2731
}
2832
return url
2933
}
3034

35+
/**
36+
* @param urlPath the url path need to be encoded safely
37+
* @returns the safely encoded url path
38+
*
39+
* for example:
40+
*
41+
* /a b/, /c d.jpg -> /a%20b/c%20d.jpg
42+
*
43+
* /a/b, /c#d.jpg -> /a/b/c%23d.jpg
44+
*/
45+
export const handleUrlPathSafeEncode = (...urlPathList: string[]): string => {
46+
const urlPath = urlPathList.join('')
47+
return urlPath.replace(/\/{2,}/g, '/').split('/').map(segment => encodeURIComponent(segment)).join('/')
48+
}
49+
3150
export const getImageSize = (file: Buffer): IImgSize => {
3251
try {
3352
const { width = 0, height = 0, type } = imageSize(file)

0 commit comments

Comments
 (0)