DriveClient.js 80 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856
  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,canComment,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,headRevisionId,modifiedDate,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. // Uses ID of file instead of revision ID in descriptor
  710. // to avoid a change of the document hash property
  711. resp.headRevisionId = resp.id;
  712. resp.id = id;
  713. this.getXmlFile(resp, success, error);
  714. }), error);
  715. }
  716. else
  717. {
  718. this.loadDescriptor(id, mxUtils.bind(this, function(resp)
  719. {
  720. try
  721. {
  722. if (this.user != null)
  723. {
  724. var binary = /\.png$/i.test(resp.title);
  725. // Handles .vsdx, .vsd, .vdx, Gliffy and PNG+XML files by creating a temporary file
  726. if (/\.v(dx|sdx?)$/i.test(resp.title) || /\.gliffy$/i.test(resp.title) ||
  727. (!this.ui.useCanvasForExport && binary))
  728. {
  729. var url = resp.downloadUrl + '&access_token=' + gapi.auth.getToken().access_token;
  730. this.ui.convertFile(url, resp.title, resp.mimeType, this.extension, success, error);
  731. }
  732. else
  733. {
  734. // Handles converted realtime files as XML files
  735. if (readXml || readLibrary || resp.mimeType == this.libraryMimeType ||
  736. resp.mimeType == this.xmlMimeType)
  737. {
  738. this.getXmlFile(resp, success, error, true, readLibrary);
  739. }
  740. else
  741. {
  742. if (this.isGoogleRealtimeMimeType(resp.mimeType))
  743. {
  744. this.convertRealtimeFile(resp, success, error);
  745. }
  746. else
  747. {
  748. this.getXmlFile(resp, success, error);
  749. }
  750. }
  751. }
  752. }
  753. else
  754. {
  755. error({message: mxResources.get('loggedOut')});
  756. }
  757. }
  758. catch (e)
  759. {
  760. if (error != null)
  761. {
  762. error(e);
  763. }
  764. else
  765. {
  766. throw e;
  767. }
  768. }
  769. }), error);
  770. }
  771. };
  772. /**
  773. * Returns true if the given mime type is for Google Realtime files.
  774. */
  775. DriveClient.prototype.isGoogleRealtimeMimeType = function(mimeType)
  776. {
  777. return mimeType != null && mimeType.substring(0, 30) == 'application/vnd.jgraph.mxfile.';
  778. };
  779. /**
  780. * Checks if the client is authorized and calls the next step.
  781. */
  782. DriveClient.prototype.getRealtimeData = function(id, success, error, retryCount)
  783. {
  784. this.executeRequest(gapi.client.drive.realtime.get({'fileId': id,
  785. 'supportsTeamDrives': true}), mxUtils.bind(this, function(resp)
  786. {
  787. var json = (resp.result != null) ? resp.result.data : null;
  788. if (json != null && json.value != null && json.value.diagrams != null)
  789. {
  790. success(json);
  791. }
  792. else if (error != null)
  793. {
  794. error({message: 'realtime.get returned invalid data for ' + id});
  795. }
  796. }), mxUtils.bind(this, function(resp)
  797. {
  798. if (retryCount == null)
  799. {
  800. retryCount = 0;
  801. }
  802. if (retryCount < 3)
  803. {
  804. window.setTimeout(mxUtils.bind(this, function()
  805. {
  806. this.getRealtimeData(id, success, error, retryCount + 1);
  807. }), (retryCount + 1) * 100);
  808. }
  809. else if (error != null)
  810. {
  811. error({message: 'realtime.get failed for ' + id});
  812. }
  813. }));
  814. };
  815. /**
  816. * Checks if the client is authorized and calls the next step.
  817. */
  818. DriveClient.prototype.loadRealtime = function(resp, success, error)
  819. {
  820. // Redirects to new app because the realtime models of different apps are not visible
  821. if (urlParams['ignoremime'] != '1' && this.appId == '420247213240' &&
  822. (resp.mimeType == 'application/vnd.jgraph.mxfile.realtime' ||
  823. resp.mimeType == 'application/mxr'))
  824. {
  825. this.redirectToNewApp(error, resp.id);
  826. }
  827. // Shows the file as read-only without conversion
  828. else
  829. {
  830. success();
  831. }
  832. };
  833. /**
  834. * Checks if the client is authorized and calls the next step. The ignoreMime argument is
  835. * used for import via getFile. Default is false. The optional
  836. * readLibrary argument is used for reading libraries. Default is false.
  837. */
  838. DriveClient.prototype.getXmlFile = function(resp, success, error, ignoreMime, readLibrary)
  839. {
  840. try
  841. {
  842. var token = gapi.auth.getToken().access_token;
  843. var url = resp.downloadUrl + '&access_token=' + token;
  844. // Loads XML to initialize realtime document if realtime is empty
  845. this.ui.loadUrl(url, mxUtils.bind(this, function(data)
  846. {
  847. try
  848. {
  849. if (data == null)
  850. {
  851. // TODO: Optional redirect to legacy if link is for old file
  852. error({message: mxResources.get('invalidOrMissingFile')});
  853. }
  854. else if (resp.mimeType == this.libraryMimeType || readLibrary)
  855. {
  856. if (resp.mimeType == this.libraryMimeType && !readLibrary)
  857. {
  858. error({message: mxResources.get('notADiagramFile')});
  859. }
  860. else
  861. {
  862. success(new DriveLibrary(this.ui, data, resp));
  863. }
  864. }
  865. else
  866. {
  867. var importFile = false;
  868. if (/\.png$/i.test(resp.title))
  869. {
  870. var index = data.lastIndexOf(',');
  871. if (index > 0)
  872. {
  873. var xml = this.ui.extractGraphModelFromPng(data.substring(index + 1));
  874. if (xml != null && xml.length > 0)
  875. {
  876. data = xml;
  877. }
  878. else
  879. {
  880. // Checks if the file contains XML data which can happen when we insert
  881. // the file and then don't post-process it when loaded into the UI which
  882. // is required for creating the images for .PNG and .SVG files.
  883. try
  884. {
  885. var xml = data.substring(index + 1);
  886. var temp = (window.atob && !mxClient.IS_IE && !mxClient.IS_IE11) ?
  887. atob(xml) : Base64.decode(xml);
  888. var node = this.ui.editor.extractGraphModel(
  889. mxUtils.parseXml(temp).documentElement, true);
  890. if (node == null || node.getElementsByTagName('parsererror').length > 0)
  891. {
  892. importFile = true;
  893. }
  894. else
  895. {
  896. data = temp;
  897. }
  898. }
  899. catch (e)
  900. {
  901. importFile = true;
  902. }
  903. }
  904. }
  905. }
  906. // Checks for base64 encoded mxfile
  907. else if (data.substring(0, 32) == 'data:image/png;base64,PG14ZmlsZS')
  908. {
  909. var temp = data.substring(22);
  910. data = (window.atob && !mxClient.IS_SF) ? atob(temp) : Base64.decode(temp);
  911. }
  912. if (Graph.fileSupport && new XMLHttpRequest().upload && this.ui.isRemoteFileFormat(data, url))
  913. {
  914. this.ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
  915. {
  916. try
  917. {
  918. if (xhr.readyState == 4)
  919. {
  920. if (xhr.status >= 200 && xhr.status <= 299)
  921. {
  922. success(new LocalFile(this.ui, xhr.responseText, resp.title + this.extension, true));
  923. }
  924. else if (error != null)
  925. {
  926. error({message: mxResources.get('errorLoadingFile')});
  927. }
  928. }
  929. }
  930. catch (e)
  931. {
  932. if (error != null)
  933. {
  934. error(e);
  935. }
  936. else
  937. {
  938. throw e;
  939. }
  940. }
  941. }), resp.title);
  942. }
  943. else
  944. {
  945. success((importFile) ? new LocalFile(this.ui, data, resp.title, true) : new DriveFile(this.ui, data, resp));
  946. }
  947. }
  948. }
  949. catch (e)
  950. {
  951. if (error != null)
  952. {
  953. error(e);
  954. }
  955. else
  956. {
  957. throw e;
  958. }
  959. }
  960. }), error, ((resp.mimeType != null && resp.mimeType.substring(0, 6) == 'image/' &&
  961. resp.mimeType.substring(0, 9) != 'image/svg')) || /\.png$/i.test(resp.title) ||
  962. /\.jpe?g$/i.test(resp.title));
  963. }
  964. catch (e)
  965. {
  966. if (error != null)
  967. {
  968. error(e);
  969. }
  970. else
  971. {
  972. throw e;
  973. }
  974. }
  975. };
  976. /**
  977. * Translates this point by the given vector.
  978. *
  979. * @param {number} dx X-coordinate of the translation.
  980. * @param {number} dy Y-coordinate of the translation.
  981. */
  982. DriveClient.prototype.saveFile = function(file, revision, success, errFn, noCheck, unloading, overwrite, properties)
  983. {
  984. var error = mxUtils.bind(this, function(e)
  985. {
  986. if (errFn != null)
  987. {
  988. errFn(e);
  989. }
  990. else
  991. {
  992. throw e;
  993. }
  994. // Logs failed save
  995. try
  996. {
  997. if (!file.isConflict(e))
  998. {
  999. var err = 'error_' + (file.getErrorMessage(e) || 'unknown');
  1000. if (e != null && e.error != null && e.error.code != null)
  1001. {
  1002. err += '-code_' + e.error.code;
  1003. }
  1004. EditorUi.logEvent({category: 'ERROR-SAVE-FILE-' + file.getHash() + '.' +
  1005. file.desc.headRevisionId + '-mod_' + file.desc.modifiedDate + '-size_' + file.getSize() +
  1006. ((this.ui.editor.autosave) ? '' : '-nosave') +
  1007. ((file.isAutosave()) ? '' : '-noauto') +
  1008. ((file.changeListenerEnabled) ? '' : '-nolisten') +
  1009. ((file.inConflictState) ? '-conflict' : '') +
  1010. ((file.invalidChecksum) ? '-invalid' : ''),
  1011. action: err, label: ((this.user != null) ? 'user_' + this.user.id : 'unknown') +
  1012. ((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')});
  1013. }
  1014. }
  1015. catch (ex)
  1016. {
  1017. // ignore
  1018. }
  1019. });
  1020. var criticalError = mxUtils.bind(this, function(e)
  1021. {
  1022. error(e);
  1023. try
  1024. {
  1025. EditorUi.logError(e.message, null, null, e);
  1026. EditorUi.sendReport('Critical error in DriveClient.saveFile ' +
  1027. new Date().toISOString() + ':' +
  1028. '\n\nBrowser=' + navigator.userAgent +
  1029. '\nFile=' + file.desc.id + '.' + file.desc.headRevisionId +
  1030. '\nUser=' + ((this.user != null) ? this.user.id : 'unknown') +
  1031. ((file.sync != null) ? '-client_' + file.sync.clientId : '-nosync') +
  1032. '\nMessage=' + e.message +
  1033. '\n\nStack:\n' + e.stack);
  1034. }
  1035. catch (e)
  1036. {
  1037. // ignore
  1038. }
  1039. });
  1040. try
  1041. {
  1042. if (file.isEditable() && file.desc != null)
  1043. {
  1044. var t0 = new Date().getTime();
  1045. var etag0 = file.desc.etag;
  1046. var mod0 = file.desc.modifiedDate;
  1047. var head0 = file.desc.headRevisionId;
  1048. var saveAsPng = this.ui.useCanvasForExport && /(\.png)$/i.test(file.getTitle());
  1049. noCheck = (noCheck != null) ? noCheck : (!this.ui.isLegacyDriveDomain() || urlParams['ignoremime'] == '1');
  1050. // NOTE: Unloading arg is currently ignored, saving during unload/beforeUnload is not possible using
  1051. // asynchronous code, which is needed to create the thumbnail, or asynchronous requests which is the only
  1052. // way to execute the gapi request below.
  1053. // If no thumbnail is created and noCheck is true (which is always true if unloading is true) in which case
  1054. // this code is synchronous, the executeRequest call is reached but the request is still not sent. This is
  1055. // true for both, calls from beforeUnload and unload handlers. Note sure how to make the call synchronous
  1056. // which is said to fix this when called from beforeUnload.
  1057. // However, this would result in a missing thumbnail in most cases so a better solution might be to reduce
  1058. // the autosave interval in DriveRealtime, but that would increase the number of requests.
  1059. unloading = (unloading != null) ? unloading : false;
  1060. // Adds optional thumbnail to upload request
  1061. var doSave = mxUtils.bind(this, function(thumb, thumbMime, keepExisting)
  1062. {
  1063. try
  1064. {
  1065. var prevDesc = null;
  1066. var pinned = false;
  1067. var meta =
  1068. {
  1069. 'mimeType': file.desc.mimeType,
  1070. 'title': file.getTitle()
  1071. };
  1072. // Overrides old mime type and creates a revision
  1073. if (this.isGoogleRealtimeMimeType(file.desc.mimeType))
  1074. {
  1075. meta.mimeType = this.xmlMimeType;
  1076. prevDesc = file.desc;
  1077. revision = true;
  1078. pinned = true;
  1079. }
  1080. // Overrides mime type for unknown file type uploads
  1081. else if (meta.mimeType == 'application/octet-stream')
  1082. {
  1083. meta.mimeType = this.xmlMimeType;
  1084. }
  1085. if (file.constructor == DriveFile)
  1086. {
  1087. if (properties == null)
  1088. {
  1089. properties = [];
  1090. }
  1091. // Channel ID appended to file ID for comms
  1092. if (file.getChannelId() == null)
  1093. {
  1094. properties.push({'key': 'channel', 'value': Editor.guid(32)});
  1095. }
  1096. // Key for encryption of comms
  1097. if (file.getChannelKey() == null)
  1098. {
  1099. properties.push({'key': 'key', 'value': Editor.guid(32)});
  1100. }
  1101. // Pass to access cache for each etag
  1102. properties.push({'key': 'secret', 'value': Editor.guid(32)});
  1103. }
  1104. // Specifies that no thumbnail should be uploaded in which case the existing thumbnail is used
  1105. if (!keepExisting)
  1106. {
  1107. // Uses placeholder thumbnail to replace existing one except when unloading
  1108. // in which case the XML is updated but the existing thumbnail is not in order
  1109. // to avoid executing asynchronous code and get the XML to the server instead
  1110. if (thumb == null && !unloading)
  1111. {
  1112. thumb = this.placeholderThumbnail;
  1113. thumbMime = this.placeholderMimeType;
  1114. }
  1115. // Adds metadata for thumbnail
  1116. if (thumb != null && thumbMime != null)
  1117. {
  1118. meta.thumbnail =
  1119. {
  1120. 'image': thumb,
  1121. 'mimeType': thumbMime
  1122. };
  1123. }
  1124. }
  1125. var savedData = file.getData();
  1126. // Updates saveDelay on drive file
  1127. var wrapper = mxUtils.bind(this, function(resp)
  1128. {
  1129. try
  1130. {
  1131. file.saveDelay = new Date().getTime() - t0;
  1132. // Checks if modified time is in the future and head revision has changed
  1133. var delta = new Date(resp.modifiedDate).getTime() - new Date(mod0).getTime();
  1134. if (delta <= 0 || etag0 == resp.etag || (revision && head0 == resp.headRevisionId))
  1135. {
  1136. var reasons = [];
  1137. if (delta <= 0)
  1138. {
  1139. reasons.push('invalid modified time');
  1140. }
  1141. if (etag0 == resp.etag)
  1142. {
  1143. reasons.push('stale etag');
  1144. }
  1145. if (revision && head0 == resp.headRevisionId)
  1146. {
  1147. reasons.push('stale revision');
  1148. }
  1149. var temp = reasons.join(', ');
  1150. error({message: mxResources.get('errorSavingFile') + ': ' + temp}, resp);
  1151. // Logs failed save
  1152. try
  1153. {
  1154. EditorUi.sendReport('Critical: Error saving to Google Drive ' +
  1155. new Date().toISOString() + ':' + '\n\nBrowser=' + navigator.userAgent +
  1156. '\nFile=' + file.desc.id + ' ' + file.desc.mimeType +
  1157. '\nUser=' + ((this.user != null) ? this.user.id : 'unknown') +
  1158. ((file.sync != null) ? '-client_' + file.sync.clientId : '-nosync') +
  1159. '\nErrors=' + temp + '\nOld=' + head0 + ' ' + mod0 + ' etag-hash=' +
  1160. this.ui.hashValue(etag0) + '\nNew=' + resp.headRevisionId + ' ' +
  1161. resp.modifiedDate + ' etag-hash=' + this.ui.hashValue(resp.etag))
  1162. EditorUi.logError('Critical: Error saving to Google Drive ' + file.desc.id,
  1163. null, 'from-' + head0 + '.' + mod0 + '-' + this.ui.hashValue(etag0) +
  1164. '-to-' + resp.headRevisionId + '.' + resp.modifiedDate + '-' +
  1165. this.ui.hashValue(resp.etag) + ((temp.length > 0) ? '-errors-' + temp : ''),
  1166. (this.user != null) ? this.user.id : 'unknown');
  1167. }
  1168. catch (e)
  1169. {
  1170. // ignore
  1171. }
  1172. }
  1173. else
  1174. {
  1175. success(resp, savedData);
  1176. if (prevDesc != null)
  1177. {
  1178. // Pins previous revision
  1179. this.executeRequest(gapi.client.drive.revisions.get(
  1180. {
  1181. 'fileId': prevDesc.id,
  1182. 'revisionId': prevDesc.headRevisionId,
  1183. 'supportsTeamDrives': true
  1184. }), mxUtils.bind(this, mxUtils.bind(this, function(resp)
  1185. {
  1186. resp.pinned = true;
  1187. this.executeRequest(gapi.client.drive.revisions.update(
  1188. {
  1189. 'fileId': prevDesc.id,
  1190. 'revisionId': prevDesc.headRevisionId,
  1191. 'resource': resp
  1192. }));
  1193. })));
  1194. // Logs conversion
  1195. try
  1196. {
  1197. EditorUi.logEvent({category: file.convertedFrom + '-CONVERT-FILE-' + file.getHash(),
  1198. action: 'from_' + prevDesc.id + '.' + prevDesc.headRevisionId +
  1199. '-to_' + file.desc.id + '.' + file.desc.headRevisionId,
  1200. label: (this.user != null) ? 'user_' + this.user.id : 'unknown' +
  1201. ((file.sync != null) ? '-client_' + file.sync.clientId : 'nosync')});
  1202. }
  1203. catch (e)
  1204. {
  1205. // ignore
  1206. }
  1207. }
  1208. // Logs successful save
  1209. try
  1210. {
  1211. EditorUi.logEvent({category: 'SUCCESS-SAVE-FILE-' + file.getHash() +
  1212. '.' + head0 + '-mod_' + mod0, action: 'saved-' + resp.headRevisionId +
  1213. '-mod_' + resp.modifiedDate + '-size_' + file.getSize() +
  1214. ((this.ui.editor.autosave) ? '' : '-nosave') +
  1215. ((file.isAutosave()) ? '' : '-noauto') +
  1216. ((file.changeListenerEnabled) ? '' : '-nolisten') +
  1217. ((file.inConflictState) ? '-conflict' : '') +
  1218. ((file.invalidChecksum) ? '-invalid' : ''),
  1219. label: ((this.user != null) ? 'user_' + this.user.id : 'unknown') +
  1220. ((file.sync != null) ? ('-client_' + file.sync.clientId) : '-nosync')});
  1221. }
  1222. catch (e)
  1223. {
  1224. // ignore
  1225. }
  1226. }
  1227. }
  1228. catch (e)
  1229. {
  1230. criticalError(e);
  1231. }
  1232. });
  1233. var doExecuteRequest = mxUtils.bind(this, function(data, binary)
  1234. {
  1235. try
  1236. {
  1237. if (properties != null)
  1238. {
  1239. meta.properties = properties;
  1240. }
  1241. // Used to check if file was changed externally
  1242. var etag = (!overwrite && file.constructor == DriveFile &&
  1243. (DrawioFile.SYNC == 'manual' || DrawioFile.SYNC == 'auto')) ?
  1244. file.getCurrentEtag() : null;
  1245. var retryCount = 0;
  1246. var executeSave = mxUtils.bind(this, function(realOverwrite)
  1247. {
  1248. try
  1249. {
  1250. var unknown = file.desc.mimeType != this.xmlMimeType && file.desc.mimeType != this.mimeType &&
  1251. file.desc.mimeType != this.libraryMimeType;
  1252. this.executeRequest(this.createUploadRequest(file.getId(), meta,
  1253. data, revision || realOverwrite || unknown, binary,
  1254. (realOverwrite) ? null : etag, pinned), wrapper,
  1255. mxUtils.bind(this, function(err)
  1256. {
  1257. try
  1258. {
  1259. if (!file.isConflict(err))
  1260. {
  1261. error(err);
  1262. }
  1263. else
  1264. {
  1265. // Check for stale etag which can happen if a file is being saved or if
  1266. // the etag simply isn't change but system still returns a 412 error (stale)
  1267. this.executeRequest(gapi.client.drive.files.get({'fileId': file.getId(),
  1268. 'fields': this.catchupFields, 'supportsTeamDrives': true}),
  1269. mxUtils.bind(this, function(resp)
  1270. {
  1271. try
  1272. {
  1273. // Stale etag detected, retry with delay
  1274. if (resp != null && resp.etag == etag)
  1275. {
  1276. if (retryCount < this.maxRetries)
  1277. {
  1278. retryCount++;
  1279. var jitter = 1 + 0.1 * (Math.random() - 0.5);
  1280. var delay = retryCount * 2 * this.coolOff * jitter;
  1281. window.setTimeout(executeSave, delay);
  1282. }
  1283. else
  1284. {
  1285. executeSave(true);
  1286. // Logs overwrite
  1287. try
  1288. {
  1289. EditorUi.logError('Warning: Stale Etag Overwrite ' + file.getHash(),
  1290. null, file.desc.id + '.' + file.desc.headRevisionId,
  1291. (this.user != null) ? this.user.id : 'unknown' +
  1292. '.' + ((file.sync != null) ? file.sync.clientId : 'nosync'));
  1293. }
  1294. catch (e)
  1295. {
  1296. // ignore
  1297. }
  1298. }
  1299. }
  1300. else if (error != null)
  1301. {
  1302. error(err, resp);
  1303. }
  1304. }
  1305. catch (e)
  1306. {
  1307. criticalError(e);
  1308. }
  1309. }), mxUtils.bind(this, function()
  1310. {
  1311. if (error != null)
  1312. {
  1313. error(err);
  1314. }
  1315. }));
  1316. }
  1317. }
  1318. catch (e)
  1319. {
  1320. criticalError(e);
  1321. }
  1322. }));
  1323. }
  1324. catch (e)
  1325. {
  1326. criticalError(e);
  1327. }
  1328. });
  1329. // Uses saved PNG data for thumbnail
  1330. if (saveAsPng && thumb == null)
  1331. {
  1332. var img = new Image();
  1333. img.onload = mxUtils.bind(this, function()
  1334. {
  1335. try
  1336. {
  1337. var s = this.thumbnailWidth / img.width;
  1338. var canvas = document.createElement('canvas');
  1339. canvas.width = this.thumbnailWidth;
  1340. canvas.height = Math.floor(img.height * s);
  1341. var ctx = canvas.getContext('2d');
  1342. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  1343. var temp = canvas.toDataURL();
  1344. temp = temp.substring(temp.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_');
  1345. meta.thumbnail =
  1346. {
  1347. 'image': temp,
  1348. 'mimeType': 'image/png'
  1349. };
  1350. executeSave(false);
  1351. }
  1352. catch (e)
  1353. {
  1354. executeSave(false)
  1355. }
  1356. });
  1357. img.src = 'data:image/png;base64,' + data;
  1358. }
  1359. else
  1360. {
  1361. executeSave(false);
  1362. }
  1363. }
  1364. catch (e)
  1365. {
  1366. criticalError(e);
  1367. }
  1368. });
  1369. if (saveAsPng)
  1370. {
  1371. this.ui.getEmbeddedPng(mxUtils.bind(this, function(data)
  1372. {
  1373. doExecuteRequest(data, true);
  1374. }), error, (this.ui.getCurrentFile() != file) ? savedData : null);
  1375. }
  1376. else
  1377. {
  1378. doExecuteRequest(savedData, false);
  1379. }
  1380. }
  1381. catch (e)
  1382. {
  1383. criticalError(e);
  1384. }
  1385. });
  1386. // Indirection to generate thumbnails if enabled and supported
  1387. // (required because generation of thumbnails is asynchronous)
  1388. var fn = mxUtils.bind(this, function()
  1389. {
  1390. // NOTE: getThumbnail is asynchronous and returns false if no thumbnails can be created
  1391. if (unloading || saveAsPng || file.constructor == DriveLibrary || !this.enableThumbnails || urlParams['thumb'] == '0' ||
  1392. (file.desc.mimeType != null && file.desc.mimeType.substring(0, 29) != 'application/vnd.jgraph.mxfile') ||
  1393. !this.ui.getThumbnail(this.thumbnailWidth, mxUtils.bind(this, function(canvas)
  1394. {
  1395. // Callback for getThumbnail
  1396. try
  1397. {
  1398. var thumb = null;
  1399. try
  1400. {
  1401. if (canvas != null)
  1402. {
  1403. // Security errors are possible
  1404. thumb = canvas.toDataURL('image/png');
  1405. }
  1406. // Maximum thumbnail size is 2MB
  1407. if (thumb != null)
  1408. {
  1409. if (thumb.length > this.maxThumbnailSize)
  1410. {
  1411. thumb = null;
  1412. }
  1413. else
  1414. {
  1415. // Converts base64 data into required format for Drive (base64url with no prefix)
  1416. thumb = thumb.substring(thumb.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_');
  1417. }
  1418. }
  1419. }
  1420. catch (e)
  1421. {
  1422. thumb = null;
  1423. }
  1424. doSave(thumb, 'image/png');
  1425. }
  1426. catch (e)
  1427. {
  1428. criticalError(e);
  1429. }
  1430. })))
  1431. {
  1432. // If-branch
  1433. doSave(null, null, file.constructor != DriveLibrary);
  1434. }
  1435. });
  1436. // New revision is required if mime type changes, but the mime type should not be replaced
  1437. // if the file has been converted to the new realtime format. To check this we make sure
  1438. // that the mime type has not changed before updating it in the case of the legacy app.
  1439. // Note: We need to always check the mime type because saveFile cancels previous save
  1440. // attempts so if the save frequency is higher than the time for all retries than you
  1441. // will never see the error message and accumulate lots of changes that will be lost.
  1442. if (noCheck || !revision)
  1443. {
  1444. fn();
  1445. }
  1446. else
  1447. {
  1448. this.verifyMimeType(file.getId(), fn, true);
  1449. }
  1450. }
  1451. else
  1452. {
  1453. this.ui.editor.graph.reset();
  1454. if (error != null)
  1455. {
  1456. error({message: mxResources.get('readOnly')});
  1457. }
  1458. }
  1459. }
  1460. catch (e)
  1461. {
  1462. criticalError(e);
  1463. }
  1464. };
  1465. /**
  1466. * Verifies the mime type of the given file ID.
  1467. */
  1468. DriveClient.prototype.verifyMimeType = function(fileId, fn, force, error)
  1469. {
  1470. if (this.lastMimeCheck == null)
  1471. {
  1472. this.lastMimeCheck = 0;
  1473. }
  1474. var now = new Date().getTime();
  1475. if (force || now - this.lastMimeCheck > this.mimeTypeCheckCoolOff)
  1476. {
  1477. this.lastMimeCheck = now;
  1478. if (!this.checkingMimeType)
  1479. {
  1480. this.checkingMimeType = true;
  1481. this.executeRequest(gapi.client.drive.files.get({'fileId': fileId, 'fields': 'mimeType',
  1482. 'supportsTeamDrives': true}), mxUtils.bind(this, function(resp)
  1483. {
  1484. this.checkingMimeType = false;
  1485. if (resp != null && resp.mimeType == 'application/vnd.jgraph.mxfile.realtime')
  1486. {
  1487. this.redirectToNewApp(error, fileId);
  1488. }
  1489. else if (fn != null)
  1490. {
  1491. fn();
  1492. }
  1493. }));
  1494. }
  1495. }
  1496. };
  1497. /**
  1498. * Checks if the client is authorized and calls the next step.
  1499. */
  1500. DriveClient.prototype.redirectToNewApp = function(error, fileId)
  1501. {
  1502. this.ui.spinner.stop();
  1503. if (!this.redirectDialogShowing)
  1504. {
  1505. this.redirectDialogShowing = true;
  1506. var url = window.location.protocol + '//' + this.newAppHostname + '/' + this.ui.getSearch(
  1507. ['create', 'title', 'mode', 'url', 'drive', 'splash', 'state']) + '#G' + fileId;
  1508. var redirect = mxUtils.bind(this, function()
  1509. {
  1510. this.redirectDialogShowing = false;
  1511. if (window.location.href == url)
  1512. {
  1513. window.location.reload();
  1514. }
  1515. else
  1516. {
  1517. window.location.href = url;
  1518. }
  1519. });
  1520. if (error != null)
  1521. {
  1522. this.ui.confirm(mxResources.get('redirectToNewApp'), redirect, mxUtils.bind(this, function()
  1523. {
  1524. this.redirectDialogShowing = false;
  1525. if (error != null)
  1526. {
  1527. error();
  1528. }
  1529. }));
  1530. }
  1531. else
  1532. {
  1533. this.ui.alert(mxResources.get('redirectToNewApp'), redirect);
  1534. }
  1535. }
  1536. };
  1537. /**
  1538. * Translates this point by the given vector.
  1539. *
  1540. * @param {number} dx X-coordinate of the translation.
  1541. * @param {number} dy Y-coordinate of the translation.
  1542. */
  1543. DriveClient.prototype.insertFile = function(title, data, folderId, success, error, mimeType, binary)
  1544. {
  1545. mimeType = (mimeType != null) ? mimeType : this.xmlMimeType;
  1546. var metadata =
  1547. {
  1548. 'mimeType': mimeType,
  1549. 'title': title
  1550. };
  1551. if (folderId != null)
  1552. {
  1553. metadata.parents = [{'kind': 'drive#fileLink', 'id': folderId}];
  1554. }
  1555. // NOTE: Cannot create thumbnail on insert since no ui has no current file
  1556. this.executeRequest(this.createUploadRequest(null, metadata, data, false, binary), mxUtils.bind(this, function(resp)
  1557. {
  1558. if (mimeType == this.libraryMimeType)
  1559. {
  1560. success(new DriveLibrary(this.ui, data, resp));
  1561. }
  1562. else if (resp == false)
  1563. {
  1564. if (error != null)
  1565. {
  1566. error({message: mxResources.get('errorSavingFile')});
  1567. }
  1568. }
  1569. else
  1570. {
  1571. success(new DriveFile(this.ui, data, resp));
  1572. }
  1573. }), error);
  1574. };
  1575. /**
  1576. * Translates this point by the given vector.
  1577. *
  1578. * @param {number} dx X-coordinate of the translation.
  1579. * @param {number} dy Y-coordinate of the translation.
  1580. */
  1581. DriveClient.prototype.createUploadRequest = function(id, metadata, data, revision, binary, etag, pinned)
  1582. {
  1583. binary = (binary != null) ? binary : false;
  1584. var bd = '-------314159265358979323846';
  1585. var delim = '\r\n--' + bd + '\r\n';
  1586. var close = '\r\n--' + bd + '--';
  1587. var ctype = 'application/octect-stream';
  1588. var headers = {'Content-Type' : 'multipart/mixed; boundary="' + bd + '"'};
  1589. if (etag != null)
  1590. {
  1591. headers['If-Match'] = etag;
  1592. }
  1593. var reqObj =
  1594. {
  1595. 'path': '/upload/drive/v2/files' + (id != null ? '/' + id : ''),
  1596. 'method': (id != null) ? 'PUT' : 'POST',
  1597. 'params': {'uploadType': 'multipart'},
  1598. 'headers': headers,
  1599. 'body': delim + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delim +
  1600. 'Content-Type: ' + ctype + '\r\n' + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' +
  1601. ((data != null) ? (binary) ? data : Base64.encode(data) : '') + close
  1602. }
  1603. if (!revision)
  1604. {
  1605. reqObj.params['newRevision'] = false;
  1606. }
  1607. if (pinned)
  1608. {
  1609. reqObj.params['pinned'] = true;
  1610. }
  1611. reqObj.params['supportsTeamDrives'] = true;
  1612. reqObj.params['fields'] = this.allFields;
  1613. return gapi.client.request(reqObj);
  1614. };
  1615. /**
  1616. * Translates this point by the given vector.
  1617. *
  1618. * @param {number} dx X-coordinate of the translation.
  1619. * @param {number} dy Y-coordinate of the translation.
  1620. */
  1621. DriveClient.prototype.pickFile = function(fn, acceptAllFiles)
  1622. {
  1623. this.filePickerCallback = (fn != null) ? fn : mxUtils.bind(this, function(id)
  1624. {
  1625. this.ui.loadFile('G' + id);
  1626. });
  1627. this.filePicked = mxUtils.bind(this, function(data)
  1628. {
  1629. if (data.action == google.picker.Action.PICKED)
  1630. {
  1631. this.filePickerCallback(data.docs[0].id);
  1632. }
  1633. });
  1634. if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
  1635. {
  1636. this.execute(mxUtils.bind(this, function()
  1637. {
  1638. try
  1639. {
  1640. this.ui.spinner.stop();
  1641. // Reuses picker as long as token doesn't change.
  1642. var token = gapi.auth.getToken().access_token;
  1643. var name = (acceptAllFiles) ? 'genericPicker' : 'filePicker';
  1644. // Click on background closes dialog as workaround for blocking dialog
  1645. // states such as 401 where the dialog cannot be closed and blocks UI
  1646. var exit = mxUtils.bind(this, function(evt)
  1647. {
  1648. // Workaround for click from appIcon on second call
  1649. if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
  1650. {
  1651. mxEvent.removeListener(document, 'click', exit);
  1652. this[name].setVisible(false);
  1653. }
  1654. });
  1655. if (this[name] == null || this[name + 'Token'] != token)
  1656. {
  1657. // FIXME: Dispose not working
  1658. // if (this[name] != null)
  1659. // {
  1660. // console.log(name, this[name]);
  1661. // this[name].dispose();
  1662. // }
  1663. this[name + 'Token'] = token;
  1664. // Pseudo-hierarchical directory view, see
  1665. // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
  1666. var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
  1667. .setParent('root')
  1668. .setIncludeFolders(true);
  1669. var view2 = new google.picker.DocsView()
  1670. .setIncludeFolders(true);
  1671. var view3 = new google.picker.DocsView()
  1672. .setEnableTeamDrives(true)
  1673. .setIncludeFolders(true);
  1674. var view4 = new google.picker.DocsUploadView()
  1675. .setIncludeFolders(true);
  1676. if (!acceptAllFiles)
  1677. {
  1678. view.setMimeTypes(this.mimeTypes);
  1679. view2.setMimeTypes(this.mimeTypes);
  1680. view3.setMimeTypes(this.mimeTypes);
  1681. }
  1682. else
  1683. {
  1684. view.setMimeTypes('*/*');
  1685. view2.setMimeTypes('*/*');
  1686. view3.setMimeTypes('*/*');
  1687. }
  1688. this[name] = new google.picker.PickerBuilder()
  1689. .setOAuthToken(this[name + 'Token'])
  1690. .setLocale(mxLanguage)
  1691. .setAppId(this.appId)
  1692. .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
  1693. .addView(view)
  1694. .addView(view2)
  1695. .addView(view3)
  1696. .addView(google.picker.ViewId.RECENTLY_PICKED)
  1697. .addView(view4)
  1698. .setCallback(mxUtils.bind(this, function(data)
  1699. {
  1700. if (data.action == google.picker.Action.PICKED ||
  1701. data.action == google.picker.Action.CANCEL)
  1702. {
  1703. mxEvent.removeListener(document, 'click', exit);
  1704. }
  1705. if (data.action == google.picker.Action.PICKED)
  1706. {
  1707. this.filePicked(data);
  1708. }
  1709. })).build();
  1710. }
  1711. mxEvent.addListener(document, 'click', exit);
  1712. this[name].setVisible(true);
  1713. }
  1714. catch (e)
  1715. {
  1716. this.ui.spinner.stop();
  1717. this.ui.handleError(e);
  1718. }
  1719. }));
  1720. }
  1721. };
  1722. /**
  1723. * Translates this point by the given vector.
  1724. *
  1725. * @param {number} dx X-coordinate of the translation.
  1726. * @param {number} dy Y-coordinate of the translation.
  1727. */
  1728. DriveClient.prototype.pickFolder = function(fn, force)
  1729. {
  1730. this.folderPickerCallback = fn;
  1731. // Picker is initialized once and points to this function
  1732. // which is overridden each time to the picker is shown
  1733. var showPicker = mxUtils.bind(this, function()
  1734. {
  1735. try
  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. // Reuses picker as long as token doesn't change.
  1745. var token = gapi.auth.getToken().access_token;
  1746. var name = 'folderPicker';
  1747. // Click on background closes dialog as workaround for blocking dialog
  1748. // states such as 401 where the dialog cannot be closed and blocks UI
  1749. var exit = mxUtils.bind(this, function(evt)
  1750. {
  1751. // Workaround for click from appIcon on second call
  1752. if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
  1753. {
  1754. mxEvent.removeListener(document, 'click', exit);
  1755. this[name].setVisible(false);
  1756. }
  1757. });
  1758. if (this[name] == null || this[name + 'Token'] != token)
  1759. {
  1760. // FIXME: Dispose not working
  1761. // if (this[name] != null)
  1762. // {
  1763. // console.log(name, this[name]);
  1764. // this[name].dispose();
  1765. // }
  1766. this[name + 'Token'] = token;
  1767. // Pseudo-hierarchical directory view, see
  1768. // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
  1769. var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
  1770. .setParent('root')
  1771. .setIncludeFolders(true)
  1772. .setSelectFolderEnabled(true)
  1773. .setMimeTypes('application/vnd.google-apps.folder');
  1774. var view2 = new google.picker.DocsView()
  1775. .setIncludeFolders(true)
  1776. .setSelectFolderEnabled(true)
  1777. .setMimeTypes('application/vnd.google-apps.folder');
  1778. var view3 = new google.picker.DocsView()
  1779. .setIncludeFolders(true)
  1780. .setEnableTeamDrives(true)
  1781. .setSelectFolderEnabled(true)
  1782. .setMimeTypes('application/vnd.google-apps.folder');
  1783. this[name] = new google.picker.PickerBuilder()
  1784. .setSelectableMimeTypes('application/vnd.google-apps.folder')
  1785. .setOAuthToken(this[name + 'Token'])
  1786. .setLocale(mxLanguage)
  1787. .setAppId(this.appId)
  1788. .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
  1789. .addView(view)
  1790. .addView(view2)
  1791. .addView(view3)
  1792. .addView(google.picker.ViewId.RECENTLY_PICKED)
  1793. .setTitle(mxResources.get('pickFolder'))
  1794. .setCallback(mxUtils.bind(this, function(data)
  1795. {
  1796. if (data.action == google.picker.Action.PICKED ||
  1797. data.action == google.picker.Action.CANCEL)
  1798. {
  1799. mxEvent.removeListener(document, 'click', exit);
  1800. }
  1801. this.folderPickerCallback(data);
  1802. })).build();
  1803. }
  1804. mxEvent.addListener(document, 'click', exit);
  1805. this[name].setVisible(true);
  1806. }
  1807. catch (e)
  1808. {
  1809. this.ui.spinner.stop();
  1810. this.ui.handleError(e);
  1811. }
  1812. }));
  1813. }
  1814. }
  1815. catch (e)
  1816. {
  1817. this.ui.handleError(e);
  1818. }
  1819. });
  1820. if (force)
  1821. {
  1822. showPicker();
  1823. }
  1824. else
  1825. {
  1826. this.ui.confirm(mxResources.get('useRootFolder'), mxUtils.bind(this, function()
  1827. {
  1828. this.folderPickerCallback({action: google.picker.Action.PICKED,
  1829. docs: [{type: 'folder', id: 'root'}]});
  1830. }), mxUtils.bind(this, function()
  1831. {
  1832. showPicker();
  1833. }), mxResources.get('yes'), mxResources.get('noPickFolder') + '...', true);
  1834. }
  1835. };
  1836. /**
  1837. * Translates this point by the given vector.
  1838. *
  1839. * @param {number} dx X-coordinate of the translation.
  1840. * @param {number} dy Y-coordinate of the translation.
  1841. */
  1842. DriveClient.prototype.pickLibrary = function(fn)
  1843. {
  1844. this.filePickerCallback = fn;
  1845. this.filePicked = mxUtils.bind(this, function(data)
  1846. {
  1847. if (data.action == google.picker.Action.PICKED)
  1848. {
  1849. this.filePickerCallback(data.docs[0].id);
  1850. }
  1851. else if (data.action == google.picker.Action.CANCEL && this.ui.getCurrentFile() == null)
  1852. {
  1853. this.ui.showSplash();
  1854. }
  1855. });
  1856. if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
  1857. {
  1858. this.execute(mxUtils.bind(this, function()
  1859. {
  1860. try
  1861. {
  1862. this.ui.spinner.stop();
  1863. // Click on background closes dialog as workaround for blocking dialog
  1864. // states such as 401 where the dialog cannot be closed and blocks UI
  1865. var exit = mxUtils.bind(this, function(evt)
  1866. {
  1867. // Workaround for click from appIcon on second call
  1868. if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
  1869. {
  1870. mxEvent.removeListener(document, 'click', exit);
  1871. this.libraryPicker.setVisible(false);
  1872. }
  1873. });
  1874. // Reuses picker as long as token doesn't change
  1875. var token = gapi.auth.getToken().access_token;
  1876. if (this.libraryPicker == null || this.libraryPickerToken != token)
  1877. {
  1878. // FIXME: Dispose not working
  1879. // if (this[name] != null)
  1880. // {
  1881. // console.log(name, this[name]);
  1882. // this[name].dispose();
  1883. // }
  1884. this.libraryPickerToken = token;
  1885. // Pseudo-hierarchical directory view, see
  1886. // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
  1887. var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
  1888. .setParent('root')
  1889. .setIncludeFolders(true)
  1890. .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
  1891. var view2 = new google.picker.DocsView()
  1892. .setIncludeFolders(true)
  1893. .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
  1894. var view3 = new google.picker.DocsView()
  1895. .setEnableTeamDrives(true)
  1896. .setIncludeFolders(true)
  1897. .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
  1898. var view4 = new google.picker.DocsUploadView()
  1899. .setIncludeFolders(true);
  1900. this.libraryPicker = new google.picker.PickerBuilder()
  1901. .setOAuthToken(this.libraryPickerToken)
  1902. .setLocale(mxLanguage)
  1903. .setAppId(this.appId)
  1904. .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
  1905. .addView(view)
  1906. .addView(view2)
  1907. .addView(view3)
  1908. .addView(google.picker.ViewId.RECENTLY_PICKED)
  1909. .addView(view4)
  1910. .setCallback(mxUtils.bind(this, function(data)
  1911. {
  1912. if (data.action == google.picker.Action.PICKED ||
  1913. data.action == google.picker.Action.CANCEL)
  1914. {
  1915. mxEvent.removeListener(document, 'click', exit);
  1916. }
  1917. if (data.action == google.picker.Action.PICKED)
  1918. {
  1919. this.filePicked(data);
  1920. }
  1921. })).build();
  1922. }
  1923. mxEvent.addListener(document, 'click', exit);
  1924. this.libraryPicker.setVisible(true);
  1925. }
  1926. catch (e)
  1927. {
  1928. this.ui.spinner.stop();
  1929. this.ui.handleError(e);
  1930. }
  1931. }));
  1932. }
  1933. };
  1934. /**
  1935. * Translates this point by the given vector.
  1936. *
  1937. * @param {number} dx X-coordinate of the translation.
  1938. * @param {number} dy Y-coordinate of the translation.
  1939. */
  1940. DriveClient.prototype.showPermissions = function(id)
  1941. {
  1942. var fallback = mxUtils.bind(this, function()
  1943. {
  1944. var dlg = new ConfirmDialog(this.ui, mxResources.get('googleSharingNotAvailable'), mxUtils.bind(this, function()
  1945. {
  1946. this.ui.editor.graph.openLink('https://drive.google.com/open?id=' + id);
  1947. }), null, mxResources.get('open'), null, null, null, null, IMAGE_PATH + '/google-share.png');
  1948. this.ui.showDialog(dlg.container, 360, 190, true, true);
  1949. dlg.init();
  1950. });
  1951. if (this.sharingFailed)
  1952. {
  1953. fallback();
  1954. }
  1955. else
  1956. {
  1957. this.checkToken(mxUtils.bind(this, function()
  1958. {
  1959. try
  1960. {
  1961. var shareClient = new gapi.drive.share.ShareClient(this.appId);
  1962. shareClient.setOAuthToken(gapi.auth.getToken().access_token);
  1963. shareClient.setItemIds([id]);
  1964. shareClient.showSettingsDialog();
  1965. // Workaround for https://stackoverflow.com/questions/54753169 is to check
  1966. // if "sharing is unavailable" is showing and invoke a fallback dialog
  1967. if ('MutationObserver' in window)
  1968. {
  1969. if (this.sharingObserver != null)
  1970. {
  1971. this.sharingObserver.disconnect();
  1972. this.sharingObserver = null;
  1973. }
  1974. // Tries again even if observer was still around as the user may have
  1975. // closed the dialog while waiting. TODO: Find condition to disconnect
  1976. // observer when dialog is closed (use removedNodes?).
  1977. this.sharingObserver = new MutationObserver(mxUtils.bind(this, function(mutations)
  1978. {
  1979. var done = false;
  1980. for (var i = 0; i < mutations.length; i++)
  1981. {
  1982. for (var j = 0; j < mutations[i].addedNodes.length; j++)
  1983. {
  1984. var child = mutations[i].addedNodes[j];
  1985. if (child.nodeName == 'BUTTON' && child.getAttribute('name') == 'ok' &&
  1986. child.parentNode != null && child.parentNode.parentNode != null &&
  1987. child.parentNode.parentNode.getAttribute('role') == 'dialog')
  1988. {
  1989. this.sharingFailed = true;
  1990. child.click();
  1991. fallback();
  1992. done = true;
  1993. }
  1994. else if (child.nodeName == 'DIV' && child.className == 'shr-q-shr-r-shr-xb')
  1995. {
  1996. done = true;
  1997. }
  1998. }
  1999. }
  2000. if (done)
  2001. {
  2002. this.sharingObserver.disconnect();
  2003. this.sharingObserver = null;
  2004. }
  2005. }));
  2006. this.sharingObserver.observe(document, {childList: true, subtree: true});
  2007. }
  2008. }
  2009. catch (e)
  2010. {
  2011. this.ui.handleError(e);
  2012. }
  2013. }));
  2014. }
  2015. };
  2016. /**
  2017. * Converts the given file from realtime to XML.
  2018. */
  2019. DriveClient.prototype.getRealtimeAge = function(desc, json)
  2020. {
  2021. var mod = (json != null && json.value != null && json.value.modifiedDate != null) ?
  2022. json.value.modifiedDate.json : null;
  2023. var result = 0;
  2024. if (mod != null && mod > 0)
  2025. {
  2026. var ts = new Date(desc.modifiedDate);
  2027. var rt = new Date(mod);
  2028. result = ts.getTime() - rt.getTime();
  2029. }
  2030. return result;
  2031. };
  2032. /**
  2033. * Converts the given file from realtime to XML.
  2034. */
  2035. DriveClient.prototype.convertRealtimeFile = function(desc, success, error)
  2036. {
  2037. var xmlSuccess = mxUtils.bind(this, function(file)
  2038. {
  2039. file.convertedFrom = 'xml';
  2040. success(file);
  2041. });
  2042. var jsonSuccess = mxUtils.bind(this, function(file)
  2043. {
  2044. file.convertedFrom = 'json';
  2045. success(file);
  2046. });
  2047. this.getRealtimeData(desc.id, mxUtils.bind(this, function(json)
  2048. {
  2049. try
  2050. {
  2051. var age = this.getRealtimeAge(desc, json);
  2052. // Uses realtime if newer or less than 5 minutes old
  2053. if (age < 300000)
  2054. {
  2055. jsonSuccess(new DriveFile(this.ui, mxUtils.getXml(
  2056. this.convertJsonToXml(json)), desc));
  2057. }
  2058. else
  2059. {
  2060. this.getXmlFile(desc, xmlSuccess, mxUtils.bind(this, function()
  2061. {
  2062. try
  2063. {
  2064. jsonSuccess(new DriveFile(this.ui, mxUtils.getXml(
  2065. this.convertJsonToXml(json)), desc));
  2066. }
  2067. catch (e)
  2068. {
  2069. this.getXmlFile(desc, xmlSuccess, error);
  2070. }
  2071. }));
  2072. }
  2073. }
  2074. catch (e)
  2075. {
  2076. this.getXmlFile(desc, xmlSuccess, error);
  2077. }
  2078. }), mxUtils.bind(this, function()
  2079. {
  2080. this.getXmlFile(desc, xmlSuccess, error);
  2081. }));
  2082. };
  2083. /**
  2084. * Returns the location as a new object.
  2085. */
  2086. DriveClient.prototype.convertJsonToXml = function(json, uncompressed)
  2087. {
  2088. if (json.value == null || json.value.diagrams == null)
  2089. {
  2090. throw Error('Invalid JSON: no diagrams in root map');
  2091. }
  2092. else
  2093. {
  2094. var node = mxUtils.createXmlDocument().createElement('mxfile');
  2095. var diagrams = json.value.diagrams.value;
  2096. for (var i = 0; i < diagrams.length; i++)
  2097. {
  2098. try
  2099. {
  2100. var diagramNode = this.decodeJsonPage(diagrams[i].value,
  2101. node.ownerDocument.createElement('diagram'),
  2102. uncompressed);
  2103. //if (diagramNode.getAttribute('name') == null)
  2104. //{
  2105. // TODO: Should only use when converting but not when comparing
  2106. //diagramNode.setAttribute('name', mxResources.get('pageWithNumber', [i + 1]));
  2107. //}
  2108. node.appendChild(diagramNode);
  2109. }
  2110. catch (e)
  2111. {
  2112. throw Error('Error on page ' + i + ': ' + e.stack);
  2113. }
  2114. }
  2115. //console.log('leaving convertJson', mxUtils.getPrettyXml(node));
  2116. return node;
  2117. }
  2118. };
  2119. /**
  2120. * Returns true if copy, export and print are not allowed for this file.
  2121. */
  2122. DriveClient.prototype.decodeJsonPage = function(json, node, uncompressed)
  2123. {
  2124. if (json == null)
  2125. {
  2126. throw Error('Invalid JSON: json for page is null');
  2127. }
  2128. else
  2129. {
  2130. var codec = new mxCodec();
  2131. var root = this.createJsonCell(json.root, codec);
  2132. if (root == null)
  2133. {
  2134. throw Error('Invalid JSON: no root cell for page');
  2135. }
  2136. else
  2137. {
  2138. // Dummy model for encoding
  2139. var modelNode = codec.encode(new mxGraphModel(root));
  2140. this.decodeJsonViewState(json, modelNode);
  2141. if (uncompressed)
  2142. {
  2143. node.appendChild(modelNode);
  2144. }
  2145. else
  2146. {
  2147. mxUtils.setTextContent(node, Graph.compressNode(modelNode));
  2148. }
  2149. // Adds attributes to diagram node
  2150. if (json.id != null)
  2151. {
  2152. node.setAttribute('id', json.id.json);
  2153. }
  2154. else
  2155. {
  2156. // Workaround for missing page ID in JSON
  2157. node.setAttribute('id', Editor.guid());
  2158. }
  2159. if (json.name != null)
  2160. {
  2161. node.setAttribute('name', json.name.json);
  2162. }
  2163. }
  2164. }
  2165. //console.log('decoded json page', json, node);
  2166. return node;
  2167. };
  2168. /**
  2169. * Writes the view state to the given node.
  2170. */
  2171. DriveClient.prototype.decodeJsonViewState = function(json, node)
  2172. {
  2173. // Page format is stored as "width,height"
  2174. var pf = (json.pageFormat != null) ? json.pageFormat.json : null;
  2175. if (pf != null && pf.length > 0)
  2176. {
  2177. var values = pf.split(',');
  2178. if (values.length > 1)
  2179. {
  2180. node.setAttribute('pageWidth', values[0]);
  2181. node.setAttribute('pageHeight', values[1]);
  2182. }
  2183. }
  2184. var bg = (json.backgroundColor != null) ? json.backgroundColor.json : null;
  2185. if (bg != null && bg.length > 0)
  2186. {
  2187. node.setAttribute('background', bg);
  2188. }
  2189. var img = (json.backgroundImage != null) ? json.backgroundImage.json : null;
  2190. if (img != null && img.length > 0)
  2191. {
  2192. node.setAttribute('backgroundImage', img);
  2193. }
  2194. node.setAttribute('fold', (json.foldingEnabled != null) ? json.foldingEnabled.json : '0');
  2195. node.setAttribute('pageScale', (json.pageScale != null) ? json.pageScale.json : mxGraph.prototype.pageScale);
  2196. node.setAttribute('math', (json.mathEnabled != null) ? json.mathEnabled.json : '0');
  2197. node.setAttribute('shadow', (json.shadowVisible != null) ? json.shadowVisible.json : '0');
  2198. return node;
  2199. };
  2200. /**
  2201. * Syncs initial state from collab model to graph model.
  2202. */
  2203. DriveClient.prototype.createJsonCell = function(json, codec)
  2204. {
  2205. if (json != null && json.id != null)
  2206. {
  2207. var val = json.value;
  2208. var cell = this.jsonToCell(val, codec);
  2209. codec.putObject(json.id, cell);
  2210. cell.source = (val.source != null) ? this.createJsonCell(val.source, codec) : null;
  2211. cell.target = (val.target != null) ? this.createJsonCell(val.target, codec) : null;
  2212. // Cells can be serialized as parents of terminals
  2213. this.createJsonCell(val.parent, codec)
  2214. for (var i = 0; i < val.children.value.length; i++)
  2215. {
  2216. var child = this.createJsonCell(val.children.value[i], codec);
  2217. if (child != null)
  2218. {
  2219. cell.insert(child);
  2220. }
  2221. else
  2222. {
  2223. throw Error('Invalid JSON: no child ' + i + ' for cell ' + json.id);
  2224. }
  2225. }
  2226. return cell;
  2227. }
  2228. else if (json != null && json.ref != null)
  2229. {
  2230. return codec.objects[json.ref];
  2231. }
  2232. else
  2233. {
  2234. return null;
  2235. }
  2236. };
  2237. /**
  2238. * Adds the listener for automatically saving the diagram for local changes.
  2239. */
  2240. DriveClient.prototype.jsonToCell = function(val, codec)
  2241. {
  2242. var cell = new mxCell();
  2243. cell.id = val.cellId.json;
  2244. cell.vertex = val.type.json == 'vertex';
  2245. cell.edge = val.type.json == 'edge';
  2246. cell.connectable = val.connectable.json != '0';
  2247. cell.collapsed = val.collapsed.json == '1';
  2248. cell.visible = val.visible.json != '0';
  2249. cell.style = (val.style != null) ? val.style.json : null;
  2250. cell.value = (val.xmlValue != null) ?
  2251. mxUtils.parseXml(val.xmlValue.json).documentElement :
  2252. ((val.value != null) ? val.value.json : null);
  2253. cell.geometry = (val.geometry != null) ?
  2254. codec.decode(mxUtils.parseXml(val.geometry.json).documentElement) : null;
  2255. return cell;
  2256. };
  2257. /**
  2258. * Invokes the given function if the user has writeable realtime files
  2259. * that must be converted.
  2260. */
  2261. DriveClient.prototype.checkRealtimeFiles = function(fn)
  2262. {
  2263. var email = (this.user != null && this.user.email != null) ? this.user.email : null;
  2264. this.executeRequest(gapi.client.drive.files.list({'maxResults': 1, 'q':
  2265. 'mimeType=\'application/vnd.jgraph.mxfile.realtime\'' +
  2266. ((email != null) ? ' and \'' + email + '\' in writers' : ''),
  2267. 'includeTeamDriveItems': true, 'supportsTeamDrives': true}), mxUtils.bind(this, function(res)
  2268. {
  2269. if (res != null && (res.nextPageToken != null || (res.items != null && res.items.length > 0)))
  2270. {
  2271. fn();
  2272. }
  2273. }));
  2274. };
  2275. /**
  2276. * Converts all old realtime files. Invoke this using
  2277. * https://www.draw.io/?mode=google&convert-realtime=1
  2278. */
  2279. DriveClient.prototype.convertRealtimeFiles = function()
  2280. {
  2281. var output = document.createElement('div');
  2282. output.style.cssText = 'position:absolute;top:0px;left:0px;right:0px;bottom:0px;padding:8px;' +
  2283. 'background:#ffffff;z-index:2;overflow:auto;white-space:nowrap;line-height:1.5em;';
  2284. document.body.appendChild(output);
  2285. var t0 = Date.now();
  2286. var print = mxUtils.bind(this, function(msg, noBr)
  2287. {
  2288. output.innerHTML += msg + ((noBr) ? '' : '<br>');
  2289. output.scrollTop = output.scrollHeight;
  2290. });
  2291. print('draw.io (' + EditorUi.VERSION + ') is searching files to be converted...');
  2292. print('<a href="https://desk.draw.io/support/solutions/articles/16000092210" target="_blank">Click here for help</a>');
  2293. if (this.ui.spinner.spin(document.body, 'Searching files...'))
  2294. {
  2295. this.checkToken(mxUtils.bind(this, function()
  2296. {
  2297. var convertDelay = 2000;
  2298. var convertedIds = {};
  2299. var converted = 0;
  2300. var fromJson = 0;
  2301. var fromXml = 0;
  2302. var loadFail = 0;
  2303. var invalid = 0;
  2304. var saveFail = 0;
  2305. var failed = 0;
  2306. var total = 0;
  2307. var queryFail = 0;
  2308. var email = (this.user != null && this.user.email != null) ? this.user.email : null;
  2309. var q = 'mimeType=\'application/vnd.jgraph.mxfile.realtime\'' +
  2310. ((email != null) ? ' and \'' + email + '\' in writers' : '');
  2311. var done = mxUtils.bind(this, function()
  2312. {
  2313. this.ui.spinner.stop();
  2314. print('<br>Conversion complete. Successfully converted ' + converted + ' file(s).', true);
  2315. if (failed > 0)
  2316. {
  2317. print(' Failed to convert ' + failed + ' file(s).<br><br><b>ACTION REQUIRED:</b><br><ul><li>Click ' +
  2318. '<a target="_blank" href="https://drive.google.com/drive/u/0/search?q=type:application/vnd.jgraph.mxfile.realtime">here</a> ' +
  2319. 'to list all affected files</li><li>Open each file in turn by right-clicking the file and selecting open with draw.io</li>' +
  2320. '<li>Open each file in turn. When loaded, select File->Save</li></ul>');
  2321. }
  2322. else
  2323. {
  2324. print('<br><br>This window can now be closed.')
  2325. }
  2326. try
  2327. {
  2328. var dt = Date.now() - t0;
  2329. // Logs conversion
  2330. EditorUi.logEvent({category: 'AUTO-CONVERT',
  2331. action: 'total-' + total + '-xml-' + fromXml +
  2332. '-json-' + fromJson + '-done-' + converted +
  2333. '-fail-' + failed + '-load-' + loadFail +
  2334. '-save-' + saveFail + '-invalid-' + invalid +
  2335. '-dt-' + Math.round(dt / 1000),
  2336. label: (this.user != null) ? this.user.id : 'unknown-user'});
  2337. }
  2338. catch (e)
  2339. {
  2340. // ignore
  2341. }
  2342. });
  2343. var getMessage = function(err)
  2344. {
  2345. return (err == null) ? '' : ((err.message != null) ? err.message : ((err.error != null &&
  2346. err.error.message != null) ? err.error.message : ''));
  2347. };
  2348. var doConvert = mxUtils.bind(this, function()
  2349. {
  2350. if (this.ui.spinner.spin(document.body, 'Converting ' + total + ' file(s)'))
  2351. {
  2352. print('Found ' + total + ' file(s). This will take up to ' + Math.ceil((total * (convertDelay + 3000)) / 60000) +
  2353. ' minute(s). <b>Please do not close this window!</b><br>');
  2354. var counter = 0;
  2355. // Does not show picker if there are no folders in the root
  2356. var nextPage = mxUtils.bind(this, function(token, delay)
  2357. {
  2358. var query = {'maxResults': 1, 'q': q, 'includeTeamDriveItems': true, 'supportsTeamDrives': true};
  2359. if (token != null)
  2360. {
  2361. query.pageToken = token;
  2362. }
  2363. var acceptResponse = true;
  2364. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  2365. {
  2366. acceptResponse = false;
  2367. nextPage(token, delay);
  2368. }), this.ui.timeout);
  2369. this.executeRequest(gapi.client.drive.files.list(query), mxUtils.bind(this, function(res)
  2370. {
  2371. window.clearTimeout(timeoutThread);
  2372. if (acceptResponse)
  2373. {
  2374. var doNextPage = mxUtils.bind(this, function()
  2375. {
  2376. if (res.nextPageToken != null)
  2377. {
  2378. nextPage(res.nextPageToken);
  2379. }
  2380. else
  2381. {
  2382. done();
  2383. }
  2384. });
  2385. if (res != null && res.error != null)
  2386. {
  2387. queryFail++;
  2388. if (queryFail < 4)
  2389. {
  2390. nextPage(token, delay);
  2391. }
  2392. else
  2393. {
  2394. this.ui.spinner.stop();
  2395. print('Query for next file failed multiple times. Exiting.<br><br>This window can now be closed.');
  2396. }
  2397. }
  2398. else if (res != null && (res.items == null || res.items.length == 0) &&
  2399. res.nextPageToken != null)
  2400. {
  2401. // Next page can still contain results, see
  2402. // https://stackoverflow.com/questions/23741845
  2403. nextPage(res.nextPageToken, 10000);
  2404. }
  2405. else if (res != null && res.items != null && res.items.length > 0)
  2406. {
  2407. var fileId = res.items[0].id;
  2408. this.ui.spinner.stop();
  2409. counter++;
  2410. if (this.ui.spinner.spin(document.body, 'Converting file ' + counter + ' of ' + total))
  2411. {
  2412. print('Converting ' + counter + ' of ' + total + ': "' + mxUtils.htmlEntities(res.items[0].title) +
  2413. '" (<a href="https://drive.google.com/open?id=' + fileId + '" target="_blank">' + fileId + '</a>)... ', true);
  2414. window.setTimeout(mxUtils.bind(this, function()
  2415. {
  2416. // Exits if same file is returned twice
  2417. if (convertedIds[fileId] == null)
  2418. {
  2419. convertedIds[fileId] = true;
  2420. acceptResponse = true;
  2421. timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  2422. {
  2423. acceptResponse = false;
  2424. failed++;
  2425. loadFail++;
  2426. print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> Timeout');
  2427. doNextPage();
  2428. }), this.ui.timeout);
  2429. this.getFile(fileId, mxUtils.bind(this, function(file)
  2430. {
  2431. window.clearTimeout(timeoutThread);
  2432. if (acceptResponse)
  2433. {
  2434. if (file.constructor == DriveFile)
  2435. {
  2436. if (file.convertedFrom == 'json')
  2437. {
  2438. fromJson++;
  2439. }
  2440. else
  2441. {
  2442. fromXml++;
  2443. }
  2444. acceptResponse = true;
  2445. timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  2446. {
  2447. acceptResponse = false;
  2448. failed++;
  2449. saveFail++;
  2450. print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> Timeout');
  2451. doNextPage();
  2452. }), this.ui.timeout);
  2453. this.saveFile(file, null, mxUtils.bind(this, function()
  2454. {
  2455. window.clearTimeout(timeoutThread);
  2456. if (acceptResponse)
  2457. {
  2458. converted++;
  2459. print('OK <img src="' + Editor.checkmarkImage + '" border="0" valign="middle"/>');
  2460. doNextPage();
  2461. }
  2462. }), mxUtils.bind(this, function(err)
  2463. {
  2464. window.clearTimeout(timeoutThread);
  2465. if (acceptResponse)
  2466. {
  2467. var msg = getMessage(err);
  2468. failed++;
  2469. saveFail++;
  2470. print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> ' + msg);
  2471. doNextPage();
  2472. }
  2473. }));
  2474. }
  2475. else
  2476. {
  2477. failed++;
  2478. invalid++;
  2479. print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> Invalid file');
  2480. doNextPage();
  2481. }
  2482. }
  2483. }), mxUtils.bind(this, function(err)
  2484. {
  2485. window.clearTimeout(timeoutThread);
  2486. if (acceptResponse)
  2487. {
  2488. var msg = getMessage(err);
  2489. failed++;
  2490. loadFail++;
  2491. print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> ' + msg);
  2492. doNextPage();
  2493. }
  2494. }));
  2495. }
  2496. else
  2497. {
  2498. this.ui.spinner.stop();
  2499. print('Search returned duplicate file ' + fileId + '. Exiting.<br><br>This window can now be closed.');
  2500. }
  2501. }), (delay != null) ? delay : convertDelay)
  2502. }
  2503. }
  2504. else
  2505. {
  2506. done();
  2507. }
  2508. }
  2509. }));
  2510. });
  2511. nextPage();
  2512. }
  2513. });
  2514. var totals = {'maxResults': 10000, 'q': q, 'includeTeamDriveItems': true, 'supportsTeamDrives': true};
  2515. var count = mxUtils.bind(this, function(token)
  2516. {
  2517. if (token != null)
  2518. {
  2519. totals.pageToken = token;
  2520. }
  2521. this.executeRequest(gapi.client.drive.files.list(totals), mxUtils.bind(this, function(res)
  2522. {
  2523. total += (res != null && res.items != null) ? res.items.length : 0;
  2524. if (res.nextPageToken != null)
  2525. {
  2526. count(res.nextPageToken);
  2527. }
  2528. else
  2529. {
  2530. this.ui.spinner.stop();
  2531. this.ui.showError('Confirm', 'You are about to convert ' + total + ' file(s)',
  2532. 'Cancel', mxUtils.bind(this, function()
  2533. {
  2534. print('Cancelled by user.<br><br>This window can now be closed.');
  2535. }), null, 'OK', doConvert, 'Help', function()
  2536. {
  2537. window.open('https://desk.draw.io/support/solutions/articles/16000092210');
  2538. }, 340, 120);
  2539. }
  2540. }));
  2541. });
  2542. count();
  2543. }));
  2544. }
  2545. else
  2546. {
  2547. this.ui.spinner.stop();
  2548. print('Busy. <br><br>This window can now be closed.');
  2549. }
  2550. };