DriveClient.js 71 KB

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