Skip to content

Commit 8b9337e

Browse files
authored
Merge pull request #774 from OWASP/modify-save
Guard in electron for unsaved changes
2 parents f2db458 + 1ce2bf7 commit 8b9337e

File tree

17 files changed

+263
-93
lines changed

17 files changed

+263
-93
lines changed

td.vue/public/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
88
// renderer to electron main
99
updateMenu: (locale) => ipcRenderer.send('update-menu', locale),
1010
modelClosed: (fileName) => ipcRenderer.send('model-closed', fileName),
11+
modelModified: (modified) => ipcRenderer.send('model-modified', modified),
1112
modelOpened: (fileName) => ipcRenderer.send('model-opened', fileName),
1213
modelPrint: (printer) => ipcRenderer.send('model-print', printer),
1314
modelSaved: (modelData, fileName) => ipcRenderer.send('model-saved', modelData, fileName),

td.vue/src/components/Graph.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export default {
7777
this.graph = diagramService.edit(this.$refs.graph_container, this.diagram);
7878
stencil.get(this.graph, this.$refs.stencil_container);
7979
console.debug('diagram ID: ' + this.diagram.id);
80+
this.$store.dispatch(tmActions.unmodified);
8081
},
8182
threatSelected(threatId) {
8283
this.$refs.threatEditDialog.editThreat(threatId);
@@ -86,10 +87,11 @@ export default {
8687
updated.cells = this.graph.toJSON().cells;
8788
this.$store.dispatch(tmActions.diagramUpdated, updated);
8889
this.$store.dispatch(tmActions.save);
90+
this.$store.dispatch(tmActions.unmodified);
8991
},
9092
async closed() {
91-
const diagramChanged = JSON.stringify(this.graph.toJSON().cells) !== JSON.stringify(this.diagram.cells);
92-
if (!diagramChanged || await this.getConfirmModal()) {
93+
if (!this.$store.getters.modelChanged || await this.getConfirmModal()) {
94+
this.$store.dispatch(tmActions.unmodified);
9395
this.$router.push({ name: `${this.providerType}ThreatModel`, params: this.$route.params });
9496
}
9597
},

td.vue/src/desktop/desktop.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ app.on('ready', async () => {
9393

9494
ipcMain.on('update-menu', handleUpdateMenu);
9595
ipcMain.on('model-closed', handleModelClosed);
96+
ipcMain.on('model-modified', handleModelModified);
9697
ipcMain.on('model-opened', handleModelOpened);
9798
ipcMain.on('model-print', handleModelPrint);
9899
ipcMain.on('model-saved', handleModelSaved);
@@ -108,7 +109,9 @@ app.on('open-file', function(event, path) {
108109
// handle this event
109110
event.preventDefault();
110111
logger.log.debug('Open file from recent documents: ' + path);
111-
menu.readModelData(path);
112+
if (menu.guardModel() === true) {
113+
menu.readModelData(path);
114+
}
112115
});
113116

114117
function handleUpdateMenu (_event, locale) {
@@ -123,6 +126,11 @@ function handleModelClosed (_event, fileName) {
123126
menu.modelClosed();
124127
}
125128

129+
function handleModelModified (_event, modified) {
130+
logger.log.debug('Modified model notification from renderer: ' + modified);
131+
menu.modelModified(modified);
132+
}
133+
126134
function handleModelOpened (_event, fileName) {
127135
logger.log.debug('Open model notification from renderer for file name: ' + fileName);
128136
menu.modelOpened();

td.vue/src/desktop/menu.js

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ var language = defaultLanguage;
3232
export const model = {
3333
fileDirectory: '',
3434
filePath: '',
35-
isOpen: false
35+
isOpen: false,
36+
isModified: false
3637
};
3738

3839
export function getMenuTemplate () {
@@ -147,9 +148,8 @@ export function getMenuTemplate () {
147148

148149
// Open file system dialog and read file contents into model
149150
function openModel () {
150-
if (model.isOpen === true) {
151-
logger.log.debug('Checking that the existing model is not modified');
152-
logger.log.warn('TODO check from renderer that existing open file is not modified');
151+
if (guardModel() === false) {
152+
return;
153153
}
154154
dialog.showOpenDialog({
155155
title: messages[language].desktop.file.open,
@@ -226,6 +226,9 @@ function saveModelDataAs (modelData, fileName) {
226226

227227
// open a new model
228228
function newModel () {
229+
if (guardModel() === false) {
230+
return;
231+
}
229232
let newName = 'new-model.json';
230233
logger.log.debug(messages[language].desktop.file.new + ': ' + newName);
231234
// prompt the renderer to open a new model
@@ -242,12 +245,38 @@ function printModel () {
242245

243246
// close the model
244247
function closeModel () {
248+
if (guardModel() === false) {
249+
return;
250+
}
245251
logger.log.debug(messages[language].desktop.file.close + ': ' + model.filePath);
246252
// prompt the renderer to close the model
247253
mainWindow.webContents.send('close-model', path.basename(model.filePath));
248254
modelClosed();
249255
}
250256

257+
// close the model
258+
export function guardModel () {
259+
if (model.isOpen === false || model.isModified === false) {
260+
return true;
261+
}
262+
logger.log.debug('Check existing open and modified model can be closed');
263+
const dialogOptions = {
264+
title: messages[language].forms.discardTitle,
265+
message: messages[language].forms.discardMessage,
266+
buttons: [ messages[language].forms.ok, messages[language].forms.cancel ],
267+
type: 'warning',
268+
defaultId: 1,
269+
cancelId: 1
270+
};
271+
let guard = false;
272+
let result = dialog.showMessageBoxSync(mainWindow, dialogOptions);
273+
if (result === 0) {
274+
guard = true;
275+
}
276+
logger.log.debug(messages[language].forms.discardTitle + ': ' + guard);
277+
return guard;
278+
}
279+
251280
// read threat model from file, eg after open-file app module event
252281
export function readModelData (filePath) {
253282
model.filePath = filePath;
@@ -282,22 +311,6 @@ function saveModelData (modelData) {
282311
}
283312
}
284313

285-
// the renderer has requested to save the model with a filename
286-
export const modelSaved = (modelData, fileName) => {
287-
// if the filePath is empty then this is the first time a save has been requested
288-
if (!model.filePath || model.filePath === '') {
289-
saveModelDataAs(modelData, fileName);
290-
} else {
291-
saveModelData(modelData);
292-
}
293-
};
294-
295-
// clear out the model, either by menu or by renderer request
296-
export const modelClosed = () => {
297-
model.filePath = '';
298-
model.isOpen = false;
299-
};
300-
301314
// Open saveAs file system dialog and write contents as HTML
302315
function saveHTMLReport (htmlPath) {
303316
htmlPath += '.html';
@@ -360,6 +373,25 @@ function savePDFReport (pdfPath) {
360373
});
361374
}
362375

376+
// clear out the model, either by menu or by renderer request
377+
export const modelClosed = () => {
378+
model.filePath = '';
379+
model.isOpen = false;
380+
};
381+
382+
// the renderer has modified the model
383+
export const modelModified = (modified) => {
384+
model.isModified = modified;
385+
};
386+
387+
// the renderer has opened a new model
388+
export const modelOpened = () => {
389+
// for security reasons the renderer can not provide the full path
390+
// so wait for a save before filling in the file path
391+
model.filePath = '';
392+
model.isOpen = true;
393+
};
394+
363395
// the renderer has requested a report to be printed
364396
export const modelPrint = (printer) => {
365397
let reportPath = path.join(path.dirname(model.filePath), path.basename(model.filePath, '.json'));
@@ -376,14 +408,17 @@ export const modelPrint = (printer) => {
376408
}
377409
};
378410

379-
// the renderer has opened a new model
380-
export const modelOpened = () => {
381-
// for security reasons the renderer can not provide the full path
382-
// so wait for a save before filling in the file path
383-
model.filePath = '';
384-
model.isOpen = true;
411+
// the renderer has requested to save the model with a filename
412+
export const modelSaved = (modelData, fileName) => {
413+
// if the filePath is empty then this is the first time a save has been requested
414+
if (!model.filePath || model.filePath === '') {
415+
saveModelDataAs(modelData, fileName);
416+
} else {
417+
saveModelData(modelData);
418+
}
385419
};
386420

421+
// the renderer has changed the language
387422
export const setLocale = (locale) => {
388423
language = languages.includes(locale) ? locale : defaultLanguage;
389424
};
@@ -394,7 +429,9 @@ export const setMainWindow = (window) => {
394429

395430
export default {
396431
getMenuTemplate,
432+
guardModel,
397433
modelClosed,
434+
modelModified,
398435
modelOpened,
399436
modelPrint,
400437
modelSaved,

td.vue/src/main.desktop.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ Vue.config.productionTip = false;
2020

2121
// informing renderer that desktop menu shell has closed the model
2222
window.electronAPI.onCloseModel((_event, fileName) => {
23-
console.warn('TODO check that any existing open model has not been modified');
24-
// getConfirmModal();
2523
console.debug('Closing model with file name : ' + fileName);
2624
app.$store.dispatch(threatmodelActions.clear);
2725
localAuth();
@@ -34,8 +32,6 @@ window.electronAPI.onCloseModel((_event, fileName) => {
3432

3533
// request from desktop menu shell -> renderer to start a new model
3634
window.electronAPI.onNewModel((_event, fileName) => {
37-
console.warn('TODO check that any existing open model has not been modified');
38-
// getConfirmModal();
3935
console.debug('New model with file name : ' + fileName);
4036
app.$store.dispatch(threatmodelActions.update, { fileName: fileName });
4137
localAuth();

td.vue/src/service/migration/diagram.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ const upgradeAndDraw = (diagram, graph) => {
3535
updated.diagramType = diagram.diagramType;
3636
graph.getCells().forEach((cell) => dataChanged.updateStyleAttrs(cell));
3737
store.get().dispatch(tmActions.diagramUpdated, updated);
38-
store.get().dispatch(tmActions.setImmutableCopy);
38+
store.get().dispatch(tmActions.setRollback);
39+
store.get().dispatch(tmActions.unmodified);
40+
3941
};
4042

4143
const drawGraph = (diagram, graph) => {

td.vue/src/service/x6/graph/events.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import dataChanged from './data-changed.js';
66
import defaultProperties from '@/service/entity/default-properties.js';
77
import store from '@/store/index.js';
88
import { CELL_SELECTED, CELL_UNSELECTED } from '@/store/actions/cell.js';
9+
import { THREATMODEL_MODIFIED } from '@/store/actions/threatmodel.js';
910
import shapes from '@/service/x6/shapes/index.js';
1011

1112
const edgeConnected = ({ isNew, edge }) => {
@@ -71,6 +72,12 @@ const cellAdded = (graph) => ({ cell }) => {
7172
cell.setData(defaultProperties.getByType(cell.type));
7273
}
7374
store.get().dispatch(CELL_SELECTED, cell);
75+
store.get().dispatch(THREATMODEL_MODIFIED);
76+
};
77+
78+
const cellDeleted = () => {
79+
console.debug('cell deleted: ');
80+
store.get().dispatch(THREATMODEL_MODIFIED);
7481
};
7582

7683
const cellSelected = ({ cell }) => {
@@ -100,6 +107,7 @@ const cellSelected = ({ cell }) => {
100107

101108
store.get().dispatch(CELL_SELECTED, cell);
102109
dataChanged.updateStyleAttrs(cell);
110+
store.get().dispatch(THREATMODEL_MODIFIED);
103111
};
104112

105113
const cellUnselected = ({ cell }) => {
@@ -142,6 +150,7 @@ const listen = (graph) => {
142150
graph.on('cell:mouseleave', removeCellTools);
143151
graph.on('cell:mouseenter', mouseEnter);
144152
graph.on('cell:added', cellAdded(graph));
153+
graph.on('cell:removed', cellDeleted);
145154
graph.on('cell:change:data', cellDataChanged);
146155
graph.on('cell:selected', cellSelected);
147156
graph.on('cell:unselected', cellUnselected);
@@ -156,6 +165,7 @@ const removeListeners = (graph) => {
156165
graph.off('cell:mouseleave', removeCellTools);
157166
graph.off('cell:mouseenter', mouseEnter);
158167
graph.off('cell:added', cellAdded(graph));
168+
graph.off('cell:removed', cellDeleted);
159169
graph.off('cell:change:data', cellDataChanged);
160170
graph.off('cell:selected', cellSelected);
161171
graph.off('cell:unselected', cellUnselected);

td.vue/src/store/actions/threatmodel.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@ export const THREATMODEL_FETCH = 'THREATMODEL_FETCH';
55
export const THREATMODEL_FETCH_ALL = 'THREATMODEL_FETCH_ALL';
66
export const THREATMODEL_CREATE = 'THREATMODEL_CREATE';
77
export const THREATMODEL_CONTRIBUTORS_UPDATED = 'THREATMODEL_CONTRIBUTORS_UPDATED';
8+
export const THREATMODEL_MODIFIED = 'THREATMODEL_MODIFIED;';
89
export const THREATMODEL_RESTORE = 'THREATMODEL_RESTORE';
910
export const THREATMODEL_SAVE = 'THREATMODEL_SAVE';
10-
export const THREATMODEL_SET_IMMUTABLE_COPY = 'THREATMODEL_SET_IMMUTABLE_COPY';
1111
export const THREATMODEL_SELECTED = 'THREATMODEL_SELECTED';
12+
export const THREATMODEL_SET_ROLLBACK = 'THREATMODEL_SET_ROLLBACK';
13+
export const THREATMODEL_UNMODIFIED = 'THREATMODEL_UNMODIFIED';
1214
export const THREATMODEL_UPDATE = 'THREATMODEL_UPDATE';
1315

1416
export default {
1517
clear: THREATMODEL_CLEAR,
18+
contributorsUpdated: THREATMODEL_CONTRIBUTORS_UPDATED,
1619
create: THREATMODEL_CREATE,
1720
diagramSelected: THREATMODEL_DIAGRAM_SELECTED,
1821
diagramUpdated: THREATMODEL_DIAGRAM_UPDATED,
1922
fetch: THREATMODEL_FETCH,
2023
fetchAll: THREATMODEL_FETCH_ALL,
24+
modified: THREATMODEL_MODIFIED,
2125
restore: THREATMODEL_RESTORE,
2226
save: THREATMODEL_SAVE,
2327
selected: THREATMODEL_SELECTED,
24-
setImmutableCopy: THREATMODEL_SET_IMMUTABLE_COPY,
28+
setRollback: THREATMODEL_SET_ROLLBACK,
29+
unmodified: THREATMODEL_UNMODIFIED,
2530
update: THREATMODEL_UPDATE
2631
};

0 commit comments

Comments
 (0)