GitLabClient.js 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163
  1. /**
  2. * Copyright (c) 2006-2019, JGraph Ltd
  3. * Copyright (c) 2006-2019, draw.io AG
  4. */
  5. GitLabClient = function(editorUi)
  6. {
  7. GitHubClient.call(this, editorUi, 'gitlabauth');
  8. };
  9. // Extends DrawioClient
  10. mxUtils.extend(GitLabClient, GitHubClient);
  11. /**
  12. * Gitlab Client ID, see https://gitlab.com/oauth/applications/135239
  13. */
  14. GitLabClient.prototype.clientId = DRAWIO_GITLAB_ID;
  15. /**
  16. * OAuth scope.
  17. */
  18. GitLabClient.prototype.scope = 'api%20read_repository%20write_repository';
  19. /**
  20. * Base URL for API calls.
  21. */
  22. GitLabClient.prototype.baseUrl = DRAWIO_GITLAB_URL + '/api/v4';
  23. /**
  24. * Authorizes the client, gets the userId and calls <open>.
  25. */
  26. GitLabClient.prototype.authenticate = function(success, error)
  27. {
  28. if (window.onGitLabCallback == null)
  29. {
  30. var auth = mxUtils.bind(this, function()
  31. {
  32. var acceptAuthResponse = true;
  33. this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, authSuccess)
  34. {
  35. var state = '123';
  36. var redirectUri = encodeURIComponent(window.location.origin + '/gitlab.html');
  37. var win = window.open(DRAWIO_GITLAB_URL + '/oauth/authorize?client_id=' +
  38. this.clientId + '&scope=' + this.scope + '&redirect_uri=' + redirectUri +
  39. '&response_type=token&state=' + state, 'gitlabauth');
  40. if (win != null)
  41. {
  42. window.onGitLabCallback = mxUtils.bind(this, function(code, authWindow)
  43. {
  44. if (acceptAuthResponse)
  45. {
  46. window.onGitLabCallback = null;
  47. acceptAuthResponse = false;
  48. if (code == null)
  49. {
  50. error({message: mxResources.get('accessDenied'), retry: auth});
  51. }
  52. else
  53. {
  54. if (authSuccess != null)
  55. {
  56. authSuccess();
  57. }
  58. this.token = code;
  59. this.setUser(null);
  60. if (remember)
  61. {
  62. this.setPersistentToken(this.token);
  63. }
  64. success();
  65. }
  66. }
  67. else if (authWindow != null)
  68. {
  69. authWindow.close();
  70. }
  71. });
  72. }
  73. else
  74. {
  75. error({message: mxResources.get('serviceUnavailableOrBlocked'), retry: auth});
  76. }
  77. }), mxUtils.bind(this, function()
  78. {
  79. if (acceptAuthResponse)
  80. {
  81. window.onGitLabCallback = null;
  82. acceptAuthResponse = false;
  83. error({message: mxResources.get('accessDenied'), retry: auth});
  84. }
  85. }));
  86. });
  87. auth();
  88. }
  89. else
  90. {
  91. error({code: App.ERROR_BUSY});
  92. }
  93. };
  94. /**
  95. * Authorizes the client, gets the userId and calls <open>.
  96. */
  97. GitLabClient.prototype.executeRequest = function(req, success, error, ignoreNotFound)
  98. {
  99. var doExecute = mxUtils.bind(this, function(failOnAuth)
  100. {
  101. var acceptResponse = true;
  102. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  103. {
  104. acceptResponse = false;
  105. error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
  106. }), this.ui.timeout);
  107. var temp = this.token;
  108. req.setRequestHeaders = function(request, params)
  109. {
  110. request.setRequestHeader('Authorization', 'Bearer ' + temp);
  111. request.setRequestHeader('PRIVATE_TOKEN', temp);
  112. request.setRequestHeader('Content-Type', 'application/json');
  113. };
  114. req.send(mxUtils.bind(this, function()
  115. {
  116. window.clearTimeout(timeoutThread);
  117. if (acceptResponse)
  118. {
  119. if ((req.getStatus() >= 200 && req.getStatus() <= 299) ||
  120. (ignoreNotFound && req.getStatus() == 404))
  121. {
  122. success(req);
  123. }
  124. else if (req.getStatus() === 401)
  125. {
  126. if (!failOnAuth)
  127. {
  128. this.authenticate(function()
  129. {
  130. doExecute(true);
  131. }, error);
  132. }
  133. else
  134. {
  135. error({message: mxResources.get('accessDenied'), retry: mxUtils.bind(this, function()
  136. {
  137. this.authenticate(function()
  138. {
  139. fn(true);
  140. }, error);
  141. })});
  142. }
  143. }
  144. else if (req.getStatus() === 403)
  145. {
  146. var tooLarge = false;
  147. try
  148. {
  149. var temp = JSON.parse(req.getText());
  150. if (temp != null && temp.errors != null && temp.errors.length > 0)
  151. {
  152. tooLarge = temp.errors[0].code == 'too_large';
  153. }
  154. }
  155. catch (e)
  156. {
  157. // ignore
  158. }
  159. error({message: mxResources.get((tooLarge) ? 'drawingTooLarge' : 'forbidden')});
  160. }
  161. else if (req.getStatus() === 404)
  162. {
  163. error({message: this.getErrorMessage(req, mxResources.get('fileNotFound'))});
  164. }
  165. else if (req.getStatus() === 400)
  166. {
  167. // Special case: flag to the caller that there was a conflict
  168. error({status: 400});
  169. }
  170. else
  171. {
  172. error({status: req.getStatus(), message: this.getErrorMessage(req,
  173. mxResources.get('error') + ' ' + req.getStatus())});
  174. }
  175. }
  176. }), error);
  177. });
  178. var fn = mxUtils.bind(this, function(failOnAuth)
  179. {
  180. if (this.user == null)
  181. {
  182. this.updateUser(function()
  183. {
  184. fn(true);
  185. }, error, failOnAuth);
  186. }
  187. else
  188. {
  189. doExecute(failOnAuth);
  190. }
  191. });
  192. if (this.token == null)
  193. {
  194. this.authenticate(function()
  195. {
  196. fn(true);
  197. }, error);
  198. }
  199. else
  200. {
  201. fn(false);
  202. }
  203. };
  204. /**
  205. * Finds index of ref in given token list. This is required to support groups and subgroups.
  206. */
  207. GitLabClient.prototype.getRefIndex = function(tokens, isFolder, success, error, knownRefPos)
  208. {
  209. if (knownRefPos != null)
  210. {
  211. success(tokens, knownRefPos);
  212. }
  213. else
  214. {
  215. var refPos = tokens.length - 2;
  216. // Finds ref in token list by checking which URL works
  217. var checkUrl = mxUtils.bind(this, function()
  218. {
  219. if (refPos < 2)
  220. {
  221. error({message: mxResources.get('fileNotFound')});
  222. }
  223. else
  224. {
  225. var repoPos = Math.max(refPos - 1, 0);
  226. var org = tokens.slice(0, repoPos).join('/');
  227. var repo = tokens[repoPos];
  228. var ref = tokens[refPos];
  229. var path = tokens.slice(refPos + 1, tokens.length).join('/');
  230. var url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) + '/repository/' +
  231. (!isFolder ? 'files/' + encodeURIComponent(path) + '?ref=' + ref :
  232. 'tree?path=' + path + '&ref=' + ref);
  233. var req = new mxXmlRequest(url, null, 'HEAD');
  234. this.executeRequest(req, mxUtils.bind(this, function()
  235. {
  236. if (req.getStatus() == 200)
  237. {
  238. success(tokens, refPos);
  239. }
  240. else
  241. {
  242. error({message: mxResources.get('fileNotFound')});
  243. }
  244. }), mxUtils.bind(this, function()
  245. {
  246. if (req.getStatus() == 404)
  247. {
  248. refPos--;
  249. checkUrl();
  250. }
  251. else
  252. {
  253. error({message: mxResources.get('fileNotFound')});
  254. }
  255. }));
  256. }
  257. });
  258. checkUrl();
  259. }
  260. };
  261. /**
  262. * Checks if the client is authorized and calls the next step.
  263. */
  264. GitLabClient.prototype.getFile = function(path, success, error, asLibrary, checkExists, knownRefPos)
  265. {
  266. asLibrary = (asLibrary != null) ? asLibrary : false;
  267. this.getRefIndex(path.split('/'), false, mxUtils.bind(this, function(tokens, refPos)
  268. {
  269. var repoPos = Math.max(refPos - 1, 0);
  270. var org = tokens.slice(0, repoPos).join('/');
  271. var repo = tokens[repoPos];
  272. var ref = tokens[refPos];
  273. path = tokens.slice(refPos + 1, tokens.length).join('/');
  274. var binary = /\.png$/i.test(path);
  275. // Handles .vsdx, Gliffy and PNG+XML files by creating a temporary file
  276. if (!checkExists && (/\.v(dx|sdx?)$/i.test(path) || /\.gliffy$/i.test(path) ||
  277. (!this.ui.useCanvasForExport && binary)))
  278. {
  279. // Should never be null
  280. if (this.token != null)
  281. {
  282. var url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) + '/repository/files/' + encodeURIComponent(ref);
  283. var tokens = path.split('/');
  284. var name = (tokens.length > 0) ? tokens[tokens.length - 1] : path;
  285. this.ui.convertFile(url, name, null, this.extension, success, error);
  286. }
  287. else
  288. {
  289. error({message: mxResources.get('accessDenied')});
  290. }
  291. }
  292. else
  293. {
  294. // Adds random parameter to bypass cache
  295. var rnd = '&t=' + new Date().getTime();
  296. url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
  297. '/repository/files/' + encodeURIComponent(path) + '?ref=' + ref;
  298. var req = new mxXmlRequest(url + rnd, null, 'GET');
  299. this.executeRequest(req, mxUtils.bind(this, function(req)
  300. {
  301. try
  302. {
  303. success(this.createGitLabFile(org, repo, ref, JSON.parse(req.getText()), asLibrary, refPos));
  304. }
  305. catch (e)
  306. {
  307. error(e);
  308. }
  309. }), error);
  310. }
  311. }), error, knownRefPos);
  312. };
  313. /**
  314. * Translates this point by the given vector.
  315. *
  316. * @param {number} dx X-coordinate of the translation.
  317. * @param {number} dy Y-coordinate of the translation.
  318. */
  319. GitLabClient.prototype.createGitLabFile = function(org, repo, ref, data, asLibrary, refPos)
  320. {
  321. var gitLabUrl = DRAWIO_GITLAB_URL + '/';
  322. var htmlUrl = gitLabUrl + org + '/' + repo + '/blob/' + ref + '/' + data.file_path;
  323. var downloadUrl = gitLabUrl + org + '/' + repo + '/raw/' + ref + '/' + data.file_path + '?inline=false';
  324. var fileName = data.file_name;
  325. var meta = {'org': org, 'repo': repo, 'ref': ref, 'name': fileName,
  326. 'path': data.file_path, 'html_url': htmlUrl, 'download_url': downloadUrl,
  327. 'last_commit_id': data.last_commit_id, 'refPos': refPos};
  328. var content = data.content;
  329. if (data.encoding === 'base64')
  330. {
  331. if (/\.jpe?g$/i.test(fileName))
  332. {
  333. content = 'data:image/jpeg;base64,' + content;
  334. }
  335. else if (/\.gif$/i.test(fileName))
  336. {
  337. content = 'data:image/gif;base64,' + content;
  338. }
  339. else
  340. {
  341. if (/\.png$/i.test(fileName))
  342. {
  343. var xml = this.ui.extractGraphModelFromPng(content);
  344. if (xml != null && xml.length > 0)
  345. {
  346. content = xml;
  347. }
  348. else
  349. {
  350. content = 'data:image/png;base64,' + content;
  351. }
  352. }
  353. else
  354. {
  355. content = Base64.decode(content);
  356. }
  357. }
  358. }
  359. return (asLibrary) ? new GitLabLibrary(this.ui, content, meta) : new GitLabFile(this.ui, content, meta);
  360. };
  361. /**
  362. * Translates this point by the given vector.
  363. *
  364. * @param {number} dx X-coordinate of the translation.
  365. * @param {number} dy Y-coordinate of the translation.
  366. */
  367. GitLabClient.prototype.insertFile = function(filename, data, success, error, asLibrary, folderId, base64Encoded)
  368. {
  369. asLibrary = (asLibrary != null) ? asLibrary : false;
  370. this.getRefIndex(folderId.split('/'), true, mxUtils.bind(this, function(tokens, refPos)
  371. {
  372. var repoPos = Math.max(refPos - 1, 0);
  373. var org = tokens.slice(0, repoPos).join('/');
  374. var repo = tokens[repoPos];
  375. var ref = tokens[refPos];
  376. path = tokens.slice(refPos + 1, tokens.length).join('/');
  377. if (path.length > 0)
  378. {
  379. path = path + '/';
  380. }
  381. path = path + filename;
  382. this.checkExists(org + '/' + repo + '/' + ref + '/' + path, true, mxUtils.bind(this, function(checked, last_commit_id)
  383. {
  384. if (checked)
  385. {
  386. // Does not insert file here as there is another writeFile implicit via fileCreated
  387. if (!asLibrary)
  388. {
  389. var gitLabUrl = DRAWIO_GITLAB_URL + '/';
  390. var htmlUrl = gitLabUrl + org + '/' + repo + '/blob/' + ref + '/' + path;
  391. var downloadUrl = gitLabUrl + org + '/' + repo + '/raw/' + ref + '/' + path + '?inline=false';
  392. success(new GitLabFile(this.ui, data, {'org': org, 'repo': repo, 'ref': ref, 'name': filename,
  393. 'path': path, 'html_url': htmlUrl, 'download_url': downloadUrl, 'refPos': refPos,
  394. 'last_commit_id': last_commit_id, isNew: true}));
  395. }
  396. else
  397. {
  398. if (!base64Encoded)
  399. {
  400. data = Base64.encode(data);
  401. }
  402. this.showCommitDialog(filename, true, mxUtils.bind(this, function(message)
  403. {
  404. this.writeFile(org, repo, ref, path, message, data, last_commit_id, mxUtils.bind(this, function(req)
  405. {
  406. try
  407. {
  408. var msg = JSON.parse(req.getText());
  409. success(this.createGitLabFile(org, repo, ref, msg.content, asLibrary, refPos));
  410. }
  411. catch (e)
  412. {
  413. error(e);
  414. }
  415. }), error);
  416. }), error);
  417. }
  418. }
  419. else
  420. {
  421. // create if it does not exists
  422. error();
  423. }
  424. }))
  425. }), error);
  426. };
  427. /**
  428. * Translates this point by the given vector.
  429. *
  430. * @param {number} dx X-coordinate of the translation.
  431. * @param {number} dy Y-coordinate of the translation.
  432. */
  433. GitLabClient.prototype.checkExists = function(path, askReplace, fn)
  434. {
  435. this.getFile(path, mxUtils.bind(this, function(file)
  436. {
  437. if (askReplace)
  438. {
  439. var resume = this.ui.spinner.pause();
  440. this.ui.confirm(mxResources.get('replaceIt', [path]), function()
  441. {
  442. resume();
  443. fn(true, file.getCurrentEtag());
  444. }, function()
  445. {
  446. resume();
  447. fn(false);
  448. });
  449. }
  450. else
  451. {
  452. this.ui.spinner.stop();
  453. this.ui.showError(mxResources.get('error'), mxResources.get('fileExists'), mxResources.get('ok'), function()
  454. {
  455. fn(false);
  456. });
  457. }
  458. }), mxUtils.bind(this, function(err)
  459. {
  460. fn(true);
  461. }), null, true);
  462. };
  463. /**
  464. *
  465. */
  466. GitLabClient.prototype.writeFile = function(org, repo, ref, path, message, data, last_commit_id, success, error)
  467. {
  468. if (data.length >= this.maxFileSize)
  469. {
  470. error({message: mxResources.get('drawingTooLarge') + ' (' +
  471. this.ui.formatFileSize(data.length) + ' / 1 MB)'});
  472. }
  473. else
  474. {
  475. var method = 'POST';
  476. var entity = {
  477. path: encodeURIComponent(path),
  478. branch: decodeURIComponent(ref),
  479. commit_message: message,
  480. content: data,
  481. encoding: 'base64'
  482. };
  483. if (last_commit_id != null)
  484. {
  485. entity.last_commit_id = last_commit_id;
  486. method = 'PUT';
  487. }
  488. // See https://docs.gitlab.com/ee/api/repository_files.html#update-existing-file-in-repository
  489. var url = this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) + '/repository/files/' + encodeURIComponent(path);
  490. var req = new mxXmlRequest(url, JSON.stringify(entity), method);
  491. this.executeRequest(req, mxUtils.bind(this, function(req)
  492. {
  493. success(req);
  494. }), error);
  495. }
  496. };
  497. /**
  498. * Translates this point by the given vector.
  499. *
  500. * @param {number} dx X-coordinate of the translation.
  501. * @param {number} dy Y-coordinate of the translation.
  502. */
  503. GitLabClient.prototype.saveFile = function(file, success, error, overwrite, message)
  504. {
  505. var org = file.meta.org;
  506. var repo = file.meta.repo;
  507. var ref = file.meta.ref;
  508. var path = file.meta.path;
  509. var fn = mxUtils.bind(this, function(last_commit_id, data)
  510. {
  511. this.writeFile(org, repo, ref, path, message, data, last_commit_id, mxUtils.bind(this, function(req)
  512. {
  513. delete file.meta.isNew;
  514. // Response does not return last_commit_id so we have to get the file
  515. // to to update last_commit_id and compare data to avoid lost commit
  516. this.getFile(org + '/' + repo + '/' + ref + '/' + path, mxUtils.bind(this, function(tempFile)
  517. {
  518. if (tempFile.getData() == file.getData())
  519. {
  520. success(tempFile.getCurrentEtag());
  521. }
  522. else
  523. {
  524. success({content: file.getCurrentEtag()});
  525. }
  526. }), error, null, null, file.meta.refPos);
  527. }), error);
  528. });
  529. var fn2 = mxUtils.bind(this, function()
  530. {
  531. if (this.ui.useCanvasForExport && /(\.png)$/i.test(path))
  532. {
  533. this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
  534. {
  535. fn(file.meta.last_commit_id, data);
  536. }), error, (this.ui.getCurrentFile() != file) ? file.getData() : null);
  537. }
  538. else
  539. {
  540. fn(file.meta.last_commit_id, Base64.encode(file.getData()));
  541. }
  542. });
  543. // LATER: Get last_commit_id is currently not possible since HEAD does
  544. // not have Access-Control-Expose-Headers for X-Gitlab-Last-Commit-Id
  545. if (overwrite)
  546. {
  547. this.getFile(org + '/' + repo + '/' + ref + '/' + path, mxUtils.bind(this, function(tempFile)
  548. {
  549. file.meta.last_commit_id = tempFile.meta.last_commit_id;
  550. fn2();
  551. }), error);
  552. }
  553. else
  554. {
  555. fn2();
  556. }
  557. };
  558. /**
  559. * Checks if the client is authorized and calls the next step.
  560. */
  561. GitLabClient.prototype.pickFolder = function(fn)
  562. {
  563. this.showGitLabDialog(false, fn);
  564. };
  565. /**
  566. * Checks if the client is authorized and calls the next step.
  567. */
  568. GitLabClient.prototype.pickFile = function(fn)
  569. {
  570. fn = (fn != null) ? fn : mxUtils.bind(this, function(path)
  571. {
  572. this.ui.loadFile('A' + encodeURIComponent(path));
  573. });
  574. this.showGitLabDialog(true, fn);
  575. };
  576. /**
  577. * LATER: Refactor to use common code with GitHubClient
  578. */
  579. GitLabClient.prototype.showGitLabDialog = function(showFiles, fn)
  580. {
  581. var org = null;
  582. var repo = null;
  583. var ref = null;
  584. var path = null;
  585. var content = document.createElement('div');
  586. content.style.whiteSpace = 'nowrap';
  587. content.style.overflow = 'hidden';
  588. content.style.height = '224px';
  589. var hd = document.createElement('h3');
  590. mxUtils.write(hd, mxResources.get((showFiles) ? 'selectFile' : 'selectFolder'));
  591. hd.style.cssText = 'width:100%;text-align:center;margin-top:0px;margin-bottom:12px';
  592. content.appendChild(hd);
  593. var div = document.createElement('div');
  594. div.style.whiteSpace = 'nowrap';
  595. div.style.border = '1px solid lightgray';
  596. div.style.boxSizing = 'border-box';
  597. div.style.padding = '4px';
  598. div.style.overflow = 'auto';
  599. div.style.lineHeight = '1.2em';
  600. div.style.height = '194px';
  601. content.appendChild(div);
  602. var listItem = document.createElement('div');
  603. listItem.style.textOverflow = 'ellipsis';
  604. listItem.style.boxSizing = 'border-box';
  605. listItem.style.overflow = 'hidden';
  606. listItem.style.padding = '4px';
  607. listItem.style.width = '100%';
  608. var dlg = new CustomDialog(this.ui, content, mxUtils.bind(this, function()
  609. {
  610. fn(org + '/' + repo + '/' + encodeURIComponent(ref) + '/' + path);
  611. }));
  612. this.ui.showDialog(dlg.container, 340, 270, true, true);
  613. if (showFiles)
  614. {
  615. dlg.okButton.parentNode.removeChild(dlg.okButton);
  616. }
  617. var createLink = mxUtils.bind(this, function(label, fn, padding)
  618. {
  619. var link = document.createElement('a');
  620. link.setAttribute('href', 'javascript:void(0);');
  621. mxUtils.write(link, label);
  622. mxEvent.addListener(link, 'click', fn);
  623. if (padding != null)
  624. {
  625. var temp = listItem.cloneNode();
  626. temp.style.padding = padding;
  627. temp.appendChild(link);
  628. link = temp;
  629. }
  630. return link;
  631. });
  632. var updatePathInfo = mxUtils.bind(this, function(hideRef)
  633. {
  634. var pathInfo = document.createElement('div');
  635. pathInfo.style.marginBottom = '8px';
  636. pathInfo.appendChild(createLink(org + '/' + repo, mxUtils.bind(this, function()
  637. {
  638. path = null;
  639. selectRepo();
  640. })));
  641. if (!hideRef)
  642. {
  643. mxUtils.write(pathInfo, ' / ');
  644. pathInfo.appendChild(createLink(decodeURIComponent(ref), mxUtils.bind(this, function()
  645. {
  646. path = null;
  647. selectRef();
  648. })));
  649. }
  650. if (path != null && path.length > 0)
  651. {
  652. var tokens = path.split('/');
  653. for (var i = 0; i < tokens.length; i++)
  654. {
  655. (function(index)
  656. {
  657. mxUtils.write(pathInfo, ' / ');
  658. pathInfo.appendChild(createLink(tokens[index], mxUtils.bind(this, function()
  659. {
  660. path = tokens.slice(0, index + 1).join('/');
  661. selectFile();
  662. })));
  663. })(i);
  664. }
  665. }
  666. div.appendChild(pathInfo);
  667. });
  668. var error = mxUtils.bind(this, function(err)
  669. {
  670. this.ui.handleError(err, null, mxUtils.bind(this, function()
  671. {
  672. this.ui.spinner.stop();
  673. if (this.getUser() != null)
  674. {
  675. org = null;
  676. repo = null;
  677. ref = null;
  678. path = null;
  679. selectRepo();
  680. }
  681. else
  682. {
  683. this.ui.hideDialog();
  684. }
  685. }));
  686. });
  687. var selectFile = mxUtils.bind(this, function()
  688. {
  689. var req = new mxXmlRequest(this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
  690. '/repository/tree?path=' + path + '&ref=' + ref, null, 'GET');
  691. dlg.okButton.removeAttribute('disabled');
  692. div.innerHTML = '';
  693. this.ui.spinner.spin(div, mxResources.get('loading'));
  694. this.executeRequest(req, mxUtils.bind(this, function(req)
  695. {
  696. updatePathInfo(!ref);
  697. this.ui.spinner.stop();
  698. var files = JSON.parse(req.getText());
  699. div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function()
  700. {
  701. if (path == '')
  702. {
  703. path = null;
  704. selectRepo();
  705. }
  706. else
  707. {
  708. var tokens = path.split('/');
  709. path = tokens.slice(0, tokens.length - 1).join('/');
  710. selectFile();
  711. }
  712. }), '4px'));
  713. if (files == null || files.length == 0)
  714. {
  715. mxUtils.write(div, mxResources.get('noFiles'));
  716. }
  717. else
  718. {
  719. var gray = true;
  720. var listFiles = mxUtils.bind(this, function(showFolders)
  721. {
  722. for (var i = 0; i < files.length; i++)
  723. {
  724. (mxUtils.bind(this, function(file)
  725. {
  726. if (showFolders == (file.type == 'tree'))
  727. {
  728. var temp = listItem.cloneNode();
  729. temp.style.backgroundColor = (gray) ? '#eeeeee' : '';
  730. gray = !gray;
  731. var typeImg = document.createElement('img');
  732. typeImg.src = IMAGE_PATH + '/' + (file.type == 'tree'? 'folder.png' : 'file.png');
  733. typeImg.setAttribute('align', 'absmiddle');
  734. typeImg.style.marginRight = '4px';
  735. typeImg.style.marginTop = '-4px';
  736. typeImg.width = 20;
  737. temp.appendChild(typeImg);
  738. temp.appendChild(createLink(file.name + ((file.type == 'tree') ? '/' : ''), mxUtils.bind(this, function()
  739. {
  740. if (file.type == 'tree')
  741. {
  742. path = file.path;
  743. selectFile();
  744. }
  745. else if (showFiles && file.type == 'blob')
  746. {
  747. this.ui.hideDialog();
  748. fn(org + '/' + repo + '/' + ref + '/' + file.path);
  749. }
  750. })));
  751. div.appendChild(temp);
  752. }
  753. }))(files[i]);
  754. }
  755. });
  756. listFiles(true);
  757. if (showFiles)
  758. {
  759. listFiles(false);
  760. }
  761. }
  762. }), error, true);
  763. });
  764. // Adds paging for repos and branches (files limited to 1000 by API)
  765. var pageSize = 100;
  766. var nextPageDiv = null;
  767. var scrollFn = null;
  768. var selectRef = mxUtils.bind(this, function(page)
  769. {
  770. if (page == null)
  771. {
  772. div.innerHTML = '';
  773. page = 1;
  774. }
  775. var req = new mxXmlRequest(this.baseUrl + '/projects/' + encodeURIComponent(org + '/' + repo) +
  776. '/repository/branches?per_page=' + pageSize + '&page=' + page, null, 'GET');
  777. dlg.okButton.setAttribute('disabled', 'disabled');
  778. this.ui.spinner.spin(div, mxResources.get('loading'));
  779. if (nextPageDiv != null && nextPageDiv.parentNode != null)
  780. {
  781. nextPageDiv.parentNode.removeChild(nextPageDiv);
  782. }
  783. nextPageDiv = document.createElement('a');
  784. nextPageDiv.style.display = 'block';
  785. nextPageDiv.setAttribute('href', 'javascript:void(0);');
  786. mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
  787. var nextPage = mxUtils.bind(this, function()
  788. {
  789. mxEvent.removeListener(div, 'scroll', scrollFn);
  790. selectRef(page + 1);
  791. });
  792. mxEvent.addListener(nextPageDiv, 'click', nextPage);
  793. this.executeRequest(req, mxUtils.bind(this, function(req)
  794. {
  795. this.ui.spinner.stop();
  796. if (page == 1)
  797. {
  798. updatePathInfo(true);
  799. div.appendChild(createLink('../ [Up]', mxUtils.bind(this, function()
  800. {
  801. path = null;
  802. selectRepo();
  803. }), '4px'));
  804. }
  805. var branches = JSON.parse(req.getText());
  806. if (branches == null || branches.length == 0)
  807. {
  808. mxUtils.write(div, mxResources.get('noFiles'));
  809. }
  810. else
  811. {
  812. for (var i = 0; i < branches.length; i++)
  813. {
  814. (mxUtils.bind(this, function(branch, idx)
  815. {
  816. var temp = listItem.cloneNode();
  817. temp.style.backgroundColor = (idx % 2 == 0) ? '#eeeeee' : '';
  818. temp.appendChild(createLink(branch.name, mxUtils.bind(this, function()
  819. {
  820. ref = encodeURIComponent(branch.name);
  821. path = '';
  822. selectFile();
  823. })));
  824. div.appendChild(temp);
  825. }))(branches[i], i);
  826. }
  827. if (branches.length == pageSize)
  828. {
  829. div.appendChild(nextPageDiv);
  830. scrollFn = function()
  831. {
  832. if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
  833. {
  834. nextPage();
  835. }
  836. };
  837. mxEvent.addListener(div, 'scroll', scrollFn);
  838. }
  839. }
  840. }), error);
  841. });
  842. dlg.okButton.setAttribute('disabled', 'disabled');
  843. this.ui.spinner.spin(div, mxResources.get('loading'));
  844. var selectRepo = mxUtils.bind(this, function(page)
  845. {
  846. this.ui.spinner.stop();
  847. if (page == null)
  848. {
  849. div.innerHTML = '';
  850. page = 1;
  851. }
  852. if (nextPageDiv != null && nextPageDiv.parentNode != null)
  853. {
  854. nextPageDiv.parentNode.removeChild(nextPageDiv);
  855. }
  856. nextPageDiv = document.createElement('a');
  857. nextPageDiv.style.display = 'block';
  858. nextPageDiv.setAttribute('href', 'javascript:void(0);');
  859. mxUtils.write(nextPageDiv, mxResources.get('more') + '...');
  860. var nextPage = mxUtils.bind(this, function()
  861. {
  862. mxEvent.removeListener(div, 'scroll', scrollFn);
  863. selectRepo(page + 1);
  864. });
  865. mxEvent.addListener(nextPageDiv, 'click', nextPage);
  866. var listGroups = mxUtils.bind(this, function(callback)
  867. {
  868. this.ui.spinner.spin(div, mxResources.get('loading'));
  869. var req = new mxXmlRequest(this.baseUrl + '/groups?per_page=100', null, 'GET');
  870. this.executeRequest(req, mxUtils.bind(this, function(req)
  871. {
  872. this.ui.spinner.stop();
  873. callback(JSON.parse(req.getText()));
  874. }), error);
  875. });
  876. var listProjects = mxUtils.bind(this, function(group, callback)
  877. {
  878. this.ui.spinner.spin(div, mxResources.get('loading'));
  879. var req = new mxXmlRequest(this.baseUrl + '/groups/' + group.id + '/projects?per_page=100', null, 'GET');
  880. this.executeRequest(req, mxUtils.bind(this, function(req)
  881. {
  882. this.ui.spinner.stop();
  883. callback(group, JSON.parse(req.getText()));
  884. }), error);
  885. });
  886. listGroups(mxUtils.bind(this, function(groups)
  887. {
  888. var req = new mxXmlRequest(this.baseUrl + '/users/' + this.user.id + '/projects?per_page=' +
  889. pageSize + '&page=' + page, null, 'GET');
  890. this.ui.spinner.spin(div, mxResources.get('loading'));
  891. this.executeRequest(req, mxUtils.bind(this, function(req)
  892. {
  893. this.ui.spinner.stop();
  894. var repos = JSON.parse(req.getText());
  895. if ((repos == null || repos.length == 0) && (groups == null || groups.length == 0))
  896. {
  897. mxUtils.write(div, mxResources.get('noFiles'));
  898. }
  899. else
  900. {
  901. if (page == 1)
  902. {
  903. div.appendChild(createLink(mxResources.get('enterValue') + '...', mxUtils.bind(this, function()
  904. {
  905. var dlg = new FilenameDialog(this.ui, 'org/repo/ref', mxResources.get('ok'), mxUtils.bind(this, function(value)
  906. {
  907. if (value != null)
  908. {
  909. var tokens = value.split('/');
  910. if (tokens.length > 1)
  911. {
  912. org = tokens[0];
  913. repo = tokens[1];
  914. ref = 'master';
  915. path = null;
  916. if (tokens.length > 2)
  917. {
  918. path = encodeURIComponent(tokens.slice(2, tokens.length).join('/'));
  919. }
  920. selectFile();
  921. }
  922. else
  923. {
  924. this.ui.spinner.stop();
  925. this.ui.handleError({message: mxResources.get('invalidName')});
  926. }
  927. }
  928. }), mxResources.get('enterValue'));
  929. this.ui.showDialog(dlg.container, 300, 80, true, false);
  930. dlg.init();
  931. })));
  932. mxUtils.br(div);
  933. mxUtils.br(div);
  934. }
  935. var gray = true;
  936. for (var i = 0; i < repos.length; i++)
  937. {
  938. (mxUtils.bind(this, function(repository, idx)
  939. {
  940. var temp = listItem.cloneNode();
  941. temp.style.backgroundColor = (gray) ? '#eeeeee' : '';
  942. gray = !gray;
  943. temp.appendChild(createLink(repository.name_with_namespace, mxUtils.bind(this, function()
  944. {
  945. org = repository.owner.username;
  946. repo = repository.path;
  947. ref = repository.default_branch || 'master';
  948. path = '';
  949. selectFile();
  950. })));
  951. div.appendChild(temp);
  952. }))(repos[i], i);
  953. }
  954. for (var i = 0; i < groups.length; i++)
  955. {
  956. listProjects(groups[i], (mxUtils.bind(this, function(group, projects)
  957. {
  958. for (var j = 0; j < projects.length; j++)
  959. {
  960. var temp = listItem.cloneNode();
  961. temp.style.backgroundColor = (gray) ? '#eeeeee' : '';
  962. gray = !gray;
  963. (mxUtils.bind(this, function(project)
  964. {
  965. temp.appendChild(createLink(project.name_with_namespace, mxUtils.bind(this, function()
  966. {
  967. org = group.full_path;
  968. repo = project.path;
  969. ref = project.default_branch || 'master';
  970. path = '';
  971. selectFile();
  972. })));
  973. div.appendChild(temp);
  974. }))(projects[j]);
  975. }
  976. })));
  977. }
  978. }
  979. if (repos.length == pageSize)
  980. {
  981. div.appendChild(nextPageDiv);
  982. scrollFn = function()
  983. {
  984. if (div.scrollTop >= div.scrollHeight - div.offsetHeight)
  985. {
  986. nextPage();
  987. }
  988. };
  989. mxEvent.addListener(div, 'scroll', scrollFn);
  990. }
  991. }), error);
  992. }));
  993. });
  994. if (!this.token)
  995. {
  996. this.authenticate(mxUtils.bind(this, function()
  997. {
  998. this.updateUser(function() { selectRepo(); }, error, true);
  999. }), error);
  1000. }
  1001. else if (!this.user)
  1002. {
  1003. this.updateUser(function()
  1004. {
  1005. selectRepo();
  1006. }, error, true);
  1007. }
  1008. else
  1009. {
  1010. selectRepo();
  1011. }
  1012. };
  1013. /**
  1014. * Checks if the client is authorized and calls the next step.
  1015. */
  1016. GitLabClient.prototype.logout = function()
  1017. {
  1018. this.clearPersistentToken();
  1019. this.setUser(null);
  1020. this.token = null;
  1021. };