1/**
2 * Copyright (c) 2006-2017, JGraph Ltd
3 * Copyright (c) 2006-2017, Gaudenz Alder
4 */
5DrawioFile = function(ui, data)
6{
7	mxEventSource.call(this);
8
9	/**
10	 * Holds the x-coordinate of the point.
11	 * @type number
12	 * @default 0
13	 */
14	this.ui = ui;
15
16	/**
17	 * Holds the x-coordinate of the point.
18	 * @type number
19	 * @default 0
20	 */
21	this.data = data || '';
22	this.shadowData = this.data;
23	this.shadowPages = null;
24	this.created = new Date().getTime();
25
26	// Creates the stats object
27	this.stats = {
28		opened: 0, /* number of calls to open */
29		merged: 0, /* number of calls to merge */
30		fileMerged: 0, /* number of calls to mergeFile */
31		fileReloaded: 0, /* number of calls to mergeFile */
32		conflicts: 0, /* number of write conflicts when saving a file */
33		timeouts: 0, /* number of time we have given up to retry after a write conflict */
34		saved: 0, /* number of calls to fileSaved */
35		closed: 0, /* number of calls to close */
36		destroyed: 0, /* number of calls to close */
37		joined: 0, /* number of join messages received */
38		checksumErrors: 0, /* number of checksum errors */
39		bytesSent: 0, /* number of bytes send in messages */
40		bytesReceived: 0, /* number of bytes received in messages */
41		msgSent: 0, /* number of messages sent */
42		msgReceived: 0, /* number of messages received */
43		cacheHits: 0, /* number of times the cache returned patches */
44		cacheMiss: 0, /* number of times we have missed a cache entry */
45		cacheFail: 0 /* number of times we have failed to read the cache */
46	};
47};
48
49/**
50 * Global switch for realtime collaboration type to use sync URL parameter
51 * with the following possible values:
52 *
53 * - none: overwrite
54 * - manual: manual sync
55 * - auto: automatic sync
56 */
57DrawioFile.SYNC = urlParams['sync'] || 'auto';
58
59/**
60 * Specifies if last write wins should be used for values and styles.
61 */
62DrawioFile.LAST_WRITE_WINS = true;
63
64// Extends mxEventSource
65mxUtils.extend(DrawioFile, mxEventSource);
66
67/**
68 * Specifies the resource key for all changes saved status message.
69 */
70DrawioFile.prototype.allChangesSavedKey = 'allChangesSaved';
71
72/**
73 * Specifies the resource key for saving spinner.
74 */
75DrawioFile.prototype.savingSpinnerKey = 'saving';
76
77/**
78 * Specifies the resource key for saving status message.
79 */
80DrawioFile.prototype.savingStatusKey = 'saving';
81
82/**
83 * Specifies the delay between the last change and the autosave.
84 */
85DrawioFile.prototype.autosaveDelay = 1500;
86
87/**
88 * Specifies the maximum delay before an autosave is forced even if the graph
89 * is being changed.
90 */
91DrawioFile.prototype.maxAutosaveDelay = 30000;
92
93/**
94 * Specifies the delay for loading the file after an optimistic sync message.
95 * This should be the delay for the file to be saved minus the delay for the
96 * sync message to travel.
97 */
98DrawioFile.prototype.optimisticSyncDelay = 300;
99
100/**
101 * Contains the thread for the next autosave.
102 */
103DrawioFile.prototype.autosaveThread = null;
104
105/**
106 * Stores the time stamp for the last autosave.
107 */
108DrawioFile.prototype.lastAutosave = null;
109
110/**
111 * Stores the time stamp for the last autosave.
112 */
113DrawioFile.prototype.lastSaved = null;
114
115/**
116 * Stores the time stamp for the last autosave.
117 */
118DrawioFile.prototype.lastChanged = null;
119
120/**
121 * Stores the time stamp when the file was opened.
122 */
123DrawioFile.prototype.opened = null;
124
125/**
126 * Stores the modified state.
127 */
128DrawioFile.prototype.modified = false;
129
130/**
131 * Stores a shadow of the modified state.
132 */
133DrawioFile.prototype.shadowModified = false;
134
135/**
136 * Holds a copy of the current file data.
137 */
138DrawioFile.prototype.data = null;
139
140/**
141 * Holds a copy of the last saved file data.
142 */
143DrawioFile.prototype.shadowData = null;
144
145/**
146 * Holds a copy of the parsed last saved file data.
147 */
148DrawioFile.prototype.shadowPages = null;
149
150/**
151 * Specifies if the graph change listener is enabled. Default is true.
152 */
153DrawioFile.prototype.changeListenerEnabled = true;
154
155/**
156 * Sets the delay for autosave in milliseconds. Default is 1500.
157 */
158DrawioFile.prototype.lastAutosaveRevision = null;
159
160/**
161 * Sets the delay between revisions when using autosave. Default is 300000
162 * ie 5 mins. Set this to 0 to create a revision on every autosave.
163 */
164DrawioFile.prototype.maxAutosaveRevisionDelay = 300000;
165
166/**
167 * Specifies if notify events should be ignored.
168 */
169DrawioFile.prototype.inConflictState = false;
170
171/**
172 * Specifies if notify events should be ignored.
173 */
174DrawioFile.prototype.invalidChecksum = false;
175
176/**
177 * Specifies if error reports should be sent.
178 */
179DrawioFile.prototype.errorReportsEnabled = false;
180
181/**
182 * Specifies if stats should be sent.
183 */
184DrawioFile.prototype.ageStart = null;
185
186/**
187 * Specifies if notify events should be ignored.
188 */
189DrawioFile.prototype.getSize = function()
190{
191	return (this.data != null) ? this.data.length : 0;
192};
193
194/**
195 * Adds the listener for automatically saving the diagram for local changes.
196 */
197DrawioFile.prototype.synchronizeFile = function(success, error)
198{
199	if (this.savingFile)
200	{
201		if (error != null)
202		{
203			error({message: mxResources.get('busy')});
204		}
205	}
206	else
207	{
208		if (this.sync != null)
209		{
210			this.sync.fileChanged(success, error);
211		}
212		else
213		{
214			this.updateFile(success, error);
215		}
216	}
217};
218
219/**
220* Adds the listener for automatically saving the diagram for local changes.
221*/
222DrawioFile.prototype.updateFile = function(success, error, abort, shadow)
223{
224	if (abort == null || !abort())
225	{
226		if (this.ui.getCurrentFile() != this || this.invalidChecksum)
227		{
228			if (error != null)
229			{
230				error();
231			}
232		}
233		else
234		{
235			this.getLatestVersion(mxUtils.bind(this, function(latestFile)
236			{
237				try
238				{
239					if (abort == null || !abort())
240					{
241						if (this.ui.getCurrentFile() != this || this.invalidChecksum)
242						{
243							if (error != null)
244							{
245								error();
246							}
247						}
248						else
249						{
250							if (latestFile != null)
251							{
252								this.mergeFile(latestFile, success, error, shadow);
253							}
254							else
255							{
256								this.reloadFile(success, error);
257							}
258						}
259					}
260				}
261				catch (e)
262				{
263					if (error != null)
264					{
265						error(e);
266					}
267				}
268			}), error);
269		}
270	}
271};
272
273/**
274 * Adds the listener for automatically saving the diagram for local changes.
275 */
276DrawioFile.prototype.mergeFile = function(file, success, error, diffShadow)
277{
278	var reportError = true;
279
280	try
281	{
282		this.stats.fileMerged++;
283
284		// Takes copy of current shadow document
285		var shadow = (this.shadowPages != null) ? this.shadowPages :
286			this.ui.getPagesForNode(mxUtils.parseXml(
287			this.shadowData).documentElement);
288
289		// Loads new document as shadow document
290		var pages = this.ui.getPagesForNode(
291			mxUtils.parseXml(file.data).
292			documentElement)
293
294		if (pages != null && pages.length > 0)
295		{
296			this.shadowPages = pages;
297
298			// Creates a patch for backup if the checksum fails
299			this.backupPatch = (this.isModified()) ?
300				this.ui.diffPages(shadow,
301				this.ui.pages) : null;
302
303			// Patches the current document
304			var patches = [this.ui.diffPages((diffShadow != null) ?
305				diffShadow : shadow, this.shadowPages)];
306			var ignored = this.ignorePatches(patches);
307
308			if (!ignored)
309			{
310				// Patching previous shadow to verify checksum
311				var patched = this.ui.patchPages(shadow, patches[0]);
312
313				var patchedDetails = {};
314				var checksum = this.ui.getHashValueForPages(patched, patchedDetails);
315				var currentDetails = {};
316				var current = this.ui.getHashValueForPages(this.shadowPages, currentDetails);
317
318				if (urlParams['test'] == '1')
319				{
320					EditorUi.debug('File.mergeFile', [this],
321						'backup', this.backupPatch,
322						'patches', patches,
323						'checksum', current == checksum, checksum);
324				}
325
326				if (checksum != null && checksum != current)
327				{
328					var fileData = this.compressReportData(this.getAnonymizedXmlForPages(pages));
329					var data = this.compressReportData(this.getAnonymizedXmlForPages(patched));
330					var from = this.ui.hashValue(file.getCurrentEtag());
331					var to = this.ui.hashValue(this.getCurrentEtag());
332
333					this.checksumError(error, patches,
334						'Shadow Details: ' + JSON.stringify(patchedDetails) +
335						'\nChecksum: ' + checksum +
336						'\nCurrent: ' + current +
337						'\nCurrent Details: ' + JSON.stringify(currentDetails) +
338						'\nFrom: ' + from +
339						'\nTo: ' + to +
340						'\n\nFile Data:\n' + fileData +
341						'\nPatched Shadow:\n' + data, null, 'mergeFile');
342
343					// Abnormal termination
344					return;
345				}
346				else
347				{
348					// Patches the current document
349					this.patch(patches,
350						(DrawioFile.LAST_WRITE_WINS) ?
351						this.backupPatch : null);
352				}
353			}
354		}
355		else
356		{
357			reportError = false;
358			throw new Error(mxResources.get('notADiagramFile'));
359		}
360
361		this.invalidChecksum = false;
362		this.inConflictState = false;
363		this.setDescriptor(file.getDescriptor());
364		this.descriptorChanged();
365		this.backupPatch = null;
366
367		if (success != null)
368		{
369			success();
370		}
371	}
372	catch (e)
373	{
374		this.inConflictState = true;
375		this.invalidChecksum = true;
376		this.descriptorChanged();
377
378		if (error != null)
379		{
380			error(e);
381		}
382
383		try
384		{
385			if (reportError)
386			{
387				if (this.errorReportsEnabled)
388				{
389					this.sendErrorReport('Error in mergeFile', null, e);
390				}
391				else
392				{
393					var user = this.getCurrentUser();
394					var uid = (user != null) ? user.id : 'unknown';
395
396					EditorUi.logError('Error in mergeFile', null,
397						this.getMode() + '.' + this.getId(),
398						uid, e);
399				}
400			}
401		}
402		catch (e2)
403		{
404			// ignore
405		}
406	}
407};
408
409/**
410 * Adds the listener for automatically saving the diagram for local changes.
411 */
412DrawioFile.prototype.getAnonymizedXmlForPages = function(pages)
413{
414	var enc = new mxCodec(mxUtils.createXmlDocument());
415	var file = enc.document.createElement('mxfile');
416
417	if (pages != null)
418	{
419		for (var i = 0; i < pages.length; i++)
420		{
421			var temp = enc.encode(new mxGraphModel(pages[i].root));
422
423			if (urlParams['dev'] != '1')
424			{
425				temp = this.ui.anonymizeNode(temp, true);
426			}
427
428			temp.setAttribute('id', pages[i].getId());
429
430			if (pages[i].viewState)
431			{
432				this.ui.editor.graph.saveViewState(pages[i].viewState, temp, true);
433			}
434
435			file.appendChild(temp);
436		}
437	}
438
439	return mxUtils.getPrettyXml(file);
440};
441
442/**
443 * Adds the listener for automatically saving the diagram for local changes.
444 */
445DrawioFile.prototype.compressReportData = function(data, limit, max)
446{
447	limit = (limit != null) ? limit : 10000;
448
449	if (max != null && data != null && data.length > max)
450	{
451		data = data.substring(0, max) + '[...]';
452	}
453	else if (data != null && data.length > limit)
454	{
455		data = Graph.compress(data) + '\n';
456	}
457
458	return data;
459};
460
461/**
462 * Adds the listener for automatically saving the diagram for local changes.
463 */
464DrawioFile.prototype.checksumError = function(error, patches, details, etag, functionName)
465{
466	this.stats.checksumErrors++;
467	this.inConflictState = true;
468	this.invalidChecksum = true;
469	this.descriptorChanged();
470
471	if (this.sync != null)
472	{
473		this.sync.updateOnlineState();
474	}
475
476	if (error != null)
477	{
478		error();
479	}
480
481	try
482	{
483		if (this.errorReportsEnabled)
484		{
485			if (patches != null)
486			{
487				for (var i = 0; i < patches.length; i++)
488				{
489					this.ui.anonymizePatch(patches[i]);
490				}
491			}
492
493			var fn = mxUtils.bind(this, function(file)
494			{
495				var json = this.compressReportData(
496					JSON.stringify(patches, null, 2));
497				var remote = (file != null) ? this.compressReportData(
498					this.getAnonymizedXmlForPages(
499					this.ui.getPagesForNode(
500					mxUtils.parseXml(file.data).documentElement)), 25000) : 'n/a';
501
502				this.sendErrorReport('Checksum Error in ' + functionName + ' ' + this.getHash(),
503					((details != null) ? (details) : '') +  '\n\nPatches:\n' + json +
504					((remote != null) ? ('\n\nRemote:\n' + remote) : ''), null, 70000);
505			});
506
507			if (etag == null)
508			{
509				fn(null);
510			}
511			else
512			{
513				this.getLatestVersion(mxUtils.bind(this, function(file)
514				{
515					if (file != null && file.getCurrentEtag() == etag)
516					{
517						fn(file);
518					}
519					else
520					{
521						fn(null);
522					}
523				}), function() {});
524			}
525		}
526		else
527		{
528			var user = this.getCurrentUser();
529			var uid = (user != null) ? user.id : 'unknown';
530
531			EditorUi.logError('Checksum Error in ' + functionName + ' ' + this.getId(),
532				null, this.getMode() + '.' + this.getId(),
533				'user_' + uid + ((this.sync != null) ?
534				'-client_' + this.sync.clientId : '-nosync'));
535
536			// Logs checksum error for file
537			try
538			{
539				EditorUi.logEvent({category: 'CHECKSUM-ERROR-SYNC-FILE-' + this.getHash(),
540					action: functionName, label: 'user_' + uid + ((this.sync != null) ?
541					'-client_' + this.sync.clientId : '-nosync')});
542			}
543			catch (e)
544			{
545				// ignore
546			}
547		}
548	}
549	catch (e)
550	{
551		// ignore
552	}
553};
554
555/**
556 * Adds the listener for automatically saving the diagram for local changes.
557 */
558DrawioFile.prototype.sendErrorReport = function(title, details, error, max)
559{
560	try
561	{
562		var shadow = this.compressReportData(
563			this.getAnonymizedXmlForPages(
564			this.shadowPages), 25000);
565		var data = this.compressReportData(
566			this.getAnonymizedXmlForPages(
567			this.ui.pages), 25000);
568		var user = this.getCurrentUser();
569		var uid = (user != null) ? this.ui.hashValue(user.id) : 'unknown';
570		var cid = (this.sync != null) ? '-client_' + this.sync.clientId : '-nosync';
571		var filename = this.getTitle();
572		var dot = filename.lastIndexOf('.');
573		var ext = 'xml';
574
575		if (dot > 0)
576		{
577			ext = filename.substring(dot);
578		}
579
580		var stack = (error != null) ? error.stack : new Error().stack;
581
582		EditorUi.sendReport(title + ' ' + new Date().toISOString() + ':' +
583			'\n\nAppVersion=' + navigator.appVersion +
584			'\nFile=' + this.ui.hashValue(this.getId()) + ' (' + this.getMode() + ')' +
585			((this.isModified()) ? ' modified' : '') +
586			'\nSize/Type=' + this.getSize() + ' (' + ext + ')' +
587			'\nUser=' + uid + cid +
588			'\nPrefix=' + this.ui.editor.graph.model.prefix +
589			'\nSync=' + DrawioFile.SYNC +
590			((this.sync != null) ? (((this.sync.enabled) ? ' enabled' : '') +
591				((this.sync.isConnected()) ? ' connected' : '')) : '') +
592			'\nPlugins=' + ((mxSettings.settings != null) ? mxSettings.getPlugins() : 'null') +
593			'\n\nStats:\n' + JSON.stringify(this.stats, null, 2) +
594			((details != null) ? ('\n\n' + details) : '') +
595			((error != null) ? ('\n\nError: ' + error.message) : '') +
596			'\n\nStack:\n' + stack +
597			'\n\nShadow:\n' + shadow +
598			'\n\nData:\n' + data, max);
599	}
600	catch (e)
601	{
602		// ignore
603	}
604};
605
606/**
607 * Adds the listener for automatically saving the diagram for local changes.
608 */
609DrawioFile.prototype.reloadFile = function(success, error)
610{
611	try
612	{
613		this.ui.spinner.stop();
614
615		var fn = mxUtils.bind(this, function()
616		{
617			this.stats.fileReloaded++;
618
619			// Restores view state and current page
620			var viewState = this.ui.editor.graph.getViewState();
621			var selection = this.ui.editor.graph.getSelectionCells();
622			var page = this.ui.currentPage;
623
624			this.ui.loadFile(this.getHash(), true, null, mxUtils.bind(this, function()
625			{
626				if (this.ui.fileLoadedError == null)
627				{
628					this.ui.restoreViewState(page, viewState, selection);
629
630					if (this.backupPatch != null)
631					{
632						this.patch([this.backupPatch]);
633					}
634
635					// Carry-over stats
636					var file = this.ui.getCurrentFile();
637
638					if (file != null)
639					{
640						file.stats = this.stats;
641					}
642
643					if (success != null)
644					{
645						success();
646					}
647				}
648			}), true);
649		});
650
651		if (this.isModified() && this.backupPatch == null)
652		{
653			this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function()
654			{
655				this.handleFileSuccess(DrawioFile.SYNC == 'manual');
656			}), fn, mxResources.get('cancel'), mxResources.get('discardChanges'));
657		}
658		else
659		{
660			fn();
661		}
662	}
663	catch (e)
664	{
665		if (error != null)
666		{
667			error(e);
668		}
669	}
670};
671
672/**
673 * Shows a conflict dialog to the user.
674 */
675DrawioFile.prototype.copyFile = function(success, error)
676{
677	this.ui.editor.editAsNew(this.ui.getFileData(true),
678		this.ui.getCopyFilename(this));
679};
680
681/**
682 * Returns true if the patches in the given array are empty.
683 */
684DrawioFile.prototype.ignorePatches = function(patches)
685{
686	var ignore = true;
687
688	for (var i = 0; i < patches.length && ignore; i++)
689	{
690		ignore = ignore && Object.keys(patches[i]).length == 0;
691	}
692
693	return ignore;
694};
695
696/**
697 * Applies the given patches to the file.
698 */
699DrawioFile.prototype.patch = function(patches, resolver, undoable)
700{
701	// Saves state of undo history
702	var undoMgr = this.ui.editor.undoManager;
703	var history = undoMgr.history.slice();
704	var nextAdd = undoMgr.indexOfNextAdd;
705
706	// Hides graph during updates
707	var graph = this.ui.editor.graph;
708	graph.container.style.visibility = 'hidden';
709
710	// Ignores change events
711	var prev = this.changeListenerEnabled;
712	this.changeListenerEnabled = undoable;
713
714	// Folding and math change require special handling
715	var fold = graph.foldingEnabled;
716	var math = graph.mathEnabled;
717
718	// Updates text editor if cell changes during validation
719	var redraw = graph.cellRenderer.redraw;
720
721	graph.cellRenderer.redraw = function(state)
722    {
723        if (state.view.graph.isEditing(state.cell))
724        {
725            state.view.graph.scrollCellToVisible(state.cell);
726        	state.view.graph.cellEditor.resize();
727        }
728
729        redraw.apply(this, arguments);
730    };
731
732	graph.model.beginUpdate();
733	try
734	{
735	    // Applies patches
736		for (var i = 0; i < patches.length; i++)
737		{
738			this.ui.pages = this.ui.patchPages(this.ui.pages,
739				patches[i], true, resolver, this.isModified());
740		}
741
742		// Always needs at least one page
743		if (this.ui.pages.length == 0)
744		{
745			this.ui.pages.push(this.ui.createPage());
746		}
747
748		// Checks if current page was removed
749		if (mxUtils.indexOf(this.ui.pages, this.ui.currentPage) < 0)
750		{
751			this.ui.selectPage(this.ui.pages[0], true);
752		}
753	}
754	finally
755	{
756		// Changes visibility before action states are updated via model event
757		graph.container.style.visibility = '';
758		graph.model.endUpdate();
759
760		// Restores previous state
761		graph.cellRenderer.redraw = redraw;
762		this.changeListenerEnabled = prev;
763
764		// Restores history state
765		if (!undoable)
766		{
767			undoMgr.history = history;
768			undoMgr.indexOfNextAdd = nextAdd;
769			undoMgr.fireEvent(new mxEventObject(mxEvent.CLEAR));
770		}
771
772		if (this.ui.currentPage == null || this.ui.currentPage.needsUpdate)
773		{
774			// Updates the graph and background
775			if (math != graph.mathEnabled)
776			{
777				this.ui.editor.updateGraphComponents();
778				graph.refresh();
779			}
780			else
781			{
782				if (fold != graph.foldingEnabled)
783				{
784					graph.view.revalidate();
785				}
786				else
787				{
788					graph.view.validate();
789				}
790
791				graph.sizeDidChange();
792			}
793		}
794
795		this.ui.updateTabContainer();
796		this.ui.editor.fireEvent(new mxEventObject('pagesPatched', 'patches', patches));
797	}
798};
799
800/**
801 * Adds the listener for automatically saving the diagram for local changes.
802 */
803DrawioFile.prototype.save = function(revision, success, error, unloading, overwrite, manual)
804{
805	try
806	{
807		if (!this.isEditable())
808		{
809			if (error != null)
810			{
811				error({message: mxResources.get('readOnly')});
812			}
813			else
814			{
815				throw new Error(mxResources.get('readOnly'));
816			}
817		}
818		else if (!overwrite && this.invalidChecksum)
819		{
820			if (error != null)
821			{
822				error({message: mxResources.get('checksum')});
823			}
824			else
825			{
826				throw new Error(mxResources.get('checksum'));
827			}
828		}
829		else
830		{
831			this.updateFileData();
832			this.clearAutosave();
833
834			if (success != null)
835			{
836				success();
837			}
838		}
839	}
840	catch (e)
841	{
842		if (error != null)
843		{
844			error(e);
845		}
846		else
847		{
848			throw e;
849		}
850	}
851};
852
853/**
854 * Translates this point by the given vector.
855 *
856 * @param {number} dx X-coordinate of the translation.
857 * @param {number} dy Y-coordinate of the translation.
858 */
859DrawioFile.prototype.updateFileData = function()
860{
861	this.setData(this.ui.getFileData(null, null, null, null, null, null, null, null, this, !this.isCompressed()));
862};
863
864/**
865 * Translates this point by the given vector.
866 *
867 * @param {number} dx X-coordinate of the translation.
868 * @param {number} dy Y-coordinate of the translation.
869 */
870DrawioFile.prototype.isCompressedStorage = function()
871{
872	return true;
873};
874
875/**
876 * Translates this point by the given vector.
877 *
878 * @param {number} dx X-coordinate of the translation.
879 * @param {number} dy Y-coordinate of the translation.
880 */
881DrawioFile.prototype.isCompressed = function()
882{
883	var compressed = (this.ui.fileNode != null) ? this.ui.fileNode.getAttribute('compressed') : null;
884
885	if (compressed != null)
886	{
887		return compressed != 'false';
888	}
889	else
890	{
891		return this.isCompressedStorage() && Editor.compressXml;
892	}
893};
894
895/**
896 * Translates this point by the given vector.
897 *
898 * @param {number} dx X-coordinate of the translation.
899 * @param {number} dy Y-coordinate of the translation.
900 */
901DrawioFile.prototype.saveAs = function(filename, success, error) { };
902
903/**
904 * Translates this point by the given vector.
905 *
906 * @param {number} dx X-coordinate of the translation.
907 * @param {number} dy Y-coordinate of the translation.
908 */
909DrawioFile.prototype.saveFile = function(title, revision, success, error) { };
910
911/**
912 * Returns true if copy, export and print are not allowed for this file.
913 */
914DrawioFile.prototype.getPublicUrl = function(fn)
915{
916	fn(null);
917};
918
919/**
920 * Returns true if copy, export and print are not allowed for this file.
921 */
922DrawioFile.prototype.isRestricted = function()
923{
924	return false;
925};
926
927/**
928 * Translates this point by the given vector.
929 *
930 * @param {number} dx X-coordinate of the translation.
931 * @param {number} dy Y-coordinate of the translation.
932 */
933DrawioFile.prototype.isModified = function()
934{
935	return this.modified;
936};
937
938/**
939 * Translates this point by the given vector.
940 *
941 * @param {number} dx X-coordinate of the translation.
942 * @param {number} dy Y-coordinate of the translation.
943 */
944DrawioFile.prototype.getShadowModified = function()
945{
946	return this.shadowModified;
947};
948
949/**
950 * Translates this point by the given vector.
951 *
952 * @param {number} dx X-coordinate of the translation.
953 * @param {number} dy Y-coordinate of the translation.
954 */
955DrawioFile.prototype.setShadowModified = function(value)
956{
957	this.shadowModified = value;
958};
959
960/**
961 * Translates this point by the given vector.
962 *
963 * @param {number} dx X-coordinate of the translation.
964 * @param {number} dy Y-coordinate of the translation.
965 */
966DrawioFile.prototype.setModified = function(value)
967{
968	this.modified = value;
969	this.shadowModified = value;
970};
971
972/**
973 * Specifies if the autosave checkbox should be shown in the document
974 * properties dialog. Default is false.
975 */
976DrawioFile.prototype.isAutosaveOptional = function()
977{
978	return false;
979};
980
981/**
982 * Translates this point by the given vector.
983 *
984 * @param {number} dx X-coordinate of the translation.
985 * @param {number} dy Y-coordinate of the translation.
986 */
987DrawioFile.prototype.isAutosave = function()
988{
989	return !this.inConflictState && this.ui.editor.autosave;
990};
991
992/**
993 * Translates this point by the given vector.
994 *
995 * @param {number} dx X-coordinate of the translation.
996 * @param {number} dy Y-coordinate of the translation.
997 */
998DrawioFile.prototype.isRenamable = function()
999{
1000	return false;
1001};
1002
1003/**
1004 * Translates this point by the given vector.
1005 *
1006 * @param {number} dx X-coordinate of the translation.
1007 * @param {number} dy Y-coordinate of the translation.
1008 */
1009DrawioFile.prototype.rename = function(title, success, error) { };
1010
1011/**
1012 * Translates this point by the given vector.
1013 *
1014 * @param {number} dx X-coordinate of the translation.
1015 * @param {number} dy Y-coordinate of the translation.
1016 */
1017DrawioFile.prototype.isMovable = function()
1018{
1019	return false;
1020};
1021
1022/**
1023 * Translates this point by the given vector.
1024 *
1025 * @param {number} dx X-coordinate of the translation.
1026 * @param {number} dy Y-coordinate of the translation.
1027 */
1028DrawioFile.prototype.isTrashed = function()
1029{
1030	return false;
1031};
1032
1033/**
1034 * Translates this point by the given vector.
1035 *
1036 * @param {number} dx X-coordinate of the translation.
1037 * @param {number} dy Y-coordinate of the translation.
1038 */
1039DrawioFile.prototype.move = function(folderId, success, error) { };
1040
1041/**
1042 * Translates this point by the given vector.
1043 *
1044 * @param {number} dx X-coordinate of the translation.
1045 * @param {number} dy Y-coordinate of the translation.
1046 */
1047DrawioFile.prototype.share = function()
1048{
1049	this.ui.alert(mxResources.get('sharingAvailable'), null, 380);
1050};
1051
1052/**
1053 * Returns the hash of the file which consists of a prefix for the storage
1054 * type and the ID of the file.
1055 */
1056DrawioFile.prototype.getHash = function()
1057{
1058	return '';
1059};
1060
1061/**
1062 * Returns the ID of the file.
1063 */
1064DrawioFile.prototype.getId = function()
1065{
1066	return '';
1067};
1068
1069/**
1070 * Returns true if the file is editable.
1071 */
1072DrawioFile.prototype.isEditable = function()
1073{
1074	return !this.ui.editor.isChromelessView() || this.ui.editor.editable;
1075};
1076
1077/**
1078 * Returns the location as a new object.
1079 * @type mx.Point
1080 */
1081DrawioFile.prototype.getUi = function()
1082{
1083	return this.ui;
1084};
1085
1086/**
1087 * Returns the current title of the file.
1088 */
1089DrawioFile.prototype.getTitle = function()
1090{
1091	return '';
1092};
1093
1094/**
1095 * Sets the current data of the file.
1096 */
1097DrawioFile.prototype.setData = function(data)
1098{
1099	this.data = data;
1100};
1101
1102/**
1103 * Returns the current data of the file.
1104 */
1105DrawioFile.prototype.getData = function()
1106{
1107	return this.data;
1108};
1109
1110/**
1111 * Opens this file in the editor.
1112 */
1113DrawioFile.prototype.open = function()
1114{
1115	this.stats.opened++;
1116	var data = this.getData();
1117
1118	if (data != null)
1119	{
1120		//Remove external fonts of previous file
1121		function removeExtFont(elems)
1122		{
1123			for (var i = 0; elems != null && i < elems.length; i++)
1124			{
1125				var e = elems[i];
1126
1127				if (e.id != null && e.id.indexOf('extFont_') == 0)
1128				{
1129					e.parentNode.removeChild(e);
1130				}
1131			}
1132		};
1133
1134		removeExtFont(document.querySelectorAll('head > style[id]'));
1135		removeExtFont(document.querySelectorAll('head > link[id]'));
1136
1137		this.ui.setFileData(data);
1138
1139		// Updates shadow in case any page IDs have been updated
1140		// only if the file has not been modified and reopened
1141		if (!this.isModified())
1142		{
1143			this.shadowData = mxUtils.getXml(this.ui.getXmlFileData());
1144			this.shadowPages = null;
1145		}
1146	}
1147
1148	this.installListeners();
1149
1150	if (this.isSyncSupported())
1151	{
1152		this.startSync();
1153	}
1154};
1155
1156/**
1157 * Hook for subclassers.
1158 */
1159DrawioFile.prototype.isSyncSupported = function()
1160{
1161	return false;
1162};
1163
1164/**
1165 * Hook for subclassers.
1166 */
1167DrawioFile.prototype.isRevisionHistorySupported = function()
1168{
1169	return false;
1170};
1171
1172/**
1173 * Hook for subclassers.
1174 */
1175DrawioFile.prototype.getRevisions = function(success, error)
1176{
1177	success(null);
1178};
1179
1180/**
1181 * Hook for subclassers to get the latest descriptor of this file
1182 * and return it in the success handler.
1183 */
1184DrawioFile.prototype.loadDescriptor = function(success, error)
1185{
1186	success(null);
1187};
1188
1189/**
1190 * Hook for subclassers to get the latest etag of this file
1191 * and return it in the success handler.
1192 */
1193DrawioFile.prototype.loadPatchDescriptor = function(success, error)
1194{
1195	this.loadDescriptor(mxUtils.bind(this, function(desc)
1196	{
1197		success(desc);
1198	}), error);
1199};
1200
1201/**
1202 * Adds the listener for automatically saving the diagram for local changes.
1203 */
1204DrawioFile.prototype.patchDescriptor = function(desc, patch)
1205{
1206	this.setDescriptorEtag(desc, this.getDescriptorEtag(patch));
1207	this.descriptorChanged();
1208};
1209
1210/**
1211 * Creates a starts the synchronization.
1212 */
1213DrawioFile.prototype.startSync = function()
1214{
1215	if ((DrawioFile.SYNC == 'auto' && urlParams['stealth'] != '1') &&
1216		(urlParams['rt'] == '1' || !this.ui.editor.chromeless ||
1217		this.ui.editor.editable))
1218	{
1219		if (this.sync == null)
1220		{
1221			this.sync = new DrawioFileSync(this);
1222		}
1223
1224		this.sync.start();
1225	}
1226};
1227
1228/**
1229 * Hook for subclassers to check if an error is a conflict.
1230 */
1231DrawioFile.prototype.isConflict = function()
1232{
1233	return false;
1234};
1235
1236/**
1237 * Gets the channel ID for sync messages.
1238 */
1239DrawioFile.prototype.getChannelId = function()
1240{
1241	// Slash, space and plus replaced with underscore
1242	return Graph.compress(this.getHash()).replace(/[\/ +]/g, '_');
1243};
1244
1245/**
1246 * Gets the channel ID from the given descriptor.
1247 */
1248DrawioFile.prototype.getChannelKey = function(desc)
1249{
1250	return null;
1251};
1252
1253/**
1254 * Returns the current etag.
1255 */
1256DrawioFile.prototype.getCurrentUser = function()
1257{
1258	return null;
1259};
1260
1261/**
1262 * Hook for subclassers to get the latest version of this file
1263 * and return it in the success handler.
1264 */
1265DrawioFile.prototype.getLatestVersion = function(success, error)
1266{
1267	success(null);
1268};
1269
1270/**
1271 * Returns the last modified date of this file.
1272 */
1273DrawioFile.prototype.getLastModifiedDate = function()
1274{
1275	return new Date();
1276};
1277
1278/**
1279 * Sets the current revision ID.
1280 */
1281DrawioFile.prototype.setCurrentRevisionId = function(id)
1282{
1283	this.setDescriptorRevisionId(this.getDescriptor(), id);
1284};
1285
1286/**
1287 * Returns the current revision ID.
1288 */
1289DrawioFile.prototype.getCurrentRevisionId = function()
1290{
1291	return this.getDescriptorRevisionId(this.getDescriptor());
1292};
1293
1294/**
1295 * Sets the current etag.
1296 */
1297DrawioFile.prototype.setCurrentEtag = function(etag)
1298{
1299	this.setDescriptorEtag(this.getDescriptor(), etag);
1300};
1301
1302/**
1303 * Returns the current etag.
1304 */
1305DrawioFile.prototype.getCurrentEtag = function()
1306{
1307	return this.getDescriptorEtag(this.getDescriptor());
1308};
1309
1310/**
1311 * Returns the descriptor from this file.
1312 */
1313DrawioFile.prototype.getDescriptor = function()
1314{
1315	return null;
1316};
1317
1318/**
1319 * Sets the descriptor for this file.
1320 */
1321DrawioFile.prototype.setDescriptor = function() { };
1322
1323/**
1324 * Updates the revision ID on the given descriptor.
1325 */
1326DrawioFile.prototype.setDescriptorRevisionId = function(desc, id)
1327{
1328	this.setDescriptorEtag(desc, id);
1329};
1330
1331/**
1332 * Returns the revision ID from the given descriptor.
1333 */
1334DrawioFile.prototype.getDescriptorRevisionId = function(desc)
1335{
1336	return this.getDescriptorEtag(desc);
1337};
1338
1339/**
1340 * Updates the etag on the given descriptor.
1341 */
1342DrawioFile.prototype.setDescriptorEtag = function(desc, etag) { };
1343
1344/**
1345 * Returns the etag from the given descriptor.
1346 */
1347DrawioFile.prototype.getDescriptorEtag = function(desc)
1348{
1349	return null;
1350};
1351
1352/**
1353 * Returns the secret from the given descriptor. This must be stored
1354 * in a custom property and generated by the saving client so that a
1355 * token can be obtained from the cache for writing the patch after
1356 * saving the file. If this cannot be saved in a custom property then
1357 * null must be returned so that no deltas are used for updating the
1358 * file (the file is reloaded every time instead). This is needed to
1359 * make sure nobody with read-only permissions can write a patch to
1360 * the cache before the saving client wrote the patch and inject
1361 * data into the file via other clients merging that data.
1362 */
1363DrawioFile.prototype.getDescriptorSecret = function(desc)
1364{
1365	return null;
1366};
1367
1368/**
1369 * Installs the change listener.
1370 */
1371DrawioFile.prototype.installListeners = function()
1372{
1373	if (this.changeListener == null)
1374	{
1375		this.changeListener = mxUtils.bind(this, function(sender, eventObject)
1376		{
1377			var edit = (eventObject != null) ? eventObject.getProperty('edit') : null;
1378
1379			if (this.changeListenerEnabled && this.isEditable() && (edit == null || !edit.ignoreEdit))
1380			{
1381				this.fileChanged();
1382			}
1383		});
1384
1385		this.ui.editor.graph.model.addListener(mxEvent.CHANGE, this.changeListener);
1386
1387		// Some options trigger autosave
1388		this.ui.editor.graph.addListener('gridSizeChanged', this.changeListener);
1389		this.ui.editor.graph.addListener('shadowVisibleChanged', this.changeListener);
1390		this.ui.addListener('pageFormatChanged', this.changeListener);
1391		this.ui.addListener('pageScaleChanged', this.changeListener);
1392		this.ui.addListener('backgroundColorChanged', this.changeListener);
1393		this.ui.addListener('backgroundImageChanged', this.changeListener);
1394		this.ui.addListener('foldingEnabledChanged', this.changeListener);
1395		this.ui.addListener('mathEnabledChanged', this.changeListener);
1396		this.ui.addListener('gridEnabledChanged', this.changeListener);
1397		this.ui.addListener('guidesEnabledChanged', this.changeListener);
1398		this.ui.addListener('tooltipsEnabledChanged', this.changeListener);
1399		this.ui.addListener('pageViewChanged', this.changeListener);
1400		this.ui.addListener('connectionPointsChanged', this.changeListener);
1401		this.ui.addListener('connectionArrowsChanged', this.changeListener);
1402	}
1403};
1404
1405/**
1406 * Returns the location as a new object.
1407 * @type mx.Point
1408 */
1409DrawioFile.prototype.addAllSavedStatus = function(status)
1410{
1411	if (this.ui.statusContainer != null && this.ui.getCurrentFile() == this)
1412	{
1413		status = (status != null) ? status : mxUtils.htmlEntities(mxResources.get(this.allChangesSavedKey));
1414		this.ui.editor.setStatus('<div title="'+ status + '">' + status + '</div>');
1415		var links = this.ui.statusContainer.getElementsByTagName('div');
1416
1417		if (links.length > 0 && this.isRevisionHistorySupported())
1418		{
1419			links[0].style.cursor = 'pointer';
1420			links[0].style.textDecoration = 'underline';
1421
1422			mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function()
1423			{
1424				this.ui.actions.get('revisionHistory').funct();
1425			}));
1426		}
1427	}
1428};
1429
1430/**
1431 * Adds the listener for automatically saving the diagram for local changes.
1432 */
1433DrawioFile.prototype.saveDraft = function()
1434{
1435	try
1436	{
1437		if (this.draftId == null)
1438		{
1439			this.draftId = Editor.guid();
1440		}
1441
1442		var draft = {type: 'draft',
1443			created: this.created,
1444			modified: new Date().getTime(),
1445			data: this.ui.getFileData(),
1446			title: this.getTitle(),
1447			aliveCheck: this.ui.draftAliveCheck};
1448		this.ui.setDatabaseItem('.draft_' + this.draftId,
1449			JSON.stringify(draft));
1450
1451		EditorUi.debug('draft saved', this.draftId, draft);
1452	}
1453	catch (e)
1454	{
1455		// Removes any stored draft
1456		this.removeDraft();
1457	}
1458};
1459
1460/**
1461 * Adds the listener for automatically saving the diagram for local changes.
1462 */
1463DrawioFile.prototype.removeDraft = function()
1464{
1465	try
1466	{
1467		if (this.draftId != null)
1468		{
1469			this.ui.removeDatabaseItem('.draft_' + this.draftId);
1470			EditorUi.debug('draft deleted', '.draft_' + this.draftId);
1471		}
1472	}
1473	catch (e)
1474	{
1475		// ignore
1476	}
1477};
1478
1479/**
1480 * Adds the listener for automatically saving the diagram for local changes.
1481 */
1482DrawioFile.prototype.addUnsavedStatus = function(err)
1483{
1484	if (!this.inConflictState && this.ui.statusContainer != null && this.ui.getCurrentFile() == this)
1485	{
1486		if (err instanceof Error && err.message != null && err.message != '')
1487		{
1488			var status = mxUtils.htmlEntities(mxResources.get('unsavedChanges'));
1489
1490			this.ui.editor.setStatus('<div title="'+ status + '" class="geStatusAlert">' +
1491				status + ' (' + mxUtils.htmlEntities(err.message) + ')</div>');
1492		}
1493		else
1494		{
1495			var msg = this.getErrorMessage(err);
1496
1497			if (msg == null && this.lastSaved != null)
1498			{
1499				var str = this.ui.timeSince(new Date(this.lastSaved));
1500
1501				// Only show if more than a minute ago
1502				if (str != null)
1503				{
1504					msg = mxResources.get('lastSaved', [str]);
1505				}
1506			}
1507
1508			if (msg != null && msg.length > 60)
1509			{
1510				msg = msg.substring(0, 60) + '...';
1511			}
1512
1513			var status = mxUtils.htmlEntities(mxResources.get('unsavedChangesClickHereToSave')) +
1514				((msg != null && msg != '') ? ' (' + mxUtils.htmlEntities(msg) + ')' : '');
1515			this.ui.editor.setStatus('<div title="'+ status + '" class="geStatusAlertOrange">' + status +
1516				' <img src="' + Editor.saveImage + '"/></div>');
1517
1518			// Installs click handler for saving
1519			var links = this.ui.statusContainer.getElementsByTagName('div');
1520
1521			if (links != null && links.length > 0)
1522			{
1523				links[0].style.cursor = 'pointer';
1524
1525				mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function()
1526				{
1527					this.ui.actions.get((this.ui.mode == null || !this.isEditable()) ?
1528						'saveAs' : 'save').funct();
1529				}));
1530			}
1531			else
1532			{
1533				var status = mxUtils.htmlEntities(mxResources.get('unsavedChanges'));
1534
1535				this.ui.editor.setStatus('<div title="'+ status + '" class="geStatusAlert">' + status +
1536					' (' + mxUtils.htmlEntities(err.message) + ')</div>');
1537			}
1538
1539			if (EditorUi.enableDrafts && (this.getMode() == null || EditorUi.isElectronApp))
1540			{
1541				if (this.saveDraftThread != null)
1542				{
1543					window.clearTimeout(this.saveDraftThread);
1544				}
1545
1546				this.saveDraftThread = window.setTimeout(mxUtils.bind(this, function()
1547				{
1548					this.saveDraft();
1549				}), 0);
1550			}
1551		}
1552	}
1553};
1554
1555/**
1556 * Halts all timers and shows a conflict status message. The optional error
1557 * handler is invoked first.
1558 */
1559DrawioFile.prototype.addConflictStatus = function(fn, message)
1560{
1561	if (this.invalidChecksum && message == null)
1562	{
1563		message = mxResources.get('checksum');
1564	}
1565
1566	this.setConflictStatus(mxUtils.htmlEntities(mxResources.get('fileChangedSync')) +
1567		((message != null && message != '') ? ' (' + mxUtils.htmlEntities(message) + ')' : ''));
1568	this.ui.spinner.stop();
1569	this.clearAutosave();
1570
1571	var links = (this.ui.statusContainer != null) ? this.ui.statusContainer.getElementsByTagName('div') : null;
1572
1573	if (links != null && links.length > 0)
1574	{
1575		links[0].style.cursor = 'pointer';
1576
1577		mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function(evt)
1578		{
1579			if (mxEvent.getSource(evt).nodeName != 'IMG')
1580			{
1581				fn();
1582			}
1583		}));
1584	}
1585	else
1586	{
1587		this.ui.alert(mxUtils.htmlEntities(mxResources.get('fileChangedSync')), fn);
1588	}
1589};
1590
1591/**
1592 * Halts all timers and shows a conflict status message. The optional error
1593 * handler is invoked first.
1594 */
1595DrawioFile.prototype.setConflictStatus = function(message)
1596{
1597	this.ui.editor.setStatus('<div title="'+ message + '" class="geStatusAlert">' + message +
1598		' <a href="https://www.diagrams.net/doc/faq/synchronize" title="' + mxResources.get('help') +
1599		'" target="_blank">' + '<img src="' + Editor.helpImage + '"/></a></div>');
1600};
1601
1602/**
1603 * Shows a conflict dialog to the user.
1604 */
1605DrawioFile.prototype.showRefreshDialog = function(success, error, message)
1606{
1607	if (message == null)
1608	{
1609		message = mxResources.get('checksum');
1610	}
1611
1612	if (this.ui.editor.isChromelessView() && !this.ui.editor.editable)
1613	{
1614		this.ui.alert(mxResources.get('fileChangedSync'), mxUtils.bind(this, function()
1615		{
1616			this.reloadFile(success, error);
1617		}));
1618	}
1619	else
1620	{
1621		// Allows for escape key to be pressed while dialog is showing
1622		this.addConflictStatus(mxUtils.bind(this, function()
1623		{
1624			this.showRefreshDialog(success, error);
1625		}), message);
1626
1627		this.ui.showError(mxResources.get('error') + ' (' + message + ')',
1628			mxResources.get('fileChangedSyncDialog'),
1629			mxResources.get('makeCopy'), mxUtils.bind(this, function()
1630		{
1631			this.copyFile(success, error);
1632		}), null, mxResources.get('synchronize'), mxUtils.bind(this, function()
1633		{
1634			this.reloadFile(success, error);
1635		}), mxResources.get('cancel'), mxUtils.bind(this, function()
1636		{
1637			this.ui.hideDialog();
1638		}), 360, 150);
1639	}
1640};
1641
1642/**
1643 * Shows a dialog with no synchronize option.
1644 */
1645DrawioFile.prototype.showCopyDialog = function(success, error, overwrite)
1646{
1647	this.inConflictState = false;
1648	this.invalidChecksum = false;
1649	this.addUnsavedStatus();
1650
1651	this.ui.showError(mxResources.get('externalChanges'),
1652		mxResources.get('fileChangedOverwriteDialog'),
1653		mxResources.get('makeCopy'), mxUtils.bind(this, function()
1654		{
1655			this.copyFile(success, error);
1656		}), null, mxResources.get('overwrite'), overwrite,
1657		mxResources.get('cancel'), mxUtils.bind(this, function()
1658	{
1659		this.ui.hideDialog();
1660	}), 360, 150);
1661};
1662
1663/**
1664 * Shows a conflict dialog to the user.
1665 */
1666DrawioFile.prototype.showConflictDialog = function(overwrite, synchronize)
1667{
1668	this.ui.showError(mxResources.get('externalChanges'),
1669		mxResources.get('fileChangedSyncDialog'),
1670		mxResources.get('overwrite'), overwrite, null,
1671		mxResources.get('synchronize'), synchronize,
1672		mxResources.get('cancel'), mxUtils.bind(this, function()
1673	{
1674		this.ui.hideDialog();
1675		this.handleFileError(null, false);
1676	}), 340, 150);
1677};
1678
1679/**
1680 * Checks if the client is authorized and calls the next step.
1681 */
1682DrawioFile.prototype.redirectToNewApp = function(error, details)
1683{
1684	this.ui.spinner.stop();
1685
1686	if (!this.redirectDialogShowing)
1687	{
1688		this.redirectDialogShowing = true;
1689
1690		var url = window.location.protocol + '//' + window.location.host + '/' + this.ui.getSearch(
1691			['create', 'title', 'mode', 'url', 'drive', 'splash', 'state']) + '#' + this.getHash();
1692		var msg = mxResources.get('redirectToNewApp');
1693
1694		if (details != null)
1695		{
1696			msg += ' (' + details + ')';
1697		}
1698
1699		var redirect = mxUtils.bind(this, function()
1700		{
1701			var fn = mxUtils.bind(this, function()
1702			{
1703				this.redirectDialogShowing = false;
1704
1705				if (window.location.href == url)
1706				{
1707					window.location.reload();
1708				}
1709				else
1710				{
1711					window.location.href = url;
1712				}
1713			});
1714
1715			if (error == null && this.isModified())
1716			{
1717				this.ui.confirm(mxResources.get('allChangesLost'), mxUtils.bind(this, function()
1718				{
1719					this.redirectDialogShowing = false;
1720				}), fn, mxResources.get('cancel'), mxResources.get('discardChanges'));
1721			}
1722			else
1723			{
1724				fn();
1725			}
1726		});
1727
1728		if (error != null)
1729		{
1730			if (this.isModified())
1731			{
1732				this.ui.confirm(msg, mxUtils.bind(this, function()
1733				{
1734					this.redirectDialogShowing = false;
1735					error();
1736				}), redirect, mxResources.get('cancel'), mxResources.get('discardChanges'));
1737			}
1738			else
1739			{
1740				this.ui.confirm(msg, redirect, mxUtils.bind(this, function()
1741				{
1742					this.redirectDialogShowing = false;
1743					error();
1744				}));
1745			}
1746		}
1747		else
1748		{
1749			this.ui.alert(mxResources.get('redirectToNewApp'), redirect);
1750		}
1751	}
1752};
1753
1754/**
1755 * Adds the listener for automatically saving the diagram for local changes.
1756 */
1757DrawioFile.prototype.handleFileSuccess = function(saved)
1758{
1759	this.ui.spinner.stop();
1760
1761	if (this.ui.getCurrentFile() == this)
1762	{
1763		if (this.isModified())
1764		{
1765			this.fileChanged();
1766		}
1767		else if (saved)
1768		{
1769			if (this.isTrashed())
1770			{
1771				this.addAllSavedStatus(mxUtils.htmlEntities(mxResources.get(this.allChangesSavedKey)) + ' (' +
1772					mxUtils.htmlEntities(mxResources.get('fileMovedToTrash')) + ')');
1773			}
1774			else
1775			{
1776				this.addAllSavedStatus();
1777			}
1778
1779			if (this.sync != null)
1780			{
1781				this.sync.resetUpdateStatusThread();
1782
1783				if (this.sync.remoteFileChanged)
1784				{
1785					this.sync.remoteFileChanged = false;
1786					this.sync.fileChangedNotify();
1787				}
1788			}
1789		}
1790		else
1791		{
1792			this.ui.editor.setStatus('');
1793		}
1794	}
1795};
1796
1797/**
1798 * Adds the listener for automatically saving the diagram for local changes.
1799 */
1800DrawioFile.prototype.handleFileError = function(err, manual)
1801{
1802	this.ui.spinner.stop();
1803
1804	if (this.ui.getCurrentFile() == this)
1805	{
1806		if (this.inConflictState)
1807		{
1808			this.handleConflictError(err, manual);
1809		}
1810		else
1811		{
1812			if (this.isModified())
1813			{
1814				this.addUnsavedStatus(err);
1815			}
1816
1817			if (manual)
1818			{
1819				this.ui.handleError(err, (err != null) ? mxResources.get('errorSavingFile') : null);
1820			}
1821			else if (!this.isModified())
1822			{
1823				var msg = this.getErrorMessage(err);
1824
1825				if (msg != null && msg.length > 60)
1826				{
1827					msg = msg.substring(0, 60) + '...';
1828				}
1829
1830				this.ui.editor.setStatus('<div class="geStatusAlert">' +
1831					mxUtils.htmlEntities(mxResources.get('error')) + ((msg != null) ?
1832					' (' + mxUtils.htmlEntities(msg) + ')' : '') + '</div>');
1833			}
1834		}
1835	}
1836};
1837
1838/**
1839 * Adds the listener for automatically saving the diagram for local changes.
1840 */
1841DrawioFile.prototype.handleConflictError = function(err, manual)
1842{
1843	var success = mxUtils.bind(this, function()
1844	{
1845		this.handleFileSuccess(true);
1846	});
1847
1848	var error = mxUtils.bind(this, function(err2)
1849	{
1850		this.handleFileError(err2, true);
1851	});
1852
1853	var overwrite = mxUtils.bind(this, function()
1854	{
1855		if (this.ui.spinner.spin(document.body, mxResources.get(this.savingSpinnerKey)))
1856		{
1857			this.ui.editor.setStatus('');
1858			var isRepoFile = (this.constructor == GitHubFile) || (this.constructor == GitLabFile);
1859			this.save(true, success, error, null, true, (isRepoFile &&
1860				err != null) ? err.commitMessage : null);
1861		}
1862	});
1863
1864	var synchronize = mxUtils.bind(this, function()
1865	{
1866		if (this.ui.spinner.spin(document.body, mxResources.get('updatingDocument')))
1867		{
1868			this.synchronizeFile(mxUtils.bind(this, function()
1869			{
1870				this.ui.spinner.stop();
1871
1872				if (this.ui.spinner.spin(document.body, mxResources.get(this.savingSpinnerKey)))
1873				{
1874					var isRepoFile = (this.constructor == GitHubFile) || (this.constructor == GitLabFile);
1875					this.save(true, success, error, null, null, (isRepoFile &&
1876						err != null) ? err.commitMessage : null);
1877				}
1878			}), error);
1879		}
1880	})
1881
1882	if (DrawioFile.SYNC == 'none')
1883	{
1884		this.showCopyDialog(success, error, overwrite);
1885	}
1886	else if (this.invalidChecksum)
1887	{
1888		this.showRefreshDialog(success, error, this.getErrorMessage(err));
1889	}
1890	else if (manual)
1891	{
1892		this.showConflictDialog(overwrite, synchronize);
1893	}
1894	else
1895	{
1896		this.addConflictStatus(mxUtils.bind(this, function()
1897		{
1898			this.ui.editor.setStatus(mxUtils.htmlEntities(
1899				mxResources.get('updatingDocument')));
1900			this.synchronizeFile(success, error);
1901		}), this.getErrorMessage(err));
1902	}
1903};
1904
1905/**
1906 * Adds the listener for automatically saving the diagram for local changes.
1907 */
1908DrawioFile.prototype.getErrorMessage = function(err)
1909{
1910	var msg = (err != null) ? ((err.error != null) ? err.error.message : err.message) : null;
1911
1912	if (msg == null && err != null && err.code == App.ERROR_TIMEOUT)
1913	{
1914		msg = mxResources.get('timeout');
1915	}
1916
1917	return msg;
1918};
1919
1920/**
1921 * Returns true if the oldest unsaved change is older than <EditorUi.warnInterval>.
1922 */
1923DrawioFile.prototype.isOverdue = function()
1924{
1925	return this.ageStart != null && (Date.now() - this.ageStart.getTime()) >= this.ui.warnInterval;
1926};
1927
1928/**
1929 * Adds the listener for automatically saving the diagram for local changes.
1930 */
1931DrawioFile.prototype.fileChanged = function()
1932{
1933	this.lastChanged = new Date();
1934	this.setModified(true);
1935
1936	if (this.isAutosave())
1937	{
1938		if (this.savingStatusKey != null)
1939		{
1940			this.addAllSavedStatus(mxUtils.htmlEntities(mxResources.get(this.savingStatusKey)) + '...');
1941		}
1942
1943		this.ui.scheduleSanityCheck();
1944
1945		if (this.ageStart == null)
1946		{
1947			this.ageStart = new Date();
1948		}
1949
1950		//Send changes immidiately if P2P is enabled
1951		this.sendFileChanges();
1952
1953		this.autosave(this.autosaveDelay, this.maxAutosaveDelay, mxUtils.bind(this, function(resp)
1954		{
1955			this.ui.stopSanityCheck();
1956
1957			// Does not update status if another autosave was scheduled
1958			if (this.autosaveThread == null)
1959			{
1960				this.handleFileSuccess(true);
1961				this.ageStart = null;
1962			}
1963			else if (this.isModified())
1964			{
1965				this.ui.scheduleSanityCheck();
1966				this.ageStart = this.lastChanged;
1967			}
1968		}), mxUtils.bind(this, function(err)
1969		{
1970			this.handleFileError(err);
1971		}));
1972	}
1973	else
1974	{
1975		this.ageStart = null;
1976
1977		if ((!this.isAutosaveOptional() || !this.ui.editor.autosave) &&
1978			!this.inConflictState)
1979		{
1980			this.addUnsavedStatus();
1981		}
1982	}
1983};
1984
1985/**
1986 * Returns true if the notification to update should be sent
1987 * together with the save request.
1988 */
1989DrawioFile.prototype.isOptimisticSync = function()
1990{
1991	return false;
1992};
1993
1994/**
1995 * Creates a secret and token pair for writing a patch to the cache.
1996 */
1997DrawioFile.prototype.createSecret = function(success)
1998{
1999	var secret = Editor.guid(32);
2000
2001	if (this.sync != null && !this.isOptimisticSync())
2002	{
2003		this.sync.createToken(secret, mxUtils.bind(this, function(token)
2004		{
2005			success(secret, token);
2006		}), mxUtils.bind(this, function()
2007		{
2008			success(secret);
2009		}));
2010	}
2011	else
2012	{
2013		success(secret);
2014	}
2015};
2016
2017/**
2018 * Invokes sync and updates shadow document.
2019 */
2020DrawioFile.prototype.fileSaving = function()
2021{
2022	if (this.sync != null && this.isOptimisticSync())
2023	{
2024		this.sync.fileSaving();
2025	}
2026
2027	if (urlParams['test'] == '1')
2028	{
2029		EditorUi.debug('DrawioFile.fileSaving', [this]);
2030	}
2031};
2032
2033DrawioFile.prototype.sendFileChanges = function()
2034{
2035	try
2036	{
2037		if (this.p2pCollab != null && this.sync != null)
2038		{
2039			//TODO Should we check for modified?
2040			this.updateFileData(); //TODO Calling this function ealy could have side effects + overhead of calling it twice (here and in save)
2041			this.sync.sendFileChanges(this.ui.getPagesForNode(
2042				mxUtils.parseXml(this.getData()).documentElement),
2043				this.desc);
2044
2045			if (urlParams['test'] == '1')
2046			{
2047				EditorUi.debug('DrawioFile.sendFileChanges', [this]);
2048			}
2049		}
2050	}
2051	catch (e)
2052	{
2053		console.log(e);
2054	}
2055};
2056
2057/**
2058 * Invokes sync and updates shadow document.
2059 */
2060DrawioFile.prototype.fileSaved = function(savedData, lastDesc, success, error, token)
2061{
2062	this.lastSaved = new Date();
2063	this.ageStart = null;
2064
2065	try
2066	{
2067		this.stats.saved++;
2068		this.inConflictState = false;
2069		this.invalidChecksum = false;
2070
2071		if (this.sync == null || this.isOptimisticSync())
2072		{
2073			this.shadowData = savedData;
2074			this.shadowPages = null;
2075
2076			if (this.sync != null)
2077			{
2078				this.sync.lastModified = this.getLastModifiedDate();
2079				this.sync.resetUpdateStatusThread();
2080			}
2081
2082			if (success != null)
2083			{
2084				success();
2085			}
2086		}
2087		else
2088		{
2089			this.sync.fileSaved(this.ui.getPagesForNode(
2090				mxUtils.parseXml(savedData).documentElement),
2091				lastDesc, success, error, token);
2092		}
2093	}
2094	catch (e)
2095	{
2096		this.inConflictState = true;
2097		this.invalidChecksum = true;
2098		this.descriptorChanged();
2099
2100		if (error != null)
2101		{
2102			error(e);
2103		}
2104
2105		try
2106		{
2107			if (this.errorReportsEnabled)
2108			{
2109				this.sendErrorReport('Error in fileSaved', null, e);
2110			}
2111			else
2112			{
2113				var user = this.getCurrentUser();
2114				var uid = (user != null) ? user.id : 'unknown';
2115
2116				EditorUi.logError('Error in fileSaved', null,
2117					this.getMode() + '.' + this.getId(),
2118					uid, e);
2119			}
2120		}
2121		catch (e2)
2122		{
2123			// ignore
2124		}
2125	}
2126
2127	if (urlParams['test'] == '1')
2128	{
2129		EditorUi.debug('DrawioFile.fileSaved', [this]);
2130	}
2131};
2132
2133/**
2134 * Adds the listener for automatically saving the diagram for local changes.
2135 */
2136DrawioFile.prototype.autosave = function(delay, maxDelay, success, error)
2137{
2138	if (this.lastAutosave == null)
2139	{
2140		this.lastAutosave = Date.now();
2141	}
2142
2143	var tmp = (Date.now() - this.lastAutosave < maxDelay) ? delay : 0;
2144	this.clearAutosave();
2145
2146	// Starts new timer or executes immediately if not unsaved for maxDelay
2147	var thread = window.setTimeout(mxUtils.bind(this, function()
2148	{
2149		this.lastAutosave = null;
2150
2151		if (this.autosaveThread == thread)
2152		{
2153			this.autosaveThread = null;
2154		}
2155
2156		// Workaround for duplicate save if UI is blocking
2157		// after save while pending autosave triggers
2158		if (this.isModified() && this.isAutosaveNow())
2159		{
2160			var rev = this.isAutosaveRevision();
2161
2162			if (rev)
2163			{
2164				this.lastAutosaveRevision = new Date().getTime();
2165			}
2166
2167			this.save(rev, mxUtils.bind(this, function(resp)
2168			{
2169				this.autosaveCompleted();
2170
2171				if (success != null)
2172				{
2173					success(resp);
2174				}
2175			}), mxUtils.bind(this, function(resp)
2176			{
2177				if (error != null)
2178				{
2179					error(resp);
2180				}
2181			}));
2182		}
2183		else
2184		{
2185			if (!this.isModified())
2186			{
2187				this.ui.editor.setStatus('');
2188			}
2189
2190			if (success != null)
2191			{
2192				success(null);
2193			}
2194		}
2195	}), tmp);
2196
2197	this.autosaveThread = thread;
2198};
2199
2200/**
2201 * Returns true if an autosave is required at the time of execution.
2202 * This implementation returns true.
2203 */
2204DrawioFile.prototype.isAutosaveNow = function()
2205{
2206	return true;
2207};
2208
2209/**
2210 * Hooks for subclassers after the autosave has completed.
2211 */
2212DrawioFile.prototype.autosaveCompleted = function() { };
2213
2214/**
2215 * Adds the listener for automatically saving the diagram for local changes.
2216 */
2217DrawioFile.prototype.clearAutosave = function()
2218{
2219	if (this.autosaveThread != null)
2220	{
2221		window.clearTimeout(this.autosaveThread);
2222		this.autosaveThread = null;
2223	}
2224};
2225
2226/**
2227 * Returns the location as a new object.
2228 * @type mx.Point
2229 */
2230DrawioFile.prototype.isAutosaveRevision = function()
2231{
2232	var now = new Date().getTime();
2233
2234	return (this.lastAutosaveRevision == null) || (now - this.lastAutosaveRevision) > this.maxAutosaveRevisionDelay;
2235};
2236
2237/**
2238 * Translates this point by the given vector.
2239 *
2240 * @param {number} dx X-coordinate of the translation.
2241 * @param {number} dy Y-coordinate of the translation.
2242 */
2243DrawioFile.prototype.descriptorChanged = function()
2244{
2245	this.fireEvent(new mxEventObject('descriptorChanged'));
2246};
2247
2248/**
2249 * Translates this point by the given vector.
2250 *
2251 * @param {number} dx X-coordinate of the translation.
2252 * @param {number} dy Y-coordinate of the translation.
2253 */
2254DrawioFile.prototype.contentChanged = function()
2255{
2256	this.fireEvent(new mxEventObject('contentChanged'));
2257};
2258
2259/**
2260 * Returns the location as a new object.
2261 */
2262DrawioFile.prototype.close = function(unloading)
2263{
2264	this.updateFileData();
2265	this.stats.closed++;
2266
2267	if (this.isAutosave() && this.isModified())
2268	{
2269		this.save(this.isAutosaveRevision(), null, null, unloading);
2270	}
2271
2272	this.destroy();
2273};
2274
2275/**
2276 * Returns the location as a new object.
2277 */
2278DrawioFile.prototype.hasSameExtension = function(title, newTitle)
2279{
2280	if (title != null && newTitle != null)
2281	{
2282		var dot = title.lastIndexOf('.');
2283		var ext = (dot > 0) ? title.substring(dot) : '';
2284		dot = newTitle.lastIndexOf('.');
2285
2286		return ext === ((dot > 0) ? newTitle.substring(dot) : '');
2287	}
2288
2289	return title == newTitle;
2290};
2291
2292/**
2293 * Removes the change listener.
2294 */
2295DrawioFile.prototype.removeListeners = function()
2296{
2297	if (this.changeListener != null)
2298	{
2299		this.ui.editor.graph.model.removeListener(this.changeListener);
2300		this.ui.editor.graph.removeListener(this.changeListener);
2301		this.ui.removeListener(this.changeListener);
2302		this.changeListener = null;
2303	}
2304};
2305
2306/**
2307 * Stops any pending autosaves and removes all listeners.
2308 */
2309DrawioFile.prototype.destroy = function()
2310{
2311	this.clearAutosave();
2312	this.removeListeners();
2313	this.stats.destroyed++;
2314
2315	if (this.sync != null)
2316	{
2317		this.sync.destroy();
2318		this.sync = null;
2319	}
2320};
2321
2322/**
2323 * Are comments supported
2324 */
2325DrawioFile.prototype.commentsSupported = function()
2326{
2327	return false; //The default is false and files that support it must explicitly state that
2328};
2329
2330/**
2331 * Show refresh button?
2332 */
2333DrawioFile.prototype.commentsRefreshNeeded = function()
2334{
2335	return true;
2336};
2337
2338/**
2339 * Show save button?
2340 */
2341DrawioFile.prototype.commentsSaveNeeded = function()
2342{
2343	return false;
2344};
2345
2346/**
2347 * Get comments of the file
2348 */
2349DrawioFile.prototype.getComments = function(success, error)
2350{
2351	success([]); //placeholder
2352};
2353
2354/**
2355 * Add a comment to the file
2356 */
2357DrawioFile.prototype.addComment = function(comment, success, error)
2358{
2359	success(Date.now()); //placeholder
2360};
2361
2362/**
2363 * Can add a reply to a reply
2364 */
2365DrawioFile.prototype.canReplyToReplies = function()
2366{
2367	return true;
2368};
2369
2370/**
2371 * Can add comments (The permission to comment to this file)
2372 */
2373DrawioFile.prototype.canComment = function()
2374{
2375	return true;
2376};
2377
2378/**
2379 * Get a new comment object
2380 */
2381DrawioFile.prototype.newComment = function(content, user)
2382{
2383	return new DrawioComment(this, null, content, Date.now(), Date.now(), false, user);
2384};
2385