Skip to content

Commit 07e0fa5

Browse files
Add cloudinary-safe url encoding (#210)
1 parent 292d17d commit 07e0fa5

File tree

4 files changed

+153
-6
lines changed

4 files changed

+153
-6
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import CloudinaryConfig from "../../../src/config/CloudinaryConfig";
2+
import {TransformableImage} from "../../../src";
3+
import {Overlay} from "../../../src/actions/Actions";
4+
import {image, text} from "../../../src/values/sources/Sources";
5+
6+
7+
const CONFIG_INSTANCE = new CloudinaryConfig({
8+
cloud: {
9+
cloudName: 'demo'
10+
}
11+
});
12+
13+
14+
describe('Tests for Encoding the URL', () => {
15+
it ('Should serialize, but not encode, when calling toString', () => {
16+
const str = new TransformableImage()
17+
.overlay(Overlay.imageLayer(text('he llo')))
18+
.toString();
19+
expect(str).toBe('l_text:he llo/fl_layer_apply');
20+
});
21+
22+
it ('Should encode cloudinary characters (",") in a publicID', () => {
23+
const url = new TransformableImage('sam,ple')
24+
.setConfig(CONFIG_INSTANCE)
25+
.toURL();
26+
expect(url).toBe('https://res.cloudinary.com/demo/image/upload/sam%252Cple');
27+
});
28+
29+
it ('Does not mutate valid / in publicID', () => {
30+
const url = new TransformableImage('folder/name')
31+
.setConfig(CONFIG_INSTANCE)
32+
.toURL();
33+
expect(url).toBe('https://res.cloudinary.com/demo/image/upload/v1/folder/name');
34+
});
35+
36+
it ('Should encode regular text in a textLayer', () => {
37+
const url = new TransformableImage('sample')
38+
.setConfig(CONFIG_INSTANCE)
39+
.overlay(Overlay.imageLayer(text('he llo').fontFamily('arial').fontSize(50)))
40+
.toURL();
41+
42+
expect(url)
43+
// Space encoded correctly to %20
44+
.toBe('https://res.cloudinary.com/demo/image/upload/l_text:arial_50:he%20llo/fl_layer_apply/sample');
45+
});
46+
47+
it ('Should encode font name in textOverlay', () => {
48+
const tx = new TransformableImage('sample')
49+
.setConfig(CONFIG_INSTANCE)
50+
.overlay(Overlay.imageLayer(text('he llo').fontFamily('roboto condensed').fontSize(50)));
51+
52+
expect(tx.toString())
53+
// Correctly serialize the cloudinary control characters
54+
.toBe('l_text:roboto condensed_50:he llo/fl_layer_apply');
55+
56+
expect(tx.toURL())
57+
// Space encoded correctly to %20
58+
.toBe('https://res.cloudinary.com/demo/image/upload/l_text:roboto%20condensed_50:he%20llo/fl_layer_apply/sample');
59+
});
60+
61+
it ('Should encode cloudinary characters ("/,") in the text of a textLayer', () => {
62+
const tx = new TransformableImage('sample')
63+
.setConfig(CONFIG_INSTANCE)
64+
.overlay(Overlay.imageLayer(text('he,/ llo').fontFamily('arial').fontSize(50)));
65+
66+
expect(tx.toString())
67+
// Correctly serialize the cloudinary control characters
68+
.toBe('l_text:arial_50:he%2C%2F llo/fl_layer_apply');
69+
70+
expect(tx.toURL())
71+
// Add URL encoding on top of serialization
72+
.toBe('https://res.cloudinary.com/demo/image/upload/l_text:arial_50:he%252C%252F%20llo/fl_layer_apply/sample');
73+
});
74+
75+
76+
77+
it ('Fetch: should not encode ("$:/") signs', () => {
78+
const url = new TransformableImage('https://res.cloudinary.com/demo/image/upload/sample?a=b')
79+
.setConfig(CONFIG_INSTANCE)
80+
.describeAsset({
81+
assetType: 'image',
82+
storageType: 'fetch'
83+
})
84+
.toURL();
85+
86+
expect(url)
87+
.toBe('https://res.cloudinary.com/demo/image/fetch/https://res.cloudinary.com/demo/image/upload/sample%3Fa%3Db');
88+
});
89+
90+
it ('Should ', () => {
91+
const url = new TransformableImage('https://www.youtube.com/watch?v=d9NF2edxy-M')
92+
.setConfig(CONFIG_INSTANCE)
93+
.describeAsset({
94+
assetType: 'image',
95+
storageType: 'youtube'
96+
})
97+
.toURL();
98+
99+
expect(url)
100+
.toBe('https://res.cloudinary.com/demo/image/youtube/https://www.youtube.com/watch%3Fv%3Dd9NF2edxy-M');
101+
});
102+
103+
it ('Should encode a space in publicID', () => {
104+
const url = new TransformableImage('sa mple')
105+
.setConfig(CONFIG_INSTANCE)
106+
.toURL();
107+
108+
expect(url)
109+
.toBe('https://res.cloudinary.com/demo/image/upload/sa%20mple');
110+
});
111+
112+
it('should serialize nested texts correctly (text inside an image inside an image)', () => {
113+
const tx = new TransformableImage('woman')
114+
.setConfig(CONFIG_INSTANCE)
115+
.overlay(Overlay.imageLayer(
116+
image('sample')
117+
.overlay(Overlay.imageLayer(text('he,/llo').fontFamily('arial').fontSize(50)))
118+
));
119+
120+
expect(tx.toString())
121+
.toBe('l_sample/l_text:arial_50:he%2C%2Fllo/fl_layer_apply/fl_layer_apply');
122+
123+
expect(tx.toURL())
124+
.toBe('https://res.cloudinary.com/demo/image/upload/l_sample/l_text:arial_50:he%252C%252Fllo/fl_layer_apply/fl_layer_apply/woman');
125+
});
126+
});

src/url/cloudinaryURL.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ function createCloudinaryURL(config: ICloudinaryConfigurations, descriptor?: IDe
2121
const signature = descriptor.signature;
2222
const transformationString = transformation ? transformation.toString() : '';
2323
const version = getUrlVersion(config.url, descriptor);
24-
const publicID = descriptor.publicID;
24+
25+
const publicID = descriptor.publicID
26+
// Serialize the publicID, but leave slashes alone.
27+
// we can't use serializeCloudinaryCharacters because that does both things.
28+
.replace(/,/g, '%2C');
2529

2630
const url = [prefix, assetType, storageType, signature, transformationString, version, publicID]
2731
.filter((a) => a)
28-
.join('/')
29-
.replace(' ', '%20');
32+
.join('/');
3033

31-
return url;
34+
return encodeURI(url)
35+
.replace(/\?/g, '%3F')
36+
.replace(/=/g, '%3D');
3237
}
3338

3439
/**
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
*
3+
* @description - Replace (,) and (/) in a string to its URL encoded equivalence
4+
* @param {string} str
5+
* @private
6+
*/
7+
function serializeCloudinaryCharacters(str = ''): string {
8+
return str
9+
.replace(/,/g, '%2C')
10+
.replace(/\//g, '%2F');
11+
}
12+
13+
export {
14+
serializeCloudinaryCharacters
15+
};

src/values/sources/sourceTypes/TextSource.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {ISource} from "../ISource";
22
import {prepareColor} from "../../../utils/prepareColor";
33
import QualifierValue from "../../../qualifier/QualifierValue";
4+
import {serializeCloudinaryCharacters} from "../../../utils/serializeCloudinaryCharacters";
45

56
/**
67
* @description Defines how to manipulate a text layer
@@ -84,11 +85,11 @@ class TextSource implements ISource {
8485
* @return {string}
8586
*/
8687
getSource(): string {
87-
const fontValue = new QualifierValue([this.fFamily, this.fSize, this.fWeight, this.fStyle])
88+
const fontValue = new QualifierValue([serializeCloudinaryCharacters(this.fFamily), this.fSize, this.fWeight, this.fStyle])
8889
.setDelimiter('_')
8990
.toString();
9091

91-
const fontAndText = new QualifierValue(['text', fontValue, this.innerText])
92+
const fontAndText = new QualifierValue(['text', fontValue, serializeCloudinaryCharacters(this.innerText)])
9293
.setDelimiter(':')
9394
.toString();
9495

0 commit comments

Comments
 (0)