Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions lib/gettext.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,32 @@ Gettext.prototype.warn = function(message) {
* @param {Object} translations An object of gettext-parser JSON shape
*/
Gettext.prototype.addTranslations = function(locale, domain, translations) {
if (typeof locale !== 'string') {
this.warn(
'You called addTranslations() with an argument of type ' + (typeof locale) + '. ' +
'The locale must be a string.'
);
return;
}

if (typeof domain !== 'string') {
this.warn(
'You called addTranslations() with an argument of type ' + (typeof domain) + '. ' +
'The domain must be a string.'
);
return;
}

if (locale in {}) {
this.warn('Can not use reserved key as locale');
return;
}

if (domain in {}) {
this.warn('Can not use reserved key as domain');
return;
}

if (!this.catalogs[locale]) {
this.catalogs[locale] = {};
}
Expand All @@ -135,6 +161,11 @@ Gettext.prototype.setLocale = function(locale) {
return;
}

if (locale in {}) {
this.warn('Can not use reserved key as locale');
return;
}

if (locale.trim() === '') {
this.warn('You called setLocale() with an empty value, which makes little sense.');
}
Expand Down Expand Up @@ -163,6 +194,11 @@ Gettext.prototype.setTextDomain = function(domain) {
return;
}

if (domain in {}) {
this.warn('Can not use reserved key as domain');
return;
}

if (domain.trim() === '') {
this.warn('You called setTextDomain() with an empty `domain` value.');
}
Expand Down
37 changes: 37 additions & 0 deletions test/gettext-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ describe('Gettext', function() {
expect(gt.catalogs['et-EE'].mydomain).to.exist;
expect(gt.catalogs['et-EE'].mydomain.charset).to.equal('iso-8859-13');
});

it('should not pollute Object.prototype via a reserved locale key', function() {
try {
gt.addTranslations('__proto__', 'polluted', 'pwned');
expect(Object.prototype.hasOwnProperty('polluted')).to.equal(false);
} finally {
delete Object.prototype.polluted;
}
});

it('should not rewrite a catalog prototype via a reserved domain key', function() {
gt.addTranslations('et-EE', '__proto__', jsonFile);
expect(gt.catalogs['et-EE']).to.not.exist;
});
});

describe('#setLocale', function() {
Expand Down Expand Up @@ -103,6 +117,13 @@ describe('Gettext', function() {
gt.setLocale();
expect(gt.locale).to.equal('');
});

it('should reject reserved locale keys', function() {
gt.setLocale('__proto__');
expect(gt.locale).to.equal('');
gt.setLocale('constructor');
expect(gt.locale).to.equal('');
});
});

describe('#setTextDomain', function() {
Expand Down Expand Up @@ -133,6 +154,13 @@ describe('Gettext', function() {
gt.setTextDomain();
expect(gt.domain).to.equal('messages');
});

it('should reject reserved domain keys', function() {
gt.setTextDomain('__proto__');
expect(gt.domain).to.equal('messages');
gt.setTextDomain('constructor');
expect(gt.domain).to.equal('messages');
});
});

describe('Resolve translations', function() {
Expand Down Expand Up @@ -260,6 +288,15 @@ describe('Gettext', function() {
expect(errorListener.callCount).to.equal(0);
});

it('should emit an error event when adding a reserved key as locale', function() {
try {
gt.addTranslations('__proto__', 'polluted', 'pwned');
expect(errorListener.callCount).to.equal(1);
} finally {
delete Object.prototype.polluted;
}
});

it('should emit an error event when a locale that has no translations is set', function() {
gt.setLocale('et-EE');
expect(errorListener.callCount).to.equal(1);
Expand Down