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