GitHubClient.js 28 KB

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