From 3aafb4027a006fc3228485bc74c6a8d402c6357a Mon Sep 17 00:00:00 2001 From: Geoff Lankow Date: Thu, 20 Oct 2022 17:52:59 +1300 Subject: [PATCH] Quote (or don't quote) parameter property values individually before joining Instead of adding quotes to the delimiting comma and around the outside, quote the individual members separately if necessary. This prevents the whole string of multiple values being quoted because of the presence of a delimiting comma. When quotes are required (multiValueSeparateDQuote is true) only URIs are valid, which will get quoted due to the colon. When not required only specific strings (WORK, VOICE, PARCEL, etc.) are valid, none of which need quotes. --- lib/ical/design.js | 2 +- lib/ical/stringify.js | 26 ++++++++++++++---------- test/stringify_test.js | 46 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/lib/ical/design.js b/lib/ical/design.js index cfd9fbd2..1778df65 100644 --- a/lib/ical/design.js +++ b/lib/ical/design.js @@ -193,7 +193,7 @@ let commonValues = { let icalParams = { // Although the syntax is DQUOTE uri DQUOTE, I don't think we should - // enfoce anything aside from it being a valid content line. + // enforce anything aside from it being a valid content line. // // At least some params require - if multi values are used - DQUOTEs // for each of its values - e.g. delegated-from="uri1","uri2" diff --git a/lib/ical/stringify.js b/lib/ical/stringify.js index 1a542862..f388cb2c 100644 --- a/lib/ical/stringify.js +++ b/lib/ical/stringify.js @@ -116,20 +116,23 @@ stringify.property = function(property, designSet, noFold) { if (designSet.propertyGroups && paramName == 'group') { continue; } - let multiValue = (paramName in designSet.param) && designSet.param[paramName].multiValue; + + let paramDesign = designSet.param[paramName]; + let multiValue = paramDesign && paramDesign.multiValue; if (multiValue && Array.isArray(value)) { - if (designSet.param[paramName].multiValueSeparateDQuote) { - multiValue = '"' + multiValue + '"'; - } - value = value.map(stringify._rfc6868Unescape); + value = value.map(function(val) { + val = stringify._rfc6868Unescape(val); + val = stringify.paramPropertyValue(val, paramDesign.multiValueSeparateDQuote); + return val; + }); value = stringify.multiValue(value, multiValue, "unknown", null, designSet); } else { value = stringify._rfc6868Unescape(value); + value = stringify.paramPropertyValue(value); } - line += ';' + paramName.toUpperCase(); - line += '=' + stringify.propertyValue(value); + line += ';' + paramName.toUpperCase() + '=' + value; } if (property.length === 3) { @@ -206,13 +209,14 @@ stringify.property = function(property, designSet, noFold) { * If any of the above are present the result is wrapped * in double quotes. * - * @function ICAL.stringify.propertyValue + * @function ICAL.stringify.paramPropertyValue * @param {String} value Raw property value + * @param {boolean} force If value should be escaped even when unnecessary * @return {String} Given or escaped value when needed */ -stringify.propertyValue = function(value) { - - if ((unescapedIndexOf(value, ',') === -1) && +stringify.paramPropertyValue = function(value, force) { + if (!force && + (unescapedIndexOf(value, ',') === -1) && (unescapedIndexOf(value, ':') === -1) && (unescapedIndexOf(value, ';') === -1)) { diff --git a/test/stringify_test.js b/test/stringify_test.js index aecfa109..40337b84 100644 --- a/test/stringify_test.js +++ b/test/stringify_test.js @@ -80,6 +80,39 @@ suite('ICAL.stringify', function() { delete ICAL.design.defaultSet.property.custom; }); + test('property with multiple parameter values', function() { + ICAL.design.defaultSet.property.custom = { defaultType: 'text' }; + ICAL.design.defaultSet.param.type = { multiValue: ',' }; + let subject = new ICAL.Property('custom'); + subject.setParameter('type', ['ABC', 'XYZ']); + subject.setValue('some value'); + assert.equal(subject.toICALString(), 'CUSTOM;TYPE=ABC,XYZ:some value'); + delete ICAL.design.defaultSet.property.custom; + delete ICAL.design.defaultSet.param.type; + }); + + test('property with multiple parameter values which must be escaped', function() { + ICAL.design.defaultSet.property.custom = { defaultType: 'text' }; + ICAL.design.defaultSet.param.type = { multiValue: ',' }; + let subject = new ICAL.Property('custom'); + subject.setParameter('type', ['ABC', '--"XYZ"--']); + subject.setValue('some value'); + assert.equal(subject.toICALString(), "CUSTOM;TYPE=ABC,--^'XYZ^'--:some value"); + delete ICAL.design.defaultSet.property.custom; + delete ICAL.design.defaultSet.param.type; + }); + + test('property with multiple parameter values with enabled quoting', function() { + ICAL.design.defaultSet.property.custom = { defaultType: 'text' }; + ICAL.design.defaultSet.param.type = { multiValue: ',', multiValueSeparateDQuote: true }; + let subject = new ICAL.Property('custom'); + subject.setParameter('type', ['ABC', 'XYZ']); + subject.setValue('some value'); + assert.equal(subject.toICALString(), 'CUSTOM;TYPE="ABC","XYZ":some value'); + delete ICAL.design.defaultSet.property.custom; + delete ICAL.design.defaultSet.param.type; + }); + test('rfc6868 roundtrip', function() { let subject = new ICAL.Property('attendee'); let input = "caret ^ dquote \" newline \n end"; @@ -90,6 +123,19 @@ suite('ICAL.stringify', function() { assert.equal(ICAL.parse.property(expected)[1].cn, input); }); + test('roundtrip for property with multiple parameters', function() { + ICAL.design.defaultSet.property.custom = { defaultType: 'text' }; + ICAL.design.defaultSet.param.type = { multiValue: ',', multiValueSeparateDQuote: true }; + let subject = new ICAL.Property('custom'); + subject.setParameter('type', ['ABC', '--"123"--']); + subject.setValue('some value'); + assert.lengthOf(ICAL.parse.property(subject.toICALString())[1].type, 2); + assert.include(ICAL.parse.property(subject.toICALString())[1].type, 'ABC'); + assert.include(ICAL.parse.property(subject.toICALString())[1].type, '--"123"--'); + delete ICAL.design.defaultSet.property.custom; + delete ICAL.design.defaultSet.param.type; + }); + test('folding', function() { let oldLength = ICAL.foldLength; let subject = new ICAL.Property("description");