1/** 2 * Copyright (c) 2006-2020, JGraph Ltd 3 * Copyright (c) 2006-2020, draw.io AG 4 */ 5 6//Add a closure to hide the class private variables without changing the code a lot 7(function() 8{ 9 10var _token = null; 11var pickers = {}; 12 13window.DriveClient = function(editorUi, isExtAuth) 14{ 15 if (isExtAuth == null && window.urlParams != null && window.urlParams['extAuth'] == '1') 16 { 17 isExtAuth = true; 18 } 19 20 mxEventSource.call(this); 21 22 DrawioClient.call(this, editorUi, 'gDriveAuthInfo'); 23 24 this.isExtAuth = isExtAuth; 25 /** 26 * Holds a reference to the UI. Needed for the sharing client. 27 */ 28 this.ui = editorUi; 29 30 // New mime type for XML files 31 this.xmlMimeType = 'application/vnd.jgraph.mxfile'; 32 this.mimeType = 'application/vnd.jgraph.mxfile.realtime'; 33 34 // Reading files now possible with no initial click in drive 35 //TODO In teams we do auth using editor app, we need to support viewer only app also 36 if (this.ui.editor.chromeless && !this.ui.editor.editable && urlParams['rt'] != '1' && urlParams['extAuth'] != '1') 37 { 38 // Uses separate name for the viewer auth tokens 39 this.cookieName = 'gDriveViewerAuthInfo'; 40 this.token = this.getPersistentToken(); 41 42 this.appId = window.DRAWIO_GOOGLE_VIEWER_APP_ID || '850530949725'; 43 this.clientId = window.DRAWIO_GOOGLE_VIEWER_CLIENT_ID || '850530949725.apps.googleusercontent.com'; 44 this.scopes = ['https://www.googleapis.com/auth/drive.readonly', 45 'https://www.googleapis.com/auth/userinfo.profile']; 46 } 47 else 48 { 49 this.appId = window.DRAWIO_GOOGLE_APP_ID || '671128082532'; 50 this.clientId = window.DRAWIO_GOOGLE_CLIENT_ID || '671128082532-jhphbq6d0e1gnsus9mn7vf8a6fjn10mp.apps.googleusercontent.com'; 51 } 52 53 this.mimeTypes = this.xmlMimeType + ',application/mxe,application/mxr,' + 54 'application/vnd.jgraph.mxfile.realtime,application/vnd.jgraph.mxfile.rtlegacy'; 55 56 var authInfo = JSON.parse(this.token); 57 58 if (authInfo != null && authInfo.current != null) 59 { 60 this.userId = authInfo.current.userId; 61 this.authCalled = false; 62 } 63}; 64 65// Extends mxEventSource 66mxUtils.extend(DriveClient, mxEventSource); 67 68// Extends DrawioClient 69mxUtils.extend(DriveClient, DrawioClient); 70 71DriveClient.prototype.redirectUri = window.location.protocol + '//' + window.location.host + '/google'; 72DriveClient.prototype.GDriveBaseUrl = 'https://www.googleapis.com/drive/v2'; 73 74/** 75 * OAuth 2.0 scopes for installing Drive Apps. 76 */ 77DriveClient.prototype.scopes = ['https://www.googleapis.com/auth/drive.file', 78 'https://www.googleapis.com/auth/drive.install', 79 'https://www.googleapis.com/auth/userinfo.profile']; 80 81/** 82 * Contains the hostname of the old app. 83 */ 84DriveClient.prototype.allFields = 'kind,id,parents,headRevisionId,etag,title,mimeType,modifiedDate,' + 85 'editable,copyable,canComment,labels,properties,downloadUrl,webContentLink,userPermission,fileSize'; 86 87/** 88 * Fields required for catchin up. 89 * 90 * TODO: Limit to etag and ekey property only 91 */ 92DriveClient.prototype.catchupFields = 'etag,headRevisionId,modifiedDate,properties(key,value)'; 93 94/** 95 * Specifies if thumbnails should be enabled. Default is true. 96 * LATER: If thumbnails are disabled, make sure to replace the 97 * existing thumbnail with the placeholder only once. 98 */ 99DriveClient.prototype.enableThumbnails = true; 100 101/** 102 * Specifies the width for thumbnails. Default is 1000. This value 103 * must be between 220 and 1600. 104 */ 105DriveClient.prototype.thumbnailWidth = 1000; 106 107/** 108 * The maximum number of bytes per thumbnail. Default is 2000000. 109 */ 110DriveClient.prototype.maxThumbnailSize = 2000000; 111 112/** 113 * Defines the base64url PNG to be used if no thumbnail was generated 114 * (including the case where thumbnails are disabled). 115 */ 116DriveClient.prototype.placeholderThumbnail = 'iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAMAAAAL34HQAAACN1BMVEXwhwXvhgX4iwXzhwXgbQzvhgXhbAzocgzqcwzldAoAAADhbgvjcQnmdgrlbgDwhgXsfwXufgjwhgXwgQfziAXxgADibgz4iwX4jAX3iwTpcwr1igXoewjsfgj3igX4iwXqcQv4jAX3iwXtfQnndQrvhAbibArwhwXgbQz//////v39jwX6jQX+/v7fagHfawzdVQDwhADgbhPgbhXwhwPocQ3uvKvwiA/faQDscgzxiAT97+XgciTgcSP6jAXgbQ3gcCHwiRfpcQzwhwfeXQD77ef74NLvhgTvegD66uPgbAf66+TvfADwjCzgcCfwiSD67ObhcjjwiBHhczvwiyrgbxj///777ujgcSHgcB/xiRzgbhveWgDeVwDhdEDgbRDqfgffYgDfXwD97+bvfQDxiz7//vvwiRr118rrcgztggbfZgDfZAD++PT98+3gbBPsgAb99vD33tPgcB7icAvuhAX//Pn66N/00sTyy7vuuqbjekLwhwzkcgr88er449n++vfutp/kh1vgcBvhbwvmdwnwgwDwgADeWQD87eLxxrTssJjqpIf0roHmjWTkhFP759n63czvvanomnjnlHDhczD22cr4y6/wwa/3xKX2wJ3rqpH0tY7qp4vpnoDymlbjf0vxjjntcwzldAroegj/kgX12s7518PzqnnnkWfynmLieUjpewjrdAD40Lj1uZTzpm3idTbiciLydQzzfwnyiQTsfgD3xqnzp3TxlkzgbCrdTwDdSwBLKUlNAAAAJ3RSTlP8/b2X/YH8wb+FAIuIggJbQin5opAM9+a/ubaubyD78NjSyr2WgRp4sjN4AAAI70lEQVR42u2cZ38SQRDGT8WGvfde4E4BxVMRRaKiUURRlJhQRDCCSgQVO/bee++9994+nMt5ywoezFJd/fm8uITi3p9n5mbYkcCpO6rVnVu2YEXd+3dRIySuo7pLv4GjGNKg7j3UHTl1l14PajmG9OFBnx7Ird4PumpYEtf1QXc112l0M7OGKXEfeg3guo3iNIyJG92Jaz61mYYxcaNacs1H/8f6j6X5j1WI/mMVIsawRFEzI49SjwOqAJa43emclk8Rp2c7AFZ+LDGyvXE2kmO2Q1Lq17RSd6ND48QIwFVuLNHTOPbEpTOz8ujMpccHGz0AV5mxIo4TpwUeUPj0YwfAVVYs0Tn7VZjnBUA8v+n6CyfERY8FR/DEJj7MQ6oL85vOvfDUAsuVC8s19s5yXuAppOPnvPk4EeSCsehCeBVTwVzHfE6RcFUQa4an8Qw91kpbw2oz4aoc1sSxniO0WAI/J24wriabmEpizZtM79bc+fr4/tUarEpiLabGElJYRsOGjbJfjGDpJCxtmosRLOEnVpqLESzZLYlLg65H1rAkLo2GESwcROwXI1jELcS1Y6OGQSzEVaupZQJLDiLhYtCtFBcbbslYhOueqKllDwtzwVhTq4RFuBh0C3EdEBl0C3OBWNUrEISLvSD+5GLQLYmLoSqfwcUiFuaqzhYDxiJc981lxqqdVsCGbHPcQLBgrtK3rwLt9tWqhblKxxI9hW3267U5ZHhuBrCKzXl4NIJTS5FrmbmMWGIEDZIouOp0/O6boYQ2jxBXWcdu13fzRILuF/2Ku+aGr96uBbhALHo5Z38+XcfXyVRZVx/+Ed513ldDCCCu0rFE0Xlo2mu5TAj8ki0XV0q6ePHilhi+d/15b9ACQGGusg3AFzc+XSMBCPzu89+CNlnB7zfD8t1z4iaLXUvDVT6sGdMOnv5pi47f6r9Qk9YF3xZ0l8S11UfMArlgLMpZM6bamYy6rWnta9q7TrZrzZPgPgoqg3atubY8WK6D8lQXHfb4p/wSK7vFfxmxSsAPQ96AlZ4LxoLNeompdkUDGQVznL5mLr4ar5ESD3PBWHA9fbpbjlT4pq1Bm6H6w9dwfOd69ePouNDYt3S3ULPGZ96S3YqtAW/Tepz1E8bgAANc+xEXhAX36ut1cslcd6rJq81SIvgEe7lmL3kY5iqxVYvOI9isswp22KeMOcrriJlWai5giwHl+yec73Ma9Mbfz+qOJndKz6hLpR5V1uPxavFuTTt0K1XfpbNeO0wKeUaR2IPBN5sMRlqu1eY8bsFmPeIFUpi0CjIGTLvSZY2EGeYSi3VL9Dgeb0I+SQl9MlcZT4TObZKzfmfS5NZSx1GsLQ5r+8Sxp7ERR/1TtDlUn2qNuGXCrZGM5URlLDiEVzDVkje5fdjXdDsm27XpXChBz4XG0UpYcDOMYaxjGc3wtyJxFtu1PohaI71f2K2imqEONcN4nrMZ9TWbMf81wg9z3VNwC26Gr3enY4ObobLqbccFefuz5AKONpVfzQp2y3NoVvrN32GLNl9orA22lTiM+Nqg5CJY1DueOjkwsdtNgAP7gidR2SWVhFqt3o9QwoKHIuiwDcwX+xT/UWztSlvCaqXGmtQBY1GadQmfh6anuE0XlkhhRFs3tGGkd+tuIVhiJN0M+brj0mlAu46lX0bcbizVLbgZrgwl4JhYA+NQa9TJQUetsSJYHscJvAVct7eJKoUbQudxPYmdirqzsYsIojhjoitD01yadH287J+vpZF1/uGt2K4ttinjshQo2C2XMzI2U64X6WY4tyZq99a7wZS3eA3BpNyrUPn1x00Z0uM1ACzilOfg7EN3VmRo8dN16WYYerYw6G9qCOSDCjQ0jQkufRbalt65LVyapaA/2mClxhK3Rxy3rsyavDxDR/DL5sMLFiyYu/7sXps7z8VldPv2Xl6PnjlTwOOuJQuytH7CXpvXCOQWoZrYeHWd4nw2Q+v22OLGnFSG0Nk1PCi0xjgjpVvTGi8hht9F+ARBGq8dtXmtOSLoDm1FhUSHnihkTecESalHkPAaWVhtFbA8jqvQGBmbt8fWkKtNn0Xw9GvAWK6DX9bBVHjzqtyvvcG9a+jXyC5oKoKV/a4YFG7Yij2ofszlgtaA3ZoRwW+pIOH3w0qZFURNh3oNtKsDsAr9LNvMC0pj93H6hTPpX9ocg8FIgTVvcgFYC03jFLBMi6ix0MDAoi8/lh7Cgt2q0VfNrSX0ayhjTa2IW0tKdotNrMq4NbPkILKZW+xdiSoGgshogfh7Ul7FcIEoFevfrPLC3+XWf6y/CEvHZoFQqlts9sQigqjLxFpQCJauakFcsqhKPXH79rGb6bE2B5Qmu0b91zn0WJtN8Wys9tgtIqfjEf2SWw7XKI8gHuKQ0X0eDsQSI44TaGBN6dYN5dlI/eFj9I7f8GWtoUJYOIgkiq6Ds/gw5T7dZDUqTrfscbLbB9eIB7JmEKsUgiii/4uO8ToBfJlhfif5tEGWEsGTMT4Mr6HDa0BBlP5Y88lcnkdkCtLhnyjMM0+Gcn2WzW6xnd/J8zn+LZq4SUeEvUBaA8LCs6Tk1p1AetXt3JoMWexWZSyr3RK6vSUGrRHbmkRUVgCLpP1HW/L4tgl5tO140mdKKFFhrkTUdxta4xleA8DCXC6n/vCYvPJFa9zAWL4m6qNaA8IiqjW73lreWnJrSj0AJYFZpvwq6RZRzjVUGEtB5tX7DdoqCXaL+PXHuEjdYsuvVqva4Sqv6NdabdW4YLeIKsoFYzHGhYPIGBd2izGuVpPaSVgAV7VEsOQgsuUXdosxLuwWxLVMW0WRK5ExLiiIpN4vq2YYVTiIbPmFgii5xRiXimCBqmIcVSS3WMqvdMqz5VcKqzdKeca4UrnVT/ryR6bi2Opuf64TwYJlfl4FLqu2Zxeux5BRXZnisvZ8103NqTtzoziuGa24+wZVRdVK9W7wyNSX1nYeOmrU6JSmjp6KhH5BR+kGvk++Ld0c/X66rPH4SEQeGl+kpq8a33eAumPqK347durWpzm9hrWhUevi1Hd4ZzVC+gGMHY0TYnDOYwAAAABJRU5ErkJggg=='.replace(/\+/g, '-').replace(/\//g, '_'); 117 118/** 119 * Mime type for the paceholder thumbnail. 120 */ 121DriveClient.prototype.placeholderMimeType = 'image/png'; 122 123/** 124 * Executes the first step for connecting to Google Drive. 125 */ 126DriveClient.prototype.libraryMimeType = 'application/vnd.jgraph.mxlibrary'; 127 128/** 129 * Contains the hostname of the new app. 130 */ 131DriveClient.prototype.newAppHostname = 'www.draw.io'; 132 133/** 134 * Executes the first step for connecting to Google Drive. 135 */ 136DriveClient.prototype.extension = '.drawio'; 137 138/** 139 * Interval for updating the access token. 140 */ 141DriveClient.prototype.tokenRefreshInterval = 0; 142 143/** 144 * Interval for updating the access token. 145 */ 146DriveClient.prototype.lastTokenRefresh = 0; 147 148/** 149 * Executes the first step for connecting to Google Drive. 150 */ 151DriveClient.prototype.maxRetries = 5; 152 153/** 154 * Executes the first step for connecting to Google Drive. 155 */ 156DriveClient.prototype.staleEtagMaxRetries = 3; 157 158/** 159 * Executes the first step for connecting to Google Drive. 160 */ 161DriveClient.prototype.coolOff = 1000; 162 163/** 164 * Executes the first step for connecting to Google Drive. 165 */ 166DriveClient.prototype.mimeTypeCheckCoolOff = 60000; 167 168/** 169 * Executes the first step for connecting to Google Drive. 170 */ 171DriveClient.prototype.user = null; 172 173/** 174 * Executes auth in same window (no popups) 175 */ 176DriveClient.prototype.sameWinAuthMode = false; 177 178/** 179 * Redirect URL of samw window mode that will get the token 180 */ 181DriveClient.prototype.sameWinRedirectUrl = null; 182 183 184/** 185 * Authorizes the client, gets the userId and calls <open>. 186 */ 187DriveClient.prototype.setUser = function(user) 188{ 189 this.user = user; 190 191 if (this.user == null) 192 { 193 this.userId = null; 194 195 if (this.tokenRefreshThread != null) 196 { 197 window.clearTimeout(this.tokenRefreshThread); 198 this.tokenRefreshThread = null; 199 } 200 } 201 else 202 { 203 this.userId = user.id; 204 } 205 206 this.fireEvent(new mxEventObject('userChanged')); 207}; 208 209DriveClient.prototype.setUserId = function(userId) 210{ 211 this.userId = userId; 212 213 if (this.user != null && this.user.id != this.userId) 214 { 215 this.user = null; 216 } 217}; 218/** 219 * Authorizes the client, gets the userId and calls <open>. 220 */ 221DriveClient.prototype.getUser = function() 222{ 223 return this.user; 224}; 225 226DriveClient.prototype.getUsersList = function() 227{ 228 var users = []; 229 var authInfo = JSON.parse(this.getPersistentToken(true)); 230 var curUserId = null; 231 232 if (authInfo != null) 233 { 234 if (authInfo.current != null) 235 { 236 curUserId = authInfo.current.userId; 237 users.push(authInfo[curUserId].user); 238 users[0].isCurrent = true; 239 240 } 241 242 for (var id in authInfo) 243 { 244 if (id == 'current' || id == curUserId) continue; 245 246 users.push(authInfo[id].user); 247 } 248 } 249 return users; 250}; 251 252DriveClient.prototype.logout = function() 253{ 254 //Send to server to clear refresh token cookie 255 this.ui.editor.loadUrl(this.redirectUri + '?doLogout=1&userId=' + this.userId + '&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname)); 256 this.clearPersistentToken(); 257 this.setUser(null); 258 _token = null; 259}; 260 261/** 262 * Authorizes the client, gets the userId and calls <open>. 263 */ 264DriveClient.prototype.execute = function(fn) 265{ 266 // Handles error in immediate authorize call via callback that shows a 267 // UI with a button that executes the second non-immediate authorize 268 var fallback = mxUtils.bind(this, function(resp) 269 { 270 // Remember is an argument for the callback that executes 271 // when the user clicks the authorize button in the UI and 272 // success executes after successful authorization. 273 this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, success) 274 { 275 this.authorize(false, mxUtils.bind(this, function() 276 { 277 if (success != null) 278 { 279 success(); 280 } 281 282 fn(); 283 }), mxUtils.bind(this, function(resp) 284 { 285 var msg = mxResources.get('cannotLogin'); 286 287 // Handles special domain policy errors 288 if (resp != null && resp.error != null) 289 { 290 if (resp.error.code == 403 && 291 resp.error.data != null && resp.error.data.length > 0 && 292 resp.error.data[0].reason == 'domainPolicy') 293 { 294 msg = resp.error.message; 295 } 296 } 297 298 this.logout(); 299 300 this.ui.showError(mxResources.get('error'), msg, mxResources.get('help'), mxUtils.bind(this, function() 301 { 302 this.ui.openLink('https://www.diagrams.net/doc/faq/gsuite-authorisation-troubleshoot'); 303 }), null, mxResources.get('ok')); 304 }), remember); 305 })); 306 }); 307 308 // First immediate authorize attempt 309 this.authorize(true, fn, fallback); 310}; 311 312/** 313 * Executes the given request. 314 */ 315DriveClient.prototype.executeRequest = function(reqObj, success, error) 316{ 317 try 318 { 319 var acceptResponse = true; 320 var timeoutThread = null; 321 var retryCount = 0; 322 323 // Cancels any pending requests 324 if (this.requestThread != null) 325 { 326 window.clearTimeout(this.requestThread); 327 } 328 329 var fn = mxUtils.bind(this, function() 330 { 331 try 332 { 333 this.requestThread = null; 334 this.currentRequest = reqObj; 335 336 if (timeoutThread != null) 337 { 338 window.clearTimeout(timeoutThread); 339 } 340 341 timeoutThread = window.setTimeout(mxUtils.bind(this, function() 342 { 343 acceptResponse = false; 344 345 if (error != null) 346 { 347 error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout'), retry: fn}); 348 } 349 }), this.ui.timeout); 350 351 var params = null; 352 var isJSON = false; 353 354 if (typeof reqObj.params === 'string') 355 { 356 params = reqObj.params; 357 } 358 else if (reqObj.params != null) 359 { 360 params = JSON.stringify(reqObj.params); 361 isJSON = true; 362 } 363 364 var url = reqObj.fullUrl || (this.GDriveBaseUrl + reqObj.url); 365 366 if (isJSON) 367 { 368 url += (url.indexOf('?') > 0 ? '&' : '?') + 'alt=json'; 369 } 370 371 var req = new mxXmlRequest(url, params, reqObj.method || 'GET'); 372 373 req.setRequestHeaders = mxUtils.bind(this, function(request, params) 374 { 375 if (reqObj.headers != null) 376 { 377 for (var key in reqObj.headers) 378 { 379 request.setRequestHeader(key, reqObj.headers[key]); 380 } 381 } 382 else if (reqObj.contentType != null) 383 { 384 request.setRequestHeader('Content-Type', reqObj.contentType); 385 } 386 else if (isJSON) 387 { 388 request.setRequestHeader('Content-Type', 'application/json'); 389 } 390 391 request.setRequestHeader('Authorization', 'Bearer ' + _token); 392 }); 393 394 req.send(mxUtils.bind(this, function(req) 395 { 396 try 397 { 398 window.clearTimeout(timeoutThread); 399 400 if (acceptResponse) 401 { 402 var resp; 403 404 try 405 { 406 resp = JSON.parse(req.getText()); 407 } 408 catch(e) 409 { 410 resp = null; 411 } 412 413 if (req.getStatus() >= 200 && req.getStatus() <= 299) 414 { 415 if (success != null) 416 { 417 success(resp); 418 } 419 } 420 else 421 { 422 // Errors for put request are in data instead of errors 423 var data = (resp != null && resp.error != null) ? ((resp.error.data != null) ? 424 resp.error.data : resp.error.errors) : null; 425 var reason = (data != null && data.length > 0) ? data[0].reason : null; 426 427 // Handles special error for saving old file where mime was changed to new 428 // LATER: Check if 403 is never auth error, for now we check the message for a specific 429 // case where the old app mime type was overridden by the new app 430 if (error != null && resp != null && resp.error != null && (resp.error.code == -1 || 431 (resp.error.code == 403 && (reason == 'domainPolicy' || resp.error.message == 432 'The requested mime type change is forbidden.')))) 433 { 434 error(resp); 435 } 436 // Handles authentication error 437 else if (resp != null && resp.error != null && (resp.error.code == 401 || 438 (resp.error.code == 403 && reason != 'rateLimitExceeded'))) 439 { 440 // Shows an error if re-authenticated but the server still doesn't allow it 441 if ((resp.error.code == 403 && this.retryAuth) || 442 (resp.error.code == 401 && this.retryAuth && reason == 'authError')) 443 { 444 if (error != null) 445 { 446 error(resp); 447 } 448 449 this.retryAuth = false; 450 } 451 else 452 { 453 this.retryAuth = true; 454 this.execute(fn); 455 } 456 } 457 // Schedules a retry if no new request was executed 458 else if (resp != null && resp.error != null && resp.error.code != 412 && resp.error.code != 404 && 459 resp.error.code != 400 && this.currentRequest == reqObj && retryCount < this.maxRetries) 460 { 461 retryCount++; 462 var jitter = 1 + 0.1 * (Math.random() - 0.5); 463 this.requestThread = window.setTimeout(fn, 464 Math.round(Math.pow(2, retryCount) * 465 jitter * this.coolOff)); 466 } 467 else if (error != null) 468 { 469 error(resp); 470 } 471 } 472 } 473 } 474 catch (e) 475 { 476 if (error != null) 477 { 478 error(e); 479 } 480 else 481 { 482 throw e; 483 } 484 } 485 })); 486 } 487 catch (e) 488 { 489 if (error != null) 490 { 491 error(e); 492 } 493 else 494 { 495 throw e; 496 } 497 } 498 }); 499 500 // Must get token before first request in this case 501 if (_token == null || !this.authCalled) 502 { 503 this.execute(fn); 504 } 505 else 506 { 507 fn(); 508 } 509 } 510 catch (e) 511 { 512 if (error != null) 513 { 514 error(e); 515 } 516 else 517 { 518 throw e; 519 } 520 } 521}; 522 523DriveClient.prototype.createAuthWin = function(url) 524{ 525 var width = 525, 526 height = 525, 527 screenX = window.screenX, 528 screenY = window.screenY, 529 outerWidth = window.outerWidth, 530 outerHeight = window.outerHeight; 531 532 var left = screenX + Math.max(outerWidth - width, 0) / 2; 533 var top = screenY + Math.max(outerHeight - height, 0) / 2; 534 535 var features = ['width=' + width, 'height=' + height, 536 'top=' + top, 'left=' + left, 537 'status=no', 'resizable=yes', 538 'toolbar=no', 'menubar=no', 539 'scrollbars=yes']; 540 return window.open(url? url : 'about:blank', 'gdauth', features.join(',')); 541}; 542 543/** 544 * Authorizes the client, gets the userId and calls <open>. 545 */ 546DriveClient.prototype.authorize = function(immediate, success, error, remember, popup) 547{ 548 if (this.isExtAuth && !immediate) 549 { 550 window.parent.driveAuth(mxUtils.bind(this, function(newAuthInfo) 551 { 552 this.updateAuthInfo(newAuthInfo, true, true, success, error); 553 }), error); 554 return; 555 } 556 557 var req = new mxXmlRequest(this.redirectUri + '?getState=1', null, 'GET'); 558 559 req.send(mxUtils.bind(this, function(req) 560 { 561 if (req.getStatus() >= 200 && req.getStatus() <= 299) 562 { 563 this.authorizeStep2(req.getText(), immediate, success, error, remember, popup); 564 } 565 else if (error != null) 566 { 567 error(req); 568 } 569 }), error); 570}; 571 572DriveClient.prototype.updateAuthInfo = function (newAuthInfo, remember, forceUserUpdate, success, error) 573{ 574 _token = newAuthInfo.access_token; 575 delete newAuthInfo.access_token; //Don't store access token 576 newAuthInfo.expires = Date.now() + parseInt(newAuthInfo.expires_in) * 1000; 577 newAuthInfo.remember = remember; 578 579 this.resetTokenRefresh(newAuthInfo); 580 this.authCalled = true; 581 582 if (forceUserUpdate || this.user == null) 583 { 584 //IE/Edge security doesn't allow access to newAuthInfo in a callback function (outside this function scope) 585 //So, stringify the object and restore it (parse) in the callback 586 var strAuthInfo = JSON.stringify(newAuthInfo); 587 588 this.updateUser(mxUtils.bind(this, function() 589 { 590 //Restore the auth info object to bypass IE/Edge security 591 var resAuthInfo = JSON.parse(strAuthInfo); 592 //Save user and new token 593 this.setPersistentToken(resAuthInfo, !remember); 594 595 if (success != null) 596 { 597 success(); 598 } 599 }), error); 600 } 601 else if (success != null) 602 { 603 this.setPersistentToken(newAuthInfo, !remember); 604 success(); 605 } 606}; 607 608DriveClient.prototype.authorizeStep2 = function(state, immediate, success, error, remember, popup) 609{ 610 try 611 { 612 // Takes userId from state URL parameter 613 if (this.ui.stateArg != null && this.ui.stateArg.userId != null) 614 { 615 this.userId = this.ui.stateArg.userId; 616 617 if (this.user != null && this.user.id != this.userId) 618 { 619 this.user = null; 620 } 621 } 622 623 if (this.userId == null) 624 { 625 var authInfo = JSON.parse(this.getPersistentToken(true)); 626 627 if (authInfo && authInfo.current != null) 628 { 629 this.userId = authInfo.current.userId; 630 } 631 } 632 633 // Immediate only possible with a refresh token (there is a userId) 634 if (immediate && this.userId == null) 635 { 636 if (error != null) 637 { 638 error(); 639 } 640 } 641 else 642 { 643 //Retry request with refreshed token (in the cookie) 644 if (immediate) //Note, we checked refresh token is not null above 645 { 646 //state is used to identify which app/domain is used 647 var req = new mxXmlRequest(this.redirectUri + '?state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state) 648 + '&userId=' + this.userId 649 , null, 'GET'); 650 651 req.send(mxUtils.bind(this, function(req) 652 { 653 if (req.getStatus() >= 200 && req.getStatus() <= 299) 654 { 655 var newAuthInfo = JSON.parse(req.getText()); 656 this.updateAuthInfo(newAuthInfo, true, false, success, error); //We set remember to true since we can only have a refresh token if user initially selected remember 657 } 658 else 659 { 660 //When the request fails (e.g, Hibernate on Windows), the status is 0, this doesn't mean the token is invalid 661 if (req.getStatus() != 0) 662 { 663 this.logout(); 664 } 665 666 if (error != null) 667 { 668 error(req); //TODO review this code path and how error is handled 669 } 670 } 671 }), error); 672 } 673 else 674 { 675 var url = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + this.clientId + 676 '&redirect_uri=' + encodeURIComponent(this.redirectUri) + 677 '&response_type=code&include_granted_scopes=true' + 678 (remember? '&access_type=offline&prompt=consent%20select_account' : '') + //Ask for consent again to get a new refresh token 679 '&scope=' + encodeURIComponent(this.scopes.join(' ')) + 680 '&state=' + encodeURIComponent('cId=' + this.clientId + '&domain=' + window.location.hostname + '&token=' + state + //To identify which app/domain is used 681 (this.sameWinRedirectUrl? '&redirect=' + this.sameWinRedirectUrl : '')); 682 683 if (this.sameWinAuthMode) 684 { 685 window.location.assign(url); 686 popup = null; //Same window doesn't use onGoogleDriveCallback or popups 687 } 688 else if (popup == null) 689 { 690 popup = this.createAuthWin(url); 691 } 692 else 693 { 694 popup.location = url; 695 } 696 697 if (popup != null) 698 { 699 window.onGoogleDriveCallback = mxUtils.bind(this, function(newAuthInfo, authWindow) 700 { 701 window.onGoogleDriveCallback = null; 702 703 try 704 { 705 if (newAuthInfo == null) 706 { 707 if (error != null) 708 { 709 error({message: mxResources.get('accessDenied')}); //TODO Check this error handling is correct 710 } 711 } 712 else 713 { 714 this.updateAuthInfo(newAuthInfo, remember, true, success, error); 715 } 716 } 717 catch (e) 718 { 719 if (error != null) 720 { 721 error(e); 722 } 723 } 724 finally 725 { 726 if (authWindow != null) 727 { 728 authWindow.close(); 729 } 730 } 731 }); 732 733 popup.focus(); 734 } 735 } 736 } 737 } 738 catch (e) 739 { 740 if (error != null) 741 { 742 error(e); 743 } 744 else 745 { 746 throw e; 747 } 748 } 749}; 750 751/** 752 * Checks if the client is authorized and calls the next step. 753 */ 754DriveClient.prototype.resetTokenRefresh = function(resp) 755{ 756 if (this.tokenRefreshThread != null) 757 { 758 window.clearTimeout(this.tokenRefreshThread); 759 this.tokenRefreshThread = null; 760 } 761 762 // Starts timer to refresh token before it expires 763 if (resp != null && resp.error == null && resp.expires_in > 0) 764 { 765 this.tokenRefreshInterval = parseInt(resp.expires_in) * 1000; 766 this.lastTokenRefresh = new Date().getTime(); 767 768 this.tokenRefreshThread = window.setTimeout(mxUtils.bind(this, function() 769 { 770 this.authorize(true, mxUtils.bind(this, function() 771 { 772 //console.log('tokenRefresh: refreshed', _token); 773 }), mxUtils.bind(this, function() 774 { 775 //console.log('tokenRefresh: error refreshing', _token); 776 })); 777 }), resp.expires_in * 900); 778 } 779}; 780 781/** 782 * Checks if the client is authorized and calls the next step. 783 */ 784DriveClient.prototype.checkToken = function(fn) 785{ 786 var connected = this.lastTokenRefresh > 0; 787 var delta = new Date().getTime() - this.lastTokenRefresh; 788 789 if (delta > this.tokenRefreshInterval || this.tokenRefreshThread == null) 790 { 791 // Uses execute instead of authorize to allow a fallback authorization if cookie was lost 792 this.execute(mxUtils.bind(this, function() 793 { 794 fn(); 795 796 if (connected) 797 { 798 this.fireEvent(new mxEventObject('disconnected')); 799 } 800 })); 801 } 802 else 803 { 804 fn(); 805 } 806}; 807 808/** 809 * Checks if the client is authorized and calls the next step. 810 */ 811DriveClient.prototype.updateUser = function(success, error) 812{ 813 try 814 { 815 var url = 'https://www.googleapis.com/oauth2/v2/userinfo?alt=json'; 816 var headers = {'Authorization': 'Bearer ' + _token}; 817 818 this.ui.editor.loadUrl(url, mxUtils.bind(this, function(data) 819 { 820 var info = JSON.parse(data); 821 822 // Requests more information about the user (email address is sometimes not in info) 823 this.executeRequest({url: '/about'}, mxUtils.bind(this, function(resp) 824 { 825 var email = mxResources.get('notAvailable'); 826 var name = email; 827 var pic = null; 828 829 if (resp != null && resp.user != null) 830 { 831 email = resp.user.emailAddress; 832 name = resp.user.displayName; 833 pic = (resp.user.picture != null) ? resp.user.picture.url : null; 834 } 835 836 this.setUser(new DrawioUser(info.id, email, name, pic, info.locale)); 837 this.userId = info.id; 838 839 if (success != null) 840 { 841 success(); 842 } 843 }), error); 844 }), error, null, null, null, null, headers); 845 } 846 catch (e) 847 { 848 if (error != null) 849 { 850 error(e); 851 } 852 else 853 { 854 throw e; 855 } 856 } 857}; 858 859/** 860 * Translates this point by the given vector. 861 * 862 * @param {number} dx X-coordinate of the translation. 863 * @param {number} dy Y-coordinate of the translation. 864 */ 865DriveClient.prototype.copyFile = function(id, title, success, error) 866{ 867 if (id != null && title != null) 868 { 869 this.executeRequest({url: '/files/' + id + '/copy?fields=' + encodeURIComponent(this.allFields) 870 + '&supportsAllDrives=true&enforceSingleParent=true', //&alt=json 871 method: 'POST', 872 params: {'title': title, 'properties': 873 [{'key': 'channel', 'value': Editor.guid()}]} 874 }, success, error); 875 } 876}; 877 878/** 879 * Translates this point by the given vector. 880 * 881 * @param {number} dx X-coordinate of the translation. 882 * @param {number} dy Y-coordinate of the translation. 883 */ 884DriveClient.prototype.renameFile = function(id, title, success, error) 885{ 886 if (id != null && title != null) 887 { 888 this.executeRequest(this.createDriveRequest( 889 id, {'title' : title}), success, error); 890 } 891}; 892 893/** 894 * Translates this point by the given vector. 895 * 896 * @param {number} dx X-coordinate of the translation. 897 * @param {number} dy Y-coordinate of the translation. 898 */ 899DriveClient.prototype.moveFile = function(id, folderId, success, error) 900{ 901 if (id != null && folderId != null) 902 { 903 this.executeRequest(this.createDriveRequest(id, {'parents': [{'kind': 904 'drive#fileLink', 'id': folderId}]}), success, error); 905 } 906}; 907 908/** 909 * Translates this point by the given vector. 910 * 911 * @param {number} dx X-coordinate of the translation. 912 * @param {number} dy Y-coordinate of the translation. 913 */ 914DriveClient.prototype.createDriveRequest = function(id, body) 915{ 916 return { 917 'url': '/files/' + id + '?uploadType=multipart&supportsAllDrives=true', 918 'method': 'PUT', 919 'contentType': 'application/json; charset=UTF-8', 920 'params': body 921 }; 922}; 923 924/** 925 * Loads the given file as a library file. 926 */ 927DriveClient.prototype.getLibrary = function(id, success, error) 928{ 929 return this.getFile(id, success, error, true, true); 930}; 931 932/** 933 * Loads the descriptorf for the given file ID. 934 */ 935DriveClient.prototype.loadDescriptor = function(id, success, error, fields) 936{ 937 this.executeRequest({ 938 url: '/files/' + id + '?supportsAllDrives=true&fields=' + (fields != null ? fields : this.allFields) 939 }, success, error); 940}; 941 942DriveClient.prototype.listFiles = function(searchStr, afterDate, mineOnly, success, error) 943{ 944 this.executeRequest({ 945 url: '/files?supportsAllDrives=true&includeItemsFromAllDrives=true&q=' + encodeURIComponent('(mimeType contains \'' + this.xmlMimeType + '\') ' + 946 (searchStr? ' and (title contains \'' + searchStr + '\')' : '') + 947 (afterDate? ' and (modifiedDate > \'' + afterDate.toISOString() + '\')' : '') + 948 (mineOnly? ' and (\'me\' in owners)' : '')) + 949 '&orderBy=modifiedDate desc,title' 950 }, success, error); 951}; 952 953/** 954 * Gets the channel ID from the given descriptor. 955 */ 956DriveClient.prototype.getCustomProperty = function(desc, key) 957{ 958 var props = desc.properties; 959 var result = null; 960 961 if (props != null) 962 { 963 for (var i = 0; i < props.length; i++) 964 { 965 if (props[i].key == key) 966 { 967 result = props[i].value; 968 969 break; 970 } 971 } 972 } 973 974 return result; 975}; 976 977/** 978 * Checks if the client is authorized and calls the next step. The optional 979 * readXml argument is used for import. Default is false. The optional 980 * readLibrary argument is used for reading libraries. Default is false. 981 */ 982DriveClient.prototype.getFile = function(id, success, error, readXml, readLibrary) 983{ 984 readXml = (readXml != null) ? readXml : false; 985 readLibrary = (readLibrary != null) ? readLibrary : false; 986 987 if (urlParams['rev'] != null) 988 { 989 this.executeRequest({ 990 url: '/files/' + id + '/revisions/' + urlParams['rev'] + '?supportsAllDrives=true' 991 }, 992 mxUtils.bind(this, function(resp) 993 { 994 // Redirects title to originalFilename to 995 // match expected descriptor interface 996 resp.title = resp.originalFilename; 997 998 // Uses ID of file instead of revision ID in descriptor 999 // to avoid a change of the document hash property 1000 resp.headRevisionId = resp.id; 1001 resp.id = id; 1002 1003 this.getXmlFile(resp, success, error); 1004 }), error); 1005 } 1006 else 1007 { 1008 this.loadDescriptor(id, mxUtils.bind(this, function(resp) 1009 { 1010 try 1011 { 1012 if (this.user != null) 1013 { 1014 var binary = /\.png$/i.test(resp.title); 1015 1016 // Handles .vsdx, .vsd, .vdx, Gliffy and PNG+XML files by creating a temporary file 1017 if (/\.v(dx|sdx?)$/i.test(resp.title) || /\.gliffy$/i.test(resp.title) || 1018 (!this.ui.useCanvasForExport && binary)) 1019 { 1020 var url = resp.downloadUrl; 1021 var headers = {'Authorization': 'Bearer ' + _token}; 1022 1023 this.ui.convertFile(url, resp.title, resp.mimeType, this.extension, success, error, null, headers); 1024 } 1025 else 1026 { 1027 // Handles converted realtime files as XML files 1028 if (readXml || readLibrary || resp.mimeType == this.libraryMimeType || 1029 resp.mimeType == this.xmlMimeType) 1030 { 1031 this.getXmlFile(resp, success, error, true, readLibrary); 1032 } 1033 else 1034 { 1035 this.getXmlFile(resp, success, error); 1036 } 1037 } 1038 } 1039 else 1040 { 1041 error({message: mxResources.get('loggedOut')}); 1042 } 1043 } 1044 catch (e) 1045 { 1046 if (error != null) 1047 { 1048 error(e); 1049 } 1050 else 1051 { 1052 throw e; 1053 } 1054 } 1055 }), error); 1056 } 1057}; 1058 1059/** 1060 * Returns true if the given mime type is for Google Realtime files. 1061 */ 1062DriveClient.prototype.isGoogleRealtimeMimeType = function(mimeType) 1063{ 1064 return mimeType != null && mimeType.substring(0, 30) == 'application/vnd.jgraph.mxfile.'; 1065}; 1066 1067/** 1068 * Checks if the client is authorized and calls the next step. The ignoreMime argument is 1069 * used for import via getFile. Default is false. The optional 1070 * readLibrary argument is used for reading libraries. Default is false. 1071 */ 1072DriveClient.prototype.getXmlFile = function(resp, success, error, ignoreMime, readLibrary) 1073{ 1074 try 1075 { 1076 var headers = {'Authorization': 'Bearer ' + _token}; 1077 var url = resp.downloadUrl; 1078 1079 // Download URL is null if no option to download for viewers 1080 if (url == null) 1081 { 1082 if (error != null) 1083 { 1084 error({message: mxResources.get('exportOptionsDisabledDetails')}); 1085 } 1086 } 1087 else 1088 { 1089 var retryCount = 0; 1090 1091 var fn = mxUtils.bind(this, function() 1092 { 1093 // Loads XML to initialize realtime document if realtime is empty 1094 this.ui.editor.loadUrl(url, mxUtils.bind(this, function(data) 1095 { 1096 try 1097 { 1098 if (data == null) 1099 { 1100 // TODO: Optional redirect to legacy if link is for old file 1101 error({message: mxResources.get('invalidOrMissingFile')}); 1102 } 1103 else if (resp.mimeType == this.libraryMimeType || readLibrary) 1104 { 1105 if (resp.mimeType == this.libraryMimeType && !readLibrary) 1106 { 1107 error({message: mxResources.get('notADiagramFile')}); 1108 } 1109 else 1110 { 1111 success(new DriveLibrary(this.ui, data, resp)); 1112 } 1113 } 1114 else 1115 { 1116 var importFile = false; 1117 1118 if (/\.png$/i.test(resp.title)) 1119 { 1120 var index = data.lastIndexOf(','); 1121 1122 if (index > 0) 1123 { 1124 var xml = this.ui.extractGraphModelFromPng(data); 1125 1126 if (xml != null && xml.length > 0) 1127 { 1128 data = xml; 1129 } 1130 else 1131 { 1132 // Checks if the file contains XML data which can happen when we insert 1133 // the file and then don't post-process it when loaded into the UI which 1134 // is required for creating the images for .PNG and .SVG files. 1135 try 1136 { 1137 var xml = data.substring(index + 1); 1138 var temp = (window.atob && !mxClient.IS_IE && !mxClient.IS_IE11) ? 1139 atob(xml) : Base64.decode(xml); 1140 var node = this.ui.editor.extractGraphModel( 1141 mxUtils.parseXml(temp).documentElement, true); 1142 1143 if (node == null || node.getElementsByTagName('parsererror').length > 0) 1144 { 1145 importFile = true; 1146 } 1147 else 1148 { 1149 data = temp; 1150 } 1151 } 1152 catch (e) 1153 { 1154 importFile = true; 1155 } 1156 } 1157 } 1158 } 1159 else if (/\.pdf$/i.test(resp.title)) 1160 { 1161 var xml = Editor.extractGraphModelFromPdf(data); 1162 1163 if (xml != null && xml.length > 0) 1164 { 1165 importFile = true; 1166 data = xml; 1167 } 1168 } 1169 // Checks for base64 encoded mxfile 1170 else if (data.substring(0, 32) == 'data:image/png;base64,PG14ZmlsZS') 1171 { 1172 var temp = data.substring(22); 1173 data = (window.atob && !mxClient.IS_SF) ? atob(temp) : Base64.decode(temp); 1174 } 1175 1176 if (Graph.fileSupport && new XMLHttpRequest().upload && this.ui.isRemoteFileFormat(data, url)) 1177 { 1178 this.ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr) 1179 { 1180 try 1181 { 1182 if (xhr.readyState == 4) 1183 { 1184 if (xhr.status >= 200 && xhr.status <= 299) 1185 { 1186 success(new LocalFile(this.ui, xhr.responseText, resp.title + this.extension, true)); 1187 } 1188 else if (error != null) 1189 { 1190 error({message: mxResources.get('errorLoadingFile')}); 1191 } 1192 } 1193 } 1194 catch (e) 1195 { 1196 if (error != null) 1197 { 1198 error(e); 1199 } 1200 else 1201 { 1202 throw e; 1203 } 1204 } 1205 }), resp.title); 1206 } 1207 else 1208 { 1209 success((importFile) ? new LocalFile(this.ui, data, resp.title, true) : new DriveFile(this.ui, data, resp)); 1210 } 1211 } 1212 } 1213 catch (e) 1214 { 1215 if (error != null) 1216 { 1217 error(e); 1218 } 1219 else 1220 { 1221 throw e; 1222 } 1223 } 1224 }), mxUtils.bind(this, function(e, req) 1225 { 1226 if (retryCount < this.maxRetries && req != null && req.getStatus() == 403) 1227 { 1228 retryCount++; 1229 var jitter = 1 + 0.1 * (Math.random() - 0.5); 1230 var delay = retryCount * 2 * this.coolOff * jitter; 1231 1232 window.setTimeout(fn, delay); 1233 } 1234 else 1235 { 1236 if (error != null) 1237 { 1238 error(e); 1239 } 1240 else 1241 { 1242 throw e; 1243 } 1244 } 1245 }), ((resp.mimeType != null && resp.mimeType.substring(0, 6) == 'image/' && 1246 resp.mimeType.substring(0, 9) != 'image/svg')) || /\.png$/i.test(resp.title) || 1247 /\.jpe?g$/i.test(resp.title) || /\.pdf$/i.test(resp.title), 1248 null, null, null, headers); 1249 }); 1250 1251 fn(); 1252 } 1253 } 1254 catch (e) 1255 { 1256 if (error != null) 1257 { 1258 error(e); 1259 } 1260 else 1261 { 1262 throw e; 1263 } 1264 } 1265}; 1266 1267/** 1268 * Translates this point by the given vector. 1269 * 1270 * @param {number} dx X-coordinate of the translation. 1271 * @param {number} dy Y-coordinate of the translation. 1272 */ 1273DriveClient.prototype.saveFile = function(file, revision, success, errFn, noCheck, unloading, overwrite, properties, secret) 1274{ 1275 try 1276 { 1277 var retryCount = 0; 1278 file.saveLevel = 1; 1279 1280 var error = mxUtils.bind(this, function(e) 1281 { 1282 if (errFn != null) 1283 { 1284 errFn(e); 1285 } 1286 else 1287 { 1288 throw e; 1289 } 1290 1291 // Logs failed save 1292 try 1293 { 1294 if (!file.isConflict(e)) 1295 { 1296 var err = 'sl_' + file.saveLevel + '-error_' + 1297 (file.getErrorMessage(e) || 'unknown'); 1298 1299 if (e != null && e.error != null && e.error.code != null) 1300 { 1301 err += '-code_' + e.error.code; 1302 } 1303 1304 EditorUi.logEvent({category: 'ERROR-SAVE-FILE-' + file.getHash() + '-rev_' + 1305 file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate + 1306 '-size_' + file.getSize() + '-mime_' + file.desc.mimeType + 1307 ((this.ui.editor.autosave) ? '' : '-nosave') + 1308 ((file.isAutosave()) ? '' : '-noauto') + 1309 ((file.changeListenerEnabled) ? '' : '-nolisten') + 1310 ((file.inConflictState) ? '-conflict' : '') + 1311 ((file.invalidChecksum) ? '-invalid' : ''), 1312 action: err, label: ((this.user != null) ? ('user_' + this.user.id) : 'nouser') + 1313 ((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')}); 1314 } 1315 } 1316 catch (ex) 1317 { 1318 // ignore 1319 } 1320 }); 1321 1322 var criticalError = mxUtils.bind(this, function(e) 1323 { 1324 error(e); 1325 1326 try 1327 { 1328 EditorUi.logError(e.message, null, null, e); 1329 1330// EditorUi.sendReport('Critical error in DriveClient.saveFile ' + 1331// new Date().toISOString() + ':' + 1332// '\n\nUserAgent=' + navigator.userAgent + 1333// '\nAppVersion=' + navigator.appVersion + 1334// '\nAppName=' + navigator.appName + 1335// '\nPlatform=' + navigator.platform + 1336// '\nFile=' + file.desc.id + '.' + file.desc.headRevisionId + 1337// '\nMime=' + file.desc.mimeType + 1338// '\nSize=' + file.getSize() + 1339// '\nUser=' + ((this.user != null) ? this.user.id : 'nouser') + 1340// ((file.sync != null) ? '-client_' + file.sync.clientId : '-nosync') + 1341// '\nSaveLevel=' + file.saveLevel + 1342// '\nSaveAsPng=' + (this.ui.useCanvasForExport && /(\.png)$/i.test(file.getTitle())) + 1343// '\nRetryCount=' + retryCount + 1344// '\nError=' + e + 1345// '\nMessage=' + e.message + 1346// '\n\nStack:\n' + e.stack); 1347 } 1348 catch (e) 1349 { 1350 // ignore 1351 } 1352 }); 1353 1354 if (file.isEditable() && file.desc != null) 1355 { 1356 var t0 = new Date().getTime(); 1357 var etag0 = file.desc.etag; 1358 var mod0 = file.desc.modifiedDate; 1359 var head0 = file.desc.headRevisionId; 1360 var saveAsPng = this.ui.useCanvasForExport && /(\.png)$/i.test(file.getTitle()); 1361 noCheck = (noCheck != null) ? noCheck : urlParams['ignoremime'] == '1'; 1362 1363 // NOTE: Unloading arg is currently ignored, saving during unload/beforeUnload is not possible using 1364 // asynchronous code, which is needed to create the thumbnail, or asynchronous requests which is the only 1365 // way to execute the gapi request below. 1366 // If no thumbnail is created and noCheck is true (which is always true if unloading is true) in which case 1367 // this code is synchronous, the executeRequest call is reached but the request is still not sent. This is 1368 // true for both, calls from beforeUnload and unload handlers. Note sure how to make the call synchronous 1369 // which is said to fix this when called from beforeUnload. 1370 // However, this would result in a missing thumbnail in most cases so a better solution might be to reduce 1371 // the autosave interval in DriveRealtime, but that would increase the number of requests. 1372 unloading = (unloading != null) ? unloading : false; 1373 var prevDesc = null; 1374 var pinned = false; 1375 var meta = 1376 { 1377 'mimeType': file.desc.mimeType, 1378 'title': file.getTitle() 1379 }; 1380 1381 // Overrides old mime type and creates a revision 1382 if (this.isGoogleRealtimeMimeType(meta.mimeType)) 1383 { 1384 meta.mimeType = this.xmlMimeType; 1385 prevDesc = file.desc; 1386 revision = true; 1387 pinned = true; 1388 } 1389 // Overrides mime type for unknown file type uploads 1390 else if (meta.mimeType == 'application/octet-stream' || 1391 (urlParams['override-mime'] == '1' && 1392 meta.mimeType != this.xmlMimeType)) 1393 { 1394 meta.mimeType = this.xmlMimeType; 1395 } 1396 1397 // Adds optional thumbnail to upload request 1398 var doSave = mxUtils.bind(this, function(thumb, thumbMime, keepExisting) 1399 { 1400 try 1401 { 1402 file.saveLevel = 3; 1403 1404 if (file.constructor == DriveFile) 1405 { 1406 if (properties == null) 1407 { 1408 properties = []; 1409 } 1410 1411 // Channel ID appended to file ID for comms 1412 if (file.getChannelId() == null) 1413 { 1414 properties.push({'key': 'channel', 'value': Editor.guid(32)}); 1415 } 1416 1417 // Key for encryption of comms 1418 if (file.getChannelKey() == null) 1419 { 1420 properties.push({'key': 'key', 'value': Editor.guid(32)}); 1421 } 1422 1423 // Pass to access cache for each etag 1424 properties.push({'key': 'secret', 'value': (secret != null) ? secret : Editor.guid(32)}); 1425 } 1426 1427 // Specifies that no thumbnail should be uploaded in which case the existing thumbnail is used 1428 if (!keepExisting) 1429 { 1430 // Uses placeholder thumbnail to replace existing one except when unloading 1431 // in which case the XML is updated but the existing thumbnail is not in order 1432 // to avoid executing asynchronous code and get the XML to the server instead 1433 if (thumb == null && !unloading) 1434 { 1435 thumb = this.placeholderThumbnail; 1436 thumbMime = this.placeholderMimeType; 1437 } 1438 1439 // Adds metadata for thumbnail 1440 if (thumb != null && thumbMime != null) 1441 { 1442 meta.thumbnail = 1443 { 1444 'image': thumb, 1445 'mimeType': thumbMime 1446 }; 1447 } 1448 } 1449 1450 var savedData = file.getData(); 1451 1452 // Updates saveDelay on drive file 1453 var wrapper = mxUtils.bind(this, function(resp) 1454 { 1455 try 1456 { 1457 file.saveDelay = new Date().getTime() - t0; 1458 file.saveLevel = 11; 1459 1460 if (resp == null) 1461 { 1462 error({message: mxResources.get('errorSavingFile') + ': Empty response'}); 1463 } 1464 else 1465 { 1466 // Checks if modified time is in the future and head revision has changed 1467 var delta = new Date(resp.modifiedDate).getTime() - new Date(mod0).getTime(); 1468 1469 if (delta <= 0 || etag0 == resp.etag || (revision && head0 == resp.headRevisionId)) 1470 { 1471 file.saveLevel = 12; 1472 var reasons = []; 1473 1474 if (delta <= 0) 1475 { 1476 reasons.push('invalid modified time'); 1477 } 1478 1479 if (etag0 == resp.etag) 1480 { 1481 reasons.push('stale etag'); 1482 } 1483 1484 if (revision && head0 == resp.headRevisionId) 1485 { 1486 reasons.push('stale revision'); 1487 } 1488 1489 var temp = reasons.join(', '); 1490 error({message: mxResources.get('errorSavingFile') + ': ' + temp}, resp); 1491 1492 // Logs failed save 1493 try 1494 { 1495 EditorUi.logError('Critical: Error saving to Google Drive ' + file.desc.id, 1496 null, 'from-' + head0 + '.' + mod0 + '-' + this.ui.hashValue(etag0) + 1497 '-to-' + resp.headRevisionId + '.' + resp.modifiedDate + '-' + 1498 this.ui.hashValue(resp.etag) + ((temp.length > 0) ? '-errors-' + temp : ''), 1499 'user-' + ((this.user != null) ? this.user.id : 'nouser') + 1500 ((file.sync != null) ? '-client_' + file.sync.clientId : '-nosync')); 1501 } 1502 catch (e) 1503 { 1504 // ignore 1505 } 1506 } 1507 else 1508 { 1509 file.saveLevel = null; 1510 success(resp, savedData); 1511 1512 if (prevDesc != null) 1513 { 1514 // Pins previous revision 1515 this.executeRequest({ 1516 url: '/files/' + prevDesc.id + '/revisions/' + prevDesc.headRevisionId + '?supportsAllDrives=true' 1517 }, mxUtils.bind(this, mxUtils.bind(this, function(resp) 1518 { 1519 resp.pinned = true; 1520 1521 this.executeRequest({ 1522 url: '/files/' + prevDesc.id + '/revisions/' + prevDesc.headRevisionId, 1523 method: 'PUT', 1524 params: resp 1525 }); 1526 }))); 1527 1528 // Logs conversion 1529 try 1530 { 1531 EditorUi.logEvent({category: file.convertedFrom + '-CONVERT-FILE-' + file.getHash(), 1532 action: 'from_' + prevDesc.id + '.' + prevDesc.headRevisionId + 1533 '-to_' + file.desc.id + '.' + file.desc.headRevisionId, 1534 label: (this.user != null) ? ('user_' + this.user.id) : 'nouser' + 1535 ((file.sync != null) ? '-client_' + file.sync.clientId : 'nosync')}); 1536 } 1537 catch (e) 1538 { 1539 // ignore 1540 } 1541 } 1542 1543 // Logs successful save 1544// try 1545// { 1546// EditorUi.logEvent({category: 'SUCCESS-SAVE-FILE-' + file.getHash() + 1547// '-rev0_' + head0 + '-mod0_' + mod0, 1548// action: 'rev-' + resp.headRevisionId + 1549// '-mod_' + resp.modifiedDate + '-size_' + file.getSize() + 1550// '-mime_' + file.desc.mimeType + 1551// ((this.ui.editor.autosave) ? '' : '-nosave') + 1552// ((file.isAutosave()) ? '' : '-noauto') + 1553// ((file.changeListenerEnabled) ? '' : '-nolisten') + 1554// ((file.inConflictState) ? '-conflict' : '') + 1555// ((file.invalidChecksum) ? '-invalid' : ''), 1556// label: ((this.user != null) ? ('user_' + this.user.id) : 'nouser') + 1557// ((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')}); 1558// } 1559// catch (e) 1560// { 1561// // ignore 1562// } 1563 } 1564 } 1565 } 1566 catch (e) 1567 { 1568 criticalError(e); 1569 } 1570 }); 1571 1572 var doExecuteRequest = mxUtils.bind(this, function(data, binary) 1573 { 1574 file.saveLevel = 4; 1575 1576 try 1577 { 1578 if (properties != null) 1579 { 1580 meta.properties = properties; 1581 } 1582 1583 // Used to check if file was changed externally 1584 var etag = (!overwrite && file.constructor == DriveFile && 1585 (DrawioFile.SYNC == 'manual' || DrawioFile.SYNC == 'auto')) ? 1586 file.getCurrentEtag() : null; 1587 1588 var doExecuteSave = mxUtils.bind(this, function(realOverwrite) 1589 { 1590 file.saveLevel = 5; 1591 1592 try 1593 { 1594 var unknown = file.desc.mimeType != this.xmlMimeType && file.desc.mimeType != this.mimeType && 1595 file.desc.mimeType != this.libraryMimeType; 1596 var acceptResponse = true; 1597 var timeoutThread = null; 1598 1599 // Allow for re-auth flow with 5x timeout 1600 try 1601 { 1602 timeoutThread = window.setTimeout(mxUtils.bind(this, function() 1603 { 1604 acceptResponse = false; 1605 error({code: App.ERROR_TIMEOUT}); 1606 }), 5 * this.ui.timeout); 1607 } 1608 catch (e) 1609 { 1610 // Ignore window closed 1611 } 1612 1613 this.executeRequest(this.createUploadRequest(file.getId(), meta, 1614 data, revision || realOverwrite || unknown, binary, 1615 (realOverwrite) ? null : etag, pinned), mxUtils.bind(this, function(resp) 1616 { 1617 window.clearTimeout(timeoutThread); 1618 1619 if (acceptResponse) 1620 { 1621 wrapper(resp); 1622 } 1623 }), mxUtils.bind(this, function(err) 1624 { 1625 window.clearTimeout(timeoutThread); 1626 1627 if (acceptResponse) 1628 { 1629 file.saveLevel = 6; 1630 1631 try 1632 { 1633 if (!file.isConflict(err)) 1634 { 1635 error(err); 1636 } 1637 else 1638 { 1639 // Workaround for correct etag and Google always returns 412 conflict error (stale etag) 1640 this.executeRequest({ 1641 url: '/files/' + file.getId() + '?supportsAllDrives=true&fields=' + this.catchupFields 1642 }, 1643 mxUtils.bind(this, function(resp) 1644 { 1645 file.saveLevel = 7; 1646 1647 try 1648 { 1649 // Stale etag detected, retry with delay 1650 if (resp != null && resp.etag == etag) 1651 { 1652 if (retryCount < this.staleEtagMaxRetries) 1653 { 1654 retryCount++; 1655 var jitter = 1 + 0.1 * (Math.random() - 0.5); 1656 var delay = retryCount * 2 * this.coolOff * jitter; 1657 window.setTimeout(executeSave, delay); 1658 1659 if (urlParams['test'] == '1') 1660 { 1661 EditorUi.debug('DriveClient: Stale Etag Detected', 1662 'retry', retryCount, 'delay', delay); 1663 } 1664 } 1665 else 1666 { 1667 executeSave(true); 1668 1669 // Logs overwrite 1670 try 1671 { 1672 EditorUi.logEvent({category: 'STALE-ETAG-SAVE-FILE-' + file.getHash(), 1673 action: 'rev_' + file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate + 1674 '-size_' + file.getSize() + '-mime_' + file.desc.mimeType + 1675 ((this.ui.editor.autosave) ? '' : '-nosave') + 1676 ((file.isAutosave()) ? '' : '-noauto') + 1677 ((file.changeListenerEnabled) ? '' : '-nolisten') + 1678 ((file.inConflictState) ? '-conflict' : '') + 1679 ((file.invalidChecksum) ? '-invalid' : ''), 1680 label: ((this.user != null) ? ('user_' + this.user.id) : 'nouser') + 1681 ((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')}); 1682 } 1683 catch (e) 1684 { 1685 // ignore 1686 } 1687 } 1688 } 1689 else 1690 { 1691 1692 if (urlParams['test'] == '1' && resp.headRevisionId == head0) 1693 { 1694 EditorUi.debug('DriveClient: Remote Etag Changed', 1695 'local', etag, 'remote', resp.etag, 1696 'rev', file.desc.headRevisionId, 1697 'response', [resp], 'file', [file]); 1698 } 1699 1700 error(err, resp); 1701 } 1702 } 1703 catch (e) 1704 { 1705 criticalError(e); 1706 } 1707 }), mxUtils.bind(this, function() 1708 { 1709 error(err); 1710 })); 1711 } 1712 } 1713 catch (e) 1714 { 1715 criticalError(e); 1716 } 1717 } 1718 })); 1719 } 1720 catch (e) 1721 { 1722 criticalError(e); 1723 } 1724 }); 1725 1726 // Workaround for Google returning the wrong etag after file save is to 1727 // update the etag before save and check if the headRevisionId changed 1728 var executeSave = mxUtils.bind(this, function(realOverwrite) 1729 { 1730 file.saveLevel = 9; 1731 1732 if (realOverwrite || etag == null) 1733 { 1734 doExecuteSave(realOverwrite); 1735 } 1736 else 1737 { 1738 var acceptResponse = true; 1739 var timeoutThread = null; 1740 1741 // Allow for re-auth flow with 3x timeout 1742 try 1743 { 1744 timeoutThread = window.setTimeout(mxUtils.bind(this, function() 1745 { 1746 acceptResponse = false; 1747 error({code: App.ERROR_TIMEOUT}); 1748 }), 3 * this.ui.timeout); 1749 } 1750 catch (e) 1751 { 1752 // Ignore window closed 1753 } 1754 1755 this.executeRequest({ 1756 url: '/files/' + file.getId() + '?supportsAllDrives=true&fields=' + this.catchupFields 1757 }, 1758 mxUtils.bind(this, function(desc2) 1759 { 1760 window.clearTimeout(timeoutThread); 1761 1762 if (acceptResponse) 1763 { 1764 file.saveLevel = 10; 1765 1766 try 1767 { 1768 // Checks head revision ID and updates etag or returns conflict 1769 if (desc2 != null && desc2.headRevisionId == head0) 1770 { 1771 if (urlParams['test'] == '1' && etag != desc2.etag) 1772 { 1773 EditorUi.debug('DriveClient: Preflight Etag Update', 1774 'from', etag, 'to', desc2.etag, 1775 'rev', file.desc.headRevisionId, 1776 'response', [desc2], 'file', [file]); 1777 } 1778 1779 etag = desc2.etag; 1780 doExecuteSave(realOverwrite); 1781 } 1782 else 1783 { 1784 error({error: {code: 412}}, desc2); 1785 } 1786 } 1787 catch (e) 1788 { 1789 criticalError(e); 1790 } 1791 } 1792 }), mxUtils.bind(this, function(err) 1793 { 1794 // Simulated 1795 window.clearTimeout(timeoutThread); 1796 1797 if (acceptResponse) 1798 { 1799 file.saveLevel = 11; 1800 error(err); 1801 } 1802 })); 1803 } 1804 }); 1805 1806 // Uses saved PNG data for thumbnail 1807 if (saveAsPng && thumb == null) 1808 { 1809 file.saveLevel = 8; 1810 var img = new Image(); 1811 1812 img.onload = mxUtils.bind(this, function() 1813 { 1814 try 1815 { 1816 var s = this.thumbnailWidth / img.width; 1817 1818 var canvas = document.createElement('canvas'); 1819 canvas.width = this.thumbnailWidth; 1820 canvas.height = Math.floor(img.height * s); 1821 1822 var ctx = canvas.getContext('2d'); 1823 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 1824 1825 var temp = canvas.toDataURL(); 1826 temp = temp.substring(temp.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_'); 1827 1828 meta.thumbnail = 1829 { 1830 'image': temp, 1831 'mimeType': 'image/png' 1832 }; 1833 1834 executeSave(false); 1835 } 1836 catch (e) 1837 { 1838 try 1839 { 1840 executeSave(false) 1841 } 1842 catch (e2) 1843 { 1844 criticalError(e2); 1845 } 1846 } 1847 }); 1848 1849 img.src = 'data:image/png;base64,' + data; 1850 } 1851 else 1852 { 1853 executeSave(false); 1854 } 1855 } 1856 catch (e) 1857 { 1858 criticalError(e); 1859 } 1860 }); 1861 1862 if (saveAsPng) 1863 { 1864 var p = this.ui.getPngFileProperties(this.ui.fileNode); 1865 1866 this.ui.getEmbeddedPng(mxUtils.bind(this, function(data) 1867 { 1868 doExecuteRequest(data, true); 1869 }), error, (this.ui.getCurrentFile() != file) ? 1870 savedData : null, p.scale, p.border); 1871 } 1872 else 1873 { 1874 doExecuteRequest(savedData, false); 1875 } 1876 } 1877 catch (e) 1878 { 1879 criticalError(e); 1880 } 1881 }); 1882 1883 // Indirection to generate thumbnails if enabled and supported 1884 // (required because generation of thumbnails is asynchronous) 1885 try 1886 { 1887 file.saveLevel = 2; 1888 1889 // NOTE: getThumbnail is asynchronous and returns false if no thumbnails can be created 1890 if (unloading || saveAsPng || file.constructor == DriveLibrary || !this.enableThumbnails || urlParams['thumb'] == '0' || 1891 (meta.mimeType != null && meta.mimeType.substring(0, 29) != 'application/vnd.jgraph.mxfile') || 1892 !this.ui.getThumbnail(this.thumbnailWidth, mxUtils.bind(this, function(canvas) 1893 { 1894 // Callback for getThumbnail 1895 try 1896 { 1897 var thumb = null; 1898 1899 try 1900 { 1901 if (canvas != null) 1902 { 1903 // Security errors are possible 1904 thumb = canvas.toDataURL('image/png'); 1905 } 1906 1907 // Maximum thumbnail size is 2MB 1908 if (thumb != null) 1909 { 1910 if (thumb.length > this.maxThumbnailSize) 1911 { 1912 thumb = null; 1913 } 1914 else 1915 { 1916 // Converts base64 data into required format for Drive (base64url with no prefix) 1917 thumb = thumb.substring(thumb.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_'); 1918 } 1919 } 1920 } 1921 catch (e) 1922 { 1923 thumb = null; 1924 } 1925 1926 doSave(thumb, 'image/png'); 1927 } 1928 catch (e) 1929 { 1930 criticalError(e); 1931 } 1932 }))) 1933 { 1934 // If-branch 1935 doSave(null, null, file.constructor != DriveLibrary); 1936 } 1937 } 1938 catch (e) 1939 { 1940 criticalError(e); 1941 } 1942 } 1943 else 1944 { 1945 this.ui.editor.graph.reset(); 1946 error({message: mxResources.get('readOnly')}); 1947 } 1948 } 1949 catch (e) 1950 { 1951 criticalError(e); 1952 } 1953}; 1954 1955/** 1956 * Translates this point by the given vector. 1957 * 1958 * @param {number} dx X-coordinate of the translation. 1959 * @param {number} dy Y-coordinate of the translation. 1960 */ 1961DriveClient.prototype.insertFile = function(title, data, folderId, success, error, mimeType, binary) 1962{ 1963 mimeType = (mimeType != null) ? mimeType : this.xmlMimeType; 1964 1965 var metadata = 1966 { 1967 'mimeType': mimeType, 1968 'title': title 1969 }; 1970 1971 if (folderId != null) 1972 { 1973 metadata.parents = [{'kind': 'drive#fileLink', 'id': folderId}]; 1974 } 1975 1976 // NOTE: Cannot create thumbnail on insert since no ui has no current file 1977 this.executeRequest(this.createUploadRequest(null, metadata, data, false, binary), mxUtils.bind(this, function(resp) 1978 { 1979 if (mimeType == this.libraryMimeType) 1980 { 1981 success(new DriveLibrary(this.ui, data, resp)); 1982 } 1983 else if (resp == false) 1984 { 1985 if (error != null) 1986 { 1987 error({message: mxResources.get('errorSavingFile')}); 1988 } 1989 } 1990 else 1991 { 1992 success(new DriveFile(this.ui, data, resp)); 1993 } 1994 }), error); 1995}; 1996 1997/** 1998 * Translates this point by the given vector. 1999 * 2000 * @param {number} dx X-coordinate of the translation. 2001 * @param {number} dy Y-coordinate of the translation. 2002 */ 2003DriveClient.prototype.createUploadRequest = function(id, metadata, data, revision, binary, etag, pinned) 2004{ 2005 binary = (binary != null) ? binary : false; 2006 var bd = '-------314159265358979323846'; 2007 var delim = '\r\n--' + bd + '\r\n'; 2008 var close = '\r\n--' + bd + '--'; 2009 var ctype = 'application/octect-stream'; 2010 2011 var headers = {'Content-Type' : 'multipart/mixed; boundary="' + bd + '"'}; 2012 2013 if (etag != null) 2014 { 2015 headers['If-Match'] = etag; 2016 } 2017 2018 var reqObj = 2019 { 2020 'fullUrl': 'https://content.googleapis.com/upload/drive/v2/files' + (id != null ? '/' + id : '') + 2021 '?uploadType=multipart&supportsAllDrives=true&enforceSingleParent=true&fields=' + this.allFields, 2022 'method': (id != null) ? 'PUT' : 'POST', 2023 'headers': headers, 2024 'params': delim + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delim + 2025 'Content-Type: ' + ctype + '\r\n' + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' + 2026 ((data != null) ? ((binary) ? data : ((window.btoa && !mxClient.IS_IE && !mxClient.IS_IE11) ? 2027 Graph.base64EncodeUnicode(data) : Base64.encode(data))) : '') + close 2028 } 2029 2030 if (!revision) 2031 { 2032 reqObj.fullUrl += '&newRevision=false'; 2033 } 2034 2035 if (pinned) 2036 { 2037 reqObj.fullUrl += '&pinned=true'; 2038 } 2039 2040 return reqObj; 2041}; 2042 2043/** 2044 * Translates this point by the given vector. 2045 * 2046 * @param {number} dx X-coordinate of the translation. 2047 * @param {number} dy Y-coordinate of the translation. 2048 */ 2049DriveClient.prototype.createLinkPicker = function() 2050{ 2051 var name = 'linkPicker'; 2052 var picker = pickers[name]; 2053 2054 if (picker == null || pickers[name + 'Token'] != _token) 2055 { 2056 pickers[name + 'Token'] = _token; 2057 2058 var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS) 2059 .setParent('root') 2060 .setIncludeFolders(true) 2061 .setSelectFolderEnabled(true); 2062 var view2 = new google.picker.DocsView() 2063 .setIncludeFolders(true) 2064 .setSelectFolderEnabled(true); 2065 var view21 = new google.picker.DocsView() 2066 .setIncludeFolders(true) 2067 .setEnableDrives(true) 2068 .setSelectFolderEnabled(true); 2069 picker = new google.picker.PickerBuilder() 2070 .setAppId(this.appId) 2071 .setLocale(mxLanguage) 2072 .setOAuthToken(pickers[name + 'Token']) 2073 .enableFeature(google.picker.Feature.SUPPORT_DRIVES) 2074 .addView(view) 2075 .addView(view2) 2076 .addView(view21) 2077 .addView(google.picker.ViewId.RECENTLY_PICKED); 2078 } 2079 2080 return picker; 2081}; 2082 2083/** 2084 * Translates this point by the given vector. 2085 * 2086 * @param {number} dx X-coordinate of the translation. 2087 * @param {number} dy Y-coordinate of the translation. 2088 */ 2089DriveClient.prototype.pickFile = function(fn, acceptAllFiles, cancelFn) 2090{ 2091 this.filePickerCallback = (fn != null) ? fn : mxUtils.bind(this, function(id) 2092 { 2093 this.ui.loadFile('G' + id); 2094 }); 2095 2096 this.filePicked = mxUtils.bind(this, function(data) 2097 { 2098 if (data.action == google.picker.Action.PICKED) 2099 { 2100 this.filePickerCallback(data.docs[0].id, data.docs[0]); 2101 } 2102 }); 2103 2104 if (this.ui.spinner.spin(document.body, mxResources.get('authorizing'))) 2105 { 2106 this.execute(mxUtils.bind(this, function() 2107 { 2108 try 2109 { 2110 this.ui.spinner.stop(); 2111 2112 // Reuses picker as long as token doesn't change. 2113 var name = (acceptAllFiles) ? 'genericPicker' : 'filePicker'; 2114 2115 // Click on background closes dialog as workaround for blocking dialog 2116 // states such as 401 where the dialog cannot be closed and blocks UI 2117 var exit = mxUtils.bind(this, function(evt) 2118 { 2119 // Workaround for click from appIcon on second call 2120 if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg') 2121 { 2122 mxEvent.removeListener(document, 'click', exit); 2123 this[name].setVisible(false); 2124 2125 if (cancelFn) 2126 { 2127 cancelFn(); 2128 } 2129 } 2130 }); 2131 2132 if (pickers[name] == null || pickers[name + 'Token'] != _token) 2133 { 2134 // FIXME: Dispose not working 2135 // if (pickers[name] != null) 2136 // { 2137 // console.log(name, pickers[name]); 2138 // pickers[name].dispose(); 2139 // } 2140 2141 pickers[name + 'Token'] = _token; 2142 2143 // Pseudo-hierarchical directory view, see 2144 // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ 2145 var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS) 2146 .setParent('root') 2147 .setIncludeFolders(true); 2148 2149 var view2 = new google.picker.DocsView() 2150 .setIncludeFolders(true); 2151 2152 var view3 = new google.picker.DocsView() 2153 .setEnableDrives(true) 2154 .setIncludeFolders(true); 2155 2156 var view4 = new google.picker.DocsUploadView() 2157 .setIncludeFolders(true); 2158 2159 if (!acceptAllFiles) 2160 { 2161 view.setMimeTypes(this.mimeTypes); 2162 view2.setMimeTypes(this.mimeTypes); 2163 view3.setMimeTypes(this.mimeTypes); 2164 } 2165 else 2166 { 2167 view.setMimeTypes('*/*'); 2168 view2.setMimeTypes('*/*'); 2169 view3.setMimeTypes('*/*'); 2170 } 2171 2172 pickers[name] = new google.picker.PickerBuilder() 2173 .setOAuthToken(pickers[name + 'Token']) 2174 .setLocale(mxLanguage) 2175 .setAppId(this.appId) 2176 .enableFeature(google.picker.Feature.SUPPORT_DRIVES) 2177 .addView(view) 2178 .addView(view2) 2179 .addView(view3) 2180 .addView(google.picker.ViewId.RECENTLY_PICKED) 2181 .addView(view4); 2182 2183 if (urlParams['gPickerSize']) 2184 { 2185 var cSize = urlParams['gPickerSize'].split(','); 2186 pickers[name] = pickers[name].setSize(cSize[0], cSize[1]); 2187 } 2188 2189 if (urlParams['topBaseUrl']) 2190 { 2191 pickers[name] = pickers[name].setOrigin(decodeURIComponent(urlParams['topBaseUrl'])); 2192 } 2193 2194 pickers[name] = pickers[name].setCallback(mxUtils.bind(this, function(data) 2195 { 2196 if (data.action == google.picker.Action.PICKED || 2197 data.action == google.picker.Action.CANCEL) 2198 { 2199 mxEvent.removeListener(document, 'click', exit); 2200 2201 if (cancelFn && data.action == google.picker.Action.CANCEL) 2202 { 2203 cancelFn(); 2204 } 2205 } 2206 2207 if (data.action == google.picker.Action.PICKED) 2208 { 2209 this.filePicked(data); 2210 } 2211 })).build(); 2212 } 2213 2214 mxEvent.addListener(document, 'click', exit); 2215 pickers[name].setVisible(true); 2216 } 2217 catch (e) 2218 { 2219 this.ui.spinner.stop(); 2220 this.ui.handleError(e); 2221 } 2222 })); 2223 } 2224}; 2225 2226/** 2227 * Translates this point by the given vector. 2228 * 2229 * @param {number} dx X-coordinate of the translation. 2230 * @param {number} dy Y-coordinate of the translation. 2231 */ 2232DriveClient.prototype.pickFolder = function(fn, force) 2233{ 2234 this.folderPickerCallback = fn; 2235 2236 // Picker is initialized once and points to this function 2237 // which is overridden each time to the picker is shown 2238 var showPicker = mxUtils.bind(this, function() 2239 { 2240 try 2241 { 2242 if (this.ui.spinner.spin(document.body, mxResources.get('authorizing'))) 2243 { 2244 this.execute(mxUtils.bind(this, function() 2245 { 2246 try 2247 { 2248 this.ui.spinner.stop(); 2249 2250 // Reuses picker as long as token doesn't change. 2251 var name = 'folderPicker'; 2252 2253 // Click on background closes dialog as workaround for blocking dialog 2254 // states such as 401 where the dialog cannot be closed and blocks UI 2255 var exit = mxUtils.bind(this, function(evt) 2256 { 2257 // Workaround for click from appIcon on second call 2258 if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg') 2259 { 2260 mxEvent.removeListener(document, 'click', exit); 2261 pickers[name].setVisible(false); 2262 } 2263 }); 2264 2265 if (pickers[name] == null || pickers[name + 'Token'] != _token) 2266 { 2267 // FIXME: Dispose not working 2268 // if (pickers[name] != null) 2269 // { 2270 // console.log(name, pickers[name]); 2271 // pickers[name].dispose(); 2272 // } 2273 2274 pickers[name + 'Token'] = _token; 2275 2276 // Pseudo-hierarchical directory view, see 2277 // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ 2278 var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS) 2279 .setParent('root') 2280 .setIncludeFolders(true) 2281 .setSelectFolderEnabled(true) 2282 .setMimeTypes('application/vnd.google-apps.folder'); 2283 2284 var view2 = new google.picker.DocsView() 2285 .setIncludeFolders(true) 2286 .setSelectFolderEnabled(true) 2287 .setMimeTypes('application/vnd.google-apps.folder'); 2288 2289 var view3 = new google.picker.DocsView() 2290 .setIncludeFolders(true) 2291 .setEnableDrives(true) 2292 .setSelectFolderEnabled(true) 2293 .setMimeTypes('application/vnd.google-apps.folder'); 2294 2295 pickers[name] = new google.picker.PickerBuilder() 2296 .setSelectableMimeTypes('application/vnd.google-apps.folder') 2297 .setOAuthToken(pickers[name + 'Token']) 2298 .setLocale(mxLanguage) 2299 .setAppId(this.appId) 2300 .enableFeature(google.picker.Feature.SUPPORT_DRIVES) 2301 .addView(view) 2302 .addView(view2) 2303 .addView(view3) 2304 .addView(google.picker.ViewId.RECENTLY_PICKED) 2305 .setTitle(mxResources.get('pickFolder')); 2306 2307 if (urlParams['gPickerSize']) 2308 { 2309 var cSize = urlParams['gPickerSize'].split(','); 2310 pickers[name] = pickers[name].setSize(cSize[0], cSize[1]); 2311 } 2312 2313 if (urlParams['topBaseUrl']) 2314 { 2315 pickers[name] = pickers[name].setOrigin(decodeURIComponent(urlParams['topBaseUrl'])); 2316 } 2317 2318 pickers[name] = pickers[name].setCallback(mxUtils.bind(this, function(data) 2319 { 2320 if (data.action == google.picker.Action.PICKED || 2321 data.action == google.picker.Action.CANCEL) 2322 { 2323 mxEvent.removeListener(document, 'click', exit); 2324 } 2325 2326 this.folderPickerCallback(data); 2327 })).build(); 2328 } 2329 2330 mxEvent.addListener(document, 'click', exit); 2331 pickers[name].setVisible(true); 2332 } 2333 catch (e) 2334 { 2335 this.ui.spinner.stop(); 2336 this.ui.handleError(e); 2337 } 2338 })); 2339 } 2340 } 2341 catch (e) 2342 { 2343 this.ui.handleError(e); 2344 } 2345 }); 2346 2347 if (force) 2348 { 2349 showPicker(); 2350 } 2351 else 2352 { 2353 this.ui.confirm(mxResources.get('useRootFolder'), mxUtils.bind(this, function() 2354 { 2355 this.folderPickerCallback({action: google.picker.Action.PICKED, 2356 docs: [{type: 'folder', id: 'root'}]}); 2357 }), mxUtils.bind(this, function() 2358 { 2359 showPicker(); 2360 }), mxResources.get('yes'), mxResources.get('noPickFolder') + '...', true); 2361 } 2362}; 2363 2364/** 2365 * Translates this point by the given vector. 2366 * 2367 * @param {number} dx X-coordinate of the translation. 2368 * @param {number} dy Y-coordinate of the translation. 2369 */ 2370DriveClient.prototype.pickLibrary = function(fn) 2371{ 2372 this.filePickerCallback = fn; 2373 2374 this.filePicked = mxUtils.bind(this, function(data) 2375 { 2376 if (data.action == google.picker.Action.PICKED) 2377 { 2378 this.filePickerCallback(data.docs[0].id); 2379 } 2380 else if (data.action == google.picker.Action.CANCEL && this.ui.getCurrentFile() == null) 2381 { 2382 this.ui.showSplash(); 2383 } 2384 }); 2385 2386 if (this.ui.spinner.spin(document.body, mxResources.get('authorizing'))) 2387 { 2388 this.execute(mxUtils.bind(this, function() 2389 { 2390 try 2391 { 2392 this.ui.spinner.stop(); 2393 2394 // Click on background closes dialog as workaround for blocking dialog 2395 // states such as 401 where the dialog cannot be closed and blocks UI 2396 var exit = mxUtils.bind(this, function(evt) 2397 { 2398 // Workaround for click from appIcon on second call 2399 if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg') 2400 { 2401 mxEvent.removeListener(document, 'click', exit); 2402 pickers.libraryPicker.setVisible(false); 2403 } 2404 }); 2405 2406 // Reuses picker as long as token doesn't change 2407 2408 if (pickers.libraryPicker == null || pickers.libraryPickerToken != _token) 2409 { 2410 // FIXME: Dispose not working 2411 // if (pickers[name] != null) 2412 // { 2413 // console.log(name, pickers[name]); 2414 // pickers[name].dispose(); 2415 // } 2416 2417 pickers.libraryPickerToken = _token; 2418 2419 // Pseudo-hierarchical directory view, see 2420 // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ 2421 var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS) 2422 .setParent('root') 2423 .setIncludeFolders(true) 2424 .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream'); 2425 2426 var view2 = new google.picker.DocsView() 2427 .setIncludeFolders(true) 2428 .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream'); 2429 2430 var view3 = new google.picker.DocsView() 2431 .setEnableDrives(true) 2432 .setIncludeFolders(true) 2433 .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream'); 2434 2435 var view4 = new google.picker.DocsUploadView() 2436 .setIncludeFolders(true); 2437 2438 pickers.libraryPicker = new google.picker.PickerBuilder() 2439 .setOAuthToken(pickers.libraryPickerToken) 2440 .setLocale(mxLanguage) 2441 .setAppId(this.appId) 2442 .enableFeature(google.picker.Feature.SUPPORT_DRIVES) 2443 .addView(view) 2444 .addView(view2) 2445 .addView(view3) 2446 .addView(google.picker.ViewId.RECENTLY_PICKED) 2447 .addView(view4); 2448 2449 if (urlParams['gPickerSize']) 2450 { 2451 var cSize = urlParams['gPickerSize'].split(','); 2452 pickers.libraryPicker = pickers.libraryPicker.setSize(cSize[0], cSize[1]); 2453 } 2454 2455 if (urlParams['topBaseUrl']) 2456 { 2457 pickers.libraryPicker = pickers.libraryPicker.setOrigin(decodeURIComponent(urlParams['topBaseUrl'])); 2458 } 2459 2460 pickers.libraryPicker = pickers.libraryPicker.setCallback(mxUtils.bind(this, function(data) 2461 { 2462 if (data.action == google.picker.Action.PICKED || 2463 data.action == google.picker.Action.CANCEL) 2464 { 2465 mxEvent.removeListener(document, 'click', exit); 2466 } 2467 2468 if (data.action == google.picker.Action.PICKED) 2469 { 2470 this.filePicked(data); 2471 } 2472 })).build(); 2473 } 2474 2475 mxEvent.addListener(document, 'click', exit); 2476 pickers.libraryPicker.setVisible(true); 2477 } 2478 catch (e) 2479 { 2480 this.ui.spinner.stop(); 2481 this.ui.handleError(e); 2482 } 2483 })); 2484 } 2485}; 2486 2487/** 2488 * Translates this point by the given vector. 2489 * 2490 * @param {number} dx X-coordinate of the translation. 2491 * @param {number} dy Y-coordinate of the translation. 2492 */ 2493DriveClient.prototype.showPermissions = function(id) 2494{ 2495 var fallback = mxUtils.bind(this, function() 2496 { 2497 var dlg = new ConfirmDialog(this.ui, mxResources.get('googleSharingNotAvailable'), mxUtils.bind(this, function() 2498 { 2499 this.ui.editor.graph.openLink('https://drive.google.com/open?id=' + id); 2500 }), null, mxResources.get('open'), null, null, null, null, IMAGE_PATH + '/google-share.png'); 2501 this.ui.showDialog(dlg.container, 360, 190, true, true); 2502 dlg.init(); 2503 }); 2504 2505 if (this.sharingFailed) 2506 { 2507 fallback(); 2508 } 2509 else 2510 { 2511 this.checkToken(mxUtils.bind(this, function() 2512 { 2513 try 2514 { 2515 var shareClient = new gapi.drive.share.ShareClient(this.appId); 2516 shareClient.setOAuthToken(_token); 2517 shareClient.setItemIds([id]); 2518 shareClient.showSettingsDialog(); 2519 2520 // Workaround for https://stackoverflow.com/questions/54753169 is to check 2521 // if "sharing is unavailable" is showing and invoke a fallback dialog 2522 if ('MutationObserver' in window) 2523 { 2524 if (this.sharingObserver != null) 2525 { 2526 this.sharingObserver.disconnect(); 2527 this.sharingObserver = null; 2528 } 2529 2530 // Tries again even if observer was still around as the user may have 2531 // closed the dialog while waiting. TODO: Find condition to disconnect 2532 // observer when dialog is closed (use removedNodes?). 2533 this.sharingObserver = new MutationObserver(mxUtils.bind(this, function(mutations) 2534 { 2535 var done = false; 2536 2537 for (var i = 0; i < mutations.length; i++) 2538 { 2539 for (var j = 0; j < mutations[i].addedNodes.length; j++) 2540 { 2541 var child = mutations[i].addedNodes[j]; 2542 2543 if (child.nodeName == 'BUTTON' && child.getAttribute('name') == 'ok' && 2544 child.parentNode != null && child.parentNode.parentNode != null && 2545 child.parentNode.parentNode.getAttribute('role') == 'dialog') 2546 { 2547 this.sharingFailed = true; 2548 child.click(); 2549 fallback(); 2550 done = true; 2551 } 2552 else if (child.nodeName == 'DIV' && child.className == 'shr-q-shr-r-shr-xb') 2553 { 2554 done = true; 2555 } 2556 } 2557 } 2558 2559 if (done) 2560 { 2561 this.sharingObserver.disconnect(); 2562 this.sharingObserver = null; 2563 } 2564 2565 })); 2566 2567 this.sharingObserver.observe(document, {childList: true, subtree: true}); 2568 } 2569 } 2570 catch (e) 2571 { 2572 this.ui.handleError(e); 2573 } 2574 })); 2575 } 2576}; 2577 2578DriveClient.prototype.clearPersistentToken = function() 2579{ 2580 //Since we have multiple accounts now, full deletion is not possible 2581 var authInfo = JSON.parse(this.getPersistentToken(true)) || {}; 2582 2583 //Delete current user info 2584 delete authInfo.current; 2585 delete authInfo[this.userId]; 2586 2587 //Set the next user as current 2588 for (var id in authInfo) 2589 { 2590 authInfo.current = {userId: id, expires: 0}; //An expired token 2591 break; 2592 } 2593 2594 DrawioClient.prototype.setPersistentToken.call(this, JSON.stringify(authInfo)); 2595}; 2596 2597DriveClient.prototype.setPersistentToken = function(userAuthInfo, sessionOnly) 2598{ 2599 var authInfo = JSON.parse(this.getPersistentToken(true)) || {}; 2600 2601 userAuthInfo.userId = this.userId; 2602 authInfo.current = userAuthInfo; 2603 authInfo[this.userId] = { 2604 user: this.user 2605 }; 2606 2607 DrawioClient.prototype.setPersistentToken.call(this, JSON.stringify(authInfo), sessionOnly); 2608}; 2609 2610})();