Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6e09a0361 | |||
| 3d98e41d2b | |||
| 7c87d3220d | |||
| 8b60ceb39c | |||
|
|
c677c7267b | ||
|
|
415a26e3f1 | ||
|
|
90b97e23e9 | ||
|
|
02d5338634 | ||
|
|
542183a593 | ||
|
|
a31f57026d | ||
|
|
98817e9d34 |
60
package-lock.json
generated
60
package-lock.json
generated
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@odfjs/odfjs",
|
||||
"version": "0.28.0",
|
||||
"version": "0.30.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@odfjs/odfjs",
|
||||
"version": "0.28.0",
|
||||
"version": "0.30.0",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"@zip.js/zip.js": "^2.7.57",
|
||||
"image-size": "^2.0.2",
|
||||
"ses": "^1.12.0"
|
||||
"ses": "^1.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
@ -42,10 +42,22 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@endo/cache-map": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@endo/cache-map/-/cache-map-1.1.0.tgz",
|
||||
"integrity": "sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@endo/env-options": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz",
|
||||
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg==",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
|
||||
"integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@endo/immutable-arraybuffer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@endo/immutable-arraybuffer/-/immutable-arraybuffer-1.1.2.tgz",
|
||||
"integrity": "sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
@ -3628,12 +3640,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ses": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz",
|
||||
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
|
||||
"integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@endo/env-options": "^1.1.8"
|
||||
"@endo/cache-map": "^1.1.0",
|
||||
"@endo/env-options": "^1.1.11",
|
||||
"@endo/immutable-arraybuffer": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
@ -4584,10 +4598,20 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@endo/cache-map": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@endo/cache-map/-/cache-map-1.1.0.tgz",
|
||||
"integrity": "sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw=="
|
||||
},
|
||||
"@endo/env-options": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.8.tgz",
|
||||
"integrity": "sha512-Xtxw9n33I4guo8q0sDyZiRuxlfaopM454AKiELgU7l3tqsylCut6IBZ0fPy4ltSHsBib7M3yF7OEMoIuLwzWVg=="
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.11.tgz",
|
||||
"integrity": "sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA=="
|
||||
},
|
||||
"@endo/immutable-arraybuffer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@endo/immutable-arraybuffer/-/immutable-arraybuffer-1.1.2.tgz",
|
||||
"integrity": "sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ=="
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
@ -7156,11 +7180,13 @@
|
||||
}
|
||||
},
|
||||
"ses": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.12.0.tgz",
|
||||
"integrity": "sha512-jvmwXE2lFxIIY1j76hFjewIIhYMR9Slo3ynWZGtGl5M7VUCw3EA0wetS+JCIbl2UcSQjAT0yGAHkyxPJreuC9w==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ses/-/ses-1.14.0.tgz",
|
||||
"integrity": "sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==",
|
||||
"requires": {
|
||||
"@endo/env-options": "^1.1.8"
|
||||
"@endo/cache-map": "^1.1.0",
|
||||
"@endo/env-options": "^1.1.11",
|
||||
"@endo/immutable-arraybuffer": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odfjs/odfjs",
|
||||
"version": "0.28.0",
|
||||
"version": "0.30.0",
|
||||
"type": "module",
|
||||
"exports": "./exports.js",
|
||||
"files": [
|
||||
@ -21,7 +21,7 @@
|
||||
"test": "ava"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/odfjs/odfjs.git"
|
||||
"url": "https://source.netsyms.com/PostalPortal/odfjs.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
@ -42,6 +42,6 @@
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
"@zip.js/zip.js": "^2.7.57",
|
||||
"image-size": "^2.0.2",
|
||||
"ses": "^1.12.0"
|
||||
"ses": "^1.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
19
readme.md
19
readme.md
@ -18,7 +18,7 @@ Small lib to parse/understand .odf files (.odt, .ods) in the browser and node.js
|
||||
### Install
|
||||
|
||||
```sh
|
||||
npm i https://github.com/odfjs/odfjs.git#v0.27.0
|
||||
npm i https://github.com/odfjs/odfjs.git#v0.30.0
|
||||
```
|
||||
|
||||
|
||||
@ -99,8 +99,7 @@ And then run the code:
|
||||
```js
|
||||
import {join} from 'node:path';
|
||||
|
||||
import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js'
|
||||
import {fillOdtTemplate} from '../scripts/node.js'
|
||||
import {getOdtTemplate, fillOdtTemplate} from '@odfjs/odfjs'
|
||||
|
||||
// replace with your template path
|
||||
const templatePath = join(import.meta.dirname, './tests/data/template-anniversaire.odt')
|
||||
@ -126,6 +125,19 @@ There are also loops in the form:
|
||||
They can be used to generate lists or tables in .odt files from data and a template using this syntax
|
||||
|
||||
|
||||
#### Securing calls to fillOdtTemplate
|
||||
|
||||
`fillOdtTemplate` evaluate arbitrary JavaScript code in `{#each <collection> as élément}` and `{#if <condition>}` and in `{<expression>}`
|
||||
|
||||
By default, `fillOdtTemplate` limits access to global functions to only ECMAScript defaults via the use of [ses' Compartment](https://www.npmjs.com/package/ses#compartment), this prevents naïve data exfiltration
|
||||
|
||||
However, `fillOdtTemplate` is vulnerable to [prototype pollution](https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html) inside template code. Two main ways to be secure are:
|
||||
- control the set of possible templates
|
||||
- call ses' `lockdown` which freezes Javascript intrinsics before calling `fillOdtTemplate` (this may lead to incompatibilities)
|
||||
|
||||
|
||||
|
||||
|
||||
### Demo
|
||||
|
||||
https://odfjs.github.io/odfjs/
|
||||
@ -146,4 +158,3 @@ npm run dev
|
||||
I hope to be credited for the work on this repo
|
||||
|
||||
Everything written by me and contributors to this repo is licenced under **CC0 1.0 (Public Domain)**
|
||||
|
||||
|
||||
@ -10,7 +10,11 @@ const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||||
office:version="1.2">
|
||||
<office:styles/>
|
||||
<office:styles>
|
||||
<style:style style:name="boldcell" style:family="table-cell">
|
||||
<style:text-properties fo:font-weight="bold"/>
|
||||
</style:style>
|
||||
</office:styles>
|
||||
<office:automatic-styles/>
|
||||
<office:master-styles/>
|
||||
</office:document-styles>`;
|
||||
@ -27,7 +31,7 @@ const manifestXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
* @param {Map<SheetName, SheetRawContent>} sheetsData
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*/
|
||||
export async function createOdsFile(sheetsData) {
|
||||
export async function createOdsFile(sheetsData, currencyData = null) {
|
||||
// Create a new zip writer
|
||||
const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet'));
|
||||
|
||||
@ -45,7 +49,7 @@ export async function createOdsFile(sheetsData) {
|
||||
}
|
||||
);
|
||||
|
||||
const contentXml = generateContentFileXMLString(sheetsData);
|
||||
const contentXml = generateContentFileXMLString(sheetsData, currencyData);
|
||||
zipWriter.add("content.xml", new TextReader(contentXml), {level: 9});
|
||||
|
||||
zipWriter.add("styles.xml", new TextReader(stylesXml));
|
||||
@ -63,7 +67,7 @@ export async function createOdsFile(sheetsData) {
|
||||
* @param {Map<SheetName, SheetRawContent>} sheetsData
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateContentFileXMLString(sheetsData) {
|
||||
function generateContentFileXMLString(sheetsData, currencyData) {
|
||||
const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content');
|
||||
const root = doc.documentElement;
|
||||
|
||||
@ -75,6 +79,52 @@ function generateContentFileXMLString(sheetsData) {
|
||||
root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
|
||||
root.setAttribute('office:version', '1.2');
|
||||
|
||||
const styleNode = doc.createElement("office:automatic-styles");
|
||||
|
||||
var currencyStyleName = "currencyStyle";
|
||||
if (currencyData != null) {
|
||||
currencyStyleName = `currency${currencyData.currencyCode.toUpperCase()}`;
|
||||
const numberStyle = doc.createElement("number:currency-style");
|
||||
numberStyle.setAttribute("style:name", currencyStyleName);
|
||||
|
||||
const numberCurrencySymbolStyle = doc.createElement("number:currency-symbol");
|
||||
numberCurrencySymbolStyle.setAttribute("number:language", "en");
|
||||
numberCurrencySymbolStyle.setAttribute("number:country", currencyData.countryCode.toUpperCase());
|
||||
numberCurrencySymbolStyle.textContent = currencyData.currencySymbol;
|
||||
numberStyle.appendChild(numberCurrencySymbolStyle);
|
||||
|
||||
const numberCurrencyStyle = doc.createElement("number:number");
|
||||
numberCurrencyStyle.setAttribute("number:min-integer-digits", "1");
|
||||
numberCurrencyStyle.setAttribute("number:decimal-places", `${currencyData.decimalPlaces}`);
|
||||
numberCurrencyStyle.setAttribute("number:min-decimal-places", `${currencyData.decimalPlaces}`);
|
||||
numberCurrencyStyle.setAttribute("number:grouping", "true");
|
||||
numberStyle.appendChild(numberCurrencyStyle);
|
||||
|
||||
styleNode.appendChild(numberStyle);
|
||||
|
||||
const currencyCellStyleNode = doc.createElement("style:style");
|
||||
currencyCellStyleNode.setAttribute("style:name", "currencycell");
|
||||
currencyCellStyleNode.setAttribute("style:family", "table-cell");
|
||||
currencyCellStyleNode.setAttribute("style:data-style-name", currencyStyleName);
|
||||
|
||||
const currencyCellTableCellProperties = doc.createElement("style:table-cell-properties");
|
||||
|
||||
currencyCellStyleNode.appendChild(currencyCellTableCellProperties);
|
||||
|
||||
styleNode.appendChild(currencyCellStyleNode);
|
||||
}
|
||||
|
||||
const boldCellStyleNode = doc.createElement("style:style");
|
||||
boldCellStyleNode.setAttribute("style:name", "boldcell");
|
||||
boldCellStyleNode.setAttribute("style:family", "table-cell");
|
||||
const boldCellTextPropsNode = doc.createElement("style:text-properties");
|
||||
boldCellTextPropsNode.setAttribute("fo:font-weight", "bold");
|
||||
boldCellStyleNode.appendChild(boldCellTextPropsNode);
|
||||
styleNode.appendChild(boldCellStyleNode);
|
||||
|
||||
|
||||
root.appendChild(styleNode);
|
||||
|
||||
const bodyNode = doc.createElement('office:body');
|
||||
root.appendChild(bodyNode);
|
||||
|
||||
@ -87,8 +137,30 @@ function generateContentFileXMLString(sheetsData) {
|
||||
tableNode.setAttribute('table:name', sheetName);
|
||||
spreadsheetNode.appendChild(tableNode);
|
||||
|
||||
const columnNode = doc.createElement('table:table-column');
|
||||
tableNode.appendChild(columnNode);
|
||||
var columnsWidthChars = {};
|
||||
for (let r = 0; r < sheetData.length; r++) {
|
||||
for (let c = 0; c < sheetData[r].length; c++) {
|
||||
var len = ((sheetData[r][c].display ?? sheetData[r][c].value) + "").length;
|
||||
if (typeof columnsWidthChars[c] == "undefined") {
|
||||
columnsWidthChars[c] = len;
|
||||
}
|
||||
columnsWidthChars[c] = Math.max(columnsWidthChars[c], len);
|
||||
}
|
||||
}
|
||||
|
||||
for (var prop in columnsWidthChars) {
|
||||
var columnNode = doc.createElement('table:table-column');
|
||||
columnNode.setAttribute("table:style-name", "colwidth" + columnsWidthChars[prop]);
|
||||
tableNode.appendChild(columnNode);
|
||||
|
||||
var columnWidthNode = doc.createElement("style:style");
|
||||
columnWidthNode.setAttribute("style:name", "colwidth" + columnsWidthChars[prop]);
|
||||
columnWidthNode.setAttribute("style:family", "table-column");
|
||||
const columnWidthPropsNode = doc.createElement("style:table-column-properties");
|
||||
columnWidthPropsNode.setAttribute("style:column-width", `${columnsWidthChars[prop] * 0.26}cm`);
|
||||
columnWidthNode.appendChild(columnWidthPropsNode);
|
||||
styleNode.appendChild(columnWidthNode);
|
||||
}
|
||||
|
||||
// Iterate through rows
|
||||
sheetData.forEach((row) => {
|
||||
@ -101,6 +173,10 @@ function generateContentFileXMLString(sheetsData) {
|
||||
const cellType = convertCellType(cell.type);
|
||||
cellNode.setAttribute('office:value-type', cellType);
|
||||
|
||||
if (cell.style && cell.style == "bold") {
|
||||
cellNode.setAttribute('table:style-name', "boldcell");
|
||||
}
|
||||
|
||||
// Add value attribute based on type
|
||||
if (cell.value !== null && cell.value !== undefined) {
|
||||
switch (cellType) {
|
||||
@ -111,6 +187,14 @@ function generateContentFileXMLString(sheetsData) {
|
||||
cellNode.setAttribute('office:value', cell.value.toString());
|
||||
cellNode.setAttribute('office:value-type', 'percentage');
|
||||
break;
|
||||
case 'currency':
|
||||
cellNode.setAttribute('office:value', cell.value.toString());
|
||||
cellNode.setAttribute('office:value-type', 'currency');
|
||||
if (currencyData != null) {
|
||||
cellNode.setAttribute("table:style-name", "currencycell");
|
||||
cellNode.setAttribute('office:currency', currencyData.currencyCode.toUpperCase());
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
cellNode.setAttribute('office:date-value', cell.value.toString());
|
||||
break;
|
||||
@ -126,7 +210,11 @@ function generateContentFileXMLString(sheetsData) {
|
||||
|
||||
if (cellType !== 'string') {
|
||||
const textNode = doc.createElement('text:p');
|
||||
textNode.textContent = cell.value.toString();
|
||||
if (typeof cell.display != "undefined") {
|
||||
textNode.textContent = cell.display.toString();
|
||||
} else {
|
||||
textNode.textContent = cell.value.toString();
|
||||
}
|
||||
cellNode.appendChild(textNode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,6 @@ import prepareTemplateDOMTree from './prepareTemplateDOMTree.js';
|
||||
import 'ses'
|
||||
import fillOdtElementTemplate from './fillOdtElementTemplate.js';
|
||||
|
||||
lockdown();
|
||||
|
||||
|
||||
/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */
|
||||
/** @import {ODFManifest, ODFManifestFileEntry} from '../manifest.js' */
|
||||
|
||||
@ -33,7 +33,7 @@ function extraxtODSCellText(cell) {
|
||||
text += pChild.nodeValue; // Append text inside <text:p>
|
||||
} else if (pChild.nodeName === 'text:line-break') {
|
||||
text += '\n'; // Append newline for <text:line-break />
|
||||
} else if (pChild.nodeName === 'text:a') {
|
||||
} else if (pChild.nodeName === 'text:a' || pChild.nodeName === 'text:span') {
|
||||
text += pChild.textContent
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import { listZipEntries } from '../helpers/zip-analysis.js';
|
||||
import { getContentDocument } from '../../scripts/odf/odt/getOdtTextContent.js';
|
||||
|
||||
|
||||
test.skip('template filling preserves images', async t => {
|
||||
test('template filling preserves images', async t => {
|
||||
const templatePath = join(import.meta.dirname, '../fixtures/template-avec-image.odt')
|
||||
|
||||
const data = {
|
||||
|
||||
BIN
tests/fixtures/cellule avec style.ods
vendored
Normal file
BIN
tests/fixtures/cellule avec style.ods
vendored
Normal file
Binary file not shown.
@ -76,3 +76,13 @@ test('.ods cells with mails should be recognized', async t => {
|
||||
t.deepEqual(row3[0].value, 'Fanny')
|
||||
t.deepEqual(row3[1].value, 'lemaildeFanny@example.com')
|
||||
});
|
||||
|
||||
test('.ods cells with partially styled content should be recognized', async t => {
|
||||
const odsFileWithStyle = (await readFile('./tests/fixtures/cellule avec style.ods')).buffer;
|
||||
const table = await getODSTableRawContent(odsFileWithStyle);
|
||||
|
||||
const feuille1 = table.get('Feuille1');
|
||||
|
||||
const row1 = feuille1[0];
|
||||
t.deepEqual(row1[0].value, 'Toto titi');
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user