|
|
@@ -1530,7 +1530,7 @@ ipcMain.on('export', exportDiagram);
|
|
|
// Renderer Helper functions
|
|
|
//================================================================
|
|
|
|
|
|
-const { O_SYNC, O_CREAT, O_WRONLY, O_TRUNC } = fs.constants;
|
|
|
+const { O_SYNC, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY } = fs.constants;
|
|
|
const DRAFT_PREFEX = '.$';
|
|
|
const OLD_DRAFT_PREFEX = '~$';
|
|
|
const DRAFT_EXT = '.dtmp';
|
|
|
@@ -1538,6 +1538,211 @@ const BKP_PREFEX = '.$';
|
|
|
const OLD_BKP_PREFEX = '~$';
|
|
|
const BKP_EXT = '.bkp';
|
|
|
|
|
|
+/**
|
|
|
+ * Checks the file content type
|
|
|
+ * Confirm content is xml, pdf, png, jpg, svg, vsdx ...
|
|
|
+ */
|
|
|
+function checkFileContent(body, enc)
|
|
|
+{
|
|
|
+ if (body != null)
|
|
|
+ {
|
|
|
+ let head, headBinay;
|
|
|
+
|
|
|
+ if (typeof body === 'string')
|
|
|
+ {
|
|
|
+ if (enc == 'base64')
|
|
|
+ {
|
|
|
+ headBinay = Buffer.from(body.substring(0, 22), 'base64');
|
|
|
+ head = headBinay.toString();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ head = body.substring(0, 16);
|
|
|
+ headBinay = Buffer.from(head);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ head = new TextDecoder("utf-8").decode(body.subarray(0, 16));
|
|
|
+ headBinay = body;
|
|
|
+ }
|
|
|
+
|
|
|
+ let c1 = head[0],
|
|
|
+ c2 = head[1],
|
|
|
+ c3 = head[2],
|
|
|
+ c4 = head[3],
|
|
|
+ c5 = head[4],
|
|
|
+ c6 = head[5],
|
|
|
+ c7 = head[6],
|
|
|
+ c8 = head[7],
|
|
|
+ c9 = head[8],
|
|
|
+ c10 = head[9],
|
|
|
+ c11 = head[10],
|
|
|
+ c12 = head[11],
|
|
|
+ c13 = head[12],
|
|
|
+ c14 = head[13],
|
|
|
+ c15 = head[14],
|
|
|
+ c16 = head[15];
|
|
|
+
|
|
|
+ let cc1 = headBinay[0],
|
|
|
+ cc2 = headBinay[1],
|
|
|
+ cc3 = headBinay[2],
|
|
|
+ cc4 = headBinay[3],
|
|
|
+ cc5 = headBinay[4],
|
|
|
+ cc6 = headBinay[5],
|
|
|
+ cc7 = headBinay[6],
|
|
|
+ cc8 = headBinay[7],
|
|
|
+ cc9 = headBinay[8],
|
|
|
+ cc10 = headBinay[9],
|
|
|
+ cc11 = headBinay[10],
|
|
|
+ cc12 = headBinay[11],
|
|
|
+ cc13 = headBinay[12],
|
|
|
+ cc14 = headBinay[13],
|
|
|
+ cc15 = headBinay[14],
|
|
|
+ cc16 = headBinay[15];
|
|
|
+
|
|
|
+ if (c1 == '<')
|
|
|
+ {
|
|
|
+ // text/html
|
|
|
+ if (c2 == '!'
|
|
|
+ || ((c2 == 'h'
|
|
|
+ && (c3 == 't' && c4 == 'm' && c5 == 'l'
|
|
|
+ || c3 == 'e' && c4 == 'a' && c5 == 'd')
|
|
|
+ || (c2 == 'b' && c3 == 'o' && c4 == 'd'
|
|
|
+ && c5 == 'y')))
|
|
|
+ || ((c2 == 'H'
|
|
|
+ && (c3 == 'T' && c4 == 'M' && c5 == 'L'
|
|
|
+ || c3 == 'E' && c4 == 'A' && c5 == 'D')
|
|
|
+ || (c2 == 'B' && c3 == 'O' && c4 == 'D'
|
|
|
+ && c5 == 'Y'))))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // application/xml
|
|
|
+ if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l'
|
|
|
+ && c6 == ' ')
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // application/svg+xml
|
|
|
+ if (c2 == 's' && c3 == 'v' && c4 == 'g' && c5 == ' ')
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // big and little (identical) endian UTF-8 encodings, with BOM
|
|
|
+ // application/xml
|
|
|
+ if (cc1 == 0xef && cc2 == 0xbb && cc3 == 0xbf)
|
|
|
+ {
|
|
|
+ if (c4 == '<' && c5 == '?' && c6 == 'x')
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // big and little endian UTF-16 encodings, with byte order mark
|
|
|
+ // application/xml
|
|
|
+ if (cc1 == 0xfe && cc2 == 0xff)
|
|
|
+ {
|
|
|
+ if (cc3 == 0 && c4 == '<' && cc5 == 0 && c6 == '?' && cc7 == 0
|
|
|
+ && c8 == 'x')
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // application/xml
|
|
|
+ if (cc1 == 0xff && cc2 == 0xfe)
|
|
|
+ {
|
|
|
+ if (c3 == '<' && cc4 == 0 && c5 == '?' && cc6 == 0 && c7 == 'x'
|
|
|
+ && cc8 == 0)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // big and little endian UTF-32 encodings, with BOM
|
|
|
+ // application/xml
|
|
|
+ if (cc1 == 0x00 && cc2 == 0x00 && cc3 == 0xfe && cc4 == 0xff)
|
|
|
+ {
|
|
|
+ if (cc5 == 0 && cc6 == 0 && cc7 == 0 && c8 == '<' && cc9 == 0
|
|
|
+ && cc10 == 0 && cc11 == 0 && c12 == '?' && cc13 == 0
|
|
|
+ && cc14 == 0 && cc15 == 0 && c16 == 'x')
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // application/xml
|
|
|
+ if (cc1 == 0xff && cc2 == 0xfe && cc3 == 0x00 && cc4 == 0x00)
|
|
|
+ {
|
|
|
+ if (c5 == '<' && cc6 == 0 && cc7 == 0 && cc8 == 0 && c9 == '?'
|
|
|
+ && cc10 == 0 && cc11 == 0 && cc12 == 0 && c13 == 'x'
|
|
|
+ && cc14 == 0 && cc15 == 0 && cc16 == 0)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // application/pdf (%PDF-)
|
|
|
+ if (cc1 == 37 && cc2 == 80 && cc3 == 68 && cc4 == 70 && cc5 == 45)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // image/png
|
|
|
+ if ((cc1 == 137 && cc2 == 80 && cc3 == 78 && cc4 == 71 && cc5 == 13
|
|
|
+ && cc6 == 10 && cc7 == 26 && cc8 == 10) ||
|
|
|
+ (cc1 == 194 && cc2 == 137 && cc3 == 80 && cc4 == 78 && cc5 == 71 && cc6 == 13 //Our embedded PNG+XML
|
|
|
+ && cc7 == 10 && cc8 == 26 && cc9 == 10))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // image/jpeg
|
|
|
+ if (cc1 == 0xFF && cc2 == 0xD8 && cc3 == 0xFF)
|
|
|
+ {
|
|
|
+ if (cc4 == 0xE0 || cc4 == 0xEE)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * File format used by digital cameras to store images.
|
|
|
+ * Exif Format can be read by any application supporting
|
|
|
+ * JPEG. Exif Spec can be found at:
|
|
|
+ * http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF
|
|
|
+ */
|
|
|
+ if ((cc4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i'
|
|
|
+ && c10 == 'f' && cc11 == 0))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // vsdx, vssx (also zip, jar, odt, ods, odp, docx, xlsx, pptx, apk, aar)
|
|
|
+ if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x04)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ else if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x06)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // mxfile, mxlibrary, mxGraphModel
|
|
|
+ if (c1 == '<' && c2 == 'm' && c3 == 'x')
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
function isConflict(origStat, stat)
|
|
|
{
|
|
|
return stat != null && origStat != null && stat.mtimeMs != origStat.mtimeMs;
|
|
|
@@ -1608,9 +1813,9 @@ async function getFileDrafts(fileObject)
|
|
|
|
|
|
async function saveDraft(fileObject, data)
|
|
|
{
|
|
|
- if (data == null || data.length == 0)
|
|
|
+ if (!checkFileContent(data))
|
|
|
{
|
|
|
- throw new Error('empty data');
|
|
|
+ throw new Error('Invalid file data');
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
@@ -1632,6 +1837,11 @@ async function saveDraft(fileObject, data)
|
|
|
|
|
|
async function saveFile(fileObject, data, origStat, overwrite, defEnc)
|
|
|
{
|
|
|
+ if (!checkFileContent(data))
|
|
|
+ {
|
|
|
+ throw new Error('Invalid file data');
|
|
|
+ }
|
|
|
+
|
|
|
var retryCount = 0;
|
|
|
var backupCreated = false;
|
|
|
var bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
|
|
|
@@ -1640,59 +1850,52 @@ async function saveFile(fileObject, data, origStat, overwrite, defEnc)
|
|
|
|
|
|
var writeFile = async function()
|
|
|
{
|
|
|
- if (data == null || data.length == 0)
|
|
|
+ let fh;
|
|
|
+
|
|
|
+ try
|
|
|
{
|
|
|
- throw new Error('empty data');
|
|
|
+ // O_SYNC is for sync I/O and reduce risk of file corruption
|
|
|
+ fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
|
|
|
+ await fsProm.writeFile(fh, data, writeEnc);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ await fh?.close();
|
|
|
}
|
|
|
- else
|
|
|
- {
|
|
|
- let fh;
|
|
|
|
|
|
- try
|
|
|
+ let stat2 = await fsProm.stat(fileObject.path);
|
|
|
+ // Workaround for possible writing errors is to check the written
|
|
|
+ // contents of the file and retry 3 times before showing an error
|
|
|
+ let writtenData = await fsProm.readFile(fileObject.path, writeEnc);
|
|
|
+
|
|
|
+ if (data != writtenData)
|
|
|
+ {
|
|
|
+ retryCount++;
|
|
|
+
|
|
|
+ if (retryCount < 3)
|
|
|
{
|
|
|
- // O_SYNC is for sync I/O and reduce risk of file corruption
|
|
|
- fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
|
|
|
- await fsProm.writeFile(fh, data, writeEnc);
|
|
|
+ return await writeFile();
|
|
|
}
|
|
|
- finally
|
|
|
+ else
|
|
|
{
|
|
|
- await fh?.close();
|
|
|
+ throw new Error('all saving trials failed');
|
|
|
}
|
|
|
-
|
|
|
- let stat2 = await fsProm.stat(fileObject.path);
|
|
|
- // Workaround for possible writing errors is to check the written
|
|
|
- // contents of the file and retry 3 times before showing an error
|
|
|
- let writtenData = await fsProm.readFile(fileObject.path, writeEnc);
|
|
|
-
|
|
|
- if (data != writtenData)
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ //We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file?
|
|
|
+ if (backupCreated)
|
|
|
{
|
|
|
- retryCount++;
|
|
|
-
|
|
|
- if (retryCount < 3)
|
|
|
- {
|
|
|
- return await writeFile();
|
|
|
- }
|
|
|
- else
|
|
|
+ //fs.unlink(bkpPath, (err) => {}); //Ignore errors!
|
|
|
+
|
|
|
+ //Delete old backup file with old prefix
|
|
|
+ if (fs.existsSync(oldBkpPath))
|
|
|
{
|
|
|
- throw new Error('all saving trials failed');
|
|
|
+ fs.unlink(oldBkpPath, (err) => {}); //Ignore errors
|
|
|
}
|
|
|
}
|
|
|
- else
|
|
|
- {
|
|
|
- //We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file?
|
|
|
- if (backupCreated)
|
|
|
- {
|
|
|
- //fs.unlink(bkpPath, (err) => {}); //Ignore errors!
|
|
|
|
|
|
- //Delete old backup file with old prefix
|
|
|
- if (fs.existsSync(oldBkpPath))
|
|
|
- {
|
|
|
- fs.unlink(oldBkpPath, (err) => {}); //Ignore errors
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return stat2;
|
|
|
- }
|
|
|
+ return stat2;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -1758,7 +1961,14 @@ async function saveFile(fileObject, data, origStat, overwrite, defEnc)
|
|
|
|
|
|
async function writeFile(path, data, enc)
|
|
|
{
|
|
|
- return await fsProm.writeFile(path, data, enc);
|
|
|
+ if (!checkFileContent(data, enc))
|
|
|
+ {
|
|
|
+ throw new Error('Invalid file data');
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return await fsProm.writeFile(path, data, enc);
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
function getAppDataFolder()
|
|
|
@@ -1856,7 +2066,14 @@ function dirname(path_p)
|
|
|
|
|
|
async function readFile(filename, encoding)
|
|
|
{
|
|
|
- return await fsProm.readFile(filename, encoding);
|
|
|
+ let data = await fsProm.readFile(filename, encoding);
|
|
|
+
|
|
|
+ if (checkFileContent(data, encoding))
|
|
|
+ {
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error('Invalid file data');
|
|
|
}
|
|
|
|
|
|
async function fileStat(file)
|
|
|
@@ -1897,7 +2114,16 @@ function clipboardAction(method, data)
|
|
|
|
|
|
async function deleteFile(file)
|
|
|
{
|
|
|
- await fsProm.unlink(file);
|
|
|
+ // Reading the header of the file to confirm it is a file we can delete
|
|
|
+ let fh = await fsProm.open(file, O_RDONLY);
|
|
|
+ let buffer = Buffer.allocUnsafe(16);
|
|
|
+ await fh.read(buffer, 0, 16);
|
|
|
+ await fh.close();
|
|
|
+
|
|
|
+ if (checkFileContent(buffer))
|
|
|
+ {
|
|
|
+ await fsProm.unlink(file);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function windowAction(method)
|