Restrict key, value, and role character limits based on unicode characters, not UTF-16 code units (re: #6817)

This commit is contained in:
Quincy Morgan
2020-06-09 15:41:15 -04:00
parent 5ab04eb0ba
commit 762307bd7d
10 changed files with 123 additions and 31 deletions
+1 -3
View File
@@ -191,11 +191,9 @@ export function coreContext() {
return context;
};
// String length limits in Unicode characters, not JavaScript UTF-16 code units
context.maxCharsForTagKey = () => 255;
context.maxCharsForTagValue = () => 255;
context.maxCharsForRelationRole = () => 255;
+6 -3
View File
@@ -1,6 +1,7 @@
import { debug } from '../index';
import { osmIsInterestingTag } from './tags';
import { utilArrayUnion } from '../util';
import { utilArrayUnion } from '../util/array';
import { utilUnicodeCharsTruncated } from '../util/util';
export function osmEntity(attrs) {
@@ -149,8 +150,10 @@ osmEntity.prototype = {
merged[k] = t2;
} else if (t1 !== t2) {
changed = true;
merged[k] = utilArrayUnion(t1.split(/;\s*/), t2.split(/;\s*/)).join(';')
.substr(0, 255); // avoid exceeding character limit; see also services/osm.js -> maxCharsForTagValue()
merged[k] = utilUnicodeCharsTruncated(
utilArrayUnion(t1.split(/;\s*/), t2.split(/;\s*/)).join(';'),
255 // avoid exceeding character limit; see also services/osm.js -> maxCharsForTagValue()
);
}
}
return changed ? this.update({ tags: merged }) : this;
+15 -15
View File
@@ -12,7 +12,7 @@ import { uiChangesetEditor } from './changeset_editor';
import { uiSectionChanges } from './sections/changes';
import { uiCommitWarnings } from './commit_warnings';
import { uiSectionRawTagEditor } from './sections/raw_tag_editor';
import { utilArrayGroupBy, utilRebind, utilUniqueDomId } from '../util';
import { utilArrayGroupBy, utilRebind, utilUnicodeCharsTruncated, utilUniqueDomId } from '../util';
import { utilDetect } from '../util/detect';
@@ -92,9 +92,9 @@ export function uiCommit(context) {
var detected = utilDetect();
var tags = {
comment: prefs('comment') || '',
created_by: ('iD ' + context.version).substr(0, tagCharLimit),
host: detected.host.substr(0, tagCharLimit),
locale: localizer.localeCode().substr(0, tagCharLimit)
created_by: utilUnicodeCharsTruncated('iD ' + context.version, tagCharLimit),
host: utilUnicodeCharsTruncated(detected.host, tagCharLimit),
locale: utilUnicodeCharsTruncated(localizer.localeCode(), tagCharLimit)
};
// call findHashtags initially - this will remove stored
@@ -126,7 +126,7 @@ export function uiCommit(context) {
}
});
tags.source = sources.join(';').substr(0, tagCharLimit);
tags.source = utilUnicodeCharsTruncated(sources.join(';'), tagCharLimit);
}
context.changeset = new osmChangeset({ tags: tags });
@@ -144,31 +144,31 @@ export function uiCommit(context) {
var tags = Object.assign({}, context.changeset.tags); // shallow copy
// assign tags for imagery used
var imageryUsed = context.history().imageryUsed().join(';').substr(0, tagCharLimit);
var imageryUsed = utilUnicodeCharsTruncated(context.history().imageryUsed().join(';'), tagCharLimit);
tags.imagery_used = imageryUsed || 'None';
// assign tags for closed issues and notes
var osmClosed = osm.getClosedIDs();
var itemType;
if (osmClosed.length) {
tags['closed:note'] = osmClosed.join(';').substr(0, tagCharLimit);
tags['closed:note'] = utilUnicodeCharsTruncated(osmClosed.join(';'), tagCharLimit);
}
if (services.keepRight) {
var krClosed = services.keepRight.getClosedIDs();
if (krClosed.length) {
tags['closed:keepright'] = krClosed.join(';').substr(0, tagCharLimit);
tags['closed:keepright'] = utilUnicodeCharsTruncated(krClosed.join(';'), tagCharLimit);
}
}
if (services.improveOSM) {
var iOsmClosed = services.improveOSM.getClosedCounts();
for (itemType in iOsmClosed) {
tags['closed:improveosm:' + itemType] = iOsmClosed[itemType].toString().substr(0, tagCharLimit);
tags['closed:improveosm:' + itemType] = utilUnicodeCharsTruncated(iOsmClosed[itemType].toString(), tagCharLimit);
}
}
if (services.osmose) {
var osmoseClosed = services.osmose.getClosedCounts();
for (itemType in osmoseClosed) {
tags['closed:osmose:' + itemType] = osmoseClosed[itemType].toString().substr(0, tagCharLimit);
tags['closed:osmose:' + itemType] = utilUnicodeCharsTruncated(osmoseClosed[itemType].toString(), tagCharLimit);
}
}
@@ -187,10 +187,10 @@ export function uiCommit(context) {
var issuesBySubtype = utilArrayGroupBy(issuesOfType, 'subtype');
for (var issueSubtype in issuesBySubtype) {
var issuesOfSubtype = issuesBySubtype[issueSubtype];
tags[prefix + ':' + issueType + ':' + issueSubtype] = issuesOfSubtype.length.toString().substr(0, tagCharLimit);
tags[prefix + ':' + issueType + ':' + issueSubtype] = utilUnicodeCharsTruncated(issuesOfSubtype.length.toString(), tagCharLimit);
}
} else {
tags[prefix + ':' + issueType] = issuesOfType.length.toString().substr(0, tagCharLimit);
tags[prefix + ':' + issueType] = utilUnicodeCharsTruncated(issuesOfType.length.toString(), tagCharLimit);
}
}
}
@@ -550,14 +550,14 @@ export function uiCommit(context) {
Object.keys(changed).forEach(function(k) {
var v = changed[k];
k = k.trim().substr(0, tagCharLimit);
k = utilUnicodeCharsTruncated(k.trim(), tagCharLimit);
if (readOnlyTags.indexOf(k) !== -1) return;
if (k !== '' && v !== undefined) {
if (onInput) {
tags[k] = v;
} else {
tags[k] = v.trim().substr(0, tagCharLimit);
tags[k] = utilUnicodeCharsTruncated(v.trim(), tagCharLimit);
}
} else {
delete tags[k];
@@ -569,7 +569,7 @@ export function uiCommit(context) {
var commentOnly = changed.hasOwnProperty('comment') && (changed.comment !== '');
var arr = findHashtags(tags, commentOnly);
if (arr.length) {
tags.hashtags = arr.join(';').substr(0, tagCharLimit);
tags.hashtags = utilUnicodeCharsTruncated(arr.join(';'), tagCharLimit);
prefs('hashtags', tags.hashtags);
} else {
delete tags.hashtags;
+3 -3
View File
@@ -9,7 +9,7 @@ import { osmEntity } from '../../osm/entity';
import { t } from '../../core/localizer';
import { services } from '../../services';
import { uiCombobox } from '../combobox';
import { utilArrayUniq, utilGetSetValue, utilNoAuto, utilRebind } from '../../util';
import { utilArrayUniq, utilGetSetValue, utilNoAuto, utilRebind, utilUnicodeCharsCount } from '../../util';
export {
uiFieldCombo as uiFieldMultiCombo,
@@ -448,7 +448,7 @@ export function uiFieldCombo(field, context) {
field.keys = _multiData.map(function(d) { return d.key; });
// limit the input length so it fits after prepending the key prefix
maxLength = context.maxCharsForTagKey() - field.key.length;
maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);
} else if (isSemi) {
@@ -480,7 +480,7 @@ export function uiFieldCombo(field, context) {
};
});
var currLength = commonValues.join(';').length;
var currLength = utilUnicodeCharsCount(commonValues.join(';'));
// limit the input length to the remaining available characters
maxLength = context.maxCharsForTagValue() - currLength;
+3 -2
View File
@@ -14,7 +14,8 @@ import { svgIcon } from '../../svg/icon';
import {
utilGetSetValue,
utilNoAuto,
utilRebind
utilRebind,
utilUnicodeCharsTruncated
} from '../../util';
import { t } from '../../core/localizer';
@@ -236,7 +237,7 @@ export function uiFieldWikidata(field, context) {
}
if (newWikipediaValue) {
newWikipediaValue = newWikipediaValue.substr(0, context.maxCharsForTagValue());
newWikipediaValue = utilUnicodeCharsTruncated(newWikipediaValue, context.maxCharsForTagValue());
}
if (typeof newWikipediaValue === 'undefined') return;
+2 -2
View File
@@ -7,7 +7,7 @@ import { actionChangeTags } from '../../actions/change_tags';
import { services } from '../../services/index';
import { svgIcon } from '../../svg/icon';
import { uiCombobox } from '../combobox';
import { utilGetSetValue, utilNoAuto, utilRebind } from '../../util';
import { utilGetSetValue, utilNoAuto, utilRebind, utilUnicodeCharsTruncated } from '../../util';
export function uiFieldWikipedia(field, context) {
@@ -192,7 +192,7 @@ export function uiFieldWikipedia(field, context) {
}
if (value) {
syncTags.wikipedia = (language()[2] + ':' + value).substr(0, context.maxCharsForTagValue());
syncTags.wikipedia = utilUnicodeCharsTruncated(language()[2] + ':' + value, context.maxCharsForTagValue());
} else {
syncTags.wikipedia = undefined;
}
+3 -3
View File
@@ -9,7 +9,7 @@ import { uiTagReference } from '../tag_reference';
import { prefs } from '../../core/preferences';
import { t } from '../../core/localizer';
import { utilArrayDifference, utilArrayIdentical } from '../../util/array';
import { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util';
import { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff, utilUnicodeCharsTruncated } from '../../util';
export function uiSectionRawTagEditor(id, context) {
@@ -343,8 +343,8 @@ export function uiSectionRawTagEditor(id, context) {
newText.split('\n').forEach(function(row) {
var m = row.match(/^\s*([^=]+)=(.*)$/);
if (m !== null) {
var k = unstringify(m[1].trim()).substr(0, maxKeyLength);
var v = unstringify(m[2].trim()).substr(0, maxValueLength);
var k = utilUnicodeCharsTruncated(unstringify(m[1].trim()), maxKeyLength);
var v = utilUnicodeCharsTruncated(unstringify(m[2].trim()), maxValueLength);
newTags[k] = v;
}
});
+2
View File
@@ -46,5 +46,7 @@ export { utilTagDiff } from './util';
export { utilTagText } from './util';
export { utilTiler } from './tiler';
export { utilTriggerEvent } from './trigger_event';
export { utilUnicodeCharsCount } from './util';
export { utilUnicodeCharsTruncated } from './util';
export { utilUniqueDomId } from './util';
export { utilWrap } from './util';
+10
View File
@@ -513,3 +513,13 @@ export function utilSafeClassName(str) {
export function utilUniqueDomId(str) {
return 'ideditor-' + utilSafeClassName(str) + '-' + new Date().getTime().toString();
}
export function utilUnicodeCharsCount(str) {
// Converting to an array gives us unicode characters instead of JavaScript
// UTF-16 code units from `String.length()`
return Array.from(str).length;
}
export function utilUnicodeCharsTruncated(str, limit) {
return Array.from(str).slice(0, limit).join('');
}
+78
View File
@@ -143,4 +143,82 @@ describe('iD.util', function() {
});
});
});
describe('utilUnicodeCharsCount', function() {
it('counts empty string', function() {
expect(iD.utilUnicodeCharsCount('')).to.eql(0);
});
it('counts latin text', function() {
expect(iD.utilUnicodeCharsCount('Lorem')).to.eql(5);
});
it('counts diacritics', function() {
expect(iD.utilUnicodeCharsCount('Ĺo͂řȩm̅')).to.eql(7);
});
it('counts Korean text', function() {
expect(iD.utilUnicodeCharsCount('뎌쉐')).to.eql(2);
});
it('counts Hindi text with combining marks', function() {
expect(iD.utilUnicodeCharsCount('अनुच्छेद')).to.eql(8);
});
it('counts demonic multiple combining marks', function() {
expect(iD.utilUnicodeCharsCount('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞')).to.eql(74);
});
it('counts emoji', function() {
expect(iD.utilUnicodeCharsCount('😎')).to.eql(1);
expect(iD.utilUnicodeCharsCount('🇨🇦')).to.eql(2);
expect(iD.utilUnicodeCharsCount('🏳️‍🌈')).to.eql(4);
expect(iD.utilUnicodeCharsCount('‍👩‍👩‍👧‍👧')).to.eql(8);
expect(iD.utilUnicodeCharsCount('👩‍❤️‍💋‍👩')).to.eql(8);
expect(iD.utilUnicodeCharsCount('😎😬😆😵😴😄🙂🤔')).to.eql(8);
});
});
describe('utilUnicodeCharsTruncated', function() {
it('truncates empty string', function() {
expect(iD.utilUnicodeCharsTruncated('', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('', 255)).to.eql('');
});
it('truncates latin text', function() {
expect(iD.utilUnicodeCharsTruncated('Lorem', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('Lorem', 3)).to.eql('Lor');
expect(iD.utilUnicodeCharsTruncated('Lorem', 5)).to.eql('Lorem');
expect(iD.utilUnicodeCharsTruncated('Lorem', 255)).to.eql('Lorem');
});
it('truncates diacritics', function() {
expect(iD.utilUnicodeCharsTruncated('Ĺo͂řȩm̅', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('Ĺo͂řȩm̅', 3)).to.eql('Ĺo͂');
expect(iD.utilUnicodeCharsTruncated('Ĺo͂řȩm̅', 7)).to.eql('Ĺo͂řȩm̅');
expect(iD.utilUnicodeCharsTruncated('Ĺo͂řȩm̅', 255)).to.eql('Ĺo͂řȩm̅');
});
it('truncates Korean text', function() {
expect(iD.utilUnicodeCharsTruncated('뎌쉐', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('뎌쉐', 1)).to.eql('뎌');
expect(iD.utilUnicodeCharsTruncated('뎌쉐', 2)).to.eql('뎌쉐');
expect(iD.utilUnicodeCharsTruncated('뎌쉐', 255)).to.eql('뎌쉐');
});
it('truncates Hindi text with combining marks', function() {
expect(iD.utilUnicodeCharsTruncated('अनुच्छेद', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('अनुच्छेद', 3)).to.eql('अनु');
expect(iD.utilUnicodeCharsTruncated('अनुच्छेद', 8)).to.eql('अनुच्छेद');
expect(iD.utilUnicodeCharsTruncated('अनुच्छेद', 255)).to.eql('अनुच्छेद');
});
it('truncates demonic multiple combining marks', function() {
expect(iD.utilUnicodeCharsTruncated('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖', 59)).to.eql('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖');
expect(iD.utilUnicodeCharsTruncated('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞', 74)).to.eql('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞');
expect(iD.utilUnicodeCharsTruncated('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞', 255)).to.eql('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞');
});
it('truncates emoji', function() {
expect(iD.utilUnicodeCharsTruncated('😎', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('😎', 1)).to.eql('😎');
expect(iD.utilUnicodeCharsTruncated('🇨🇦', 1)).to.eql('🇨');
expect(iD.utilUnicodeCharsTruncated('🏳️‍🌈', 2)).to.eql('🏳️');
expect(iD.utilUnicodeCharsTruncated('‍👩‍👩‍👧‍👧', 4)).to.eql('‍👩‍👩');
expect(iD.utilUnicodeCharsTruncated('👩‍❤️‍💋‍👩', 6)).to.eql('👩‍❤️‍💋');
expect(iD.utilUnicodeCharsTruncated('😎😬😆😵😴😄🙂🤔', 0)).to.eql('');
expect(iD.utilUnicodeCharsTruncated('😎😬😆😵😴😄🙂🤔', 4)).to.eql('😎😬😆😵');
expect(iD.utilUnicodeCharsTruncated('😎😬😆😵😴😄🙂🤔', 8)).to.eql('😎😬😆😵😴😄🙂🤔');
expect(iD.utilUnicodeCharsTruncated('😎😬😆😵😴😄🙂🤔', 255)).to.eql('😎😬😆😵😴😄🙂🤔');
});
});
});