From d086dbecbf20bfc11cb6877c01193996919d1ac5 Mon Sep 17 00:00:00 2001 From: Mior Date: Tue, 4 Apr 2017 20:41:11 +0200 Subject: [PATCH] XLSX write Sheet Protection fixes #363 h/t @Mior note: @sheetjsdev rewrote implementation, original PR author used --- README.md | 22 ++++++++++++++++++++++ bits/67_wsxml.js | 20 ++++++++++++++++++++ docbits/54_shobject.md | 22 ++++++++++++++++++++++ tests/write.js | 7 +++++++ xlsx.flow.js | 20 ++++++++++++++++++++ xlsx.js | 20 ++++++++++++++++++++ 6 files changed, 111 insertions(+) diff --git a/README.md b/README.md index 8945b08..975bb19 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,28 @@ In addition to the base sheet keys, worksheets also add: will write all cells in the merge range if they exist, so be sure that only the first cell (upper-left) in the range is set. +- `ws['protect']`: object of write sheet protection properties. The `password` + key specifies the password. The writer uses the XOR obfuscation method. The + following keys control the sheet protection (same as ECMA-376 18.3.1.85): + +| key | functionality disabled if value is true | +|:----------------------|:-----------------------------------------------------| +| `selectLockedCells` | Select locked cells | +| `selectUnlockedCells` | Select unlocked cells | +| `formatCells` | Format cells | +| `formatColumns` | Format columns | +| `formatRows` | Format rows | +| `insertColumns` | Insert columns | +| `insertRows` | Insert rows | +| `insertHyperlinks` | Insert hyperlinks | +| `deleteColumns` | Delete columns | +| `deleteRows` | Delete rows | +| `sort` | Sort | +| `autoFilter` | Filter | +| `pivotTables` | Use PivotTable reports | +| `objects` | Edit objects | +| `scenarios` | Edit scenarios | + #### Chartsheet Object Chartsheets are represented as standard sheets. They are distinguished with the diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 6013fe4..b592d7c 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -70,6 +70,24 @@ function write_ws_xml_merges(merges) { return o + ''; } +/* 18.3.1.85 sheetPr CT_SheetProtection */ +function write_ws_xml_protection(sp)/*:string*/ { + // algorithmName, hashValue, saltValue, spinCountpassword + var o = ({sheet:1}/*:any*/); + var deffalse = ["objects", "scenarios", "selectLockedCells", "selectUnlockedCells"]; + var deftrue = [ + "formatColumns", "formatRows", "formatCells", + "insertColumns", "insertRows", "insertHyperlinks", + "deleteColumns", "deleteRows", + "sort", "autoFilter", "pivotTables" + ]; + deffalse.forEach(function(n) { if(sp[n] != null && sp[n]) o[n] = "1"; }); + deftrue.forEach(function(n) { if(sp[n] != null && !sp[n]) o[n] = "0"; }); + /* TODO: algorithm */ + if(sp.password) o.password = crypto_CreatePasswordVerifier_Method1(sp.password).toString(16).toUpperCase(); + return writextag('sheetProtection', null, o); +} + function parse_ws_xml_hlinks(s, data/*:Array*/, rels) { for(var i = 0; i != data.length; ++i) { var val = parsexmltag(data[i], true); @@ -348,6 +366,8 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { } if(o.length>sidx+1) { o[o.length] = (''); o[sidx]=o[sidx].replace("/>",">"); } + if(ws['!protect'] != null) o[o.length] = write_ws_xml_protection(ws['!protect']); + if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); var relc = -1, rel, rId = -1; diff --git a/docbits/54_shobject.md b/docbits/54_shobject.md index f85464a..9a8f467 100644 --- a/docbits/54_shobject.md +++ b/docbits/54_shobject.md @@ -13,6 +13,28 @@ In addition to the base sheet keys, worksheets also add: will write all cells in the merge range if they exist, so be sure that only the first cell (upper-left) in the range is set. +- `ws['protect']`: object of write sheet protection properties. The `password` + key specifies the password. The writer uses the XOR obfuscation method. The + following keys control the sheet protection (same as ECMA-376 18.3.1.85): + +| key | functionality disabled if value is true | +|:----------------------|:-----------------------------------------------------| +| `selectLockedCells` | Select locked cells | +| `selectUnlockedCells` | Select unlocked cells | +| `formatCells` | Format cells | +| `formatColumns` | Format columns | +| `formatRows` | Format rows | +| `insertColumns` | Insert columns | +| `insertRows` | Insert rows | +| `insertHyperlinks` | Insert hyperlinks | +| `deleteColumns` | Delete columns | +| `deleteRows` | Delete rows | +| `sort` | Sort | +| `autoFilter` | Filter | +| `pivotTables` | Use PivotTable reports | +| `objects` | Edit objects | +| `scenarios` | Edit scenarios | + #### Chartsheet Object Chartsheets are represented as standard sheets. They are distinguished with the diff --git a/tests/write.js b/tests/write.js index d8ea1d5..865b9df 100644 --- a/tests/write.js +++ b/tests/write.js @@ -102,6 +102,13 @@ wb.Props = { ws['A4'].c = []; ws['A4'].c.push({a:"SheetJS",t:"I'm a little comment, short and stout!\n\nWell, Stout may be the wrong word"}); +/* TEST: sheet protection */ +ws['!protect'] = { + password:"password", + objects:1, + scenarios:1 +}; + console.log("Worksheet Model:") console.log(ws); diff --git a/xlsx.flow.js b/xlsx.flow.js index 1c18093..5d444bf 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -9715,6 +9715,24 @@ function write_ws_xml_merges(merges) { return o + ''; } +/* 18.3.1.85 sheetPr CT_SheetProtection */ +function write_ws_xml_protection(sp)/*:string*/ { + // algorithmName, hashValue, saltValue, spinCountpassword + var o = ({sheet:1}/*:any*/); + var deffalse = ["objects", "scenarios", "selectLockedCells", "selectUnlockedCells"]; + var deftrue = [ + "formatColumns", "formatRows", "formatCells", + "insertColumns", "insertRows", "insertHyperlinks", + "deleteColumns", "deleteRows", + "sort", "autoFilter", "pivotTables" + ]; + deffalse.forEach(function(n) { if(sp[n] != null && sp[n]) o[n] = "1"; }); + deftrue.forEach(function(n) { if(sp[n] != null && !sp[n]) o[n] = "0"; }); + /* TODO: algorithm */ + if(sp.password) o.password = crypto_CreatePasswordVerifier_Method1(sp.password).toString(16).toUpperCase(); + return writextag('sheetProtection', null, o); +} + function parse_ws_xml_hlinks(s, data/*:Array*/, rels) { for(var i = 0; i != data.length; ++i) { var val = parsexmltag(data[i], true); @@ -9993,6 +10011,8 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { } if(o.length>sidx+1) { o[o.length] = (''); o[sidx]=o[sidx].replace("/>",">"); } + if(ws['!protect'] != null) o[o.length] = write_ws_xml_protection(ws['!protect']); + if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); var relc = -1, rel, rId = -1; diff --git a/xlsx.js b/xlsx.js index e214229..1f606ce 100644 --- a/xlsx.js +++ b/xlsx.js @@ -9658,6 +9658,24 @@ function write_ws_xml_merges(merges) { return o + ''; } +/* 18.3.1.85 sheetPr CT_SheetProtection */ +function write_ws_xml_protection(sp) { + // algorithmName, hashValue, saltValue, spinCountpassword + var o = ({sheet:1}); + var deffalse = ["objects", "scenarios", "selectLockedCells", "selectUnlockedCells"]; + var deftrue = [ + "formatColumns", "formatRows", "formatCells", + "insertColumns", "insertRows", "insertHyperlinks", + "deleteColumns", "deleteRows", + "sort", "autoFilter", "pivotTables" + ]; + deffalse.forEach(function(n) { if(sp[n] != null && sp[n]) o[n] = "1"; }); + deftrue.forEach(function(n) { if(sp[n] != null && !sp[n]) o[n] = "0"; }); + /* TODO: algorithm */ + if(sp.password) o.password = crypto_CreatePasswordVerifier_Method1(sp.password).toString(16).toUpperCase(); + return writextag('sheetProtection', null, o); +} + function parse_ws_xml_hlinks(s, data, rels) { for(var i = 0; i != data.length; ++i) { var val = parsexmltag(data[i], true); @@ -9936,6 +9954,8 @@ function write_ws_xml(idx, opts, wb, rels) { } if(o.length>sidx+1) { o[o.length] = (''); o[sidx]=o[sidx].replace("/>",">"); } + if(ws['!protect'] != null) o[o.length] = write_ws_xml_protection(ws['!protect']); + if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); var relc = -1, rel, rId = -1;