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;
11
12window.OneDriveClient = function(editorUi, isExtAuth, inlinePicker, noLogout)
13{
14	if (isExtAuth == null && window.urlParams != null && window.urlParams['extAuth'] == '1')
15	{
16		isExtAuth = true;
17	}
18
19	if (inlinePicker == null) //Use inline picker as default
20	{
21		inlinePicker = window.Editor != null? Editor.oneDriveInlinePicker : true;
22	}
23
24	if (noLogout == null && window.urlParams != null && window.urlParams['noLogoutOD'] == '1')
25	{
26		noLogout = true;
27	}
28
29	DrawioClient.call(this, editorUi, isExtAuth? 'oneDriveExtAuthInfo' : 'oneDriveAuthInfo');
30
31	this.isExtAuth = isExtAuth;
32	this.inlinePicker = inlinePicker;
33	this.noLogout = noLogout;
34	var authInfo = JSON.parse(this.token);
35
36	if (authInfo != null)
37	{
38		this.endpointHint = authInfo.endpointHint != null ? authInfo.endpointHint.replace('/Documents', '/_layouts/15/onedrive.aspx') : authInfo.endpointHint;
39	}
40};
41
42// Extends DrawioClient
43mxUtils.extend(OneDriveClient, DrawioClient);
44
45/**
46 * Specifies if thumbnails should be enabled. Default is true.
47 * LATER: If thumbnails are disabled, make sure to replace the
48 * existing thumbnail with the placeholder only once.
49 */
50OneDriveClient.prototype.clientId = window.DRAWIO_MSGRAPH_CLIENT_ID || ((window.location.hostname == 'test.draw.io') ?
51	'2e598409-107f-4b59-89ca-d7723c8e00a4' : '45c10911-200f-4e27-a666-9e9fca147395');
52
53OneDriveClient.prototype.clientId = window.location.hostname == 'app.diagrams.net' ?
54		'b5ff67d6-3155-4fca-965a-59a3655c4476' : OneDriveClient.prototype.clientId;
55
56OneDriveClient.prototype.clientId = window.location.hostname == 'viewer.diagrams.net' ?
57		'417a451a-a343-4788-b6c1-901e63182565' : OneDriveClient.prototype.clientId;
58/**
59 * OAuth 2.0 scopes for installing Drive Apps.
60 */
61OneDriveClient.prototype.scopes = 'user.read files.readwrite.all sites.read.all';
62
63/**
64 * OAuth 2.0 scopes for installing Drive Apps.
65 */
66OneDriveClient.prototype.redirectUri = window.location.protocol + '//' + window.location.host + '/microsoft';
67OneDriveClient.prototype.pickerRedirectUri = window.location.protocol + '//' + window.location.host + '/onedrive3.html';
68
69/**
70 * This is the default endpoint for personal accounts
71 */
72OneDriveClient.prototype.defEndpointHint = 'api.onedrive.com';
73OneDriveClient.prototype.endpointHint = OneDriveClient.prototype.defEndpointHint;
74
75/**
76 * Executes the first step for connecting to Google Drive.
77 */
78OneDriveClient.prototype.extension = '.drawio';
79
80/**
81 * Executes the first step for connecting to Google Drive.
82 */
83OneDriveClient.prototype.baseUrl = 'https://graph.microsoft.com/v1.0';
84
85/**
86 * Empty function used when no callback is needed
87 */
88OneDriveClient.prototype.emptyFn = function(){};
89
90OneDriveClient.prototype.invalidFilenameRegExs = [
91	/[~"#%\*:<>\?\/\\{\|}]/,
92	/^\.lock$/i,
93	/^CON$/i,
94	/^PRN$/i,
95	/^AUX$/i,
96	/^NUL$/i,
97	/^COM\d$/i,
98	/^LPT\d$/i,
99	/^desktop\.ini$/i,
100	/_vti_/i
101];
102
103/**
104 * Check if the file/folder name is valid
105 */
106OneDriveClient.prototype.isValidFilename = function(filename)
107{
108	if (filename == null || filename === '') return false;
109
110	for (var i = 0; i < this.invalidFilenameRegExs.length; i++)
111	{
112		if (this.invalidFilenameRegExs[i].test(filename)) return false;
113	}
114
115	return true;
116};
117
118
119/**
120 * Checks if the client is authorized and calls the next step.
121 */
122OneDriveClient.prototype.get = function(url, onload, onerror)
123{
124	var req = new mxXmlRequest(url, null, 'GET');
125
126	req.setRequestHeaders = mxUtils.bind(this, function(request, params)
127	{
128		request.setRequestHeader('Authorization', 'Bearer ' + _token);
129	});
130
131	req.send(onload, onerror);
132
133	return req;
134};
135
136/**
137 * Checks if the client is authorized and calls the next step.
138 */
139OneDriveClient.prototype.updateUser = function(success, error, failOnAuth)
140{
141	var acceptResponse = true;
142
143	var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
144	{
145		acceptResponse = false;
146		error({code: App.ERROR_TIMEOUT});
147	}), this.ui.timeout);
148
149	this.get(this.baseUrl + '/me', mxUtils.bind(this, function(req)
150	{
151		window.clearTimeout(timeoutThread);
152
153		if (acceptResponse)
154		{
155			if (req.getStatus() < 200 || req.getStatus() >= 300)
156			{
157				if (!failOnAuth)
158				{
159					this.logout();
160
161					this.authenticate(mxUtils.bind(this, function()
162					{
163						this.updateUser(success, error, true);
164					}), error);
165				}
166				else
167				{
168					error({message: mxResources.get('accessDenied')});
169				}
170			}
171			else
172			{
173				var data = JSON.parse(req.getText());
174				this.setUser(new DrawioUser(data.id, data.mail, data.displayName));
175				success();
176			}
177		}
178	}), mxUtils.bind(this, function(err)
179	{
180		window.clearTimeout(timeoutThread);
181
182		if (acceptResponse)
183		{
184			error(err);
185		}
186	}));
187};
188
189OneDriveClient.prototype.resetTokenRefresh = function(expires_in)
190{
191	if (this.tokenRefreshThread != null)
192	{
193		window.clearTimeout(this.tokenRefreshThread);
194		this.tokenRefreshThread = null;
195	}
196
197	// Starts timer to refresh token before it expires
198	if (expires_in > 0)
199	{
200		this.tokenRefreshInterval = expires_in * 1000;
201
202		this.tokenRefreshThread = window.setTimeout(mxUtils.bind(this, function()
203		{
204			//Get a new fresh accessToken
205			this.authenticate(this.emptyFn, this.emptyFn, true);
206		}), expires_in * 900);
207	}
208};
209
210
211/**
212 * Authorizes the client, gets the userId and calls <open>.
213 */
214OneDriveClient.prototype.authenticate = function(success, error, failOnAuth)
215{
216	if (this.isExtAuth)
217	{
218		window.parent.oneDriveAuth(mxUtils.bind(this, function(newAuthInfo)
219		{
220			this.updateAuthInfo(newAuthInfo, true, this.endpointHint == null, success, error);
221		}), error, window.urlParams != null && urlParams['odAuthCancellable'] == '1');
222		return;
223	}
224
225	var req = new mxXmlRequest(this.redirectUri + '?getState=1', null, 'GET');
226
227	req.send(mxUtils.bind(this, function(req)
228	{
229		if (req.getStatus() >= 200 && req.getStatus() <= 299)
230		{
231			this.authenticateStep2(req.getText(), success, error, failOnAuth);
232		}
233		else if (error != null)
234		{
235			error(req);
236		}
237	}), error);
238};
239
240OneDriveClient.prototype.updateAuthInfo = function(newAuthInfo, remember, forceUserUpdate, success, error)
241{
242	if (forceUserUpdate)
243	{
244		this.setUser(null);
245	}
246
247	_token = newAuthInfo.access_token;
248	delete newAuthInfo.access_token; //Don't store access token
249	newAuthInfo.expiresOn = Date.now() + newAuthInfo.expires_in * 1000;
250	this.tokenExpiresOn = newAuthInfo.expiresOn;
251
252	newAuthInfo.remember = remember;
253	this.setPersistentToken(JSON.stringify(newAuthInfo), !remember);
254	this.resetTokenRefresh(newAuthInfo.expires_in);
255
256	if (forceUserUpdate)
257	{
258		//Find out the type of the account + endpoint
259		this.getAccountTypeAndEndpoint(mxUtils.bind(this, function()
260		{
261			success();
262		}), error);
263	}
264	else
265	{
266		success();
267	}
268};
269
270OneDriveClient.prototype.authenticateStep2 = function(state, success, error, failOnAuth)
271{
272	if (window.onOneDriveCallback == null)
273	{
274		var auth = mxUtils.bind(this, function()
275		{
276			var acceptAuthResponse = true;
277
278			//Retry request with refreshed token
279			var authInfo = JSON.parse(this.getPersistentToken(true));
280
281			if (authInfo != null)
282			{
283				var req = new mxXmlRequest(this.redirectUri + '?state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state), null, 'GET'); //To identify which app/domain is used
284
285				req.send(mxUtils.bind(this, function(req)
286				{
287					if (req.getStatus() >= 200 && req.getStatus() <= 299)
288					{
289						this.updateAuthInfo(JSON.parse(req.getText()), authInfo.remember, false, success, error);
290					}
291					else
292					{
293						this.clearPersistentToken();
294						this.setUser(null);
295						_token = null;
296
297						if (req.getStatus() == 401 && !failOnAuth) // (Unauthorized) [e.g, invalid refresh token]
298						{
299							auth();
300						}
301						else
302						{
303							error({message: mxResources.get('accessDenied'), retry: auth});
304						}
305					}
306				}), error);
307			}
308			else
309			{
310				this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, authSuccess)
311				{
312					var url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' +
313						'?client_id=' + this.clientId + '&response_type=code' +
314						'&redirect_uri=' + encodeURIComponent(this.redirectUri) +
315						'&scope=' + encodeURIComponent(this.scopes + (remember? ' offline_access' : '')) +
316						'&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state); //To identify which app/domain is used
317
318					var width = 525,
319						height = 525,
320						screenX = window.screenX,
321						screenY = window.screenY,
322						outerWidth = window.outerWidth,
323						outerHeight = window.outerHeight;
324
325					var left = screenX + Math.max(outerWidth - width, 0) / 2;
326					var top = screenY + Math.max(outerHeight - height, 0) / 2;
327
328					var features = ['width=' + width, 'height=' + height,
329					                'top=' + top, 'left=' + left,
330					                'status=no', 'resizable=yes',
331					                'toolbar=no', 'menubar=no',
332					                'scrollbars=yes'];
333					var popup = window.open(url, 'odauth', features.join(','));
334
335					if (popup != null)
336					{
337						window.onOneDriveCallback = mxUtils.bind(this, function(authInfo, authWindow)
338						{
339							if (acceptAuthResponse)
340							{
341								window.onOneDriveCallback = null;
342								acceptAuthResponse = false;
343
344								try
345								{
346									if (authInfo == null)
347									{
348										error({message: mxResources.get('accessDenied'), retry: auth});
349									}
350									else
351									{
352										if (authSuccess != null)
353										{
354											authSuccess();
355										}
356
357										this.updateAuthInfo(authInfo, remember, true, success, error);
358									}
359								}
360								catch (e)
361								{
362									error(e);
363								}
364								finally
365								{
366									if (authWindow != null)
367									{
368										authWindow.close();
369									}
370								}
371							}
372							else if (authWindow != null)
373							{
374								authWindow.close();
375							}
376						});
377
378						popup.focus();
379					}
380				}), mxUtils.bind(this, function()
381				{
382					if (acceptAuthResponse)
383					{
384						window.onOneDriveCallback = null;
385						acceptAuthResponse = false;
386						error({message: mxResources.get('accessDenied'), retry: auth});
387					}
388				}));
389			}
390		});
391
392		auth();
393	}
394	else
395	{
396		error({code: App.ERROR_BUSY});
397	}
398};
399
400
401OneDriveClient.prototype.getAccountTypeAndEndpoint = function(success, error)
402{
403	this.get(this.baseUrl + '/me/drive/root', mxUtils.bind(this, function(req)
404	{
405		try
406		{
407			if (req.getStatus() >= 200 && req.getStatus() <= 299)
408			{
409				var resp = JSON.parse(req.getText());
410
411				if (resp.webUrl.indexOf('.sharepoint.com') > 0)
412			 	{
413					//TODO Confirm this works with all sharepoint sites
414					this.endpointHint = resp.webUrl.replace('/Documents', '/_layouts/15/onedrive.aspx');
415				}
416				else
417				{
418					this.endpointHint = this.defEndpointHint;
419				}
420
421			 	//Update authInfo with endpointHint
422			 	var authInfo = JSON.parse(this.getPersistentToken(true));
423
424			 	if (authInfo != null)
425		 		{
426				 	authInfo.endpointHint = this.endpointHint;
427				 	this.setPersistentToken(JSON.stringify(authInfo), !authInfo.remember);
428		 		}
429
430				success();
431				return;
432			}
433		}
434		catch(e) {}
435		//It is expected to work as this call immediately follows getting a fresh access token
436		error({message: mxResources.get('unknownError') + ' (Code: ' + req.getStatus() + ')'});
437
438	}), error);
439};
440
441/**
442 * Checks if the client is authorized and calls the next step.
443 */
444OneDriveClient.prototype.executeRequest = function(url, success, error)
445{
446	var doExecute = mxUtils.bind(this, function(failOnAuth)
447	{
448		var acceptResponse = true;
449
450		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
451		{
452			acceptResponse = false;
453			error({code: App.ERROR_TIMEOUT, retry: doExecute});
454		}), this.ui.timeout);
455
456		this.get(url, mxUtils.bind(this, function(req)
457		{
458			window.clearTimeout(timeoutThread);
459
460			if (acceptResponse)
461			{
462				// 404 (file not found) is a valid response for checkExists
463				if ((req.getStatus() >= 200 && req.getStatus() <= 299) || req.getStatus() == 404)
464				{
465					if (this.user == null)
466					{
467						this.updateUser(this.emptyFn, this.emptyFn, true);
468					}
469
470					success(req);
471				}
472				// 400 is returns if wrong user for this file
473				else if (!failOnAuth && (req.getStatus() === 401 || req.getStatus() === 400))
474				{
475					//Authorize again using the refresh token
476					this.authenticate(function()
477					{
478						doExecute(true);
479					}, error, failOnAuth);
480				}
481				else
482				{
483					error(this.parseRequestText(req));
484				}
485			}
486		}), mxUtils.bind(this, function(err)
487		{
488			window.clearTimeout(timeoutThread);
489
490			if (acceptResponse)
491			{
492				error(err);
493			}
494		}));
495	});
496
497	if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
498	{
499		this.authenticate(function()
500		{
501			doExecute(true);
502		}, error);
503	}
504	else
505	{
506		doExecute(false);
507	}
508};
509
510/**
511 * Checks if the client is authorized and calls the next step.
512 */
513OneDriveClient.prototype.checkToken = function(fn)
514{
515	if (_token == null || this.tokenRefreshThread == null || this.tokenExpiresOn - Date.now() < 60000)
516	{
517		this.authenticate(fn, this.emptyFn);
518	}
519	else
520	{
521		fn();
522	}
523};
524
525OneDriveClient.prototype.getItemRef = function(id)
526{
527	var idParts = id.split('/');
528
529	if (idParts.length > 1)
530	{
531		return {driveId: idParts[0], id: idParts[1]};
532	}
533	else
534	{
535		return {id: id};
536	}
537};
538
539OneDriveClient.prototype.getItemURL = function(id, relative)
540{
541	var idParts = id.split('/');
542
543	if (idParts.length > 1)
544	{
545		var driveId = idParts[0];
546		var itemId = idParts[1];
547		return (relative? '' : this.baseUrl) + '/drives/' + driveId + (itemId == 'root' ? '/root' : '/items/' + itemId);
548	}
549	else
550	{
551		return (relative? '' : this.baseUrl) + '/me/drive/items/' + id;
552	}
553};
554
555/**
556 * Checks if the client is authorized and calls the next step.
557 */
558OneDriveClient.prototype.getLibrary = function(id, success, error)
559{
560	this.getFile(id, success, error, false, true);
561};
562
563/**
564 * Workaround for added content to HTML files in Sharepoint.
565 */
566OneDriveClient.prototype.removeExtraHtmlContent = function(data)
567{
568	var idx = data.lastIndexOf('<html><head><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"><meta name="Robots" ');
569
570	if (idx > 0)
571	{
572		data = data.substring(0, idx);
573	}
574
575	return data;
576};
577
578/**
579 * Checks if the client is authorized and calls the next step.
580 */
581OneDriveClient.prototype.getFile = function(id, success, error, denyConvert, asLibrary)
582{
583	asLibrary = (asLibrary != null) ? asLibrary : false;
584
585	this.executeRequest(this.getItemURL(id), mxUtils.bind(this, function(req)
586	{
587		if (req.getStatus() >= 200 && req.getStatus() <= 299)
588		{
589			var meta = JSON.parse(req.getText());
590			var binary = /\.png$/i.test(meta.name);
591
592			// Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file
593			if (/\.v(dx|sdx?)$/i.test(meta.name) || /\.gliffy$/i.test(meta.name) ||
594				/\.pdf$/i.test(meta.name) || (!this.ui.useCanvasForExport && binary))
595			{
596				var mimeType = (meta.file != null) ? meta.file.mimeType : null;
597				this.ui.convertFile(meta['@microsoft.graph.downloadUrl'], meta.name, mimeType,
598					this.extension, success, error);
599			}
600			else
601			{
602				var acceptResponse = true;
603
604				var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
605				{
606					acceptResponse = false;
607					error({code: App.ERROR_TIMEOUT})
608				}), this.ui.timeout);
609
610				this.ui.editor.loadUrl(meta['@microsoft.graph.downloadUrl'], mxUtils.bind(this, function(data)
611				{
612					try
613					{
614						window.clearTimeout(timeoutThread);
615
616				    	if (acceptResponse)
617				    	{
618				    		if (/\.html$/i.test(meta.name))
619				    		{
620				    			data = this.removeExtraHtmlContent(data);
621				    		}
622
623							var index = (binary) ? data.lastIndexOf(',') : -1;
624							var file = null;
625
626							if (index > 0)
627							{
628								var xml = this.ui.extractGraphModelFromPng(data);
629
630								if (xml != null && xml.length > 0)
631								{
632									data = xml;
633								}
634								else
635								{
636									// Imports as PNG image
637									file = new LocalFile(this.ui, data, meta.name, true);
638								}
639							}
640							// Checks for base64 encoded mxfile
641							else if (data.substring(0, 32) == 'data:image/png;base64,PG14ZmlsZS')
642							{
643								var temp = data.substring(22);
644								data = (window.atob && !mxClient.IS_SF) ? atob(temp) : Base64.decode(temp);
645							}
646
647							if (Graph.fileSupport && new XMLHttpRequest().upload && this.ui.isRemoteFileFormat(data, meta['@microsoft.graph.downloadUrl']))
648							{
649								this.ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
650								{
651									try
652									{
653										if (xhr.readyState == 4)
654										{
655											if (xhr.status >= 200 && xhr.status <= 299)
656											{
657												success(new LocalFile(this.ui, xhr.responseText, meta.name + this.extension, true));
658											}
659											else if (error != null)
660											{
661												error({message: mxResources.get('errorLoadingFile')});
662											}
663										}
664									}
665									catch (e)
666									{
667										if (error != null)
668										{
669											error(e);
670										}
671										else
672										{
673											throw e;
674										}
675									}
676								}), meta.name);
677							}
678							else
679							{
680								if (file != null)
681								{
682									success(file);
683								}
684								else if (asLibrary)
685								{
686									success(new OneDriveLibrary(this.ui, data, meta));
687								}
688								else
689								{
690									success(new OneDriveFile(this.ui, data, meta));
691								}
692							}
693				    	}
694					}
695					catch (e)
696					{
697						if (error != null)
698						{
699							error(e);
700						}
701						else
702						{
703							throw e;
704						}
705					}
706    			}), mxUtils.bind(this, function(req)
707				{
708					window.clearTimeout(timeoutThread);
709
710			    	if (acceptResponse)
711			    	{
712			    		error(this.parseRequestText(req));
713			    	}
714				}), binary || (meta.file != null && meta.file.mimeType != null &&
715					((meta.file.mimeType.substring(0, 6) == 'image/' &&
716					meta.file.mimeType.substring(0, 9) != 'image/svg') ||
717					meta.file.mimeType == 'application/pdf')));
718			}
719		}
720		else
721		{
722			if (this.isExtAuth)
723			{
724				error({message: mxResources.get('fileNotFoundOrDenied'),
725						ownerEmail: window.urlParams != null? urlParams['ownerEml'] : null});
726			}
727			else
728			{
729				error(this.parseRequestText(req));
730			}
731		}
732	}), error);
733};
734
735/**
736 * Translates this point by the given vector.
737 *
738 * @param {number} dx X-coordinate of the translation.
739 * @param {number} dy Y-coordinate of the translation.
740 */
741OneDriveClient.prototype.renameFile = function(file, filename, success, error)
742{
743	if (file != null && filename != null)
744	{
745		if (!this.isValidFilename(filename))
746		{
747			error({message: this.invalidFilenameRegExs[0].test(filename) ?
748					mxResources.get('oneDriveCharsNotAllowed') : mxResources.get('oneDriveInvalidDeviceName')});
749			return;
750		}
751
752		// TODO: How to force overwrite file with same name?
753		this.checkExists(file.getParentId(), filename, false, mxUtils.bind(this, function(checked)
754		{
755			if (checked)
756			{
757				this.writeFile(this.getItemURL(file.getId()), JSON.stringify({name: filename}), 'PATCH', 'application/json', success, error);
758			}
759			else
760			{
761				error();
762			}
763		}));
764	}
765};
766
767/**
768 * Translates this point by the given vector.
769 *
770 * @param {number} dx X-coordinate of the translation.
771 * @param {number} dy Y-coordinate of the translation.
772 */
773OneDriveClient.prototype.moveFile = function(id, folderId, success, error)
774{
775	//check that the source and destination are on the same drive
776	var folderInfo = this.getItemRef(folderId);
777	var fileInfo = this.getItemRef(id);
778
779	if (folderInfo.driveId != fileInfo.driveId)
780	{
781		error({message: mxResources.get('cannotMoveOneDrive', null, 'Moving a file between accounts is not supported yet.')});
782	}
783	else
784	{
785		this.writeFile(this.getItemURL(id), JSON.stringify({parentReference: folderInfo}), 'PATCH', 'application/json', success, error);
786	}
787};
788
789/**
790 * Translates this point by the given vector.
791 *
792 * @param {number} dx X-coordinate of the translation.
793 * @param {number} dy Y-coordinate of the translation.
794 */
795OneDriveClient.prototype.insertLibrary = function(filename, data, success, error, folderId)
796{
797	this.insertFile(filename, data, success, error, true, folderId);
798};
799
800/**
801 * Translates this point by the given vector.
802 *
803 * @param {number} dx X-coordinate of the translation.
804 * @param {number} dy Y-coordinate of the translation.
805 */
806OneDriveClient.prototype.insertFile = function(filename, data, success, error, asLibrary, folderId)
807{
808	if (!this.isValidFilename(filename))
809	{
810		error({message: this.invalidFilenameRegExs[0].test(filename) ?
811				mxResources.get('oneDriveCharsNotAllowed') : mxResources.get('oneDriveInvalidDeviceName')});
812		return;
813	}
814
815	asLibrary = (asLibrary != null) ? asLibrary : false;
816
817	this.checkExists(folderId, filename, true, mxUtils.bind(this, function(checked)
818	{
819		if (checked)
820		{
821			var folder = '/me/drive/root';
822
823			if (folderId != null)
824			{
825				folder = this.getItemURL(folderId, true);
826			}
827
828			var insertSuccess = mxUtils.bind(this, function(meta)
829			{
830				if (asLibrary)
831				{
832					success(new OneDriveLibrary(this.ui, data, meta));
833				}
834				else
835				{
836					success(new OneDriveFile(this.ui, data, meta));
837				}
838			});
839
840			var url = this.baseUrl + folder + '/children/' + encodeURIComponent(filename) + '/content';
841
842			//OneDrive has a limit on PUT API of 4MB, larger files needs to use the upload session method
843			if (data.length >= 4000000 /*4MB*/)
844			{
845				//Create empty file first then upload. TODO Can we get an upload session for non-existing files?
846				this.writeFile(url, '', 'PUT', null, mxUtils.bind(this, function(meta)
847				{
848					this.writeLargeFile(this.getItemURL(meta.id), data, insertSuccess, error);
849				}), error);
850			}
851			else
852			{
853				this.writeFile(url, data, 'PUT', null, insertSuccess, error);
854			}
855		}
856		else
857		{
858			error();
859		}
860	}))
861};
862
863/**
864 * Translates this point by the given vector.
865 *
866 * @param {number} dx X-coordinate of the translation.
867 * @param {number} dy Y-coordinate of the translation.
868 */
869OneDriveClient.prototype.checkExists = function(parentId, filename, askReplace, fn)
870{
871	var folder = '/me/drive/root';
872
873	if (parentId != null)
874	{
875		folder = this.getItemURL(parentId, true);
876	}
877
878	this.executeRequest(this.baseUrl + folder + '/children/' + encodeURIComponent(filename), mxUtils.bind(this, function(req)
879	{
880		if (req.getStatus() == 404)
881		{
882			fn(true);
883		}
884		else
885		{
886			if (askReplace)
887			{
888				this.ui.spinner.stop();
889
890				this.ui.confirm(mxResources.get('replaceIt', [filename]), function()
891				{
892					fn(true);
893				}, function()
894				{
895					fn(false);
896				});
897			}
898			else
899			{
900				this.ui.spinner.stop();
901
902				this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function()
903				{
904					fn(false);
905				});
906			}
907		}
908	}), function(req)
909	{
910		fn(false);
911	}, true);
912};
913
914/**
915 * Translates this point by the given vector.
916 *
917 * @param {number} dx X-coordinate of the translation.
918 * @param {number} dy Y-coordinate of the translation.
919 */
920OneDriveClient.prototype.saveFile = function(file, success, error, etag)
921{
922	try
923	{
924		var savedData = file.getData();
925
926		var fn = mxUtils.bind(this, function(data)
927		{
928			var saveSuccess = mxUtils.bind(this, function(resp)
929			{
930				success(resp, savedData);
931			});
932
933			var url = this.getItemURL(file.getId());
934
935			//OneDrive has a limit on PUT API of 4MB, larger files needs to use the upload session method
936			if (data.length >= 4000000 /*4MB*/)
937			{
938				this.writeLargeFile(url, data, saveSuccess, error, etag);
939			}
940			else
941			{
942				this.writeFile(url + '/content/', data, 'PUT', null, saveSuccess, error, etag);
943			}
944		});
945
946		if (this.ui.useCanvasForExport && /(\.png)$/i.test(file.meta.name))
947		{
948			var p = this.ui.getPngFileProperties(this.ui.fileNode);
949
950			this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
951			{
952				fn(this.ui.base64ToBlob(data, 'image/png'));
953			}), error, (this.ui.getCurrentFile() != file) ?
954				savedData : null, p.scale, p.border);
955		}
956		else
957		{
958			fn(savedData);
959		}
960	}
961	catch (e)
962	{
963		error(e);
964	}
965};
966
967OneDriveClient.prototype.writeLargeFile = function(url, data, success, error, etag)
968{
969	try
970	{
971		var chunkSize = 4 * 1024 * 1024; //4MB chunk;
972
973		if (data != null)
974		{
975			var uploadPart = mxUtils.bind(this, function(uploadUrl, index, retryCount)
976			{
977				try
978				{
979					retryCount = retryCount || 0;
980					var acceptResponse = true;
981					var timeoutThread = null;
982
983					timeoutThread = window.setTimeout(mxUtils.bind(this, function()
984					{
985						acceptResponse = false;
986						error({code: App.ERROR_TIMEOUT});
987					}), this.ui.timeout);
988
989					var part = data.substr(index, chunkSize);
990					var req = new mxXmlRequest(uploadUrl, part, 'PUT');
991
992					req.setRequestHeaders = mxUtils.bind(this, function(request, params)
993					{
994						request.setRequestHeader('Content-Length', part.length);
995						request.setRequestHeader('Content-Range', 'bytes ' + index + '-' + (index + part.length - 1) + '/' + data.length);
996					});
997
998					req.send(mxUtils.bind(this, function(req)
999					{
1000				    	window.clearTimeout(timeoutThread);
1001
1002				    	if (acceptResponse)
1003				    	{
1004							var status = req.getStatus();
1005					    	if (status >= 200 && status <= 299)
1006							{
1007								var nextByte = index + part.length;
1008
1009								if (nextByte == data.length)
1010								{
1011									success(JSON.parse(req.getText()));
1012								}
1013								else
1014								{
1015									uploadPart(uploadUrl, nextByte, retryCount);
1016								}
1017							}
1018							else if (status >= 500 && status <= 599 && retryCount < 2) //Retry on server errors
1019							{
1020								retryCount++;
1021								uploadPart(uploadUrl, index, retryCount);
1022							}
1023							else
1024							{
1025								error(this.parseRequestText(req), req);
1026							}
1027				    	}
1028					}), mxUtils.bind(this, function(req)
1029					{
1030				    	window.clearTimeout(timeoutThread);
1031
1032				    	if (acceptResponse)
1033				    	{
1034							error(this.parseRequestText(req));
1035				    	}
1036					}));
1037				}
1038				catch (e)
1039				{
1040					error(e);
1041				}
1042			});
1043
1044			var doExecute = mxUtils.bind(this, function(failOnAuth)
1045			{
1046				try
1047				{
1048					var acceptResponse = true;
1049					var timeoutThread = null;
1050
1051					try
1052					{
1053						timeoutThread = window.setTimeout(mxUtils.bind(this, function()
1054						{
1055							acceptResponse = false;
1056							error({code: App.ERROR_TIMEOUT});
1057						}), this.ui.timeout);
1058					}
1059					catch (e)
1060					{
1061						// Ignore window closed
1062					}
1063
1064					var req = new mxXmlRequest(url + '/createUploadSession', '{}', 'POST');
1065
1066					req.setRequestHeaders = mxUtils.bind(this, function(request, params)
1067					{
1068						request.setRequestHeader('Content-Type', 'application/json');
1069						request.setRequestHeader('Authorization', 'Bearer ' + _token);
1070
1071						if (etag != null)
1072						{
1073							request.setRequestHeader('If-Match', etag);
1074						}
1075					});
1076
1077					req.send(mxUtils.bind(this, function(req)
1078					{
1079				    	window.clearTimeout(timeoutThread);
1080
1081				    	if (acceptResponse)
1082				    	{
1083					    	if (req.getStatus() >= 200 && req.getStatus() <= 299)
1084							{
1085								var resp = JSON.parse(req.getText());
1086					    		uploadPart(resp.uploadUrl, 0);
1087							}
1088							else if (!failOnAuth && req.getStatus() === 401)
1089							{
1090								this.authenticate(function()
1091								{
1092									doExecute(true);
1093								}, error, failOnAuth);
1094							}
1095							else
1096							{
1097								error(this.parseRequestText(req), req);
1098							}
1099				    	}
1100					}), mxUtils.bind(this, function(req)
1101					{
1102				    	window.clearTimeout(timeoutThread);
1103
1104				    	if (acceptResponse)
1105				    	{
1106							error(this.parseRequestText(req));
1107				    	}
1108					}));
1109				}
1110				catch (e)
1111				{
1112					error(e);
1113				}
1114			});
1115
1116			if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
1117			{
1118				this.authenticate(function()
1119				{
1120					doExecute(true);
1121				}, error);
1122			}
1123			else
1124			{
1125				doExecute(false);
1126			}
1127		}
1128		else
1129		{
1130			error({message: mxResources.get('unknownError')});
1131		}
1132	}
1133	catch (e)
1134	{
1135		error(e);
1136	}
1137};
1138
1139/**
1140 * Translates this point by the given vector.
1141 *
1142 * @param {number} dx X-coordinate of the translation.
1143 * @param {number} dy Y-coordinate of the translation.
1144 */
1145OneDriveClient.prototype.writeFile = function(url, data, method, contentType, success, error, etag)
1146{
1147	try
1148	{
1149		if (url != null && data != null)
1150		{
1151			var doExecute = mxUtils.bind(this, function(failOnAuth)
1152			{
1153				try
1154				{
1155					var acceptResponse = true;
1156					var timeoutThread = null;
1157
1158					try
1159					{
1160						timeoutThread = window.setTimeout(mxUtils.bind(this, function()
1161						{
1162							acceptResponse = false;
1163							error({code: App.ERROR_TIMEOUT});
1164						}), this.ui.timeout);
1165					}
1166					catch (e)
1167					{
1168						// Ignore window closed
1169					}
1170
1171					var req = new mxXmlRequest(url, data, method);
1172
1173					req.setRequestHeaders = mxUtils.bind(this, function(request, params)
1174					{
1175						// Space deletes content type header. Specification says "text/plain"
1176						// should work but returns an 415 Unsupported Media Type error
1177						request.setRequestHeader('Content-Type', contentType || ' ');
1178						//TODO This header is needed for moving a file between two different drives.
1179						//		Note: the response is empty when this header is used, also the server may take some time to really execute the request (i.e. async)
1180						//request.setRequestHeader('Prefer', 'respond-async');
1181						request.setRequestHeader('Authorization', 'Bearer ' + _token);
1182
1183						if (etag != null)
1184						{
1185							request.setRequestHeader('If-Match', etag);
1186						}
1187					});
1188
1189					req.send(mxUtils.bind(this, function(req)
1190					{
1191				    	window.clearTimeout(timeoutThread);
1192
1193				    	if (acceptResponse)
1194				    	{
1195					    	if (req.getStatus() >= 200 && req.getStatus() <= 299)
1196							{
1197					    		if (this.user == null)
1198								{
1199									this.updateUser(this.emptyFn, this.emptyFn, true);
1200								}
1201
1202								success(JSON.parse(req.getText()));
1203							}
1204							else if (!failOnAuth && req.getStatus() === 401)
1205							{
1206								this.authenticate(function()
1207								{
1208									doExecute(true);
1209								}, error, failOnAuth);
1210							}
1211							else
1212							{
1213								error(this.parseRequestText(req), req);
1214							}
1215				    	}
1216					}), mxUtils.bind(this, function(req)
1217					{
1218				    	window.clearTimeout(timeoutThread);
1219
1220				    	if (acceptResponse)
1221				    	{
1222							error(this.parseRequestText(req));
1223				    	}
1224					}));
1225				}
1226				catch (e)
1227				{
1228					error(e);
1229				}
1230			});
1231
1232			if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
1233			{
1234				this.authenticate(function()
1235				{
1236					doExecute(true);
1237				}, error);
1238			}
1239			else
1240			{
1241				doExecute(false);
1242			}
1243		}
1244		else
1245		{
1246			error({message: mxResources.get('unknownError')});
1247		}
1248	}
1249	catch (e)
1250	{
1251		error(e);
1252	}
1253};
1254
1255/**
1256 * Checks if the client is authorized and calls the next step.
1257 */
1258OneDriveClient.prototype.parseRequestText = function(req)
1259{
1260	var result = {message: mxResources.get('unknownError')};
1261
1262	try
1263	{
1264		result = JSON.parse(req.getText());
1265		result.status = req.getStatus();
1266
1267		if (result.error)
1268		{
1269			result.error.status = result.status;
1270			result.error.code = result.status;
1271		}
1272	}
1273	catch (e)
1274	{
1275		// ignore
1276	}
1277
1278	return result;
1279};
1280
1281/**
1282 * Checks if the client is authorized and calls the next step.
1283 */
1284OneDriveClient.prototype.pickLibrary = function(fn)
1285{
1286	this.pickFile(function(id)
1287	{
1288		// Ignores second argument
1289		fn(id);
1290	});
1291};
1292
1293OneDriveClient.prototype.createInlinePicker = function(fn, foldersOnly)
1294{
1295	return mxUtils.bind(this, function()
1296	{
1297		var odPicker = null;
1298		var div = document.createElement('div');
1299		div.style.position = 'relative';
1300
1301		var dlg = new CustomDialog(this.ui, div, mxUtils.bind(this, function()
1302		{
1303			var item = odPicker.getSelectedItem();
1304
1305			if (item != null)
1306			{
1307				if (foldersOnly && typeof item.folder == 'object')
1308				{
1309					fn({
1310						value: [item]
1311					});
1312					return;
1313				}
1314				else if (!item.folder)
1315				{
1316					fn(OneDriveFile.prototype.getIdOf(item));
1317					return;
1318				}
1319			}
1320
1321			return mxResources.get('invalidSel', null, 'Invalid selection');
1322		}), null, mxResources.get(foldersOnly? 'save' :'open'), null, null, null, null, true);
1323
1324		this.ui.showDialog(dlg.container, 550, 500, true, true);
1325		//Set width/height of the picker container
1326		div.style.width = dlg.container.parentNode.style.width;
1327		div.style.height = (parseInt(dlg.container.parentNode.style.height) - 60) + 'px';
1328
1329		odPicker = new mxODPicker(div, null, mxUtils.bind(this, function(url, success, error)
1330		{
1331			this.executeRequest(this.baseUrl + url, function(req)
1332			{
1333				success(JSON.parse(req.getText()));
1334			}, error);
1335		}), mxUtils.bind(this, function(id, driveId, success, error)
1336		{
1337			this.executeRequest(this.baseUrl + '/drives/' + driveId + '/items/' + id, function(req)
1338			{
1339				success(JSON.parse(req.getText()));
1340			}, error);
1341		}), null, null, function(item)
1342		{
1343			if (foldersOnly) //Currently this is not called when in foldersOnly mode
1344			{
1345				fn({
1346					value: [item]
1347				});
1348			}
1349			else
1350			{
1351				fn(OneDriveFile.prototype.getIdOf(item));
1352			}
1353		},
1354		mxUtils.bind(this, function(err)
1355		{
1356			this.ui.showError(mxResources.get('error'), err);
1357		}), foldersOnly);
1358	});
1359};
1360
1361/**
1362 * Checks if the client is authorized and calls the next step.
1363 */
1364OneDriveClient.prototype.pickFolder = function(fn, direct)
1365{
1366	var errorFn = mxUtils.bind(this, function(e)
1367	{
1368		this.ui.showError(mxResources.get('error'), e && e.message? e.message : e);
1369	});
1370
1371	var odSaveDlg = mxUtils.bind(this, function(direct)
1372	{
1373		var openSaveDlg = this.inlinePicker? this.createInlinePicker(fn, true) :
1374								mxUtils.bind(this, function()
1375		{
1376			OneDrive.save(
1377			{
1378				clientId: this.clientId,
1379				action: 'query',
1380				openInNewWindow: true,
1381				advanced:
1382				{
1383					'endpointHint': mxClient.IS_IE11? null : this.endpointHint, //IE11 doen't work with our modified version, so, setting endpointHint disable using our token BUT will force relogin!
1384					'redirectUri': this.pickerRedirectUri,
1385					'queryParameters': 'select=id,name,parentReference',
1386					'accessToken': _token,
1387					isConsumerAccount: false
1388				},
1389				success: mxUtils.bind(this, function(files)
1390				{
1391					fn(files);
1392
1393					//Update the token in case a login with a different user
1394					if (mxClient.IS_IE11)
1395					{
1396						_token = files.accessToken;
1397					}
1398				}),
1399				cancel: mxUtils.bind(this, function()
1400				{
1401					// do nothing
1402				}),
1403				error: errorFn
1404			});
1405		});
1406
1407		if (direct)
1408		{
1409			openSaveDlg();
1410		}
1411		else
1412		{
1413			this.ui.confirm(mxResources.get('useRootFolder'), mxUtils.bind(this, function()
1414			{
1415				fn({value: [{id: 'root', name: 'root', parentReference: {driveId: 'me'}}]});
1416
1417			}), openSaveDlg, mxResources.get('yes'), mxResources.get('noPickFolder') + '...', true);
1418		}
1419
1420		if (this.user == null)
1421		{
1422			this.updateUser(this.emptyFn, this.emptyFn, true);
1423		}
1424	});
1425
1426	if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
1427	{
1428		this.authenticate(mxUtils.bind(this, function()
1429		{
1430			odSaveDlg(false);
1431		}), errorFn);
1432	}
1433	else
1434	{
1435		odSaveDlg(direct);
1436	}
1437};
1438
1439/**
1440 * Checks if the client is authorized and calls the next step.
1441 */
1442OneDriveClient.prototype.pickFile = function(fn)
1443{
1444	fn = (fn != null) ? fn : mxUtils.bind(this, function(id)
1445	{
1446		this.ui.loadFile('W' + encodeURIComponent(id));
1447	});
1448
1449	var errorFn = mxUtils.bind(this, function(e)
1450	{
1451		this.ui.showError(mxResources.get('error'), e && e.message? e.message : e);
1452	});
1453
1454	var odOpenDlg = this.inlinePicker? this.createInlinePicker(fn) :
1455							mxUtils.bind(this, function()
1456	{
1457		OneDrive.open(
1458		{
1459			clientId: this.clientId,
1460			action: 'query',
1461			multiSelect: false,
1462			advanced:
1463			{
1464				'endpointHint': mxClient.IS_IE11? null : this.endpointHint, //IE11 doen't work with our modified version, so, setting endpointHint disable using our token BUT will force relogin!
1465				'redirectUri': this.pickerRedirectUri,
1466				'queryParameters': 'select=id,name,parentReference', //We can also get @microsoft.graph.downloadUrl within this request but it will break the normal process
1467				'accessToken': _token,
1468				isConsumerAccount: false
1469			},
1470			success: mxUtils.bind(this, function(files)
1471			{
1472				if (files != null && files.value != null && files.value.length > 0)
1473				{
1474					//Update the token in case a login with a different user
1475					if (mxClient.IS_IE11)
1476					{
1477						_token = files.accessToken;
1478					}
1479
1480					fn(OneDriveFile.prototype.getIdOf(files.value[0]), files);
1481				}
1482			}),
1483			cancel: mxUtils.bind(this, function()
1484			{
1485				// do nothing
1486			}),
1487			error: errorFn
1488		});
1489
1490		if (this.user == null)
1491		{
1492			this.updateUser(this.emptyFn, this.emptyFn, true);
1493		}
1494	});
1495
1496	if (_token == null || this.tokenExpiresOn - Date.now() < 60000) //60 sec tolerance window
1497	{
1498		this.authenticate(mxUtils.bind(this, function()
1499		{
1500			if (this.inlinePicker)
1501			{
1502				this.ui.hideDialog();
1503				odOpenDlg();
1504			}
1505			else
1506			{
1507				this.ui.showDialog(new BtnDialog(this.ui, this, mxResources.get('open'), mxUtils.bind(this, function()
1508				{
1509					this.ui.hideDialog();
1510					odOpenDlg();
1511				})).container, 300, 140, true, true);
1512			}
1513		}), errorFn);
1514	}
1515	else
1516	{
1517		odOpenDlg();
1518	}
1519};
1520
1521/**
1522 * Checks if the client is authorized and calls the next step.
1523 */
1524OneDriveClient.prototype.logout = function()
1525{
1526	if (isLocalStorage)
1527	{
1528		var check = localStorage.getItem('odpickerv7cache');
1529
1530		if (check != null && check.substring(0, 19) == '{"odsdkLoginHint":{')
1531		{
1532			localStorage.removeItem('odpickerv7cache');
1533		}
1534	}
1535
1536	window.open('https://login.microsoftonline.com/common/oauth2/v2.0/logout', 'logout', 'width=525,height=525,status=no,resizable=yes,toolbar=no,menubar=no,scrollbars=yes');
1537	//Send to server to clear refresh token cookie
1538	this.ui.editor.loadUrl(this.redirectUri + '?doLogout=1&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname));
1539	this.clearPersistentToken();
1540	this.setUser(null);
1541	_token = null;
1542};
1543
1544})();