1/**
2 * Copyright (c) 2006-2017, JGraph Ltd
3 * Copyright (c) 2006-2017, Gaudenz Alder
4 */
5DriveFile = function(ui, data, desc)
6{
7	DrawioFile.call(this, ui, data);
8
9	this.desc = desc;
10};
11
12//Extends mxEventSource
13mxUtils.extend(DriveFile, DrawioFile);
14
15/**
16 * Delay for last save in ms.
17 */
18DriveFile.prototype.saveDelay = 0;
19
20/**
21 * Delay for last save in ms.
22 */
23DriveFile.prototype.allChangesSavedKey = 'allChangesSavedInDrive';
24
25/**
26 * Specifies if notify events should be ignored.
27 */
28DriveFile.prototype.getSize = function()
29{
30	return this.desc.fileSize;
31};
32
33/**
34 * Returns true if copy, export and print are not allowed for this file.
35 */
36DriveFile.prototype.isRestricted = function()
37{
38	return this.desc.userPermission != null && this.desc.labels != null &&
39		this.desc.userPermission.role == 'reader' && this.desc.labels.restricted;
40};
41
42/**
43 * Adds the listener for automatically saving the diagram for local changes.
44 */
45DriveFile.prototype.isConflict = function(err)
46{
47	return err != null && err.error != null && err.error.code == 412;
48};
49
50/**
51 * Returns the current etag.
52 */
53DriveFile.prototype.getCurrentUser = function()
54{
55	return (this.ui.drive != null) ? this.ui.drive.user : null;
56};
57
58/**
59 * Translates this point by the given vector.
60 *
61 * @param {number} dx X-coordinate of the translation.
62 * @param {number} dy Y-coordinate of the translation.
63 */
64DriveFile.prototype.getMode = function()
65{
66	return App.MODE_GOOGLE;
67};
68
69/**
70 * Returns true if copy, export and print are not allowed for this file.
71 */
72DriveFile.prototype.getPublicUrl = function(fn)
73{
74	this.ui.drive.executeRequest({
75		url: '/files/' + this.desc.id + '/permissions?supportsAllDrives=true'
76	},
77	mxUtils.bind(this, function(resp)
78	{
79		if (resp != null && resp.items != null)
80		{
81			for (var i = 0; i < resp.items.length; i++)
82			{
83				if (resp.items[i].id === 'anyoneWithLink' ||
84					resp.items[i].id === 'anyone')
85				{
86					fn(this.desc.webContentLink);
87
88					return;
89				}
90			}
91		}
92
93		fn(null);
94	}), mxUtils.bind(this, function()
95	{
96		fn(null)
97	}));
98};
99
100/**
101 * Overridden to enable the autosave option in the document properties dialog
102 * if realtime is not used.
103 */
104DriveFile.prototype.isAutosaveOptional = function()
105{
106	return true;
107};
108
109/**
110 * Translates this point by the given vector.
111 *
112 * @param {number} dx X-coordinate of the translation.
113 * @param {number} dy Y-coordinate of the translation.
114 */
115DriveFile.prototype.isRenamable = function()
116{
117	return this.isEditable() && DrawioFile.prototype.isEditable.apply(this, arguments);
118};
119
120/**
121 * Translates this point by the given vector.
122 *
123 * @param {number} dx X-coordinate of the translation.
124 * @param {number} dy Y-coordinate of the translation.
125 */
126DriveFile.prototype.isMovable = function()
127{
128	return this.isEditable();
129};
130
131/**
132 * Translates this point by the given vector.
133 *
134 * @param {number} dx X-coordinate of the translation.
135 * @param {number} dy Y-coordinate of the translation.
136 */
137DriveFile.prototype.isTrashed = function()
138{
139	return this.desc.labels.trashed;
140};
141
142/**
143 * Translates this point by the given vector.
144 *
145 * @param {number} dx X-coordinate of the translation.
146 * @param {number} dy Y-coordinate of the translation.
147 */
148DriveFile.prototype.save = function(revision, success, error, unloading, overwrite)
149{
150	DrawioFile.prototype.save.apply(this, [revision, mxUtils.bind(this, function()
151	{
152		this.saveFile(null, revision, success, error, unloading, overwrite);
153	}), error, unloading, overwrite]);
154};
155
156/**
157 * Translates this point by the given vector.
158 *
159 * @param {number} dx X-coordinate of the translation.
160 * @param {number} dy Y-coordinate of the translation.
161 */
162DriveFile.prototype.saveFile = function(title, revision, success, error, unloading, overwrite)
163{
164	try
165	{
166		if (!this.isEditable())
167		{
168			if (success != null)
169			{
170				success();
171			}
172		}
173		else if (!this.savingFile)
174		{
175			// Sets shadow modified state during save
176			this.savingFileTime = new Date();
177			this.setShadowModified(false);
178			this.savingFile = true;
179
180			this.createSecret(mxUtils.bind(this, function(secret, token)
181			{
182				var doSave = mxUtils.bind(this, function(realOverwrite, realRevision)
183				{
184					try
185					{
186						var lastDesc = this.desc;
187
188						this.ui.drive.saveFile(this, realRevision, mxUtils.bind(this, function(resp, savedData)
189						{
190							try
191							{
192								this.savingFile = false;
193
194								// Handles special case where resp is false eg
195								// if the old file was converted to realtime
196								if (resp != false)
197								{
198									// Checks for changes during save
199									this.setModified(this.getShadowModified());
200
201									if (revision)
202									{
203										this.lastAutosaveRevision = new Date().getTime();
204									}
205
206									// Adaptive autosave delay
207									this.autosaveDelay = Math.round(Math.min(10000,
208										Math.max(DriveFile.prototype.autosaveDelay,
209											this.saveDelay)));
210									this.desc = resp;
211
212									// Shows possible errors but keeps the modified flag as the
213									// file was saved but the cache entry could not be written
214									if (token != null)
215									{
216										this.fileSaved(savedData, lastDesc, mxUtils.bind(this, function()
217										{
218											this.contentChanged();
219
220											if (success != null)
221											{
222												success(resp);
223											}
224										}), error, token);
225									}
226									else if (success != null)
227									{
228										success(resp);
229									}
230								}
231								else if (error != null)
232								{
233									error(resp);
234								}
235							}
236							catch (e)
237							{
238								this.savingFile = false;
239
240								if (error != null)
241								{
242									error(e);
243								}
244								else
245								{
246									throw e;
247								}
248							}
249						}), mxUtils.bind(this, function(err, desc)
250						{
251							try
252							{
253								this.savingFile = false;
254
255								if (this.isConflict(err))
256								{
257									this.inConflictState = true;
258
259									if (this.sync != null)
260									{
261										this.savingFile = true;
262
263										this.sync.fileConflict(desc, mxUtils.bind(this, function()
264										{
265											// Adds random cool-off
266											window.setTimeout(mxUtils.bind(this, function()
267											{
268												this.updateFileData();
269												this.setShadowModified(false);
270												doSave(realOverwrite, true);
271											}), 100 + Math.random() * 500);
272										}), mxUtils.bind(this, function()
273										{
274											this.savingFile = false;
275
276											if (error != null)
277											{
278												error();
279											}
280										}));
281									}
282									else if (error != null)
283									{
284										error();
285									}
286								}
287								else if (error != null)
288								{
289									error(err);
290								}
291							}
292							catch (e)
293							{
294								this.savingFile = false;
295
296								if (error != null)
297								{
298									error(e);
299								}
300								else
301								{
302									throw e;
303								}
304							}
305						}), unloading, unloading, realOverwrite, null, secret);
306					}
307					catch (e)
308					{
309						this.savingFile = false;
310
311						if (error != null)
312						{
313							error(e);
314						}
315						else
316						{
317							throw e;
318						}
319					}
320				});
321
322				doSave(overwrite, revision);
323			}));
324		}
325	}
326	catch (e)
327	{
328		if (error != null)
329		{
330			error(e);
331		}
332		else
333		{
334			throw e;
335		}
336	}
337};
338
339/**
340 * Shows a conflict dialog to the user.
341 */
342DriveFile.prototype.copyFile = function(success, error)
343{
344	if (!this.isRestricted())
345	{
346		this.makeCopy(mxUtils.bind(this, function()
347		{
348			if (this.ui.spinner.spin(document.body, mxResources.get('saving')))
349			{
350				try
351				{
352					this.save(true, success, error)
353				}
354				catch (e)
355				{
356					error(e);
357				}
358			}
359		}), error, true);
360	}
361	else
362	{
363		DrawioFile.prototype.copyFile.apply(this, arguments);
364	}
365};
366
367/**
368 * Shows a conflict dialog to the user.
369 */
370DriveFile.prototype.makeCopy = function(success, error, timestamp)
371{
372	if (this.ui.spinner.spin(document.body, mxResources.get('saving')))
373	{
374		// Uses copyFile internally which is a remote REST call with the advantage of keeping
375		// the parents of the file in-place, but copies the remote file contents so needs to
376		// be updated as soon as we have the ID.
377		this.saveAs(this.ui.getCopyFilename(this, timestamp), mxUtils.bind(this, function(resp)
378		{
379			this.desc = resp;
380			this.ui.spinner.stop();
381			this.setModified(false);
382
383			this.backupPatch = null;
384			this.invalidChecksum = false;
385			this.inConflictState = false;
386
387			this.descriptorChanged();
388			success();
389		}), mxUtils.bind(this, function()
390		{
391			this.ui.spinner.stop();
392
393			if (error != null)
394			{
395				error();
396			}
397		}));
398	}
399};
400
401/**
402 * Translates this point by the given vector.
403 *
404 * @param {number} dx X-coordinate of the translation.
405 * @param {number} dy Y-coordinate of the translation.
406 */
407DriveFile.prototype.saveAs = function(filename, success, error)
408{
409	this.ui.drive.copyFile(this.getId(), filename, success, error);
410};
411
412/**
413 * Translates this point by the given vector.
414 *
415 * @param {number} dx X-coordinate of the translation.
416 * @param {number} dy Y-coordinate of the translation.
417 */
418DriveFile.prototype.rename = function(title, success, error)
419{
420	var etag = this.getCurrentEtag();
421
422	this.ui.drive.renameFile(this.getId(), title, mxUtils.bind(this, function(desc)
423	{
424		if (!this.hasSameExtension(title, this.getTitle()))
425		{
426			this.desc = desc;
427
428			if (this.sync != null)
429			{
430				this.sync.descriptorChanged(etag);
431			}
432
433			this.save(true, success, error);
434		}
435		else
436		{
437			this.desc = desc;
438			this.descriptorChanged();
439
440			if (this.sync != null)
441			{
442				this.sync.descriptorChanged(etag);
443			}
444
445			if (success != null)
446			{
447				success(desc);
448			}
449		}
450	}), error);
451};
452
453/**
454 * Translates this point by the given vector.
455 *
456 * @param {number} dx X-coordinate of the translation.
457 * @param {number} dy Y-coordinate of the translation.
458 */
459DriveFile.prototype.move = function(folderId, success, error)
460{
461	this.ui.drive.moveFile(this.getId(), folderId, mxUtils.bind(this, function(resp)
462	{
463		this.desc = resp;
464		this.descriptorChanged();
465
466		if (success != null)
467		{
468			success(resp);
469		}
470	}), error);
471};
472
473/**
474 * Translates this point by the given vector.
475 *
476 * @param {number} dx X-coordinate of the translation.
477 * @param {number} dy Y-coordinate of the translation.
478 */
479DriveFile.prototype.share = function()
480{
481	this.ui.drive.showPermissions(this.getId());
482};
483
484/**
485 * Translates this point by the given vector.
486 *
487 * @param {number} dx X-coordinate of the translation.
488 * @param {number} dy Y-coordinate of the translation.
489 */
490DriveFile.prototype.getTitle = function()
491{
492	return this.desc.title;
493};
494
495/**
496 * Translates this point by the given vector.
497 *
498 * @param {number} dx X-coordinate of the translation.
499 * @param {number} dy Y-coordinate of the translation.
500 */
501DriveFile.prototype.getHash = function()
502{
503	return 'G' + this.getId();
504};
505
506/**
507 * Translates this point by the given vector.
508 *
509 * @param {number} dx X-coordinate of the translation.
510 * @param {number} dy Y-coordinate of the translation.
511 */
512DriveFile.prototype.getId = function()
513{
514	return this.desc.id;
515};
516
517/**
518 * Translates this point by the given vector.
519 *
520 * @param {number} dx X-coordinate of the translation.
521 * @param {number} dy Y-coordinate of the translation.
522 */
523DriveFile.prototype.isEditable = function()
524{
525	return DrawioFile.prototype.isEditable.apply(this, arguments) &&
526		this.desc.editable;
527};
528
529/**
530 * Hook for subclassers.
531 */
532DriveFile.prototype.isSyncSupported = function()
533{
534	return true;
535};
536
537/**
538 * Hook for subclassers.
539 */
540DriveFile.prototype.isRevisionHistorySupported = function()
541{
542	return true;
543};
544
545/**
546 * Hook for subclassers.
547 */
548DriveFile.prototype.getRevisions = function(success, error)
549{
550	this.ui.drive.executeRequest(
551	{
552		url: '/files/' + this.getId() + '/revisions'
553	},
554	mxUtils.bind(this, function(resp)
555	{
556		for (var i = 0; i < resp.items.length; i++)
557		{
558			(mxUtils.bind(this, function(item)
559			{
560				// Redirects title to originalFilename to
561				// match expected descriptor interface
562				item.title = item.originalFilename;
563
564				item.getXml = mxUtils.bind(this, function(itemSuccess, itemError)
565				{
566					this.ui.drive.getXmlFile(item, mxUtils.bind(this, function(file)
567		   			{
568						itemSuccess(file.getData());
569		   			}), itemError);
570				});
571
572				item.getUrl = mxUtils.bind(this, function(page)
573				{
574					return this.ui.getUrl(window.location.pathname + '?rev=' + item.id +
575						'&chrome=0&nav=1&layers=1&edit=_blank' + ((page != null) ?
576						'&page=' + page : '')) + window.location.hash;
577				});
578			}))(resp.items[i]);
579		}
580
581		success(resp.items);
582	}), error);
583};
584
585/**
586 * Adds the listener for automatically saving the diagram for local changes.
587 */
588DriveFile.prototype.getLatestVersion = function(success, error)
589{
590	this.ui.drive.getFile(this.getId(), success, error, true);
591};
592
593/**
594 * Adds all listeners.
595 */
596DriveFile.prototype.getChannelId = function()
597{
598	var chan = this.ui.drive.getCustomProperty(this.desc, 'channel');
599
600	if (chan != null)
601	{
602		chan = 'G-' + this.getId() + '.' + chan;
603	}
604
605	return chan;
606};
607
608/**
609 * Gets the channel ID from the given descriptor.
610 */
611DriveFile.prototype.getChannelKey = function()
612{
613	return this.ui.drive.getCustomProperty(this.desc, 'key');
614};
615
616/**
617 * Adds all listeners.
618 */
619DriveFile.prototype.getLastModifiedDate = function()
620{
621	return new Date(this.desc.modifiedDate);
622};
623
624/**
625 * Adds all listeners.
626 */
627DriveFile.prototype.getDescriptor = function()
628{
629	return this.desc;
630};
631
632/**
633* Updates the descriptor of this file with the one from the given file.
634*/
635DriveFile.prototype.setDescriptor = function(desc)
636{
637	this.desc = desc;
638};
639
640/**
641 * Returns the etag from the given descriptor.
642 */
643DriveFile.prototype.getDescriptorSecret = function(desc)
644{
645	return this.ui.drive.getCustomProperty(desc, 'secret');
646};
647
648/**
649 * Updates the revision ID on the given descriptor.
650 */
651DriveFile.prototype.setDescriptorRevisionId = function(desc, id)
652{
653	desc.headRevisionId = id;
654};
655
656/**
657 * Returns the revision ID from the given descriptor.
658 */
659DriveFile.prototype.getDescriptorRevisionId = function(desc)
660{
661	return desc.headRevisionId;
662};
663
664/**
665 * Adds all listeners.
666 */
667DriveFile.prototype.getDescriptorEtag = function(desc)
668{
669	return desc.etag;
670};
671
672/**
673 * Adds the listener for automatically saving the diagram for local changes.
674 */
675DriveFile.prototype.setDescriptorEtag = function(desc, etag)
676{
677	desc.etag = etag;
678};
679
680/**
681 * Adds the listener for automatically saving the diagram for local changes.
682 */
683DriveFile.prototype.loadPatchDescriptor = function(success, error)
684{
685	this.ui.drive.executeRequest(
686	{
687		url: '/files/' + this.getId() + '?supportsAllDrives=true&fields=' + this.ui.drive.catchupFields
688	},
689	mxUtils.bind(this, function(desc)
690	{
691		success(desc);
692	}), error);
693};
694
695/**
696 * Adds the listener for automatically saving the diagram for local changes.
697 */
698DriveFile.prototype.patchDescriptor = function(desc, patch)
699{
700	desc.headRevisionId = patch.headRevisionId;
701	desc.modifiedDate = patch.modifiedDate;
702
703	DrawioFile.prototype.patchDescriptor.apply(this, arguments);
704};
705
706/**
707 * Adds the listener for automatically saving the diagram for local changes.
708 */
709DriveFile.prototype.loadDescriptor = function(success, error)
710{
711	this.ui.drive.loadDescriptor(this.getId(), success, error);
712};
713
714/**
715 * Are comments supported
716 */
717DriveFile.prototype.commentsSupported = function()
718{
719	return true;
720};
721
722/**
723 * Get comments of the file
724 */
725DriveFile.prototype.getComments = function(success, error)
726{
727	var currentUser = this.ui.getCurrentUser();
728
729	function driveCommentToDrawio(file, gComment, pCommentId)
730	{
731		if (gComment.deleted) return null; //skip deleted comments
732
733		var comment = new DriveComment(file, gComment.commentId || gComment.replyId, gComment.content,
734				gComment.modifiedDate, gComment.createdDate, gComment.status == 'resolved',
735				gComment.author.isAuthenticatedUser? currentUser :
736				new DrawioUser(gComment.author.permissionId, gComment.author.emailAddress,
737						gComment.author.displayName, gComment.author.picture.url), pCommentId);
738
739		for (var i = 0; gComment.replies != null && i < gComment.replies.length; i++)
740		{
741			comment.addReplyDirect(driveCommentToDrawio(file, gComment.replies[i], gComment.commentId));
742		}
743
744		return comment;
745	};
746
747	this.ui.drive.executeRequest(
748	{
749		url: '/files/' + this.getId() + '/comments'
750	},
751	mxUtils.bind(this, function(resp)
752	{
753		var comments = [];
754
755		for (var i = 0; i < resp.items.length; i++)
756		{
757			var comment = driveCommentToDrawio(this, resp.items[i]);
758
759			if (comment != null) comments.push(comment);
760		}
761
762		success(comments);
763	}), error);
764};
765
766/**
767 * Add a comment to the file
768 */
769DriveFile.prototype.addComment = function(comment, success, error)
770{
771	var body = {'content': comment.content};
772
773	this.ui.drive.executeRequest(
774	{
775		url: '/files/' + this.getId() + '/comments',
776		method: 'POST',
777		params: body
778	},
779	mxUtils.bind(this, function(resp)
780	{
781		success(resp.commentId); //pass comment id
782	}), error);
783};
784
785/**
786 * Can add a reply to a reply
787 */
788DriveFile.prototype.canReplyToReplies = function()
789{
790	return false;
791};
792
793/**
794 * Can add comments (The permission to comment to this file)
795 */
796DriveFile.prototype.canComment = function()
797{
798	return this.desc.canComment;
799};
800
801/**
802 * Get a new comment object
803 */
804DriveFile.prototype.newComment = function(content, user)
805{
806	return new DriveComment(this, null, content, Date.now(), Date.now(), false, user);
807};
808