1/**
2 * Copyright (c) 2006-2020, JGraph Ltd
3 * Copyright (c) 2006-2020, draw.io AG
4 */
5
6//Add a closure to hide the class private variables without changing the code a lot
7(function()
8{
9
10var _token = null;
11var pickers = {};
12
13window.DriveClient = function(editorUi, isExtAuth)
14{
15	if (isExtAuth == null && window.urlParams != null && window.urlParams['extAuth'] == '1')
16	{
17		isExtAuth = true;
18	}
19
20	mxEventSource.call(this);
21
22	DrawioClient.call(this, editorUi, 'gDriveAuthInfo');
23
24	this.isExtAuth = isExtAuth;
25	/**
26	 * Holds a reference to the UI. Needed for the sharing client.
27	 */
28	this.ui = editorUi;
29
30	// New mime type for XML files
31	this.xmlMimeType = 'application/vnd.jgraph.mxfile';
32	this.mimeType = 'application/vnd.jgraph.mxfile.realtime';
33
34	// Reading files now possible with no initial click in drive
35	//TODO In teams we do auth using editor app, we need to support viewer only app also
36	if (this.ui.editor.chromeless && !this.ui.editor.editable && urlParams['rt'] != '1' && urlParams['extAuth'] != '1')
37	{
38		// Uses separate name for the viewer auth tokens
39		this.cookieName = 'gDriveViewerAuthInfo';
40		this.token = this.getPersistentToken();
41
42		this.appId = window.DRAWIO_GOOGLE_VIEWER_APP_ID || '850530949725';
43		this.clientId = window.DRAWIO_GOOGLE_VIEWER_CLIENT_ID || '850530949725.apps.googleusercontent.com';
44		this.scopes = ['https://www.googleapis.com/auth/drive.readonly',
45			'https://www.googleapis.com/auth/userinfo.profile'];
46	}
47	else
48	{
49		this.appId = window.DRAWIO_GOOGLE_APP_ID || '671128082532';
50		this.clientId = window.DRAWIO_GOOGLE_CLIENT_ID || '671128082532-jhphbq6d0e1gnsus9mn7vf8a6fjn10mp.apps.googleusercontent.com';
51	}
52
53	this.mimeTypes = this.xmlMimeType + ',application/mxe,application/mxr,' +
54		'application/vnd.jgraph.mxfile.realtime,application/vnd.jgraph.mxfile.rtlegacy';
55
56	var authInfo = JSON.parse(this.token);
57
58	if (authInfo != null && authInfo.current != null)
59	{
60		this.userId = authInfo.current.userId;
61		this.authCalled = false;
62	}
63};
64
65// Extends mxEventSource
66mxUtils.extend(DriveClient, mxEventSource);
67
68// Extends DrawioClient
69mxUtils.extend(DriveClient, DrawioClient);
70
71DriveClient.prototype.redirectUri = window.location.protocol + '//' + window.location.host + '/google';
72DriveClient.prototype.GDriveBaseUrl = 'https://www.googleapis.com/drive/v2';
73
74/**
75 * OAuth 2.0 scopes for installing Drive Apps.
76 */
77DriveClient.prototype.scopes = ['https://www.googleapis.com/auth/drive.file',
78								'https://www.googleapis.com/auth/drive.install',
79								'https://www.googleapis.com/auth/userinfo.profile'];
80
81/**
82 * Contains the hostname of the old app.
83 */
84DriveClient.prototype.allFields = 'kind,id,parents,headRevisionId,etag,title,mimeType,modifiedDate,' +
85	'editable,copyable,canComment,labels,properties,downloadUrl,webContentLink,userPermission,fileSize';
86
87/**
88 * Fields required for catchin up.
89 *
90 * TODO: Limit to etag and ekey property only
91 */
92DriveClient.prototype.catchupFields = 'etag,headRevisionId,modifiedDate,properties(key,value)';
93
94/**
95 * Specifies if thumbnails should be enabled. Default is true.
96 * LATER: If thumbnails are disabled, make sure to replace the
97 * existing thumbnail with the placeholder only once.
98 */
99DriveClient.prototype.enableThumbnails = true;
100
101/**
102 * Specifies the width for thumbnails. Default is 1000. This value
103 * must be between 220 and 1600.
104 */
105DriveClient.prototype.thumbnailWidth = 1000;
106
107/**
108 * The maximum number of bytes per thumbnail. Default is 2000000.
109 */
110DriveClient.prototype.maxThumbnailSize = 2000000;
111
112/**
113 * Defines the base64url PNG to be used if no thumbnail was generated
114 * (including the case where thumbnails are disabled).
115 */
116DriveClient.prototype.placeholderThumbnail = 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAMAAAAL34HQAAACN1BMVEXwhwXvhgX4iwXzhwXgbQzvhgXhbAzocgzqcwzldAoAAADhbgvjcQnmdgrlbgDwhgXsfwXufgjwhgXwgQfziAXxgADibgz4iwX4jAX3iwTpcwr1igXoewjsfgj3igX4iwXqcQv4jAX3iwXtfQnndQrvhAbibArwhwXgbQz//////v39jwX6jQX+/v7fagHfawzdVQDwhADgbhPgbhXwhwPocQ3uvKvwiA/faQDscgzxiAT97+XgciTgcSP6jAXgbQ3gcCHwiRfpcQzwhwfeXQD77ef74NLvhgTvegD66uPgbAf66+TvfADwjCzgcCfwiSD67ObhcjjwiBHhczvwiyrgbxj///777ujgcSHgcB/xiRzgbhveWgDeVwDhdEDgbRDqfgffYgDfXwD97+bvfQDxiz7//vvwiRr118rrcgztggbfZgDfZAD++PT98+3gbBPsgAb99vD33tPgcB7icAvuhAX//Pn66N/00sTyy7vuuqbjekLwhwzkcgr88er449n++vfutp/kh1vgcBvhbwvmdwnwgwDwgADeWQD87eLxxrTssJjqpIf0roHmjWTkhFP759n63czvvanomnjnlHDhczD22cr4y6/wwa/3xKX2wJ3rqpH0tY7qp4vpnoDymlbjf0vxjjntcwzldAroegj/kgX12s7518PzqnnnkWfynmLieUjpewjrdAD40Lj1uZTzpm3idTbiciLydQzzfwnyiQTsfgD3xqnzp3TxlkzgbCrdTwDdSwBLKUlNAAAAJ3RSTlP8/b2X/YH8wb+FAIuIggJbQin5opAM9+a/ubaubyD78NjSyr2WgRp4sjN4AAAI70lEQVR42u2cZ38SQRDGT8WGvfde4E4BxVMRRaKiUURRlJhQRDCCSgQVO/bee++9994+nMt5ywoezFJd/fm8uITi3p9n5mbYkcCpO6rVnVu2YEXd+3dRIySuo7pLv4GjGNKg7j3UHTl1l14PajmG9OFBnx7Ird4PumpYEtf1QXc112l0M7OGKXEfeg3guo3iNIyJG92Jaz61mYYxcaNacs1H/8f6j6X5j1WI/mMVIsawRFEzI49SjwOqAJa43emclk8Rp2c7AFZ+LDGyvXE2kmO2Q1Lq17RSd6ND48QIwFVuLNHTOPbEpTOz8ujMpccHGz0AV5mxIo4TpwUeUPj0YwfAVVYs0Tn7VZjnBUA8v+n6CyfERY8FR/DEJj7MQ6oL85vOvfDUAsuVC8s19s5yXuAppOPnvPk4EeSCsehCeBVTwVzHfE6RcFUQa4an8Qw91kpbw2oz4aoc1sSxniO0WAI/J24wriabmEpizZtM79bc+fr4/tUarEpiLabGElJYRsOGjbJfjGDpJCxtmosRLOEnVpqLESzZLYlLg65H1rAkLo2GESwcROwXI1jELcS1Y6OGQSzEVaupZQJLDiLhYtCtFBcbbslYhOueqKllDwtzwVhTq4RFuBh0C3EdEBl0C3OBWNUrEISLvSD+5GLQLYmLoSqfwcUiFuaqzhYDxiJc981lxqqdVsCGbHPcQLBgrtK3rwLt9tWqhblKxxI9hW3267U5ZHhuBrCKzXl4NIJTS5FrmbmMWGIEDZIouOp0/O6boYQ2jxBXWcdu13fzRILuF/2Ku+aGr96uBbhALHo5Z38+XcfXyVRZVx/+Ed513ldDCCCu0rFE0Xlo2mu5TAj8ki0XV0q6ePHilhi+d/15b9ACQGGusg3AFzc+XSMBCPzu89+CNlnB7zfD8t1z4iaLXUvDVT6sGdMOnv5pi47f6r9Qk9YF3xZ0l8S11UfMArlgLMpZM6bamYy6rWnta9q7TrZrzZPgPgoqg3atubY8WK6D8lQXHfb4p/wSK7vFfxmxSsAPQ96AlZ4LxoLNeompdkUDGQVznL5mLr4ar5ESD3PBWHA9fbpbjlT4pq1Bm6H6w9dwfOd69ePouNDYt3S3ULPGZ96S3YqtAW/Tepz1E8bgAANc+xEXhAX36ut1cslcd6rJq81SIvgEe7lmL3kY5iqxVYvOI9isswp22KeMOcrriJlWai5giwHl+yec73Ma9Mbfz+qOJndKz6hLpR5V1uPxavFuTTt0K1XfpbNeO0wKeUaR2IPBN5sMRlqu1eY8bsFmPeIFUpi0CjIGTLvSZY2EGeYSi3VL9Dgeb0I+SQl9MlcZT4TObZKzfmfS5NZSx1GsLQ5r+8Sxp7ERR/1TtDlUn2qNuGXCrZGM5URlLDiEVzDVkje5fdjXdDsm27XpXChBz4XG0UpYcDOMYaxjGc3wtyJxFtu1PohaI71f2K2imqEONcN4nrMZ9TWbMf81wg9z3VNwC26Gr3enY4ObobLqbccFefuz5AKONpVfzQp2y3NoVvrN32GLNl9orA22lTiM+Nqg5CJY1DueOjkwsdtNgAP7gidR2SWVhFqt3o9QwoKHIuiwDcwX+xT/UWztSlvCaqXGmtQBY1GadQmfh6anuE0XlkhhRFs3tGGkd+tuIVhiJN0M+brj0mlAu46lX0bcbizVLbgZrgwl4JhYA+NQa9TJQUetsSJYHscJvAVct7eJKoUbQudxPYmdirqzsYsIojhjoitD01yadH287J+vpZF1/uGt2K4ttinjshQo2C2XMzI2U64X6WY4tyZq99a7wZS3eA3BpNyrUPn1x00Z0uM1ACzilOfg7EN3VmRo8dN16WYYerYw6G9qCOSDCjQ0jQkufRbalt65LVyapaA/2mClxhK3Rxy3rsyavDxDR/DL5sMLFiyYu/7sXps7z8VldPv2Xl6PnjlTwOOuJQuytH7CXpvXCOQWoZrYeHWd4nw2Q+v22OLGnFSG0Nk1PCi0xjgjpVvTGi8hht9F+ARBGq8dtXmtOSLoDm1FhUSHnihkTecESalHkPAaWVhtFbA8jqvQGBmbt8fWkKtNn0Xw9GvAWK6DX9bBVHjzqtyvvcG9a+jXyC5oKoKV/a4YFG7Yij2ofszlgtaA3ZoRwW+pIOH3w0qZFURNh3oNtKsDsAr9LNvMC0pj93H6hTPpX9ocg8FIgTVvcgFYC03jFLBMi6ix0MDAoi8/lh7Cgt2q0VfNrSX0ayhjTa2IW0tKdotNrMq4NbPkILKZW+xdiSoGgshogfh7Ul7FcIEoFevfrPLC3+XWf6y/CEvHZoFQqlts9sQigqjLxFpQCJauakFcsqhKPXH79rGb6bE2B5Qmu0b91zn0WJtN8Wys9tgtIqfjEf2SWw7XKI8gHuKQ0X0eDsQSI44TaGBN6dYN5dlI/eFj9I7f8GWtoUJYOIgkiq6Ds/gw5T7dZDUqTrfscbLbB9eIB7JmEKsUgiii/4uO8ToBfJlhfif5tEGWEsGTMT4Mr6HDa0BBlP5Y88lcnkdkCtLhnyjMM0+Gcn2WzW6xnd/J8zn+LZq4SUeEvUBaA8LCs6Tk1p1AetXt3JoMWexWZSyr3RK6vSUGrRHbmkRUVgCLpP1HW/L4tgl5tO140mdKKFFhrkTUdxta4xleA8DCXC6n/vCYvPJFa9zAWL4m6qNaA8IiqjW73lreWnJrSj0AJYFZpvwq6RZRzjVUGEtB5tX7DdoqCXaL+PXHuEjdYsuvVqva4Sqv6NdabdW4YLeIKsoFYzHGhYPIGBd2izGuVpPaSVgAV7VEsOQgsuUXdosxLuwWxLVMW0WRK5ExLiiIpN4vq2YYVTiIbPmFgii5xRiXimCBqmIcVSS3WMqvdMqz5VcKqzdKeca4UrnVT/ryR6bi2Opuf64TwYJlfl4FLqu2Zxeux5BRXZnisvZ8103NqTtzoziuGa24+wZVRdVK9W7wyNSX1nYeOmrU6JSmjp6KhH5BR+kGvk++Ld0c/X66rPH4SEQeGl+kpq8a33eAumPqK347durWpzm9hrWhUevi1Hd4ZzVC+gGMHY0TYnDOYwAAAABJRU5ErkJggg=='.replace(/\+/g, '-').replace(/\//g, '_');
117
118/**
119 * Mime type for the paceholder thumbnail.
120 */
121DriveClient.prototype.placeholderMimeType = 'image/png';
122
123/**
124 * Executes the first step for connecting to Google Drive.
125 */
126DriveClient.prototype.libraryMimeType = 'application/vnd.jgraph.mxlibrary';
127
128/**
129 * Contains the hostname of the new app.
130 */
131DriveClient.prototype.newAppHostname = 'www.draw.io';
132
133/**
134 * Executes the first step for connecting to Google Drive.
135 */
136DriveClient.prototype.extension = '.drawio';
137
138/**
139 * Interval for updating the access token.
140 */
141DriveClient.prototype.tokenRefreshInterval = 0;
142
143/**
144 * Interval for updating the access token.
145 */
146DriveClient.prototype.lastTokenRefresh = 0;
147
148/**
149 * Executes the first step for connecting to Google Drive.
150 */
151DriveClient.prototype.maxRetries = 5;
152
153/**
154 * Executes the first step for connecting to Google Drive.
155 */
156DriveClient.prototype.staleEtagMaxRetries = 3;
157
158/**
159 * Executes the first step for connecting to Google Drive.
160 */
161DriveClient.prototype.coolOff = 1000;
162
163/**
164 * Executes the first step for connecting to Google Drive.
165 */
166DriveClient.prototype.mimeTypeCheckCoolOff = 60000;
167
168/**
169 * Executes the first step for connecting to Google Drive.
170 */
171DriveClient.prototype.user = null;
172
173/**
174 * Executes auth in same window (no popups)
175 */
176DriveClient.prototype.sameWinAuthMode = false;
177
178/**
179 * Redirect URL of samw window mode that will get the token
180 */
181DriveClient.prototype.sameWinRedirectUrl = null;
182
183
184/**
185 * Authorizes the client, gets the userId and calls <open>.
186 */
187DriveClient.prototype.setUser = function(user)
188{
189	this.user = user;
190
191	if (this.user == null)
192	{
193		this.userId = null;
194
195		if (this.tokenRefreshThread != null)
196		{
197			window.clearTimeout(this.tokenRefreshThread);
198			this.tokenRefreshThread = null;
199		}
200	}
201	else
202	{
203		this.userId = user.id;
204	}
205
206	this.fireEvent(new mxEventObject('userChanged'));
207};
208
209DriveClient.prototype.setUserId = function(userId)
210{
211	this.userId = userId;
212
213	if (this.user != null && this.user.id != this.userId)
214	{
215		this.user = null;
216	}
217};
218/**
219 * Authorizes the client, gets the userId and calls <open>.
220 */
221DriveClient.prototype.getUser = function()
222{
223	return this.user;
224};
225
226DriveClient.prototype.getUsersList = function()
227{
228	var users = [];
229	var authInfo = JSON.parse(this.getPersistentToken(true));
230	var curUserId = null;
231
232	if (authInfo != null)
233	{
234		if (authInfo.current != null)
235		{
236			curUserId = authInfo.current.userId;
237			users.push(authInfo[curUserId].user);
238			users[0].isCurrent = true;
239
240		}
241
242		for (var id in authInfo)
243		{
244			if (id == 'current' || id == curUserId) continue;
245
246			users.push(authInfo[id].user);
247		}
248	}
249	return users;
250};
251
252DriveClient.prototype.logout = function()
253{
254	//Send to server to clear refresh token cookie
255	this.ui.editor.loadUrl(this.redirectUri + '?doLogout=1&userId=' + this.userId + '&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname));
256	this.clearPersistentToken();
257	this.setUser(null);
258	_token = null;
259};
260
261/**
262 * Authorizes the client, gets the userId and calls <open>.
263 */
264DriveClient.prototype.execute = function(fn)
265{
266	// Handles error in immediate authorize call via callback that shows a
267	// UI with a button that executes the second non-immediate authorize
268	var fallback = mxUtils.bind(this, function(resp)
269	{
270		// Remember is an argument for the callback that executes
271		// when the user clicks the authorize button in the UI and
272		// success executes after successful authorization.
273		this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, success)
274		{
275			this.authorize(false, mxUtils.bind(this, function()
276			{
277				if (success != null)
278				{
279					success();
280				}
281
282				fn();
283			}), mxUtils.bind(this, function(resp)
284			{
285				var msg = mxResources.get('cannotLogin');
286
287				// Handles special domain policy errors
288				if (resp != null && resp.error != null)
289				{
290					if (resp.error.code == 403 &&
291						resp.error.data != null && resp.error.data.length > 0 &&
292						resp.error.data[0].reason == 'domainPolicy')
293					{
294						msg = resp.error.message;
295					}
296				}
297
298				this.logout();
299
300				this.ui.showError(mxResources.get('error'), msg, mxResources.get('help'), mxUtils.bind(this, function()
301				{
302					this.ui.openLink('https://www.diagrams.net/doc/faq/gsuite-authorisation-troubleshoot');
303				}), null, mxResources.get('ok'));
304			}), remember);
305		}));
306	});
307
308	// First immediate authorize attempt
309	this.authorize(true, fn, fallback);
310};
311
312/**
313 * Executes the given request.
314 */
315DriveClient.prototype.executeRequest = function(reqObj, success, error)
316{
317	try
318	{
319		var acceptResponse = true;
320		var timeoutThread = null;
321		var retryCount = 0;
322
323		// Cancels any pending requests
324		if (this.requestThread != null)
325		{
326			window.clearTimeout(this.requestThread);
327		}
328
329		var fn = mxUtils.bind(this, function()
330		{
331			try
332			{
333				this.requestThread = null;
334				this.currentRequest = reqObj;
335
336				if (timeoutThread != null)
337				{
338					window.clearTimeout(timeoutThread);
339				}
340
341				timeoutThread = window.setTimeout(mxUtils.bind(this, function()
342				{
343					acceptResponse = false;
344
345					if (error != null)
346					{
347						error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout'), retry: fn});
348					}
349				}), this.ui.timeout);
350
351				var params = null;
352				var isJSON = false;
353
354				if (typeof reqObj.params === 'string')
355				{
356					params = reqObj.params;
357				}
358				else if (reqObj.params != null)
359				{
360					params = JSON.stringify(reqObj.params);
361					isJSON = true;
362				}
363
364				var url = reqObj.fullUrl || (this.GDriveBaseUrl + reqObj.url);
365
366				if (isJSON)
367				{
368					url += (url.indexOf('?') > 0 ? '&' : '?') + 'alt=json';
369				}
370
371				var req = new mxXmlRequest(url, params, reqObj.method || 'GET');
372
373				req.setRequestHeaders = mxUtils.bind(this, function(request, params)
374				{
375					if (reqObj.headers != null)
376					{
377						for (var key in reqObj.headers)
378						{
379							request.setRequestHeader(key, reqObj.headers[key]);
380						}
381					}
382					else if (reqObj.contentType != null)
383					{
384						request.setRequestHeader('Content-Type', reqObj.contentType);
385					}
386					else if (isJSON)
387					{
388						request.setRequestHeader('Content-Type', 'application/json');
389					}
390
391					request.setRequestHeader('Authorization', 'Bearer ' + _token);
392				});
393
394				req.send(mxUtils.bind(this, function(req)
395				{
396					try
397					{
398						window.clearTimeout(timeoutThread);
399
400						if (acceptResponse)
401						{
402							var resp;
403
404							try
405							{
406								resp = JSON.parse(req.getText());
407							}
408							catch(e)
409							{
410								resp = null;
411							}
412
413							if (req.getStatus() >= 200 && req.getStatus() <= 299)
414							{
415								if (success != null)
416								{
417									success(resp);
418								}
419							}
420							else
421							{
422								// Errors for put request are in data instead of errors
423								var data = (resp != null && resp.error != null) ? ((resp.error.data != null) ?
424									resp.error.data : resp.error.errors) : null;
425								var reason = (data != null && data.length > 0) ? data[0].reason : null;
426
427								// Handles special error for saving old file where mime was changed to new
428								// LATER: Check if 403 is never auth error, for now we check the message for a specific
429								// case where the old app mime type was overridden by the new app
430								if (error != null && resp != null && resp.error != null && (resp.error.code == -1 ||
431									(resp.error.code == 403 && (reason == 'domainPolicy' || resp.error.message ==
432									'The requested mime type change is forbidden.'))))
433								{
434									error(resp);
435								}
436								// Handles authentication error
437								else if (resp != null && resp.error != null && (resp.error.code == 401 ||
438									(resp.error.code == 403 && reason != 'rateLimitExceeded')))
439								{
440									// Shows an error if re-authenticated but the server still doesn't allow it
441									if ((resp.error.code == 403 && this.retryAuth) ||
442										(resp.error.code == 401 && this.retryAuth && reason == 'authError'))
443									{
444										if (error != null)
445										{
446											error(resp);
447										}
448
449										this.retryAuth = false;
450									}
451									else
452									{
453										this.retryAuth = true;
454										this.execute(fn);
455									}
456								}
457								// Schedules a retry if no new request was executed
458								else if (resp != null && resp.error != null && resp.error.code != 412 && resp.error.code != 404 &&
459									resp.error.code != 400 && this.currentRequest == reqObj && retryCount < this.maxRetries)
460								{
461									retryCount++;
462									var jitter = 1 + 0.1 * (Math.random() - 0.5);
463									this.requestThread = window.setTimeout(fn,
464										Math.round(Math.pow(2, retryCount) *
465										jitter * this.coolOff));
466								}
467								else if (error != null)
468								{
469									error(resp);
470								}
471							}
472						}
473					}
474					catch (e)
475					{
476						if (error != null)
477						{
478							error(e);
479						}
480						else
481						{
482							throw e;
483						}
484					}
485				}));
486			}
487			catch (e)
488			{
489				if (error != null)
490				{
491					error(e);
492				}
493				else
494				{
495					throw e;
496				}
497			}
498		});
499
500		// Must get token before first request in this case
501		if (_token == null || !this.authCalled)
502		{
503			this.execute(fn);
504		}
505		else
506		{
507			fn();
508		}
509	}
510	catch (e)
511	{
512		if (error != null)
513		{
514			error(e);
515		}
516		else
517		{
518			throw e;
519		}
520	}
521};
522
523DriveClient.prototype.createAuthWin = function(url)
524{
525	var width = 525,
526	height = 525,
527	screenX = window.screenX,
528	screenY = window.screenY,
529	outerWidth = window.outerWidth,
530	outerHeight = window.outerHeight;
531
532	var left = screenX + Math.max(outerWidth - width, 0) / 2;
533	var top = screenY + Math.max(outerHeight - height, 0) / 2;
534
535	var features = ['width=' + width, 'height=' + height,
536	                'top=' + top, 'left=' + left,
537	                'status=no', 'resizable=yes',
538	                'toolbar=no', 'menubar=no',
539	                'scrollbars=yes'];
540	return window.open(url? url : 'about:blank', 'gdauth', features.join(','));
541};
542
543/**
544 * Authorizes the client, gets the userId and calls <open>.
545 */
546DriveClient.prototype.authorize = function(immediate, success, error, remember, popup)
547{
548	if (this.isExtAuth && !immediate)
549	{
550		window.parent.driveAuth(mxUtils.bind(this, function(newAuthInfo)
551		{
552			this.updateAuthInfo(newAuthInfo, true, true, success, error);
553		}), error);
554		return;
555	}
556
557	var req = new mxXmlRequest(this.redirectUri + '?getState=1', null, 'GET');
558
559	req.send(mxUtils.bind(this, function(req)
560	{
561		if (req.getStatus() >= 200 && req.getStatus() <= 299)
562		{
563			this.authorizeStep2(req.getText(), immediate, success, error, remember, popup);
564		}
565		else if (error != null)
566		{
567			error(req);
568		}
569	}), error);
570};
571
572DriveClient.prototype.updateAuthInfo = function (newAuthInfo, remember, forceUserUpdate, success, error)
573{
574	_token = newAuthInfo.access_token;
575	delete newAuthInfo.access_token; //Don't store access token
576	newAuthInfo.expires = Date.now() + parseInt(newAuthInfo.expires_in) * 1000;
577	newAuthInfo.remember = remember;
578
579	this.resetTokenRefresh(newAuthInfo);
580	this.authCalled = true;
581
582	if (forceUserUpdate || this.user == null)
583	{
584		//IE/Edge security doesn't allow access to newAuthInfo in a callback function (outside this function scope)
585		//So, stringify the object and restore it (parse) in the callback
586		var strAuthInfo = JSON.stringify(newAuthInfo);
587
588		this.updateUser(mxUtils.bind(this, function()
589		{
590			//Restore the auth info object to bypass IE/Edge security
591			var resAuthInfo = JSON.parse(strAuthInfo);
592			//Save user and new token
593			this.setPersistentToken(resAuthInfo, !remember);
594
595			if (success != null)
596			{
597				success();
598			}
599		}), error);
600	}
601	else if (success != null)
602	{
603		this.setPersistentToken(newAuthInfo, !remember);
604		success();
605	}
606};
607
608DriveClient.prototype.authorizeStep2 = function(state, immediate, success, error, remember, popup)
609{
610	try
611	{
612		// Takes userId from state URL parameter
613		if (this.ui.stateArg != null && this.ui.stateArg.userId != null)
614		{
615			this.userId = this.ui.stateArg.userId;
616
617			if (this.user != null && this.user.id != this.userId)
618			{
619				this.user = null;
620			}
621		}
622
623		if (this.userId == null)
624		{
625			var authInfo = JSON.parse(this.getPersistentToken(true));
626
627			if (authInfo && authInfo.current != null)
628			{
629				this.userId = authInfo.current.userId;
630			}
631		}
632
633		// Immediate only possible with a refresh token (there is a userId)
634		if (immediate && this.userId == null)
635		{
636			if (error != null)
637			{
638				error();
639			}
640		}
641		else
642		{
643			//Retry request with refreshed token (in the cookie)
644			if (immediate) //Note, we checked refresh token is not null above
645			{
646				//state is used to identify which app/domain is used
647				var req = new mxXmlRequest(this.redirectUri + '?state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state)
648						+ '&userId=' + this.userId
649						, null, 'GET');
650
651				req.send(mxUtils.bind(this, function(req)
652				{
653					if (req.getStatus() >= 200 && req.getStatus() <= 299)
654					{
655						var newAuthInfo = JSON.parse(req.getText());
656						this.updateAuthInfo(newAuthInfo, true, false, success, error); //We set remember to true since we can only have a refresh token if user initially selected remember
657					}
658					else
659					{
660						//When the request fails (e.g, Hibernate on Windows), the status is 0, this doesn't mean the token is invalid
661						if (req.getStatus() != 0)
662						{
663							this.logout();
664						}
665
666						if (error != null)
667						{
668							error(req); //TODO review this code path and how error is handled
669						}
670					}
671				}), error);
672			}
673			else
674			{
675				var url = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + this.clientId +
676						'&redirect_uri=' + encodeURIComponent(this.redirectUri) +
677						'&response_type=code&include_granted_scopes=true' +
678						(remember? '&access_type=offline&prompt=consent%20select_account' : '') + //Ask for consent again to get a new refresh token
679						'&scope=' + encodeURIComponent(this.scopes.join(' ')) +
680						'&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state + //To identify which app/domain is used
681						(this.sameWinRedirectUrl? '&redirect=' + this.sameWinRedirectUrl : ''));
682
683				if (this.sameWinAuthMode)
684				{
685					window.location.assign(url);
686					popup = null; //Same window doesn't use onGoogleDriveCallback or popups
687				}
688				else if (popup == null)
689				{
690					popup = this.createAuthWin(url);
691				}
692				else
693				{
694					popup.location = url;
695				}
696
697				if (popup != null)
698				{
699					window.onGoogleDriveCallback = mxUtils.bind(this, function(newAuthInfo, authWindow)
700					{
701						window.onGoogleDriveCallback = null;
702
703						try
704						{
705							if (newAuthInfo == null)
706							{
707								if (error != null)
708								{
709									error({message: mxResources.get('accessDenied')}); //TODO Check this error handling is correct
710								}
711							}
712							else
713							{
714								this.updateAuthInfo(newAuthInfo, remember, true, success, error);
715							}
716						}
717						catch (e)
718						{
719							if (error != null)
720							{
721								error(e);
722							}
723						}
724						finally
725						{
726							if (authWindow != null)
727							{
728								authWindow.close();
729							}
730						}
731					});
732
733					popup.focus();
734				}
735			}
736		}
737	}
738	catch (e)
739	{
740		if (error != null)
741		{
742			error(e);
743		}
744		else
745		{
746			throw e;
747		}
748	}
749};
750
751/**
752 * Checks if the client is authorized and calls the next step.
753 */
754DriveClient.prototype.resetTokenRefresh = function(resp)
755{
756	if (this.tokenRefreshThread != null)
757	{
758		window.clearTimeout(this.tokenRefreshThread);
759		this.tokenRefreshThread = null;
760	}
761
762	// Starts timer to refresh token before it expires
763	if (resp != null && resp.error == null && resp.expires_in > 0)
764	{
765		this.tokenRefreshInterval = parseInt(resp.expires_in) * 1000;
766		this.lastTokenRefresh = new Date().getTime();
767
768		this.tokenRefreshThread = window.setTimeout(mxUtils.bind(this, function()
769		{
770			this.authorize(true, mxUtils.bind(this, function()
771			{
772				//console.log('tokenRefresh: refreshed', _token);
773			}), mxUtils.bind(this, function()
774			{
775				//console.log('tokenRefresh: error refreshing', _token);
776			}));
777		}), resp.expires_in * 900);
778	}
779};
780
781/**
782 * Checks if the client is authorized and calls the next step.
783 */
784DriveClient.prototype.checkToken = function(fn)
785{
786	var connected = this.lastTokenRefresh > 0;
787	var delta = new Date().getTime() - this.lastTokenRefresh;
788
789	if (delta > this.tokenRefreshInterval || this.tokenRefreshThread == null)
790	{
791		// Uses execute instead of authorize to allow a fallback authorization if cookie was lost
792		this.execute(mxUtils.bind(this, function()
793		{
794			fn();
795
796			if (connected)
797			{
798				this.fireEvent(new mxEventObject('disconnected'));
799			}
800		}));
801	}
802	else
803	{
804		fn();
805	}
806};
807
808/**
809 * Checks if the client is authorized and calls the next step.
810 */
811DriveClient.prototype.updateUser = function(success, error)
812{
813	try
814	{
815		var url = 'https://www.googleapis.com/oauth2/v2/userinfo?alt=json';
816		var headers = {'Authorization': 'Bearer ' + _token};
817
818		this.ui.editor.loadUrl(url, mxUtils.bind(this, function(data)
819		{
820	    	var info = JSON.parse(data);
821
822	    	// Requests more information about the user (email address is sometimes not in info)
823	    	this.executeRequest({url: '/about'}, mxUtils.bind(this, function(resp)
824	    	{
825	    		var email = mxResources.get('notAvailable');
826	    		var name = email;
827	    		var pic = null;
828
829	    		if (resp != null && resp.user != null)
830	    		{
831	    			email = resp.user.emailAddress;
832	    			name = resp.user.displayName;
833	    			pic = (resp.user.picture != null) ? resp.user.picture.url : null;
834	    		}
835
836	    		this.setUser(new DrawioUser(info.id, email, name, pic, info.locale));
837	    		this.userId = info.id;
838
839	    		if (success != null)
840				{
841					success();
842				}
843	    	}), error);
844		}), error, null, null, null, null, headers);
845	}
846	catch (e)
847	{
848		if (error != null)
849		{
850			error(e);
851		}
852		else
853		{
854			throw e;
855		}
856	}
857};
858
859/**
860 * Translates this point by the given vector.
861 *
862 * @param {number} dx X-coordinate of the translation.
863 * @param {number} dy Y-coordinate of the translation.
864 */
865DriveClient.prototype.copyFile = function(id, title, success, error)
866{
867	if (id != null && title != null)
868	{
869		this.executeRequest({url: '/files/' + id + '/copy?fields=' + encodeURIComponent(this.allFields)
870				+ '&supportsAllDrives=true&enforceSingleParent=true', //&alt=json
871				method: 'POST',
872				params: {'title': title, 'properties':
873					[{'key': 'channel', 'value': Editor.guid()}]}
874			}, success, error);
875	}
876};
877
878/**
879 * Translates this point by the given vector.
880 *
881 * @param {number} dx X-coordinate of the translation.
882 * @param {number} dy Y-coordinate of the translation.
883 */
884DriveClient.prototype.renameFile = function(id, title, success, error)
885{
886	if (id != null && title != null)
887	{
888		this.executeRequest(this.createDriveRequest(
889			id, {'title' : title}), success, error);
890	}
891};
892
893/**
894 * Translates this point by the given vector.
895 *
896 * @param {number} dx X-coordinate of the translation.
897 * @param {number} dy Y-coordinate of the translation.
898 */
899DriveClient.prototype.moveFile = function(id, folderId, success, error)
900{
901	if (id != null && folderId != null)
902	{
903		this.executeRequest(this.createDriveRequest(id, {'parents': [{'kind':
904			'drive#fileLink', 'id': folderId}]}), success, error);
905	}
906};
907
908/**
909 * Translates this point by the given vector.
910 *
911 * @param {number} dx X-coordinate of the translation.
912 * @param {number} dy Y-coordinate of the translation.
913 */
914DriveClient.prototype.createDriveRequest = function(id, body)
915{
916	return {
917		'url': '/files/' + id + '?uploadType=multipart&supportsAllDrives=true',
918		'method': 'PUT',
919		'contentType': 'application/json; charset=UTF-8',
920		'params': body
921	};
922};
923
924/**
925 * Loads the given file as a library file.
926 */
927DriveClient.prototype.getLibrary = function(id, success, error)
928{
929	return this.getFile(id, success, error, true, true);
930};
931
932/**
933 * Loads the descriptorf for the given file ID.
934 */
935DriveClient.prototype.loadDescriptor = function(id, success, error, fields)
936{
937	this.executeRequest({
938		url: '/files/' + id + '?supportsAllDrives=true&fields=' + (fields != null ? fields : this.allFields)
939	}, success, error);
940};
941
942DriveClient.prototype.listFiles = function(searchStr, afterDate, mineOnly, success, error)
943{
944	this.executeRequest({
945		url: '/files?supportsAllDrives=true&includeItemsFromAllDrives=true&q=' + encodeURIComponent('(mimeType contains \'' + this.xmlMimeType + '\') ' +
946		(searchStr? ' and (title contains \'' + searchStr + '\')' : '') +
947		(afterDate? ' and (modifiedDate > \'' + afterDate.toISOString() + '\')' : '') +
948		(mineOnly? ' and (\'me\' in owners)' : '')) +
949		'&orderBy=modifiedDate desc,title'
950	}, success, error);
951};
952
953/**
954 * Gets the channel ID from the given descriptor.
955 */
956DriveClient.prototype.getCustomProperty = function(desc, key)
957{
958	var props = desc.properties;
959	var result = null;
960
961	if (props != null)
962	{
963		for (var i = 0; i < props.length; i++)
964		{
965			if (props[i].key == key)
966			{
967				result = props[i].value;
968
969				break;
970			}
971		}
972	}
973
974	return result;
975};
976
977/**
978 * Checks if the client is authorized and calls the next step. The optional
979 * readXml argument is used for import. Default is false. The optional
980 * readLibrary argument is used for reading libraries. Default is false.
981 */
982DriveClient.prototype.getFile = function(id, success, error, readXml, readLibrary)
983{
984	readXml = (readXml != null) ? readXml : false;
985	readLibrary = (readLibrary != null) ? readLibrary : false;
986
987	if (urlParams['rev'] != null)
988	{
989		this.executeRequest({
990				url: '/files/' + id + '/revisions/' + urlParams['rev'] + '?supportsAllDrives=true'
991			},
992			mxUtils.bind(this, function(resp)
993			{
994				// Redirects title to originalFilename to
995				// match expected descriptor interface
996				resp.title = resp.originalFilename;
997
998				// Uses ID of file instead of revision ID in descriptor
999				// to avoid a change of the document hash property
1000				resp.headRevisionId = resp.id;
1001				resp.id = id;
1002
1003   				this.getXmlFile(resp, success, error);
1004			}), error);
1005	}
1006	else
1007	{
1008		this.loadDescriptor(id, mxUtils.bind(this, function(resp)
1009		{
1010			try
1011			{
1012				if (this.user != null)
1013				{
1014					var binary = /\.png$/i.test(resp.title);
1015
1016					// Handles .vsdx, .vsd, .vdx, Gliffy and PNG+XML files by creating a temporary file
1017					if (/\.v(dx|sdx?)$/i.test(resp.title) || /\.gliffy$/i.test(resp.title) ||
1018						(!this.ui.useCanvasForExport && binary))
1019					{
1020						var url = resp.downloadUrl;
1021						var headers = {'Authorization': 'Bearer ' + _token};
1022
1023						this.ui.convertFile(url, resp.title, resp.mimeType, this.extension, success, error, null, headers);
1024					}
1025					else
1026					{
1027						// Handles converted realtime files as XML files
1028						if (readXml || readLibrary || resp.mimeType == this.libraryMimeType ||
1029							resp.mimeType == this.xmlMimeType)
1030						{
1031							this.getXmlFile(resp, success, error, true, readLibrary);
1032						}
1033						else
1034						{
1035							this.getXmlFile(resp, success, error);
1036						}
1037					}
1038				}
1039				else
1040				{
1041					error({message: mxResources.get('loggedOut')});
1042				}
1043			}
1044			catch (e)
1045			{
1046				if (error != null)
1047				{
1048					error(e);
1049				}
1050				else
1051				{
1052					throw e;
1053				}
1054			}
1055		}), error);
1056	}
1057};
1058
1059/**
1060 * Returns true if the given mime type is for Google Realtime files.
1061 */
1062DriveClient.prototype.isGoogleRealtimeMimeType = function(mimeType)
1063{
1064	return mimeType != null && mimeType.substring(0, 30) == 'application/vnd.jgraph.mxfile.';
1065};
1066
1067/**
1068 * Checks if the client is authorized and calls the next step. The ignoreMime argument is
1069 * used for import via getFile. Default is false. The optional
1070 * readLibrary argument is used for reading libraries. Default is false.
1071 */
1072DriveClient.prototype.getXmlFile = function(resp, success, error, ignoreMime, readLibrary)
1073{
1074	try
1075	{
1076		var headers = {'Authorization': 'Bearer ' + _token};
1077		var url = resp.downloadUrl;
1078
1079		// Download URL is null if no option to download for viewers
1080		if (url == null)
1081		{
1082			if (error != null)
1083			{
1084				error({message: mxResources.get('exportOptionsDisabledDetails')});
1085			}
1086		}
1087		else
1088		{
1089			var retryCount = 0;
1090
1091			var fn = mxUtils.bind(this, function()
1092			{
1093				// Loads XML to initialize realtime document if realtime is empty
1094				this.ui.editor.loadUrl(url, mxUtils.bind(this, function(data)
1095				{
1096					try
1097					{
1098						if (data == null)
1099						{
1100							// TODO: Optional redirect to legacy if link is for old file
1101							error({message: mxResources.get('invalidOrMissingFile')});
1102						}
1103						else if (resp.mimeType == this.libraryMimeType || readLibrary)
1104						{
1105							if (resp.mimeType == this.libraryMimeType && !readLibrary)
1106							{
1107								error({message: mxResources.get('notADiagramFile')});
1108							}
1109							else
1110							{
1111								success(new DriveLibrary(this.ui, data, resp));
1112							}
1113						}
1114						else
1115						{
1116							var importFile = false;
1117
1118							if (/\.png$/i.test(resp.title))
1119							{
1120								var index = data.lastIndexOf(',');
1121
1122								if (index > 0)
1123								{
1124									var xml = this.ui.extractGraphModelFromPng(data);
1125
1126									if (xml != null && xml.length > 0)
1127									{
1128										data = xml;
1129									}
1130									else
1131									{
1132										// Checks if the file contains XML data which can happen when we insert
1133										// the file and then don't post-process it when loaded into the UI which
1134										// is required for creating the images for .PNG and .SVG files.
1135										try
1136										{
1137											var xml = data.substring(index + 1);
1138											var temp = (window.atob && !mxClient.IS_IE && !mxClient.IS_IE11) ?
1139												atob(xml) : Base64.decode(xml);
1140											var node = this.ui.editor.extractGraphModel(
1141												mxUtils.parseXml(temp).documentElement, true);
1142
1143											if (node == null || node.getElementsByTagName('parsererror').length > 0)
1144											{
1145												importFile = true;
1146											}
1147											else
1148											{
1149												data = temp;
1150											}
1151										}
1152										catch (e)
1153										{
1154											importFile = true;
1155										}
1156									}
1157								}
1158							}
1159							else if (/\.pdf$/i.test(resp.title))
1160							{
1161								var xml = Editor.extractGraphModelFromPdf(data);
1162
1163								if (xml != null && xml.length > 0)
1164								{
1165									importFile = true;
1166									data = xml;
1167								}
1168							}
1169							// Checks for base64 encoded mxfile
1170							else if (data.substring(0, 32) == 'data:image/png;base64,PG14ZmlsZS')
1171							{
1172								var temp = data.substring(22);
1173								data = (window.atob && !mxClient.IS_SF) ? atob(temp) : Base64.decode(temp);
1174							}
1175
1176							if (Graph.fileSupport && new XMLHttpRequest().upload && this.ui.isRemoteFileFormat(data, url))
1177							{
1178								this.ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
1179								{
1180									try
1181									{
1182										if (xhr.readyState == 4)
1183										{
1184											if (xhr.status >= 200 && xhr.status <= 299)
1185											{
1186												success(new LocalFile(this.ui, xhr.responseText, resp.title + this.extension, true));
1187											}
1188											else if (error != null)
1189											{
1190												error({message: mxResources.get('errorLoadingFile')});
1191											}
1192										}
1193									}
1194									catch (e)
1195									{
1196										if (error != null)
1197										{
1198											error(e);
1199										}
1200										else
1201										{
1202											throw e;
1203										}
1204									}
1205								}), resp.title);
1206							}
1207							else
1208							{
1209								success((importFile) ? new LocalFile(this.ui, data, resp.title, true) : new DriveFile(this.ui, data, resp));
1210							}
1211						}
1212					}
1213					catch (e)
1214					{
1215						if (error != null)
1216						{
1217							error(e);
1218						}
1219						else
1220						{
1221							throw e;
1222						}
1223					}
1224				}), mxUtils.bind(this, function(e, req)
1225				{
1226					if (retryCount < this.maxRetries && req != null && req.getStatus() == 403)
1227					{
1228						retryCount++;
1229						var jitter = 1 + 0.1 * (Math.random() - 0.5);
1230						var delay = retryCount * 2 * this.coolOff * jitter;
1231
1232						window.setTimeout(fn, delay);
1233					}
1234					else
1235					{
1236						if (error != null)
1237						{
1238							error(e);
1239						}
1240						else
1241						{
1242							throw e;
1243						}
1244					}
1245				}), ((resp.mimeType != null && resp.mimeType.substring(0, 6) == 'image/' &&
1246					resp.mimeType.substring(0, 9) != 'image/svg')) || /\.png$/i.test(resp.title) ||
1247					/\.jpe?g$/i.test(resp.title) || /\.pdf$/i.test(resp.title),
1248					null, null, null, headers);
1249			});
1250
1251			fn();
1252		}
1253	}
1254	catch (e)
1255	{
1256		if (error != null)
1257		{
1258			error(e);
1259		}
1260		else
1261		{
1262			throw e;
1263		}
1264	}
1265};
1266
1267/**
1268 * Translates this point by the given vector.
1269 *
1270 * @param {number} dx X-coordinate of the translation.
1271 * @param {number} dy Y-coordinate of the translation.
1272 */
1273DriveClient.prototype.saveFile = function(file, revision, success, errFn, noCheck, unloading, overwrite, properties, secret)
1274{
1275	try
1276	{
1277		var retryCount = 0;
1278		file.saveLevel = 1;
1279
1280		var error = mxUtils.bind(this, function(e)
1281		{
1282			if (errFn != null)
1283			{
1284				errFn(e);
1285			}
1286			else
1287			{
1288				throw e;
1289			}
1290
1291			// Logs failed save
1292			try
1293			{
1294				if (!file.isConflict(e))
1295				{
1296					var err = 'sl_' + file.saveLevel + '-error_' +
1297						(file.getErrorMessage(e) || 'unknown');
1298
1299					if (e != null && e.error != null && e.error.code != null)
1300					{
1301						err += '-code_' + e.error.code;
1302					}
1303
1304					EditorUi.logEvent({category: 'ERROR-SAVE-FILE-' + file.getHash() + '-rev_' +
1305						file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate +
1306							'-size_' + file.getSize() + '-mime_' + file.desc.mimeType +
1307						((this.ui.editor.autosave) ? '' : '-nosave') +
1308						((file.isAutosave()) ? '' : '-noauto') +
1309						((file.changeListenerEnabled) ? '' : '-nolisten') +
1310						((file.inConflictState) ? '-conflict' : '') +
1311						((file.invalidChecksum) ? '-invalid' : ''),
1312						action: err, label: ((this.user != null) ? ('user_' + this.user.id) : 'nouser') +
1313						((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')});
1314				}
1315			}
1316			catch (ex)
1317			{
1318				// ignore
1319			}
1320		});
1321
1322		var criticalError = mxUtils.bind(this, function(e)
1323		{
1324			error(e);
1325
1326			try
1327			{
1328				EditorUi.logError(e.message, null, null, e);
1329
1330//				EditorUi.sendReport('Critical error in DriveClient.saveFile ' +
1331//					new Date().toISOString() + ':' +
1332//					'\n\nUserAgent=' + navigator.userAgent +
1333//					'\nAppVersion=' + navigator.appVersion +
1334//					'\nAppName=' + navigator.appName +
1335//					'\nPlatform=' + navigator.platform +
1336//					'\nFile=' + file.desc.id + '.' + file.desc.headRevisionId +
1337//					'\nMime=' + file.desc.mimeType +
1338//					'\nSize=' + file.getSize() +
1339//					'\nUser=' + ((this.user != null) ? this.user.id : 'nouser') +
1340//					 	((file.sync != null) ? '-client_' + file.sync.clientId : '-nosync') +
1341//					'\nSaveLevel=' + file.saveLevel +
1342//					'\nSaveAsPng=' + (this.ui.useCanvasForExport && /(\.png)$/i.test(file.getTitle())) +
1343//					'\nRetryCount=' + retryCount +
1344//					'\nError=' + e +
1345//					'\nMessage=' + e.message +
1346//					'\n\nStack:\n' + e.stack);
1347			}
1348			catch (e)
1349			{
1350				// ignore
1351			}
1352		});
1353
1354		if (file.isEditable() && file.desc != null)
1355		{
1356			var t0 = new Date().getTime();
1357			var etag0 = file.desc.etag;
1358			var mod0 = file.desc.modifiedDate;
1359			var head0 = file.desc.headRevisionId;
1360			var saveAsPng = this.ui.useCanvasForExport && /(\.png)$/i.test(file.getTitle());
1361			noCheck = (noCheck != null) ? noCheck : urlParams['ignoremime'] == '1';
1362
1363			// NOTE: Unloading arg is currently ignored, saving during unload/beforeUnload is not possible using
1364			// asynchronous code, which is needed to create the thumbnail, or asynchronous requests which is the only
1365			// way to execute the gapi request below.
1366			// If no thumbnail is created and noCheck is true (which is always true if unloading is true) in which case
1367			// this code is synchronous, the executeRequest call is reached but the request is still not sent. This is
1368			// true for both, calls from beforeUnload and unload handlers. Note sure how to make the call synchronous
1369			// which is said to fix this when called from beforeUnload.
1370			// However, this would result in a missing thumbnail in most cases so a better solution might be to reduce
1371			// the autosave interval in DriveRealtime, but that would increase the number of requests.
1372			unloading = (unloading != null) ? unloading : false;
1373			var prevDesc = null;
1374			var pinned = false;
1375			var meta =
1376			{
1377				'mimeType': file.desc.mimeType,
1378				'title': file.getTitle()
1379			};
1380
1381			// Overrides old mime type and creates a revision
1382			if (this.isGoogleRealtimeMimeType(meta.mimeType))
1383			{
1384				meta.mimeType = this.xmlMimeType;
1385				prevDesc = file.desc;
1386				revision = true;
1387				pinned = true;
1388			}
1389			// Overrides mime type for unknown file type uploads
1390			else if (meta.mimeType == 'application/octet-stream' ||
1391				(urlParams['override-mime'] == '1' &&
1392				meta.mimeType != this.xmlMimeType))
1393			{
1394				meta.mimeType = this.xmlMimeType;
1395			}
1396
1397			// Adds optional thumbnail to upload request
1398			var doSave = mxUtils.bind(this, function(thumb, thumbMime, keepExisting)
1399			{
1400				try
1401				{
1402					file.saveLevel = 3;
1403
1404					if (file.constructor == DriveFile)
1405					{
1406						if (properties == null)
1407						{
1408							properties = [];
1409						}
1410
1411						// Channel ID appended to file ID for comms
1412						if (file.getChannelId() == null)
1413						{
1414							properties.push({'key': 'channel', 'value': Editor.guid(32)});
1415						}
1416
1417						// Key for encryption of comms
1418						if (file.getChannelKey() == null)
1419						{
1420							properties.push({'key': 'key', 'value': Editor.guid(32)});
1421						}
1422
1423						// Pass to access cache for each etag
1424						properties.push({'key': 'secret', 'value': (secret != null) ? secret : Editor.guid(32)});
1425					}
1426
1427					// Specifies that no thumbnail should be uploaded in which case the existing thumbnail is used
1428					if (!keepExisting)
1429					{
1430						// Uses placeholder thumbnail to replace existing one except when unloading
1431						// in which case the XML is updated but the existing thumbnail is not in order
1432						// to avoid executing asynchronous code and get the XML to the server instead
1433						if (thumb == null && !unloading)
1434						{
1435							thumb = this.placeholderThumbnail;
1436							thumbMime = this.placeholderMimeType;
1437						}
1438
1439						// Adds metadata for thumbnail
1440						if (thumb != null && thumbMime != null)
1441						{
1442							meta.thumbnail =
1443							{
1444								'image': thumb,
1445								'mimeType': thumbMime
1446							};
1447						}
1448					}
1449
1450					var savedData = file.getData();
1451
1452					// Updates saveDelay on drive file
1453					var wrapper = mxUtils.bind(this, function(resp)
1454					{
1455						try
1456						{
1457							file.saveDelay = new Date().getTime() - t0;
1458							file.saveLevel = 11;
1459
1460							if (resp == null)
1461							{
1462								error({message: mxResources.get('errorSavingFile') + ': Empty response'});
1463							}
1464							else
1465							{
1466								// Checks if modified time is in the future and head revision has changed
1467								var delta = new Date(resp.modifiedDate).getTime() - new Date(mod0).getTime();
1468
1469								if (delta <= 0 || etag0 == resp.etag || (revision && head0 == resp.headRevisionId))
1470								{
1471									file.saveLevel = 12;
1472									var reasons = [];
1473
1474									if (delta <= 0)
1475									{
1476										reasons.push('invalid modified time');
1477									}
1478
1479									if (etag0 == resp.etag)
1480									{
1481										reasons.push('stale etag');
1482									}
1483
1484									if (revision && head0 == resp.headRevisionId)
1485									{
1486										reasons.push('stale revision');
1487									}
1488
1489									var temp = reasons.join(', ');
1490									error({message: mxResources.get('errorSavingFile') + ': ' + temp}, resp);
1491
1492									// Logs failed save
1493									try
1494									{
1495										EditorUi.logError('Critical: Error saving to Google Drive ' + file.desc.id,
1496											null, 'from-' + head0 + '.' + mod0 + '-' + this.ui.hashValue(etag0) +
1497											'-to-' + resp.headRevisionId + '.' + resp.modifiedDate + '-' +
1498											this.ui.hashValue(resp.etag) + ((temp.length > 0) ? '-errors-' + temp : ''),
1499											'user-' + ((this.user != null) ? this.user.id : 'nouser') +
1500										 	((file.sync != null) ? '-client_' + file.sync.clientId : '-nosync'));
1501									}
1502									catch (e)
1503									{
1504										// ignore
1505									}
1506								}
1507								else
1508								{
1509									file.saveLevel = null;
1510							    	success(resp, savedData);
1511
1512							    	if (prevDesc != null)
1513									{
1514							    		// Pins previous revision
1515										this.executeRequest({
1516											url: '/files/' + prevDesc.id + '/revisions/' + prevDesc.headRevisionId + '?supportsAllDrives=true'
1517										}, mxUtils.bind(this, mxUtils.bind(this, function(resp)
1518										{
1519											resp.pinned = true;
1520
1521											this.executeRequest({
1522												url: '/files/' + prevDesc.id + '/revisions/' + prevDesc.headRevisionId,
1523												method: 'PUT',
1524												params: resp
1525											});
1526										})));
1527
1528										// Logs conversion
1529										try
1530										{
1531											EditorUi.logEvent({category: file.convertedFrom + '-CONVERT-FILE-' + file.getHash(),
1532												action: 'from_' + prevDesc.id + '.' + prevDesc.headRevisionId +
1533												'-to_' + file.desc.id + '.' + file.desc.headRevisionId,
1534												label: (this.user != null) ? ('user_' + this.user.id) : 'nouser' +
1535												((file.sync != null) ? '-client_' + file.sync.clientId : 'nosync')});
1536										}
1537										catch (e)
1538										{
1539											// ignore
1540										}
1541									}
1542
1543									// Logs successful save
1544//									try
1545//									{
1546//										EditorUi.logEvent({category: 'SUCCESS-SAVE-FILE-' + file.getHash() +
1547//											'-rev0_' + head0 + '-mod0_' + mod0,
1548//											action: 'rev-' + resp.headRevisionId +
1549//											'-mod_' + resp.modifiedDate + '-size_' + file.getSize() +
1550//											'-mime_' + file.desc.mimeType +
1551//											((this.ui.editor.autosave) ? '' : '-nosave') +
1552//											((file.isAutosave()) ? '' : '-noauto') +
1553//											((file.changeListenerEnabled) ? '' : '-nolisten') +
1554//											((file.inConflictState) ? '-conflict' : '') +
1555//											((file.invalidChecksum) ? '-invalid' : ''),
1556//											label: ((this.user != null) ? ('user_' + this.user.id) : 'nouser') +
1557//											((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')});
1558//									}
1559//									catch (e)
1560//									{
1561//										// ignore
1562//									}
1563								}
1564							}
1565						}
1566						catch (e)
1567						{
1568							criticalError(e);
1569						}
1570					});
1571
1572					var doExecuteRequest = mxUtils.bind(this, function(data, binary)
1573					{
1574						file.saveLevel = 4;
1575
1576						try
1577						{
1578							if (properties != null)
1579							{
1580								meta.properties = properties;
1581							}
1582
1583							// Used to check if file was changed externally
1584							var etag = (!overwrite && file.constructor == DriveFile &&
1585								(DrawioFile.SYNC == 'manual' || DrawioFile.SYNC == 'auto')) ?
1586								file.getCurrentEtag() : null;
1587
1588							var doExecuteSave = mxUtils.bind(this, function(realOverwrite)
1589							{
1590								file.saveLevel = 5;
1591
1592								try
1593								{
1594									var unknown = file.desc.mimeType != this.xmlMimeType && file.desc.mimeType != this.mimeType &&
1595										file.desc.mimeType != this.libraryMimeType;
1596									var acceptResponse = true;
1597									var timeoutThread = null;
1598
1599									// Allow for re-auth flow with 5x timeout
1600									try
1601									{
1602										timeoutThread = window.setTimeout(mxUtils.bind(this, function()
1603										{
1604											acceptResponse = false;
1605											error({code: App.ERROR_TIMEOUT});
1606										}), 5 * this.ui.timeout);
1607									}
1608									catch (e)
1609									{
1610										// Ignore window closed
1611									}
1612
1613									this.executeRequest(this.createUploadRequest(file.getId(), meta,
1614										data, revision || realOverwrite || unknown, binary,
1615										(realOverwrite) ? null : etag, pinned), mxUtils.bind(this, function(resp)
1616									{
1617										window.clearTimeout(timeoutThread);
1618
1619										if (acceptResponse)
1620										{
1621											wrapper(resp);
1622										}
1623									}), mxUtils.bind(this, function(err)
1624									{
1625										window.clearTimeout(timeoutThread);
1626
1627										if (acceptResponse)
1628										{
1629											file.saveLevel = 6;
1630
1631											try
1632											{
1633												if (!file.isConflict(err))
1634												{
1635													error(err);
1636												}
1637												else
1638												{
1639													// Workaround for correct etag and Google always returns 412 conflict error (stale etag)
1640													this.executeRequest({
1641														url: '/files/' + file.getId() + '?supportsAllDrives=true&fields=' + this.catchupFields
1642													},
1643													mxUtils.bind(this, function(resp)
1644													{
1645														file.saveLevel = 7;
1646
1647														try
1648														{
1649															// Stale etag detected, retry with delay
1650															if (resp != null && resp.etag == etag)
1651															{
1652																if (retryCount < this.staleEtagMaxRetries)
1653																{
1654																	retryCount++;
1655																	var jitter = 1 + 0.1 * (Math.random() - 0.5);
1656																	var delay = retryCount * 2 * this.coolOff * jitter;
1657																	window.setTimeout(executeSave, delay);
1658
1659																	if (urlParams['test'] == '1')
1660																	{
1661																		EditorUi.debug('DriveClient: Stale Etag Detected',
1662																			'retry', retryCount, 'delay', delay);
1663																	}
1664																}
1665																else
1666																{
1667																	executeSave(true);
1668
1669																	// Logs overwrite
1670																	try
1671																	{
1672																		EditorUi.logEvent({category: 'STALE-ETAG-SAVE-FILE-' + file.getHash(),
1673																			action: 'rev_' + file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate +
1674																				'-size_' + file.getSize() + '-mime_' + file.desc.mimeType +
1675																			((this.ui.editor.autosave) ? '' : '-nosave') +
1676																			((file.isAutosave()) ? '' : '-noauto') +
1677																			((file.changeListenerEnabled) ? '' : '-nolisten') +
1678																			((file.inConflictState) ? '-conflict' : '') +
1679																			((file.invalidChecksum) ? '-invalid' : ''),
1680																			label: ((this.user != null) ? ('user_' + this.user.id) : 'nouser') +
1681																			((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')});
1682																	}
1683																	catch (e)
1684																	{
1685																		// ignore
1686																	}
1687																}
1688															}
1689															else
1690															{
1691
1692																if (urlParams['test'] == '1' && resp.headRevisionId == head0)
1693																{
1694																	EditorUi.debug('DriveClient: Remote Etag Changed',
1695																		'local', etag, 'remote', resp.etag,
1696																		'rev', file.desc.headRevisionId,
1697																		'response', [resp], 'file', [file]);
1698																}
1699
1700																error(err, resp);
1701															}
1702														}
1703														catch (e)
1704														{
1705															criticalError(e);
1706														}
1707													}), mxUtils.bind(this, function()
1708													{
1709														error(err);
1710													}));
1711												}
1712											}
1713											catch (e)
1714											{
1715												criticalError(e);
1716											}
1717										}
1718									}));
1719								}
1720								catch (e)
1721								{
1722									criticalError(e);
1723								}
1724							});
1725
1726							// Workaround for Google returning the wrong etag after file save is to
1727							// update the etag before save and check if the headRevisionId changed
1728							var executeSave = mxUtils.bind(this, function(realOverwrite)
1729							{
1730								file.saveLevel = 9;
1731
1732								if (realOverwrite || etag == null)
1733								{
1734									doExecuteSave(realOverwrite);
1735								}
1736								else
1737								{
1738									var acceptResponse = true;
1739									var timeoutThread = null;
1740
1741									// Allow for re-auth flow with 3x timeout
1742									try
1743									{
1744										timeoutThread = window.setTimeout(mxUtils.bind(this, function()
1745										{
1746											acceptResponse = false;
1747											error({code: App.ERROR_TIMEOUT});
1748										}), 3 * this.ui.timeout);
1749									}
1750									catch (e)
1751									{
1752										// Ignore window closed
1753									}
1754
1755									this.executeRequest({
1756										url: '/files/' + file.getId() + '?supportsAllDrives=true&fields=' + this.catchupFields
1757									},
1758									mxUtils.bind(this, function(desc2)
1759									{
1760										window.clearTimeout(timeoutThread);
1761
1762										if (acceptResponse)
1763										{
1764											file.saveLevel = 10;
1765
1766											try
1767											{
1768												// Checks head revision ID and updates etag or returns conflict
1769												if (desc2 != null && desc2.headRevisionId == head0)
1770												{
1771													if (urlParams['test'] == '1' && etag != desc2.etag)
1772													{
1773														EditorUi.debug('DriveClient: Preflight Etag Update',
1774															'from', etag, 'to', desc2.etag,
1775															'rev', file.desc.headRevisionId,
1776															'response', [desc2], 'file', [file]);
1777													}
1778
1779													etag = desc2.etag;
1780													doExecuteSave(realOverwrite);
1781												}
1782												else
1783												{
1784													error({error: {code: 412}}, desc2);
1785												}
1786											}
1787											catch (e)
1788											{
1789												criticalError(e);
1790											}
1791										}
1792									}), mxUtils.bind(this, function(err)
1793									{
1794										// Simulated
1795										window.clearTimeout(timeoutThread);
1796
1797										if (acceptResponse)
1798										{
1799											file.saveLevel = 11;
1800											error(err);
1801										}
1802									}));
1803								}
1804							});
1805
1806							// Uses saved PNG data for thumbnail
1807							if (saveAsPng && thumb == null)
1808							{
1809								file.saveLevel = 8;
1810								var img = new Image();
1811
1812								img.onload = mxUtils.bind(this, function()
1813								{
1814							    	try
1815							    	{
1816										var s = this.thumbnailWidth / img.width;
1817
1818										var canvas = document.createElement('canvas');
1819									    canvas.width = this.thumbnailWidth;
1820									    canvas.height = Math.floor(img.height * s);
1821
1822									    var ctx = canvas.getContext('2d');
1823									    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
1824
1825									    var temp = canvas.toDataURL();
1826									    temp = temp.substring(temp.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_');
1827
1828									    meta.thumbnail =
1829										{
1830											'image': temp,
1831											'mimeType': 'image/png'
1832										};
1833
1834									    executeSave(false);
1835							    	}
1836							    	catch (e)
1837							    	{
1838							    		try
1839							    		{
1840							    			executeSave(false)
1841							    		}
1842										catch (e2)
1843										{
1844											criticalError(e2);
1845										}
1846							    	}
1847								});
1848
1849								img.src = 'data:image/png;base64,' + data;
1850							}
1851							else
1852							{
1853								executeSave(false);
1854							}
1855						}
1856						catch (e)
1857						{
1858							criticalError(e);
1859						}
1860					});
1861
1862					if (saveAsPng)
1863					{
1864						var p = this.ui.getPngFileProperties(this.ui.fileNode);
1865
1866						this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
1867						{
1868							doExecuteRequest(data, true);
1869						}), error, (this.ui.getCurrentFile() != file) ?
1870							savedData : null, p.scale, p.border);
1871					}
1872					else
1873					{
1874						doExecuteRequest(savedData, false);
1875					}
1876				}
1877				catch (e)
1878				{
1879					criticalError(e);
1880				}
1881			});
1882
1883			// Indirection to generate thumbnails if enabled and supported
1884			// (required because generation of thumbnails is asynchronous)
1885			try
1886			{
1887				file.saveLevel = 2;
1888
1889				// NOTE: getThumbnail is asynchronous and returns false if no thumbnails can be created
1890				if (unloading || saveAsPng || file.constructor == DriveLibrary || !this.enableThumbnails || urlParams['thumb'] == '0' ||
1891					(meta.mimeType != null && meta.mimeType.substring(0, 29) != 'application/vnd.jgraph.mxfile') ||
1892					!this.ui.getThumbnail(this.thumbnailWidth, mxUtils.bind(this, function(canvas)
1893					{
1894						// Callback for getThumbnail
1895						try
1896						{
1897							var thumb = null;
1898
1899							try
1900							{
1901								if (canvas != null)
1902								{
1903									// Security errors are possible
1904									thumb = canvas.toDataURL('image/png');
1905								}
1906
1907								// Maximum thumbnail size is 2MB
1908								if (thumb != null)
1909								{
1910									if (thumb.length > this.maxThumbnailSize)
1911									{
1912										thumb = null;
1913									}
1914									else
1915									{
1916										// Converts base64 data into required format for Drive (base64url with no prefix)
1917										thumb = thumb.substring(thumb.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_');
1918									}
1919								}
1920							}
1921							catch (e)
1922							{
1923								thumb = null;
1924							}
1925
1926							doSave(thumb, 'image/png');
1927						}
1928						catch (e)
1929						{
1930							criticalError(e);
1931						}
1932					})))
1933				{
1934					// If-branch
1935					doSave(null, null, file.constructor != DriveLibrary);
1936				}
1937			}
1938			catch (e)
1939			{
1940				criticalError(e);
1941			}
1942		}
1943		else
1944		{
1945			this.ui.editor.graph.reset();
1946			error({message: mxResources.get('readOnly')});
1947		}
1948	}
1949	catch (e)
1950	{
1951		criticalError(e);
1952	}
1953};
1954
1955/**
1956 * Translates this point by the given vector.
1957 *
1958 * @param {number} dx X-coordinate of the translation.
1959 * @param {number} dy Y-coordinate of the translation.
1960 */
1961DriveClient.prototype.insertFile = function(title, data, folderId, success, error, mimeType, binary)
1962{
1963	mimeType = (mimeType != null) ? mimeType : this.xmlMimeType;
1964
1965	var metadata =
1966	{
1967		'mimeType': mimeType,
1968		'title': title
1969	};
1970
1971	if (folderId != null)
1972	{
1973		metadata.parents = [{'kind': 'drive#fileLink', 'id': folderId}];
1974	}
1975
1976	// NOTE: Cannot create thumbnail on insert since no ui has no current file
1977	this.executeRequest(this.createUploadRequest(null, metadata, data, false, binary), mxUtils.bind(this, function(resp)
1978	{
1979		if (mimeType == this.libraryMimeType)
1980		{
1981			success(new DriveLibrary(this.ui, data, resp));
1982		}
1983		else if (resp == false)
1984		{
1985			if (error != null)
1986			{
1987				error({message: mxResources.get('errorSavingFile')});
1988			}
1989		}
1990		else
1991		{
1992			success(new DriveFile(this.ui, data, resp));
1993		}
1994	}), error);
1995};
1996
1997/**
1998 * Translates this point by the given vector.
1999 *
2000 * @param {number} dx X-coordinate of the translation.
2001 * @param {number} dy Y-coordinate of the translation.
2002 */
2003DriveClient.prototype.createUploadRequest = function(id, metadata, data, revision, binary, etag, pinned)
2004{
2005	binary = (binary != null) ? binary : false;
2006	var bd = '-------314159265358979323846';
2007	var delim = '\r\n--' + bd + '\r\n';
2008	var close = '\r\n--' + bd + '--';
2009	var ctype = 'application/octect-stream';
2010
2011	var headers = {'Content-Type' : 'multipart/mixed; boundary="' + bd + '"'};
2012
2013	if (etag != null)
2014	{
2015		headers['If-Match'] = etag;
2016	}
2017
2018	var reqObj =
2019	{
2020		'fullUrl': 'https://content.googleapis.com/upload/drive/v2/files' + (id != null ? '/' + id : '') +
2021			'?uploadType=multipart&supportsAllDrives=true&enforceSingleParent=true&fields=' + this.allFields,
2022		'method': (id != null) ? 'PUT' : 'POST',
2023		'headers': headers,
2024		'params': delim + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delim +
2025			'Content-Type: ' + ctype + '\r\n' + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' +
2026			((data != null) ? ((binary) ? data : ((window.btoa && !mxClient.IS_IE && !mxClient.IS_IE11) ?
2027				Graph.base64EncodeUnicode(data) : Base64.encode(data))) : '') + close
2028	}
2029
2030	if (!revision)
2031	{
2032		reqObj.fullUrl += '&newRevision=false';
2033	}
2034
2035	if (pinned)
2036	{
2037		reqObj.fullUrl += '&pinned=true';
2038	}
2039
2040	return reqObj;
2041};
2042
2043/**
2044 * Translates this point by the given vector.
2045 *
2046 * @param {number} dx X-coordinate of the translation.
2047 * @param {number} dy Y-coordinate of the translation.
2048 */
2049DriveClient.prototype.createLinkPicker = function()
2050{
2051	var name = 'linkPicker';
2052	var picker = pickers[name];
2053
2054	if (picker == null || pickers[name + 'Token'] != _token)
2055	{
2056		pickers[name + 'Token'] = _token;
2057
2058		var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
2059			.setParent('root')
2060			.setIncludeFolders(true)
2061			.setSelectFolderEnabled(true);
2062		var view2 = new google.picker.DocsView()
2063			.setIncludeFolders(true)
2064			.setSelectFolderEnabled(true);
2065		var view21 = new google.picker.DocsView()
2066			.setIncludeFolders(true)
2067			.setEnableDrives(true)
2068			.setSelectFolderEnabled(true);
2069		picker = new google.picker.PickerBuilder()
2070			.setAppId(this.appId)
2071			.setLocale(mxLanguage)
2072			.setOAuthToken(pickers[name + 'Token'])
2073			.enableFeature(google.picker.Feature.SUPPORT_DRIVES)
2074			.addView(view)
2075			.addView(view2)
2076			.addView(view21)
2077			.addView(google.picker.ViewId.RECENTLY_PICKED);
2078	}
2079
2080	return picker;
2081};
2082
2083/**
2084 * Translates this point by the given vector.
2085 *
2086 * @param {number} dx X-coordinate of the translation.
2087 * @param {number} dy Y-coordinate of the translation.
2088 */
2089DriveClient.prototype.pickFile = function(fn, acceptAllFiles, cancelFn)
2090{
2091	this.filePickerCallback = (fn != null) ? fn : mxUtils.bind(this, function(id)
2092	{
2093		this.ui.loadFile('G' + id);
2094	});
2095
2096	this.filePicked = mxUtils.bind(this, function(data)
2097	{
2098		if (data.action == google.picker.Action.PICKED)
2099		{
2100    		this.filePickerCallback(data.docs[0].id, data.docs[0]);
2101		}
2102	});
2103
2104	if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
2105	{
2106		this.execute(mxUtils.bind(this, function()
2107		{
2108			try
2109			{
2110				this.ui.spinner.stop();
2111
2112				// Reuses picker as long as token doesn't change.
2113				var name = (acceptAllFiles) ? 'genericPicker' : 'filePicker';
2114
2115				// Click on background closes dialog as workaround for blocking dialog
2116				// states such as 401 where the dialog cannot be closed and blocks UI
2117				var exit = mxUtils.bind(this, function(evt)
2118				{
2119					// Workaround for click from appIcon on second call
2120					if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
2121					{
2122						mxEvent.removeListener(document, 'click', exit);
2123						this[name].setVisible(false);
2124
2125						if (cancelFn)
2126						{
2127							cancelFn();
2128						}
2129					}
2130				});
2131
2132				if (pickers[name] == null || pickers[name + 'Token'] != _token)
2133				{
2134					// FIXME: Dispose not working
2135	//				if (pickers[name] != null)
2136	//				{
2137	//					console.log(name, pickers[name]);
2138	//					pickers[name].dispose();
2139	//				}
2140
2141					pickers[name + 'Token'] = _token;
2142
2143					// Pseudo-hierarchical directory view, see
2144					// https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
2145					var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
2146				        	.setParent('root')
2147				        	.setIncludeFolders(true);
2148
2149					var view2 = new google.picker.DocsView()
2150						.setIncludeFolders(true);
2151
2152					var view3 = new google.picker.DocsView()
2153						.setEnableDrives(true)
2154						.setIncludeFolders(true);
2155
2156					var view4 = new google.picker.DocsUploadView()
2157						.setIncludeFolders(true);
2158
2159					if (!acceptAllFiles)
2160					{
2161						view.setMimeTypes(this.mimeTypes);
2162						view2.setMimeTypes(this.mimeTypes);
2163						view3.setMimeTypes(this.mimeTypes);
2164					}
2165					else
2166					{
2167						view.setMimeTypes('*/*');
2168						view2.setMimeTypes('*/*');
2169						view3.setMimeTypes('*/*');
2170					}
2171
2172					pickers[name] = new google.picker.PickerBuilder()
2173				        .setOAuthToken(pickers[name + 'Token'])
2174				        .setLocale(mxLanguage)
2175				        .setAppId(this.appId)
2176				        .enableFeature(google.picker.Feature.SUPPORT_DRIVES)
2177				        .addView(view)
2178				        .addView(view2)
2179				        .addView(view3)
2180				        .addView(google.picker.ViewId.RECENTLY_PICKED)
2181				        .addView(view4);
2182
2183					if (urlParams['gPickerSize'])
2184					{
2185						var cSize = urlParams['gPickerSize'].split(',');
2186						pickers[name] = pickers[name].setSize(cSize[0], cSize[1]);
2187					}
2188
2189					if (urlParams['topBaseUrl'])
2190				    {
2191						pickers[name] = pickers[name].setOrigin(decodeURIComponent(urlParams['topBaseUrl']));
2192					}
2193
2194					pickers[name] = pickers[name].setCallback(mxUtils.bind(this, function(data)
2195				        {
2196				        	if (data.action == google.picker.Action.PICKED ||
2197				        		data.action == google.picker.Action.CANCEL)
2198				        	{
2199				        		mxEvent.removeListener(document, 'click', exit);
2200
2201								if (cancelFn && data.action == google.picker.Action.CANCEL)
2202								{
2203									cancelFn();
2204								}
2205				        	}
2206
2207				        	if (data.action == google.picker.Action.PICKED)
2208				    		{
2209				        		this.filePicked(data);
2210				    		}
2211				        })).build();
2212				}
2213
2214				mxEvent.addListener(document, 'click', exit);
2215				pickers[name].setVisible(true);
2216			}
2217			catch (e)
2218			{
2219				this.ui.spinner.stop();
2220				this.ui.handleError(e);
2221			}
2222		}));
2223	}
2224};
2225
2226/**
2227 * Translates this point by the given vector.
2228 *
2229 * @param {number} dx X-coordinate of the translation.
2230 * @param {number} dy Y-coordinate of the translation.
2231 */
2232DriveClient.prototype.pickFolder = function(fn, force)
2233{
2234	this.folderPickerCallback = fn;
2235
2236	// Picker is initialized once and points to this function
2237	// which is overridden each time to the picker is shown
2238	var showPicker = mxUtils.bind(this, function()
2239	{
2240		try
2241		{
2242			if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
2243			{
2244				this.execute(mxUtils.bind(this, function()
2245				{
2246					try
2247					{
2248						this.ui.spinner.stop();
2249
2250						// Reuses picker as long as token doesn't change.
2251						var name = 'folderPicker';
2252
2253						// Click on background closes dialog as workaround for blocking dialog
2254						// states such as 401 where the dialog cannot be closed and blocks UI
2255						var exit = mxUtils.bind(this, function(evt)
2256						{
2257							// Workaround for click from appIcon on second call
2258							if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
2259							{
2260								mxEvent.removeListener(document, 'click', exit);
2261								pickers[name].setVisible(false);
2262							}
2263						});
2264
2265						if (pickers[name] == null || pickers[name + 'Token'] != _token)
2266						{
2267							// FIXME: Dispose not working
2268			//				if (pickers[name] != null)
2269			//				{
2270			//					console.log(name, pickers[name]);
2271			//					pickers[name].dispose();
2272			//				}
2273
2274							pickers[name + 'Token'] = _token;
2275
2276							// Pseudo-hierarchical directory view, see
2277							// https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
2278							var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
2279								.setParent('root')
2280								.setIncludeFolders(true)
2281								.setSelectFolderEnabled(true)
2282					        		.setMimeTypes('application/vnd.google-apps.folder');
2283
2284							var view2 = new google.picker.DocsView()
2285								.setIncludeFolders(true)
2286								.setSelectFolderEnabled(true)
2287								.setMimeTypes('application/vnd.google-apps.folder');
2288
2289							var view3 = new google.picker.DocsView()
2290								.setIncludeFolders(true)
2291								.setEnableDrives(true)
2292								.setSelectFolderEnabled(true)
2293								.setMimeTypes('application/vnd.google-apps.folder');
2294
2295							pickers[name] = new google.picker.PickerBuilder()
2296								.setSelectableMimeTypes('application/vnd.google-apps.folder')
2297						        .setOAuthToken(pickers[name + 'Token'])
2298						        .setLocale(mxLanguage)
2299						        .setAppId(this.appId)
2300							    .enableFeature(google.picker.Feature.SUPPORT_DRIVES)
2301						        .addView(view)
2302						        .addView(view2)
2303						        .addView(view3)
2304						        .addView(google.picker.ViewId.RECENTLY_PICKED)
2305						        .setTitle(mxResources.get('pickFolder'));
2306
2307							if (urlParams['gPickerSize'])
2308							{
2309								var cSize = urlParams['gPickerSize'].split(',');
2310								pickers[name] = pickers[name].setSize(cSize[0], cSize[1]);
2311							}
2312
2313							if (urlParams['topBaseUrl'])
2314						    {
2315								pickers[name] = pickers[name].setOrigin(decodeURIComponent(urlParams['topBaseUrl']));
2316							}
2317
2318					        pickers[name] = pickers[name].setCallback(mxUtils.bind(this, function(data)
2319						        {
2320						        	if (data.action == google.picker.Action.PICKED ||
2321						        		data.action == google.picker.Action.CANCEL)
2322						        	{
2323						        		mxEvent.removeListener(document, 'click', exit);
2324						        	}
2325
2326					        		this.folderPickerCallback(data);
2327						        })).build();
2328						}
2329
2330						mxEvent.addListener(document, 'click', exit);
2331						pickers[name].setVisible(true);
2332					}
2333					catch (e)
2334					{
2335						this.ui.spinner.stop();
2336						this.ui.handleError(e);
2337					}
2338				}));
2339			}
2340		}
2341		catch (e)
2342		{
2343			this.ui.handleError(e);
2344		}
2345	});
2346
2347	if (force)
2348	{
2349		showPicker();
2350	}
2351	else
2352	{
2353		this.ui.confirm(mxResources.get('useRootFolder'), mxUtils.bind(this, function()
2354		{
2355			this.folderPickerCallback({action: google.picker.Action.PICKED,
2356				docs: [{type: 'folder', id: 'root'}]});
2357		}), mxUtils.bind(this, function()
2358		{
2359			showPicker();
2360		}), mxResources.get('yes'), mxResources.get('noPickFolder') + '...', true);
2361	}
2362};
2363
2364/**
2365 * Translates this point by the given vector.
2366 *
2367 * @param {number} dx X-coordinate of the translation.
2368 * @param {number} dy Y-coordinate of the translation.
2369 */
2370DriveClient.prototype.pickLibrary = function(fn)
2371{
2372	this.filePickerCallback = fn;
2373
2374	this.filePicked = mxUtils.bind(this, function(data)
2375	{
2376		if (data.action == google.picker.Action.PICKED)
2377		{
2378    		this.filePickerCallback(data.docs[0].id);
2379		}
2380    	else if (data.action == google.picker.Action.CANCEL && this.ui.getCurrentFile() == null)
2381		{
2382    		this.ui.showSplash();
2383		}
2384	});
2385
2386	if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
2387	{
2388		this.execute(mxUtils.bind(this, function()
2389		{
2390			try
2391			{
2392				this.ui.spinner.stop();
2393
2394				// Click on background closes dialog as workaround for blocking dialog
2395				// states such as 401 where the dialog cannot be closed and blocks UI
2396				var exit = mxUtils.bind(this, function(evt)
2397				{
2398					// Workaround for click from appIcon on second call
2399					if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
2400					{
2401						mxEvent.removeListener(document, 'click', exit);
2402						pickers.libraryPicker.setVisible(false);
2403					}
2404				});
2405
2406				// Reuses picker as long as token doesn't change
2407
2408				if (pickers.libraryPicker == null || pickers.libraryPickerToken != _token)
2409				{
2410					// FIXME: Dispose not working
2411	//				if (pickers[name] != null)
2412	//				{
2413	//					console.log(name, pickers[name]);
2414	//					pickers[name].dispose();
2415	//				}
2416
2417					pickers.libraryPickerToken = _token;
2418
2419					// Pseudo-hierarchical directory view, see
2420					// https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
2421					var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
2422				        	.setParent('root')
2423				        	.setIncludeFolders(true)
2424						.setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
2425
2426					var view2 = new google.picker.DocsView()
2427			        		.setIncludeFolders(true)
2428						.setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
2429
2430					var view3 = new google.picker.DocsView()
2431						.setEnableDrives(true)
2432						.setIncludeFolders(true)
2433						.setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
2434
2435					var view4 = new google.picker.DocsUploadView()
2436						.setIncludeFolders(true);
2437
2438				    pickers.libraryPicker = new google.picker.PickerBuilder()
2439				        .setOAuthToken(pickers.libraryPickerToken)
2440				        .setLocale(mxLanguage)
2441				        .setAppId(this.appId)
2442				        .enableFeature(google.picker.Feature.SUPPORT_DRIVES)
2443				        .addView(view)
2444				        .addView(view2)
2445				        .addView(view3)
2446				        .addView(google.picker.ViewId.RECENTLY_PICKED)
2447				        .addView(view4);
2448
2449					if (urlParams['gPickerSize'])
2450					{
2451						var cSize = urlParams['gPickerSize'].split(',');
2452						pickers.libraryPicker = pickers.libraryPicker.setSize(cSize[0], cSize[1]);
2453					}
2454
2455					if (urlParams['topBaseUrl'])
2456				    {
2457						pickers.libraryPicker = pickers.libraryPicker.setOrigin(decodeURIComponent(urlParams['topBaseUrl']));
2458					}
2459
2460				    pickers.libraryPicker = pickers.libraryPicker.setCallback(mxUtils.bind(this, function(data)
2461				        {
2462					        	if (data.action == google.picker.Action.PICKED ||
2463					        		data.action == google.picker.Action.CANCEL)
2464					        	{
2465					        		mxEvent.removeListener(document, 'click', exit);
2466					        	}
2467
2468					        	if (data.action == google.picker.Action.PICKED)
2469					    		{
2470					        		this.filePicked(data);
2471					    		}
2472				        })).build();
2473				}
2474
2475				mxEvent.addListener(document, 'click', exit);
2476				pickers.libraryPicker.setVisible(true);
2477			}
2478			catch (e)
2479			{
2480				this.ui.spinner.stop();
2481				this.ui.handleError(e);
2482			}
2483		}));
2484	}
2485};
2486
2487/**
2488 * Translates this point by the given vector.
2489 *
2490 * @param {number} dx X-coordinate of the translation.
2491 * @param {number} dy Y-coordinate of the translation.
2492 */
2493DriveClient.prototype.showPermissions = function(id)
2494{
2495	var fallback = mxUtils.bind(this, function()
2496	{
2497		var dlg = new ConfirmDialog(this.ui, mxResources.get('googleSharingNotAvailable'), mxUtils.bind(this, function()
2498		{
2499			this.ui.editor.graph.openLink('https://drive.google.com/open?id=' + id);
2500		}), null, mxResources.get('open'), null, null, null, null, IMAGE_PATH + '/google-share.png');
2501		this.ui.showDialog(dlg.container, 360, 190, true, true);
2502		dlg.init();
2503	});
2504
2505	if (this.sharingFailed)
2506	{
2507		fallback();
2508	}
2509	else
2510	{
2511		this.checkToken(mxUtils.bind(this, function()
2512		{
2513			try
2514			{
2515				var shareClient = new gapi.drive.share.ShareClient(this.appId);
2516				shareClient.setOAuthToken(_token);
2517				shareClient.setItemIds([id]);
2518				shareClient.showSettingsDialog();
2519
2520				// Workaround for https://stackoverflow.com/questions/54753169 is to check
2521				// if "sharing is unavailable" is showing and invoke a fallback dialog
2522				if ('MutationObserver' in window)
2523				{
2524					if (this.sharingObserver != null)
2525					{
2526						this.sharingObserver.disconnect();
2527						this.sharingObserver = null;
2528					}
2529
2530					// Tries again even if observer was still around as the user may have
2531					// closed the dialog while waiting. TODO: Find condition to disconnect
2532					// observer when dialog is closed (use removedNodes?).
2533					this.sharingObserver = new MutationObserver(mxUtils.bind(this, function(mutations)
2534					{
2535						var done = false;
2536
2537						for (var i = 0; i < mutations.length; i++)
2538						{
2539							for (var j = 0; j < mutations[i].addedNodes.length; j++)
2540							{
2541								var child = mutations[i].addedNodes[j];
2542
2543								if (child.nodeName == 'BUTTON' && child.getAttribute('name') == 'ok' &&
2544					        		child.parentNode != null && child.parentNode.parentNode != null &&
2545					        		child.parentNode.parentNode.getAttribute('role') == 'dialog')
2546					        	{
2547				        			this.sharingFailed = true;
2548					        		child.click();
2549				        			fallback();
2550				        			done = true;
2551					        	}
2552					        	else if (child.nodeName == 'DIV' && child.className == 'shr-q-shr-r-shr-xb')
2553					        	{
2554					        		done = true;
2555					        	}
2556					        }
2557					    }
2558
2559						if (done)
2560						{
2561			        		this.sharingObserver.disconnect();
2562		        			this.sharingObserver = null;
2563						}
2564
2565					}));
2566
2567					this.sharingObserver.observe(document, {childList: true, subtree: true});
2568				}
2569			}
2570			catch (e)
2571			{
2572				this.ui.handleError(e);
2573			}
2574		}));
2575	}
2576};
2577
2578DriveClient.prototype.clearPersistentToken = function()
2579{
2580	//Since we have multiple accounts now, full deletion is not possible
2581	var authInfo = JSON.parse(this.getPersistentToken(true)) || {};
2582
2583	//Delete current user info
2584	delete authInfo.current;
2585	delete authInfo[this.userId];
2586
2587	//Set the next user as current
2588	for (var id in authInfo)
2589	{
2590		authInfo.current = {userId: id, expires: 0}; //An expired token
2591		break;
2592	}
2593
2594	DrawioClient.prototype.setPersistentToken.call(this, JSON.stringify(authInfo));
2595};
2596
2597DriveClient.prototype.setPersistentToken = function(userAuthInfo, sessionOnly)
2598{
2599	var authInfo = JSON.parse(this.getPersistentToken(true)) || {};
2600
2601	userAuthInfo.userId = this.userId;
2602	authInfo.current = userAuthInfo;
2603	authInfo[this.userId] = {
2604		user: this.user
2605	};
2606
2607	DrawioClient.prototype.setPersistentToken.call(this, JSON.stringify(authInfo), sessionOnly);
2608};
2609
2610})();