DriveClient.js 70 KB

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