1/**
2 * Copyright (c) 2006-2020, JGraph Ltd
3 * Copyright (c) 2006-2020, draw.io AG
4 */
5//Add a closure to hide the class private variables without changing the code a lot
6(function()
7{
8
9var _token = null;
10
11window.GitLabClient = function(editorUi)
12{
13	GitHubClient.call(this, editorUi, 'gitlabauth');
14};
15
16// Extends DrawioClient
17mxUtils.extend(GitLabClient, GitHubClient);
18
19/**
20 * Gitlab Client ID, see https://gitlab.com/oauth/applications/135239
21 */
22GitLabClient.prototype.clientId = DRAWIO_GITLAB_ID;
23
24/**
25 * OAuth scope.
26 */
27GitLabClient.prototype.scope = 'api%20read_repository%20write_repository';
28
29/**
30 * Base URL for API calls.
31 */
32GitLabClient.prototype.baseUrl = DRAWIO_GITLAB_URL + '/api/v4';
33
34/**
35 * Maximum file size of the GitLab REST API.
36 */
37GitLabClient.prototype.maxFileSize = 10000000 /*10MB*/;
38
39/**
40 * Name for the auth token header.
41 */
42GitLabClient.prototype.authToken = 'Bearer';
43
44GitLabClient.prototype.redirectUri = window.location.protocol + '//' + window.location.host + '/gitlab';
45
46/**
47 * Authorizes the client, gets the userId and calls <open>.
48 */
49GitLabClient.prototype.authenticate = function(success, error)
50{
51	var req = new mxXmlRequest(this.redirectUri + '?getState=1', null, 'GET');
52
53	req.send(mxUtils.bind(this, function(req)
54	{
55		if (req.getStatus() >= 200 && req.getStatus() <= 299)
56		{
57			this.authenticateStep2(req.getText(), success, error);
58		}
59		else if (error != null)
60		{
61			error(req);
62		}
63	}), error);
64};
65
66GitLabClient.prototype.authenticateStep2 = function(state, success, error)
67{
68	if (window.onGitLabCallback == null)
69	{
70		var auth = mxUtils.bind(this, function()
71		{
72			var acceptAuthResponse = true;
73
74			var authRemembered = this.getPersistentToken(true);
75
76			if (authRemembered != null)
77			{
78				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
79
80				req.send(mxUtils.bind(this, function(req)
81				{
82					if (req.getStatus() >= 200 && req.getStatus() <= 299)
83					{
84						_token = JSON.parse(req.getText()).access_token;
85						this.setToken(_token);
86						this.setUser(null);
87						success();
88					}
89					else
90					{
91						this.clearPersistentToken();
92						this.setUser(null);
93						_token = null;
94						this.setToken(null);
95
96						if (req.getStatus() == 401) // (Unauthorized) [e.g, invalid refresh token]
97						{
98							auth();
99						}
100						else
101						{
102							error({message: mxResources.get('accessDenied'), retry: auth});
103						}
104					}
105				}), error);
106			}
107			else
108			{
109				this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, authSuccess)
110				{
111					var win = window.open(DRAWIO_GITLAB_URL + '/oauth/authorize?client_id=' +
112						this.clientId + '&scope=' + this.scope +
113						'&redirect_uri=' + encodeURIComponent(this.redirectUri) +
114						'&response_type=code&state=' + encodeURIComponent('cId=' + this.clientId + //To identify which app/domain is used
115							'&domain=' + window.location.hostname + '&token=' + state) , 'gitlabauth');
116
117					if (win != null)
118					{
119						window.onGitLabCallback = mxUtils.bind(this, function(newAuthInfo, authWindow)
120						{
121							if (acceptAuthResponse)
122							{
123								window.onGitLabCallback = null;
124								acceptAuthResponse = false;
125
126								if (newAuthInfo == null)
127								{
128									error({message: mxResources.get('accessDenied'), retry: auth});
129								}
130								else
131								{
132									if (authSuccess != null)
133									{
134										authSuccess();
135									}
136
137									_token = newAuthInfo.access_token;
138									this.setToken(_token);
139									this.setUser(null);
140
141									if (remember)
142									{
143										this.setPersistentToken('remembered');
144									}
145
146									success();
147
148									if (authWindow != null)
149									{
150										authWindow.close();
151									}
152								}
153							}
154							else if (authWindow != null)
155							{
156								authWindow.close();
157							}
158						});
159					}
160					else
161					{
162						error({message: mxResources.get('serviceUnavailableOrBlocked'), retry: auth});
163					}
164
165				}), mxUtils.bind(this, function()
166				{
167					if (acceptAuthResponse)
168					{
169						window.onGitLabCallback = null;
170						acceptAuthResponse = false;
171						error({message: mxResources.get('accessDenied'), retry: auth});
172					}
173				}));
174			}
175		});
176
177		auth();
178	}
179	else
180	{
181		error({code: App.ERROR_BUSY});
182	}
183};
184
185/**
186 * Authorizes the client, gets the userId and calls <open>.
187 */
188GitLabClient.prototype.executeRequest = function(req, success, error, ignoreNotFound)
189{
190	var doExecute = mxUtils.bind(this, function(failOnAuth)
191	{
192		var acceptResponse = true;
193
194		var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
195		{
196			acceptResponse = false;
197			error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
198		}), this.ui.timeout);
199
200		var temp = this.authToken + ' ' + _token;
201
202		req.setRequestHeaders = function(request, params)
203		{
204			request.setRequestHeader('Authorization', temp);
205			request.setRequestHeader('PRIVATE_TOKEN', temp);
206			request.setRequestHeader('Content-Type', 'application/json');
207		};
208
209		req.send(mxUtils.bind(this, function()
210		{
211			window.clearTimeout(timeoutThread);
212
213			if (acceptResponse)
214			{
215				if ((req.getStatus() >= 200 && req.getStatus() <= 299) ||
216					(ignoreNotFound && req.getStatus() == 404))
217				{
218					success(req);
219				}
220				else if (req.getStatus() === 401)
221				{
222					if (!failOnAuth)
223					{
224						this.authenticate(function()
225						{
226							doExecute(true);
227						}, error);
228					}
229					else
230					{
231						error({message: mxResources.get('accessDenied'), retry: mxUtils.bind(this, function()
232						{
233							this.authenticate(function()
234							{
235								fn(true);
236							}, error);
237						})});
238					}
239				}
240				else if (req.getStatus() === 403)
241				{
242					var tooLarge = false;
243
244					try
245					{
246						var temp = JSON.parse(req.getText());
247
248						if (temp != null && temp.errors != null && temp.errors.length > 0)
249						{
250							tooLarge = temp.errors[0].code == 'too_large';
251						}
252					}
253					catch (e)
254					{
255						// ignore
256					}
257
258					error({message: mxResources.get((tooLarge) ? 'drawingTooLarge' : 'forbidden')});
259				}
260				else if (req.getStatus() === 404)
261				{
262					error({message: this.getErrorMessage(req, mxResources.get('fileNotFound'))});
263				}
264				else if (req.getStatus() === 400)
265				{
266					// Special case: flag to the caller that there was a conflict
267					error({status: 400});
268				}
269				else
270				{
271					error({status: req.getStatus(), message: this.getErrorMessage(req,
272						mxResources.get('error') + ' ' + req.getStatus())});
273				}
274			}
275		}), mxUtils.bind(this, function(err)
276		{
277			window.clearTimeout(timeoutThread);
278
279			if (acceptResponse)
280			{
281				error(err);
282			}
283		}));
284	});
285
286	var fn = mxUtils.bind(this, function(failOnAuth)
287	{
288		if (this.user == null)
289		{
290			this.updateUser(function()
291			{
292				fn(true);
293			}, error, failOnAuth);
294		}
295		else
296		{
297			doExecute(failOnAuth);
298		}
299	});
300
301	if (_token == null)
302	{
303		this.authenticate(function()
304		{
305			fn(true);
306		}, error);
307	}
308	else
309	{
310		fn(false);
311	}
312};
313
314/**
315 * Finds index of ref in given token list. This is required to support groups and subgroups.
316 */
317GitLabClient.prototype.getRefIndex = function(tokens, isFolder, success, error, knownRefPos, checkRepo)
318{
319	if (knownRefPos != null)
320	{
321		success(tokens, knownRefPos);
322	}
323	else
324	{
325		var refPos = tokens.length - 2;
326
327		// Finds ref in token list by checking which URL works
328		var checkUrl = mxUtils.bind(this, function()
329		{
330			if (refPos < 2)
331			{
332				error({message: mxResources.get('fileNotFound')});
333			}
334			else
335			{
336				var repoPos = Math.max(refPos - 1, 0);
337				var org = tokens.slice(0, repoPos).join('/');
338				var repo = tokens[repoPos];
339				var ref = tokens[refPos];
340				var path = tokens.slice(refPos + 1, tokens.length).join('/');
341				var url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) + '/repository/' +
342					(!isFolder ? 'files/' + encodeURIComponent(path) + '?ref=' + ref : (checkRepo ?
343					'branches?per_page=1&page=1&ref=' + ref : 'tree?path=' + path + '&ref=' + ref));
344
345				var req = new mxXmlRequest(url, null, 'HEAD');
346
347				this.executeRequest(req, mxUtils.bind(this, function()
348				{
349					if (req.getStatus() == 200)
350					{
351						success(tokens, refPos);
352					}
353					else
354					{
355						error({message: mxResources.get('fileNotFound')});
356					}
357				}), mxUtils.bind(this, function()
358				{
359					if (req.getStatus() == 404)
360					{
361						refPos--;
362						checkUrl();
363					}
364					else
365					{
366						error({message: mxResources.get('fileNotFound')});
367					}
368				}));
369			}
370		});
371
372		checkUrl();
373	}
374};
375
376/**
377 * Checks if the client is authorized and calls the next step.
378 */
379GitLabClient.prototype.getFile = function(path, success, error, asLibrary, checkExists, knownRefPos)
380{
381	asLibrary = (asLibrary != null) ? asLibrary : false;
382
383	this.getRefIndex(path.split('/'), false, mxUtils.bind(this, function(tokens, refPos)
384	{
385		var repoPos = Math.max(refPos - 1, 0);
386		var org = tokens.slice(0, repoPos).join('/');
387		var repo = tokens[repoPos];
388		var ref = tokens[refPos];
389		path = tokens.slice(refPos + 1, tokens.length).join('/');
390		var binary = /\.png$/i.test(path);
391
392		// Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file
393		if (!checkExists && (/\.v(dx|sdx?)$/i.test(path) || /\.gliffy$/i.test(path) ||
394			/\.pdf$/i.test(path) || (!this.ui.useCanvasForExport && binary)))
395		{
396			// Should never be null
397			if (_token != null)
398			{
399				// Adds random parameter to bypass cache
400				var rnd = '&t=' + new Date().getTime();
401				var url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
402					'/repository/files/' + encodeURIComponent(path) + '?ref=' + ref;
403				var tokens = path.split('/');
404				var name = (tokens.length > 0) ? tokens[tokens.length - 1] : path;
405
406				this.ui.convertFile(url + rnd, name, null, this.extension, success, error, mxUtils.bind(this, function(url, cb, err)
407				{
408					var req = new mxXmlRequest(url, null, 'GET');
409
410					this.executeRequest(req, mxUtils.bind(this, function(req)
411					{
412						try
413						{
414							cb(this.getFileContent(JSON.parse(req.getText())));
415						}
416						catch (e)
417						{
418							err(e);
419						}
420					}), err);
421				}));
422			}
423			else
424			{
425				error({message: mxResources.get('accessDenied')});
426			}
427		}
428		else
429		{
430			// Adds random parameter to bypass cache
431			var rnd = '&t=' + new Date().getTime();
432			url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
433				'/repository/files/' + encodeURIComponent(path) + '?ref=' + ref;
434			var req = new mxXmlRequest(url + rnd, null, 'GET');
435
436			this.executeRequest(req, mxUtils.bind(this, function(req)
437			{
438				try
439				{
440					success(this.createGitLabFile(org, repo, ref, JSON.parse(req.getText()), asLibrary, refPos));
441				}
442				catch (e)
443				{
444					error(e);
445				}
446			}), error);
447		}
448	}), error, knownRefPos);
449};
450
451/**
452 * Translates this point by the given vector.
453 *
454 * @param {number} dx X-coordinate of the translation.
455 * @param {number} dy Y-coordinate of the translation.
456 */
457GitLabClient.prototype.getFileContent = function(data)
458{
459	var fileName = data.file_name;
460	var content = data.content;
461
462	if (data.encoding === 'base64')
463	{
464		if (/\.jpe?g$/i.test(fileName))
465		{
466			content = 'data:image/jpeg;base64,' + content;
467		}
468		else if (/\.gif$/i.test(fileName))
469		{
470			content = 'data:image/gif;base64,' + content;
471		}
472		else if (/\.pdf$/i.test(fileName))
473		{
474			content = 'data:application/pdf;base64,' + content;
475		}
476		else
477		{
478			if (/\.png$/i.test(fileName))
479			{
480				var xml = this.ui.extractGraphModelFromPng(content);
481
482				if (xml != null && xml.length > 0)
483				{
484					content = xml;
485				}
486				else
487				{
488					content = 'data:image/png;base64,' + content;
489				}
490			}
491			else
492			{
493				content = Base64.decode(content);
494			}
495		}
496	}
497
498	return content;
499};
500
501/**
502 * Translates this point by the given vector.
503 *
504 * @param {number} dx X-coordinate of the translation.
505 * @param {number} dy Y-coordinate of the translation.
506 */
507GitLabClient.prototype.createGitLabFile = function(org, repo, ref, data, asLibrary, refPos)
508{
509	var gitLabUrl = DRAWIO_GITLAB_URL + '/';
510	var htmlUrl = gitLabUrl + org + '/' + repo + '/blob/' + ref + '/' + data.file_path;
511	var downloadUrl = gitLabUrl + org + '/' + repo + '/raw/' + ref + '/' + data.file_path + '?inline=false';
512	var fileName = data.file_name;
513
514	var meta = {'org': org, 'repo': repo, 'ref': ref, 'name': fileName,
515		'path': data.file_path, 'html_url': htmlUrl, 'download_url': downloadUrl,
516		'last_commit_id': data.last_commit_id, 'refPos': refPos};
517	var content = this.getFileContent(data);
518
519	return (asLibrary) ? new GitLabLibrary(this.ui, content, meta) : new GitLabFile(this.ui, content, meta);
520};
521
522/**
523 * Translates this point by the given vector.
524 *
525 * @param {number} dx X-coordinate of the translation.
526 * @param {number} dy Y-coordinate of the translation.
527 */
528GitLabClient.prototype.insertFile = function(filename, data, success, error, asLibrary, folderId, base64Encoded)
529{
530	asLibrary = (asLibrary != null) ? asLibrary : false;
531	var tok = folderId.split('/');
532
533	this.getRefIndex(tok, true, mxUtils.bind(this, function(tokens, refPos)
534	{
535		var repoPos = Math.max(refPos - 1, 0);
536		var org = tokens.slice(0, repoPos).join('/');
537		var repo = tokens[repoPos];
538		var ref = tokens[refPos];
539		path = tokens.slice(refPos + 1, tokens.length).join('/');
540
541		if (path.length > 0)
542		{
543			path = path + '/';
544		}
545
546		path = path + filename;
547
548		this.checkExists(org + '/' + repo + '/' + ref + '/' + path, true, mxUtils.bind(this, function(checked, last_commit_id)
549		{
550			if (checked)
551			{
552				// Does not insert file here as there is another writeFile implicit via fileCreated
553				if (!asLibrary)
554				{
555					var gitLabUrl = DRAWIO_GITLAB_URL + '/';
556					var htmlUrl = gitLabUrl + org + '/' + repo + '/blob/' + ref + '/' + path;
557					var downloadUrl = gitLabUrl + org + '/' + repo + '/raw/' + ref + '/' + path + '?inline=false';
558
559					success(new GitLabFile(this.ui, data, {'org': org, 'repo': repo, 'ref': ref, 'name': filename,
560						'path': path, 'html_url': htmlUrl, 'download_url': downloadUrl, 'refPos': refPos,
561						'last_commit_id': last_commit_id, isNew: true}));
562				}
563				else
564				{
565					if (!base64Encoded)
566					{
567						data = Base64.encode(data);
568					}
569
570					this.showCommitDialog(filename, true, mxUtils.bind(this, function(message)
571					{
572						this.writeFile(org, repo, ref, path, message, data, last_commit_id, mxUtils.bind(this, function(req)
573						{
574							try
575							{
576								var msg = JSON.parse(req.getText());
577
578								success(this.createGitLabFile(org, repo, ref,
579									(msg.content != null) ? msg.content : msg,
580									asLibrary, refPos));
581							}
582							catch (e)
583							{
584								error(e);
585							}
586						}), error);
587					}), error);
588				}
589			}
590			else
591			{
592				// create if it does not exists
593				error();
594			}
595		}))
596	}), error, null, tok.length <= 4);
597};
598
599/**
600 * Translates this point by the given vector.
601 *
602 * @param {number} dx X-coordinate of the translation.
603 * @param {number} dy Y-coordinate of the translation.
604 */
605GitLabClient.prototype.checkExists = function(path, askReplace, fn)
606{
607	this.getFile(path, mxUtils.bind(this, function(file)
608	{
609		if (askReplace)
610		{
611			var resume = this.ui.spinner.pause();
612
613			this.ui.confirm(mxResources.get('replaceIt', [path]), function()
614			{
615				resume();
616				fn(true, file.getCurrentEtag());
617			}, function()
618			{
619				resume();
620				fn(false);
621			});
622		}
623		else
624		{
625			this.ui.spinner.stop();
626
627			this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function()
628			{
629				fn(false);
630			});
631		}
632	}), mxUtils.bind(this, function(err)
633	{
634		fn(true);
635	}), null, true);
636};
637
638/**
639 *
640 */
641GitLabClient.prototype.writeFile = function(org, repo, ref, path, message, data, last_commit_id, success, error)
642{
643	if (data.length >= this.maxFileSize)
644	{
645		error({message: mxResources.get('drawingTooLarge') + ' (' +
646			this.ui.formatFileSize(data.length) + ' / 10 MB)'});
647	}
648	else
649	{
650		var method = 'POST';
651
652		var entity = {
653			path: encodeURIComponent(path),
654			branch: decodeURIComponent(ref),
655			commit_message: message,
656			content: data,
657			encoding: 'base64'
658		};
659
660		if (last_commit_id != null)
661		{
662			entity.last_commit_id = last_commit_id;
663			method = 'PUT';
664		}
665
666		// See https://docs.gitlab.com/ee/api/repository_files.html#update-existing-file-in-repository
667		var url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) + '/repository/files/' + encodeURIComponent(path);
668		var req = new mxXmlRequest(url, JSON.stringify(entity), method);
669
670		this.executeRequest(req, mxUtils.bind(this, function(req)
671		{
672			success(req);
673		}), error);
674	}
675};
676
677/**
678 * Translates this point by the given vector.
679 *
680 * @param {number} dx X-coordinate of the translation.
681 * @param {number} dy Y-coordinate of the translation.
682 */
683GitLabClient.prototype.saveFile = function(file, success, error, overwrite, message)
684{
685	var org = file.meta.org;
686	var repo = file.meta.repo;
687	var ref = file.meta.ref;
688	var path = file.meta.path;
689
690	var fn = mxUtils.bind(this, function(last_commit_id, data)
691	{
692		this.writeFile(org, repo, ref, path, message, data, last_commit_id, mxUtils.bind(this, function(req)
693		{
694			delete file.meta.isNew;
695
696			// Response does not return last_commit_id so we have to get the file
697			// to to update last_commit_id and compare data to avoid lost commit
698			this.getFile(org + '/' + repo + '/' + ref + '/' + path, mxUtils.bind(this, function(tempFile)
699			{
700				if (tempFile.getData() == file.getData())
701				{
702					success(tempFile.getCurrentEtag());
703				}
704				else
705				{
706					success({content: file.getCurrentEtag()});
707				}
708			}), error, null, null, file.meta.refPos);
709		}), error);
710	});
711
712	var fn2 = mxUtils.bind(this, function()
713	{
714		if (this.ui.useCanvasForExport && /(\.png)$/i.test(path))
715		{
716			var p = this.ui.getPngFileProperties(this.ui.fileNode);
717
718			this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
719			{
720				fn(file.meta.last_commit_id, data);
721			}), error, (this.ui.getCurrentFile() != file) ?
722				file.getData() : null, p.scale, p.border);
723		}
724		else
725		{
726			fn(file.meta.last_commit_id, Base64.encode(file.getData()));
727		}
728	});
729
730	// LATER: Get last_commit_id is currently not possible since HEAD does
731	// not have Access-Control-Expose-Headers for X-Gitlab-Last-Commit-Id
732	if (overwrite)
733	{
734		this.getFile(org + '/' + repo + '/' + ref + '/' + path, mxUtils.bind(this, function(tempFile)
735		{
736			file.meta.last_commit_id = tempFile.meta.last_commit_id;
737			fn2();
738		}), error);
739	}
740	else
741	{
742		fn2();
743	}
744};
745
746/**
747 * Checks if the client is authorized and calls the next step.
748 */
749GitLabClient.prototype.pickFolder = function(fn)
750{
751	this.showGitLabDialog(false, fn);
752};
753
754/**
755 * Checks if the client is authorized and calls the next step.
756 */
757GitLabClient.prototype.pickFile = function(fn)
758{
759	fn = (fn != null) ? fn : mxUtils.bind(this, function(path)
760	{
761		this.ui.loadFile('A' + encodeURIComponent(path));
762	});
763
764	this.showGitLabDialog(true, fn);
765};
766
767/**
768 * LATER: Refactor to use common code with GitHubClient
769 */
770GitLabClient.prototype.showGitLabDialog = function(showFiles, fn)
771{
772	var org = null;
773	var repo = null;
774	var ref = null;
775	var path = null;
776
777	var content = document.createElement('div');
778	content.style.whiteSpace = 'nowrap';
779	content.style.overflow = 'hidden';
780	content.style.height = '304px';
781
782	var hd = document.createElement('h3');
783	mxUtils.write(hd, mxResources.get((showFiles) ? 'selectFile' : 'selectFolder'));
784	hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
785	content.appendChild(hd);
786
787	var div = document.createElement('div');
788	div.style.whiteSpace = 'nowrap';
789	div.style.border = '1px solid lightgray';
790	div.style.boxSizing = 'border-box';
791	div.style.padding = '4px';
792	div.style.overflow = 'auto';
793	div.style.lineHeight = '1.2em';
794	div.style.height = '274px';
795	content.appendChild(div);
796
797	var listItem = document.createElement('div');
798	listItem.style.textOverflow = 'ellipsis';
799	listItem.style.boxSizing = 'border-box';
800	listItem.style.overflow = 'hidden';
801	listItem.style.padding = '4px';
802	listItem.style.width = '100%';
803
804	var dlg = new CustomDialog(this.ui, content, mxUtils.bind(this, function()
805	{
806		fn(org + '/' + repo + '/' + encodeURIComponent(ref) + '/' + path);
807	}));
808	this.ui.showDialog(dlg.container, 420, 370, true, true);
809
810	if (showFiles)
811	{
812		dlg.okButton.parentNode.removeChild(dlg.okButton);
813	}
814
815	var createLink = mxUtils.bind(this, function(label, fn, padding, underline)
816	{
817		var link = document.createElement('a');
818		link.setAttribute('title', label);
819		link.style.cursor = 'pointer';
820		mxUtils.write(link,  label);
821		mxEvent.addListener(link, 'click', fn);
822
823		if (underline)
824		{
825			link.style.textDecoration = 'underline';
826		}
827
828		if (padding != null)
829		{
830			var temp = listItem.cloneNode();
831			temp.style.padding = padding;
832			temp.appendChild(link);
833
834			link = temp;
835		}
836
837		return link;
838	});
839
840	var updatePathInfo = mxUtils.bind(this, function(hideRef)
841	{
842		var pathInfo = document.createElement('div');
843		pathInfo.style.marginBottom = '8px';
844
845		pathInfo.appendChild(createLink(org + '/' + repo, mxUtils.bind(this, function()
846		{
847			path = null;
848			selectRepo();
849		}), null, true));
850
851		if (!hideRef)
852		{
853			mxUtils.write(pathInfo, ' / ');
854			pathInfo.appendChild(createLink(decodeURIComponent(ref), mxUtils.bind(this, function()
855			{
856				path = null;
857				selectRef();
858			}), null, true));
859		}
860
861		if (path != null && path.length > 0)
862		{
863			var tokens = path.split('/');
864
865			for (var i = 0; i < tokens.length; i++)
866			{
867				(function(index)
868				{
869					mxUtils.write(pathInfo, ' / ');
870					pathInfo.appendChild(createLink(tokens[index], mxUtils.bind(this, function()
871					{
872						path = tokens.slice(0, index + 1).join('/');
873						selectFile();
874					}), null, true));
875				})(i);
876			}
877		}
878
879		div.appendChild(pathInfo);
880	});
881
882	var error = mxUtils.bind(this, function(err)
883	{
884		this.ui.handleError(err, null, mxUtils.bind(this, function()
885		{
886			this.ui.spinner.stop();
887
888			if (this.getUser() != null)
889			{
890				org = null;
891				repo = null;
892				ref = null;
893				path = null;
894
895				selectRepo();
896			}
897			else
898			{
899				this.ui.hideDialog();
900			}
901		}));
902	});
903
904	// Adds paging for repos, branches and files
905	var nextPageDiv = null;
906	var scrollFn = null;
907	var pageSize = 100;
908
909	var selectFile = mxUtils.bind(this, function(page)
910	{
911		if (page == null)
912		{
913			div.innerHTML = '';
914			page = 1;
915		}
916
917		var req = new mxXmlRequest(this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
918			'/repository/tree?path=' + path + '&ref=' + ref + '&per_page=' + pageSize + '&page=' + page, null, 'GET');
919		this.ui.spinner.spin(div, mxResources.get('loading'));
920		dlg.okButton.removeAttribute('disabled');
921
922		if (scrollFn != null)
923		{
924			mxEvent.removeListener(div, 'scroll', scrollFn);
925			scrollFn = null;
926		}
927
928		if (nextPageDiv != null && nextPageDiv.parentNode != null)
929		{
930			nextPageDiv.parentNode.removeChild(nextPageDiv);
931		}
932
933		nextPageDiv = document.createElement('a');
934		nextPageDiv.style.display = 'block';
935		nextPageDiv.style.cursor = 'pointer';
936		mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
937
938		var nextPage = mxUtils.bind(this, function()
939		{
940			selectFile(page + 1);
941		});
942
943		mxEvent.addListener(nextPageDiv, 'click', nextPage);
944
945		this.executeRequest(req, mxUtils.bind(this, function(req)
946		{
947			this.ui.spinner.stop();
948
949			if (page == 1)
950			{
951				updatePathInfo(!ref);
952
953				div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function()
954				{
955					if (path == '')
956					{
957						path = null;
958						selectRepo();
959					}
960					else
961					{
962						var tokens = path.split('/');
963						path = tokens.slice(0, tokens.length - 1).join('/');
964						selectFile();
965					}
966				}), '4px'));
967			}
968
969			var files = JSON.parse(req.getText());
970
971			if (files == null || files.length == 0)
972			{
973				mxUtils.write(div, mxResources.get('noFiles'));
974			}
975			else
976			{
977				var gray = true;
978				var count = 0;
979
980				var listFiles = mxUtils.bind(this, function(showFolders)
981				{
982					for (var i = 0; i < files.length; i++)
983					{
984						(mxUtils.bind(this, function(file)
985						{
986							if (showFolders == (file.type == 'tree'))
987							{
988								var temp = listItem.cloneNode();
989								temp.style.backgroundColor = (gray) ?
990									((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : '';
991								gray = !gray;
992
993								var typeImg = document.createElement('img');
994								typeImg.src = IMAGE_PATH + '/' + (file.type == 'tree'? 'folder.png' : 'file.png');
995								typeImg.setAttribute('align', 'absmiddle');
996								typeImg.style.marginRight = '4px';
997								typeImg.style.marginTop = '-4px';
998								typeImg.width = 20;
999								temp.appendChild(typeImg);
1000
1001								temp.appendChild(createLink(file.name + ((file.type == 'tree') ? '/' : ''), mxUtils.bind(this, function()
1002								{
1003									if (file.type == 'tree')
1004									{
1005										path = file.path;
1006										selectFile();
1007									}
1008									else if (showFiles && file.type == 'blob')
1009									{
1010										this.ui.hideDialog();
1011										fn(org + '/' + repo + '/' + ref + '/' + file.path);
1012									}
1013								})));
1014
1015								div.appendChild(temp);
1016								count++;
1017							}
1018						}))(files[i]);
1019					}
1020				});
1021
1022				listFiles(true);
1023
1024				if (showFiles)
1025				{
1026					listFiles(false);
1027				}
1028
1029				if (count == pageSize)
1030				{
1031					div.appendChild(nextPageDiv);
1032
1033					scrollFn = function()
1034					{
1035						if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
1036						{
1037							nextPage();
1038						}
1039					};
1040
1041					mxEvent.addListener(div, 'scroll', scrollFn);
1042				}
1043			}
1044		}), error, true);
1045	});
1046
1047	var selectRef = mxUtils.bind(this, function(page, auto)
1048	{
1049		if (page == null)
1050		{
1051			div.innerHTML = '';
1052			page = 1;
1053		}
1054
1055		var req = new mxXmlRequest(this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
1056			'/repository/branches?per_page=' + pageSize + '&page=' + page, null, 'GET');
1057		dlg.okButton.setAttribute('disabled', 'disabled');
1058		this.ui.spinner.spin(div, mxResources.get('loading'));
1059
1060		if (scrollFn != null)
1061		{
1062			mxEvent.removeListener(div, 'scroll', scrollFn);
1063			scrollFn = null;
1064		}
1065
1066		if (nextPageDiv != null && nextPageDiv.parentNode != null)
1067		{
1068			nextPageDiv.parentNode.removeChild(nextPageDiv);
1069		}
1070
1071		nextPageDiv = document.createElement('a');
1072		nextPageDiv.style.display = 'block';
1073		nextPageDiv.style.cursor = 'pointer';
1074		mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
1075
1076		var nextPage = mxUtils.bind(this, function()
1077		{
1078			selectRef(page + 1);
1079		});
1080
1081		mxEvent.addListener(nextPageDiv, 'click', nextPage);
1082
1083		this.executeRequest(req, mxUtils.bind(this, function(req)
1084		{
1085			this.ui.spinner.stop();
1086
1087			if (page == 1)
1088			{
1089				updatePathInfo(true);
1090
1091				div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function()
1092				{
1093					path = null;
1094					selectRepo();
1095				}), '4px'));
1096			}
1097
1098			var branches = JSON.parse(req.getText());
1099
1100			if (branches == null || branches.length == 0)
1101			{
1102				mxUtils.write(div, mxResources.get('noFiles'));
1103			}
1104			else if (branches.length == 1 && auto)
1105			{
1106				ref = branches[0].name;
1107				path = '';
1108				selectFile();
1109			}
1110			else
1111			{
1112				for (var i = 0; i < branches.length; i++)
1113				{
1114					(mxUtils.bind(this, function(branch, idx)
1115					{
1116						var temp = listItem.cloneNode();
1117						temp.style.backgroundColor = (idx % 2 == 0) ?
1118							((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : '';
1119
1120						temp.appendChild(createLink(branch.name, mxUtils.bind(this, function()
1121						{
1122							ref = encodeURIComponent(branch.name);
1123							path = '';
1124							selectFile();
1125						})));
1126
1127						div.appendChild(temp);
1128					}))(branches[i], i);
1129				}
1130
1131				if (branches.length == pageSize)
1132				{
1133					div.appendChild(nextPageDiv);
1134
1135					scrollFn = function()
1136					{
1137						if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
1138						{
1139							nextPage();
1140						}
1141					};
1142
1143					mxEvent.addListener(div, 'scroll', scrollFn);
1144				}
1145			}
1146		}), error);
1147	});
1148
1149	dlg.okButton.setAttribute('disabled', 'disabled');
1150	this.ui.spinner.spin(div, mxResources.get('loading'));
1151
1152	var selectRepo = mxUtils.bind(this, function(page)
1153	{
1154		var spinner = this.ui.spinner;
1155		var inFlightRequests = 0;
1156		this.ui.spinner.stop();
1157
1158		var spinnerRequestStarted = function()
1159		{
1160			spinner.spin(div, mxResources.get('loading'));
1161			inFlightRequests += 1;
1162		}
1163
1164		var spinnerRequestFinished = function()
1165		{
1166			inFlightRequests -= 1;
1167
1168			if (inFlightRequests === 0)
1169			{
1170				spinner.stop();
1171			}
1172		}
1173
1174		if (page == null)
1175		{
1176			div.innerHTML = '';
1177			page = 1;
1178		}
1179
1180		if (scrollFn != null)
1181		{
1182			mxEvent.removeListener(div, 'scroll', scrollFn);
1183			scrollFn = null;
1184		}
1185
1186		if (nextPageDiv != null && nextPageDiv.parentNode != null)
1187		{
1188			nextPageDiv.parentNode.removeChild(nextPageDiv);
1189		}
1190
1191		nextPageDiv = document.createElement('a');
1192		nextPageDiv.style.display = 'block';
1193		nextPageDiv.style.cursor = 'pointer';
1194		mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
1195
1196		var nextPage = mxUtils.bind(this, function()
1197		{
1198			if (inFlightRequests === 0)
1199			{
1200				selectRepo(page + 1);
1201			}
1202		});
1203
1204		mxEvent.addListener(nextPageDiv, 'click', nextPage);
1205
1206		var listGroups = mxUtils.bind(this, function(callback)
1207		{
1208			spinnerRequestStarted();
1209			var req = new mxXmlRequest(this.baseUrl + '/groups?per_page=100', null, 'GET');
1210
1211			this.executeRequest(req, mxUtils.bind(this, function(req)
1212			{
1213				callback(JSON.parse(req.getText()));
1214				spinnerRequestFinished();
1215			}), error);
1216		});
1217
1218		var listProjects = mxUtils.bind(this, function(group, callback)
1219		{
1220			spinnerRequestStarted();
1221			var req = new mxXmlRequest(this.baseUrl + '/groups/' + group.id + '/projects?per_page=100', null, 'GET');
1222
1223			this.executeRequest(req, mxUtils.bind(this, function(req)
1224			{
1225				callback(group, JSON.parse(req.getText()));
1226				spinnerRequestFinished();
1227			}), error);
1228		});
1229
1230		listGroups(mxUtils.bind(this, function(groups)
1231		{
1232			if (this.user == null)
1233			{
1234				mxUtils.write(div, mxResources.get('loggedOut'));
1235			}
1236			else
1237			{
1238				spinnerRequestStarted();
1239				var req = new mxXmlRequest(this.baseUrl + '/users/' + this.user.id + '/projects?per_page=' +
1240					pageSize + '&page=' + page, null, 'GET');
1241
1242				this.executeRequest(req, mxUtils.bind(this, function(req)
1243				{
1244					var repos = JSON.parse(req.getText());
1245
1246					if ((repos == null || repos.length == 0) && (groups == null || groups.length == 0))
1247					{
1248						spinnerRequestFinished();
1249						mxUtils.write(div, mxResources.get('noFiles'));
1250					}
1251					else
1252					{
1253						if (page == 1)
1254						{
1255							div.appendChild(createLink(mxResources.get('enterValue') + '...', mxUtils.bind(this, function()
1256							{
1257								if (inFlightRequests === 0)
1258								{
1259									var dlg = new FilenameDialog(this.ui, 'org/repo/ref', mxResources.get('ok'), mxUtils.bind(this, function(value)
1260									{
1261										if (value != null)
1262										{
1263											var tokens = value.split('/');
1264
1265											if (tokens.length > 1)
1266											{
1267												org = tokens[0];
1268												repo = tokens[1];
1269												path = null;
1270												ref = null;
1271
1272												if (tokens.length > 2)
1273												{
1274													ref = encodeURIComponent(tokens.slice(2, tokens.length).join('/'));
1275													selectFile();
1276												}
1277												else
1278												{
1279													selectRef(null, true);
1280												}
1281											}
1282											else
1283											{
1284												this.ui.spinner.stop();
1285												this.ui.handleError({message: mxResources.get('invalidName')});
1286											}
1287										}
1288									}), mxResources.get('enterValue'));
1289									this.ui.showDialog(dlg.container, 300, 80, true, false);
1290									dlg.init();
1291								}
1292							})));
1293
1294							mxUtils.br(div);
1295							mxUtils.br(div);
1296						}
1297
1298						var gray = true;
1299
1300						for (var i = 0; i < repos.length; i++)
1301						{
1302							(mxUtils.bind(this, function(repository)
1303							{
1304								var temp = listItem.cloneNode();
1305								temp.style.backgroundColor = (gray) ?
1306									((Editor.isDarkMode()) ? '#000000' : '#eeeeee') : '';
1307								gray = !gray;
1308
1309								temp.appendChild(createLink(repository.name_with_namespace, mxUtils.bind(this, function()
1310								{
1311									if (inFlightRequests === 0)
1312									{
1313										org = repository.owner.username;
1314										repo = repository.path;
1315										path = '';
1316
1317										selectRef(null, true);
1318									}
1319								})));
1320
1321								div.appendChild(temp);
1322							}))(repos[i]);
1323						}
1324
1325						for (var i = 0; i < groups.length; i++)
1326						{
1327							spinnerRequestStarted();
1328
1329							listProjects(groups[i], (mxUtils.bind(this, function(group, projects)
1330							{
1331								spinnerRequestFinished();
1332
1333								for (var j = 0; j < projects.length; j++)
1334								{
1335									var temp = listItem.cloneNode();
1336									temp.style.backgroundColor = (gray) ?
1337										((uiTheme == 'dark') ? '#000000' : '#eeeeee') : '';
1338									gray = !gray;
1339
1340									(mxUtils.bind(this, function(project)
1341									{
1342										temp.appendChild(createLink(project.name_with_namespace, mxUtils.bind(this, function()
1343										{
1344											if (inFlightRequests === 0)
1345											{
1346												org = group.full_path;
1347												repo = project.path;
1348												path = '';
1349
1350												selectRef(null, true);
1351											}
1352										})));
1353
1354										div.appendChild(temp);
1355									}))(projects[j]);
1356								}
1357							})));
1358						}
1359
1360						spinnerRequestFinished();
1361					}
1362
1363					if (repos.length == pageSize)
1364					{
1365						div.appendChild(nextPageDiv);
1366
1367						scrollFn = function()
1368						{
1369							if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
1370							{
1371								nextPage();
1372							}
1373						};
1374
1375						mxEvent.addListener(div, 'scroll', scrollFn);
1376					}
1377				}), error);
1378			}
1379		}));
1380	});
1381
1382	if (!_token)
1383	{
1384		this.authenticate(mxUtils.bind(this, function()
1385		{
1386			this.updateUser(function()
1387			{
1388				selectRepo();
1389			}, error, true);
1390		}), error);
1391	}
1392	else if (!this.user)
1393	{
1394		this.updateUser(function()
1395		{
1396			selectRepo();
1397		}, error, true);
1398	}
1399	else
1400	{
1401		selectRepo();
1402	}
1403};
1404
1405/**
1406 * Checks if the client is authorized and calls the next step.
1407 */
1408GitLabClient.prototype.logout = function()
1409{
1410	//Send to server to clear refresh token cookie
1411	this.ui.editor.loadUrl(this.redirectUri + '?doLogout=1&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname));
1412	this.clearPersistentToken();
1413	this.setUser(null);
1414	_token = null;
1415	this.setToken(null);
1416};
1417
1418})();