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