GitHubClient.js 35 KB

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