DriveClient.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410
  1. /**
  2. * Copyright (c) 2006-2016, JGraph Ltd
  3. * Copyright (c) 2006-2016, Gaudenz Alder
  4. */
  5. /**
  6. * Constructs a new point for the optional x and y coordinates. If no
  7. * coordinates are given, then the default values for <x> and <y> are used.
  8. * @constructor
  9. * @class Implements a basic 2D point. Known subclassers = {@link mxRectangle}.
  10. * @param {number} x X-coordinate of the point.
  11. * @param {number} y Y-coordinate of the point.
  12. */
  13. DriveClient = function(editorUi)
  14. {
  15. mxEventSource.call(this);
  16. /**
  17. * Holds a reference to the UI. Needed for the sharing client.
  18. */
  19. this.ui = editorUi;
  20. if (this.ui.editor.chromeless && urlParams['rt'] != '1')
  21. {
  22. this.appId = '850530949725';
  23. this.clientId = '850530949725.apps.googleusercontent.com';
  24. this.scopes = ['https://www.googleapis.com/auth/drive.readonly', 'openid'];
  25. // Only used for writing files which is disabled in viewer app
  26. this.mimeType = 'all_types_supported';
  27. }
  28. else if (this.ui.isDriveDomain())
  29. {
  30. this.appId = '671128082532';
  31. this.clientId = '671128082532.apps.googleusercontent.com';
  32. this.mimeType = 'application/vnd.jgraph.mxfile.realtime';
  33. }
  34. else
  35. {
  36. // Uses a different mime-type and realtime model than the drive domain
  37. // because realtime models for different app IDs are not compatible
  38. this.appId = '420247213240';
  39. this.clientId = '420247213240-hnbju1pt13seqrc1hhd5htpotk4g9q7u.apps.googleusercontent.com';
  40. this.mimeType = 'application/vnd.jgraph.mxfile.rtlegacy';
  41. }
  42. this.mimeTypes = 'application/mxe,application/vnd.jgraph.mxfile,' +
  43. 'application/mxr,application/vnd.jgraph.mxfile.realtime,' +
  44. 'application/vnd.jgraph.mxfile.rtlegacy';
  45. };
  46. // Extends mxEventSource
  47. mxUtils.extend(DriveClient, mxEventSource);
  48. /**
  49. * OAuth 2.0 scopes for installing Drive Apps.
  50. */
  51. DriveClient.prototype.scopes = (urlParams['photos'] == '1') ?
  52. ['https://www.googleapis.com/auth/drive.file',
  53. 'https://www.googleapis.com/auth/drive.install',
  54. 'https://www.googleapis.com/auth/photos',
  55. 'https://www.googleapis.com/auth/photos.upload',
  56. 'https://www.googleapis.com/auth/userinfo.profile'] :
  57. ['https://www.googleapis.com/auth/drive.file',
  58. 'https://www.googleapis.com/auth/drive.install',
  59. 'https://www.googleapis.com/auth/userinfo.profile'];
  60. /**
  61. * Specifies if thumbnails should be enabled. Default is true.
  62. * LATER: If thumbnails are disabled, make sure to replace the
  63. * existing thumbnail with the placeholder only once.
  64. */
  65. DriveClient.prototype.enableThumbnails = true;
  66. /**
  67. * Specifies the width for thumbnails. Default is 480. This value
  68. * must be between 220 and 1600.
  69. */
  70. DriveClient.prototype.thumbnailWidth = 480;
  71. /**
  72. * The maximum number of bytes per thumbnail. Default is 2000000.
  73. */
  74. DriveClient.prototype.maxThumbnailSize = 2000000;
  75. /**
  76. * Defines the base64url PNG to be used if no thumbnail was generated
  77. * (including the case where thumbnails are disabled).
  78. */
  79. 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+LDGyvXE2kmO2Q1Lq17RSd6ND48QIwFVuLNHTOPbEpTOz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'.replace(/\+/g, '-').replace(/\//g, '_');
  80. /**
  81. * Mime type for the paceholder thumbnail.
  82. */
  83. DriveClient.prototype.placeholderMimeType = 'image/png';
  84. /**
  85. * Executes the first step for connecting to Google Drive.
  86. */
  87. DriveClient.prototype.libraryMimeType = 'application/vnd.jgraph.mxlibrary';
  88. /**
  89. * Contains the hostname of the new app.
  90. */
  91. DriveClient.prototype.newAppHostname = 'www.draw.io';
  92. /**
  93. * Contains the hostname of the old app.
  94. */
  95. DriveClient.prototype.oldAppHostname = 'legacy.draw.io';
  96. /**
  97. * Executes the first step for connecting to Google Drive.
  98. */
  99. DriveClient.prototype.extension = '.html';
  100. /**
  101. * Interval for updating the access token.
  102. */
  103. DriveClient.prototype.tokenRefreshInterval = 0;
  104. /**
  105. * Interval for updating the access token.
  106. */
  107. DriveClient.prototype.lastTokenRefresh = 0;
  108. /**
  109. * Executes the first step for connecting to Google Drive.
  110. */
  111. DriveClient.prototype.maxRetries = 4;
  112. /**
  113. * Executes the first step for connecting to Google Drive.
  114. */
  115. DriveClient.prototype.mimeTypeCheckCoolOff = 60000;
  116. /**
  117. * Executes the first step for connecting to Google Drive.
  118. */
  119. DriveClient.prototype.user = null;
  120. /**
  121. * Authorizes the client, gets the userId and calls <open>.
  122. */
  123. DriveClient.prototype.setUser = function(user)
  124. {
  125. this.user = user;
  126. if (this.user == null && this.tokenRefreshThread != null)
  127. {
  128. window.clearTimeout(this.tokenRefreshThread);
  129. this.tokenRefreshThread = null;
  130. }
  131. this.fireEvent(new mxEventObject('userChanged'));
  132. };
  133. /**
  134. * Authorizes the client, gets the userId and calls <open>.
  135. */
  136. DriveClient.prototype.getUser = function()
  137. {
  138. return this.user;
  139. };
  140. /**
  141. * Authorizes the client, gets the userId and calls <open>.
  142. */
  143. DriveClient.prototype.setUserId = function(userId, remember)
  144. {
  145. if (typeof(Storage) != 'undefined')
  146. {
  147. try
  148. {
  149. sessionStorage.setItem('GUID', userId);
  150. if (remember)
  151. {
  152. var expiry = new Date();
  153. expiry.setYear(expiry.getFullYear() + 1);
  154. document.cookie = 'GUID=' + userId + '; expires=' + expiry.toUTCString();
  155. }
  156. }
  157. catch (e)
  158. {
  159. // any errors for storing the user ID can be safely ignored
  160. }
  161. }
  162. };
  163. /**
  164. * Authorizes the client, gets the userId and calls <open>.
  165. */
  166. DriveClient.prototype.clearUserId = function()
  167. {
  168. if (typeof(Storage) != 'undefined')
  169. {
  170. sessionStorage.removeItem('GUID');
  171. var expiry = new Date();
  172. expiry.setYear(expiry.getFullYear() - 1);
  173. document.cookie = 'GUID=; expires=' + expiry.toUTCString();
  174. }
  175. };
  176. /**
  177. * Authorizes the client, gets the userId and calls <open>.
  178. */
  179. DriveClient.prototype.getUserId = function()
  180. {
  181. var uid = null;
  182. if (this.user != null)
  183. {
  184. uid = this.user.id;
  185. }
  186. if (typeof(Storage) != 'undefined')
  187. {
  188. if (uid == null)
  189. {
  190. uid = sessionStorage.getItem('GUID');
  191. }
  192. if (uid == null)
  193. {
  194. var cookies = document.cookie.split(";");
  195. for (var i = 0; i < cookies.length; i++)
  196. {
  197. // Removes spaces around cookie
  198. var cookie = mxUtils.trim(cookies[i]);
  199. if (cookie.substring(0, 5) == 'GUID=')
  200. {
  201. uid = cookie.substring(5);
  202. break;
  203. }
  204. }
  205. }
  206. }
  207. return uid;
  208. };
  209. /**
  210. * Authorizes the client, gets the userId and calls <open>.
  211. */
  212. DriveClient.prototype.execute = function(fn)
  213. {
  214. // Handles error in immediate authorize call via callback that shows a
  215. // UI with a button that executes the second non-immediate authorize
  216. var fallback = mxUtils.bind(this, function(resp)
  217. {
  218. // Remember is an argument for the callback that executes
  219. // when the user clicks the authorize button in the UI and
  220. // success executes after successful authorization.
  221. this.ui.showAuthDialog(this, true, mxUtils.bind(this, function(remember, success)
  222. {
  223. this.authorize(false, function()
  224. {
  225. if (success != null)
  226. {
  227. success();
  228. }
  229. fn();
  230. }, mxUtils.bind(this, function(resp)
  231. {
  232. var msg = mxResources.get('cannotLogin');
  233. // Handles special domain policy errors
  234. if (resp != null && resp.error != null && resp.error.code == 403 &&
  235. resp.error.data != null && resp.error.data[0].reason == 'domainPolicy')
  236. {
  237. msg = resp.error.message;
  238. }
  239. this.ui.drive.clearUserId();
  240. this.ui.drive.setUser(null);
  241. gapi.auth.signOut();
  242. this.ui.showError(mxResources.get('error'), msg, mxResources.get('ok'));
  243. }), remember);
  244. }));
  245. });
  246. // First immediate authorize attempt
  247. this.authorize(true, fn, fallback);
  248. };
  249. /**
  250. * Translates this point by the given vector.
  251. *
  252. * @param {number} dx X-coordinate of the translation.
  253. * @param {number} dy Y-coordinate of the translation.
  254. */
  255. DriveClient.prototype.executeRequest = function(req, success, error)
  256. {
  257. var acceptResponse = true;
  258. var timeoutThread = null;
  259. var retryCount = 0;
  260. // Cancels any pending requests
  261. if (this.requestThread != null)
  262. {
  263. window.clearTimeout(this.requestThread);
  264. }
  265. var fn = mxUtils.bind(this, function()
  266. {
  267. this.requestThread = null;
  268. this.currentRequest = req;
  269. if (timeoutThread != null)
  270. {
  271. window.clearTimeout(timeoutThread);
  272. }
  273. timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  274. {
  275. acceptResponse = false;
  276. if (error != null)
  277. {
  278. error({code: App.ERROR_TIMEOUT, retry: fn});
  279. }
  280. }), this.ui.timeout);
  281. req.execute(mxUtils.bind(this, function(resp)
  282. {
  283. window.clearTimeout(timeoutThread);
  284. if (acceptResponse)
  285. {
  286. if (resp != null && resp.error == null)
  287. {
  288. if (success != null)
  289. {
  290. success(resp);
  291. }
  292. }
  293. else
  294. {
  295. // Handles special error for saving old file where mime was changed to new
  296. // LATER: Check if 403 is never auth error, for now we check the message for a specific
  297. // case where the old app mime type was overridden by the new app
  298. if (error != null && resp != null && resp.error != null && resp.error.code == 403 &&
  299. (resp.error.message == 'The requested mime type change is forbidden.' ||
  300. resp.error.errors != null && resp.error.errors[0].reason == 'domainPolicy'))
  301. {
  302. error(resp);
  303. }
  304. // Handles authentication error
  305. else if (resp != null && resp.error != null && (resp.error.code == 401 || resp.error.code == 403))
  306. {
  307. // Shows an error if we're authenticated but the server still doesn't allow it
  308. if (resp.error.code == 403 && this.user != null)
  309. {
  310. if (error != null)
  311. {
  312. error(resp);
  313. }
  314. }
  315. else
  316. {
  317. this.execute(fn);
  318. }
  319. }
  320. // Schedules a retry if no new request was executed
  321. // TODO: Check for 'rateLimitExceeded', 'userRateLimitExceeded' in errors
  322. // see https://developers.google.com/drive/handle-errors
  323. else if (resp != null && resp.error != null && resp.error.code != 404 && this.currentRequest == req && retryCount < this.maxRetries)
  324. {
  325. retryCount++;
  326. var jitter = 1 + 0.1 * (Math.random() - 0.5);
  327. this.requestThread = window.setTimeout(fn, Math.round(Math.pow(2, retryCount) * jitter * 1000));
  328. }
  329. else if (error != null)
  330. {
  331. error(resp);
  332. }
  333. }
  334. }
  335. }));
  336. });
  337. fn();
  338. },
  339. /**
  340. * Authorizes the client, gets the userId and calls <open>.
  341. */
  342. DriveClient.prototype.authorize = function(immediate, success, error, remember)
  343. {
  344. var userId = this.getUserId();
  345. // Immediate only possible with userId
  346. if (immediate && userId == null)
  347. {
  348. if (error != null)
  349. {
  350. error();
  351. }
  352. }
  353. else
  354. {
  355. var params =
  356. {
  357. scope: this.scopes,
  358. client_id: this.clientId
  359. };
  360. if (immediate && userId != null)
  361. {
  362. params.immediate = true;
  363. params.user_id = userId;
  364. }
  365. else
  366. {
  367. params.immediate = false;
  368. params.authuser = -1;
  369. }
  370. gapi.auth.authorize(params, mxUtils.bind(this, function(resp)
  371. {
  372. // Updates the current user info
  373. if (resp != null && resp.error == null)
  374. {
  375. if (this.user == null || !immediate || this.user.id != userId)
  376. {
  377. this.updateUser(success, error, remember);
  378. }
  379. else if (success != null)
  380. {
  381. success();
  382. }
  383. }
  384. else if (error != null)
  385. {
  386. error(resp);
  387. }
  388. this.resetTokenRefresh(resp);
  389. }));
  390. }
  391. };
  392. /**
  393. * Checks if the client is authorized and calls the next step.
  394. */
  395. DriveClient.prototype.resetTokenRefresh = function(resp)
  396. {
  397. if (this.tokenRefreshThread != null)
  398. {
  399. window.clearTimeout(this.tokenRefreshThread);
  400. this.tokenRefreshThread = null;
  401. }
  402. // Starts timer to refresh token before it expires
  403. if (resp != null && resp.error == null && resp.expires_in > 0)
  404. {
  405. this.tokenRefreshInterval = parseInt(resp.expires_in) * 1000;
  406. this.lastTokenRefresh = new Date().getTime();
  407. this.tokenRefreshThread = window.setTimeout(mxUtils.bind(this, function()
  408. {
  409. this.authorize(true, mxUtils.bind(this, function()
  410. {
  411. //console.log('tokenRefresh: refreshed', gapi.auth.getToken());
  412. }), mxUtils.bind(this, function()
  413. {
  414. //console.log('tokenRefresh: error refreshing', gapi.auth.getToken());
  415. }));
  416. }), resp.expires_in * 900);
  417. }
  418. };
  419. /**
  420. * Checks if the client is authorized and calls the next step.
  421. */
  422. DriveClient.prototype.checkToken = function(fn)
  423. {
  424. var connected = this.lastTokenRefresh > 0;
  425. var delta = new Date().getTime() - this.lastTokenRefresh;
  426. if (delta > this.tokenRefreshInterval || this.tokenRefreshThread == null)
  427. {
  428. // Uses execute instead of authorize to allow a fallback authorization if cookie was lost
  429. this.execute(mxUtils.bind(this, function()
  430. {
  431. fn();
  432. if (connected)
  433. {
  434. this.fireEvent(new mxEventObject('disconnected'));
  435. }
  436. }));
  437. }
  438. else
  439. {
  440. fn();
  441. }
  442. };
  443. /**
  444. * Checks if the client is authorized and calls the next step.
  445. */
  446. DriveClient.prototype.updateUser = function(success, error, remember)
  447. {
  448. var token = gapi.auth.getToken().access_token;
  449. var url = 'https://www.googleapis.com/oauth2/v2/userinfo?alt=json&access_token=' + token;
  450. this.ui.loadUrl(url, mxUtils.bind(this, function(data)
  451. {
  452. var info = JSON.parse(data);
  453. // Requests more information about the user
  454. this.executeRequest(gapi.client.drive.about.get(), mxUtils.bind(this, function(resp)
  455. {
  456. this.setUser(new DrawioUser(info.id, resp.user.emailAddress, resp.user.displayName,
  457. (resp.user.picture != null) ? resp.user.picture.url : null));
  458. this.setUserId(info.id, remember);
  459. if (success != null)
  460. {
  461. success();
  462. }
  463. }), error);
  464. }), error);
  465. };
  466. /**
  467. * Translates this point by the given vector.
  468. *
  469. * @param {number} dx X-coordinate of the translation.
  470. * @param {number} dy Y-coordinate of the translation.
  471. */
  472. DriveClient.prototype.copyFile = function(id, title, success, error)
  473. {
  474. if (id != null && title != null)
  475. {
  476. this.executeRequest(gapi.client.drive.files.copy({'fileId': id, 'resource': {'title' : title}}), success, error);
  477. }
  478. };
  479. /**
  480. * Translates this point by the given vector.
  481. *
  482. * @param {number} dx X-coordinate of the translation.
  483. * @param {number} dy Y-coordinate of the translation.
  484. */
  485. DriveClient.prototype.renameFile = function(id, title, success, error)
  486. {
  487. if (id != null && title != null)
  488. {
  489. this.executeRequest(this.createDriveRequest(id, {'title' : title}), success, error);
  490. }
  491. };
  492. /**
  493. * Translates this point by the given vector.
  494. *
  495. * @param {number} dx X-coordinate of the translation.
  496. * @param {number} dy Y-coordinate of the translation.
  497. */
  498. DriveClient.prototype.moveFile = function(id, folderId, success, error)
  499. {
  500. if (id != null && folderId != null)
  501. {
  502. this.executeRequest(this.createDriveRequest(id, {'parents': [{'kind': 'drive#fileLink', 'id': folderId}]}), success, error);
  503. }
  504. };
  505. /**
  506. * Translates this point by the given vector.
  507. *
  508. * @param {number} dx X-coordinate of the translation.
  509. * @param {number} dy Y-coordinate of the translation.
  510. */
  511. DriveClient.prototype.createDriveRequest = function(id, body)
  512. {
  513. return gapi.client.request({
  514. 'path': '/drive/v2/files/' + id,
  515. 'method': 'PUT',
  516. 'params': {'uploadType' : 'multipart'},
  517. 'headers': {'Content-Type': 'application/json; charset=UTF-8'},
  518. 'body': JSON.stringify(body)
  519. });
  520. };
  521. /**
  522. * Loads the given file as a library file.
  523. */
  524. DriveClient.prototype.getLibrary = function(id, success, error)
  525. {
  526. return this.getFile(id, success, error, true, true);
  527. };
  528. /**
  529. * Checks if the client is authorized and calls the next step. The optional
  530. * readXml argument is used for import. Default is false. The optional
  531. * readLibrary argument is used for reading libraries. Default is false.
  532. */
  533. DriveClient.prototype.convertFile = function(resp, success, error)
  534. {
  535. var name = resp.title;
  536. name = name.substring(0, name.lastIndexOf('.')) + this.extension;
  537. // Gets file data
  538. var token = gapi.auth.getToken().access_token;
  539. var url = resp.downloadUrl + '&access_token=' + token;
  540. this.ui.loadUrl(url, mxUtils.bind(this, function(data)
  541. {
  542. this.ui.parseFile(new Blob([data], {type: 'application/octet-stream'}), mxUtils.bind(this, function(xhr)
  543. {
  544. if (xhr.readyState == 4)
  545. {
  546. if (xhr.status == 200 && xhr.responseText.substring(0, 13) == '<mxGraphModel')
  547. {
  548. this.insertFile(name, xhr.responseText, (resp.parents != null && resp.parents.length > 0) ?
  549. resp.parents[0].id : null, success, error);
  550. }
  551. else if (error != null)
  552. {
  553. error({message: mxResources.get('errorLoadingFile')});
  554. }
  555. }
  556. }), resp.title);
  557. }));
  558. };
  559. /**
  560. * Checks if the client is authorized and calls the next step. The optional
  561. * readXml argument is used for import. Default is false. The optional
  562. * readLibrary argument is used for reading libraries. Default is false.
  563. */
  564. DriveClient.prototype.getFile = function(id, success, error, readXml, readLibrary)
  565. {
  566. readXml = (readXml != null) ? readXml : false;
  567. readLibrary = (readLibrary != null) ? readLibrary : false;
  568. if (urlParams['rev'] != null)
  569. {
  570. this.executeRequest(gapi.client.drive.revisions.get({'fileId': id, 'revisionId': urlParams['rev']}), mxUtils.bind(this, function(resp)
  571. {
  572. this.getXmlFile(resp, null, success, error);
  573. }), error);
  574. }
  575. else
  576. {
  577. this.executeRequest(gapi.client.drive.files.get({'fileId': id}), mxUtils.bind(this, function(resp)
  578. {
  579. if (this.user != null)
  580. {
  581. // Handles .vsdx and Gliffy files by creating a new file
  582. if (!readLibrary && !readXml && Graph.fileSupport && new XMLHttpRequest().upload &&
  583. (/(\.vsdx)$/i.test(resp.title) || /(\.gliffy)$/i.test(resp.title)))
  584. {
  585. this.convertFile(resp, success, error);
  586. }
  587. else
  588. {
  589. if (readXml || readLibrary || resp.mimeType == this.libraryMimeType)
  590. {
  591. this.getXmlFile(resp, null, success, error, true, readLibrary);
  592. }
  593. else
  594. {
  595. this.loadRealtime(resp, mxUtils.bind(this, function(doc)
  596. {
  597. try
  598. {
  599. // Converts XML files to realtime including old realtime model
  600. if (doc == null || doc.getModel() == null || doc.getModel().getRoot() == null ||
  601. doc.getModel().getRoot().isEmpty() || (doc.getModel().getRoot().has('cells') &&
  602. !doc.getModel().getRoot().has(DriveRealtime.prototype.diagramsKey)))
  603. {
  604. this.getXmlFile(resp, doc, success, error);
  605. }
  606. else
  607. {
  608. // XML not required here since the realtime model is not empty
  609. success(new DriveFile(this.ui, null, resp, doc));
  610. }
  611. }
  612. catch (e)
  613. {
  614. error(e);
  615. }
  616. }), error);
  617. }
  618. }
  619. }
  620. else
  621. {
  622. error({message: mxResources.get('loggedOut')});
  623. }
  624. }), error);
  625. }
  626. };
  627. /**
  628. * Checks if the client is authorized and calls the next step.
  629. */
  630. DriveClient.prototype.loadRealtime = function(resp, success, error)
  631. {
  632. // Redirects to new app because the realtime models of different apps are not visible
  633. if (urlParams['ignoremime'] != '1' && this.appId == '420247213240' && (resp.mimeType == 'application/mxr' || resp.mimeType == 'application/vnd.jgraph.mxfile.realtime'))
  634. {
  635. this.redirectToNewApp(error, resp.id);
  636. }
  637. // Checks if we're in viewer app or if the file is writeable if it needs to be converted
  638. else if (this.appId != '850530949725' && (resp.editable || (resp.mimeType != 'application/mxe' &&
  639. resp.mimeType != 'application/vnd.jgraph.mxfile')))
  640. {
  641. var fn = mxUtils.bind(this, function()
  642. {
  643. var acceptResponse = true;
  644. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  645. {
  646. acceptResponse = false;
  647. error({code: App.ERROR_TIMEOUT, retry: fn});
  648. }), this.ui.timeout);
  649. gapi.drive.realtime.load(resp.id, mxUtils.bind(this, function(doc)
  650. {
  651. window.clearTimeout(timeoutThread);
  652. if (acceptResponse)
  653. {
  654. success(doc);
  655. }
  656. }));
  657. });
  658. fn();
  659. }
  660. // Shows the file as read-only without conversion
  661. else
  662. {
  663. success();
  664. }
  665. };
  666. /**
  667. * Checks if the client is authorized and calls the next step. The ignoreMime argument is
  668. * used for import via getFile. Default is false. The optional
  669. * readLibrary argument is used for reading libraries. Default is false.
  670. */
  671. DriveClient.prototype.getXmlFile = function(resp, doc, success, error, ignoreMime, readLibrary)
  672. {
  673. var token = gapi.auth.getToken().access_token;
  674. var url = resp.downloadUrl + '&access_token=' + token;
  675. // Loads XML to initialize realtime document if realtime is empty
  676. this.ui.loadUrl(url, mxUtils.bind(this, function(data)
  677. {
  678. if (data == null)
  679. {
  680. // TODO: Optional redirect to legacy if link is for old file
  681. error({message: mxResources.get('invalidOrMissingFile')});
  682. }
  683. else if (resp.mimeType == this.libraryMimeType || readLibrary)
  684. {
  685. if (resp.mimeType == this.libraryMimeType && !readLibrary)
  686. {
  687. error({message: mxResources.get('notADiagramFile')});
  688. }
  689. else
  690. {
  691. success(new DriveLibrary(this.ui, data, resp));
  692. }
  693. }
  694. else
  695. {
  696. var file = new DriveFile(this.ui, data, resp, doc);
  697. // Checks if mime-type needs to be updated if the file is editable and no viewer app
  698. if (!ignoreMime && this.appId != '850530949725' && file.isEditable() && resp.mimeType != this.mimeType)
  699. {
  700. // Overwrites mime-type (only mutable on update when uploading new content)
  701. this.saveFile(file, true, mxUtils.bind(this, function(resp)
  702. {
  703. file.desc = resp;
  704. success(file);
  705. }), error, true);
  706. }
  707. else
  708. {
  709. success(file);
  710. }
  711. }
  712. }), error, resp.mimeType == 'image/png');
  713. };
  714. /**
  715. * Translates this point by the given vector.
  716. *
  717. * @param {number} dx X-coordinate of the translation.
  718. * @param {number} dy Y-coordinate of the translation.
  719. */
  720. DriveClient.prototype.saveFile = function(file, revision, success, error, noCheck, unloading)
  721. {
  722. if (file.isEditable())
  723. {
  724. var t0 = new Date().getTime();
  725. noCheck = (noCheck != null) ? noCheck : (!this.ui.isLegacyDriveDomain() || urlParams['ignoremime'] == '1');
  726. // NOTE: Unloading arg is currently ignored, saving during unload/beforeUnload is not possible using
  727. // asynchronous code, which is needed to create the thumbnail, or asynchronous requests which is the only
  728. // way to execute the gapi request below.
  729. // If no thumbnail is created and noCheck is true (which is always true if unloading is true) in which case
  730. // this code is synchronous, the executeRequest call is reached but the request is still not sent. This is
  731. // true for both, calls from beforeUnload and unload handlers. Note sure how to make the call synchronous
  732. // which is said to fix this when called from beforeUnload.
  733. // However, this would result in a missing thumbnail in most cases so a better solution might be to reduce
  734. // the autosave interval in DriveRealtime, but that would increase the number of requests.
  735. unloading = (unloading != null) ? unloading : false;
  736. // Adds optional thumbnail to upload request
  737. var doSave = mxUtils.bind(this, function(thumb, thumbMime, keepExisting)
  738. {
  739. var meta =
  740. {
  741. 'mimeType': (file.constructor == DriveLibrary) ? this.libraryMimeType : this.mimeType,
  742. 'title': file.getTitle()
  743. };
  744. // Specifies that no thumbnail should be uploaded in which case the existing thumbnail is used
  745. if (!keepExisting)
  746. {
  747. // Uses placeholder thumbnail to replace existing one except when unloading
  748. // in which case the XML is updated but the existing thumbnail is not in order
  749. // to avoid executing asynchronous code and get the XML to the server instead
  750. if (thumb == null && !unloading)
  751. {
  752. thumb = this.placeholderThumbnail;
  753. thumbMime = this.placeholderMimeType;
  754. }
  755. // Adds metadata for thumbnail
  756. if (thumb != null && thumbMime != null)
  757. {
  758. meta.thumbnail =
  759. {
  760. 'image': thumb,
  761. 'mimeType': thumbMime
  762. };
  763. }
  764. }
  765. // Updates saveDelay on drive file
  766. var wrapper = function()
  767. {
  768. file.saveDelay = new Date().getTime() - t0;
  769. success.apply(this, arguments);
  770. };
  771. this.executeRequest(this.createUploadRequest(file.getId(), meta,
  772. file.getData(), revision || (file.desc.mimeType != this.mimeType &&
  773. file.desc.mimeType != this.libraryMimeType)), wrapper, error);
  774. });
  775. // Indirection to generate thumbnails if enabled and supported
  776. // (required because generation of thumbnails is asynchronous)
  777. var fn = mxUtils.bind(this, function()
  778. {
  779. var keepExistingThumb = this.ui.currentPage != null && this.ui.currentPage != this.ui.pages[0];
  780. // NOTE: getThumbnail is asynchronous and returns false if no thumbnails can be created
  781. if (unloading || file.constructor == DriveLibrary || !this.enableThumbnails || urlParams['thumb'] == '0' ||
  782. keepExistingThumb || !this.ui.getThumbnail(this.thumbnailWidth, mxUtils.bind(this, function(canvas)
  783. {
  784. // Callback for getThumbnail
  785. var thumb = null;
  786. if (canvas != null)
  787. {
  788. try
  789. {
  790. // Security errors are possible
  791. thumb = canvas.toDataURL('image/png');
  792. }
  793. catch (e)
  794. {
  795. // ignore and continue with placeholder
  796. }
  797. }
  798. // Maximum thumbnail size is 2MB
  799. if (thumb == null || thumb.length > this.maxThumbnailSize)
  800. {
  801. thumb = null;
  802. }
  803. else
  804. {
  805. // Converts base64 data into required format for Drive (base64url with no prefix)
  806. thumb = thumb.substring(thumb.indexOf(',') + 1).replace(/\+/g, '-').replace(/\//g, '_');
  807. }
  808. doSave(thumb, 'image/png');
  809. })))
  810. {
  811. // If-branch
  812. doSave(null, null, file.constructor != DriveLibrary && keepExistingThumb);
  813. }
  814. });
  815. // New revision is required if mime type changes, but the mime type should not be replaced
  816. // if the file has been converted to the new realtime format. To check this we make sure
  817. // that the mime type has not changed before updating it in the case of the legacy app.
  818. // Note: We need to always check the mime type because saveFile cancels previous save
  819. // attempts so if the save frequency is higher than the time for all retries than you
  820. // will never see the error message and accumulate lots of changes that will be lost.
  821. if (noCheck || !revision)
  822. {
  823. fn();
  824. }
  825. else
  826. {
  827. this.verifyMimeType(file.getId(), fn, true);
  828. }
  829. }
  830. else
  831. {
  832. this.ui.editor.graph.reset();
  833. if (error != null)
  834. {
  835. error({message: mxResources.get('readOnly')});
  836. }
  837. }
  838. };
  839. /**
  840. * Verifies the mime type of the given file ID.
  841. */
  842. DriveClient.prototype.verifyMimeType = function(fileId, fn, force, error)
  843. {
  844. if (this.lastMimeCheck == null)
  845. {
  846. this.lastMimeCheck = 0;
  847. }
  848. var now = new Date().getTime();
  849. if (force || now - this.lastMimeCheck > this.mimeTypeCheckCoolOff)
  850. {
  851. this.lastMimeCheck = now;
  852. if (!this.checkingMimeType)
  853. {
  854. this.checkingMimeType = true;
  855. this.executeRequest(gapi.client.drive.files.get({'fileId': fileId, 'fields': 'mimeType'}), mxUtils.bind(this, function(resp)
  856. {
  857. this.checkingMimeType = false;
  858. if (resp != null && resp.mimeType == 'application/vnd.jgraph.mxfile.realtime')
  859. {
  860. this.redirectToNewApp(error, fileId);
  861. }
  862. else if (fn != null)
  863. {
  864. fn();
  865. }
  866. }));
  867. }
  868. }
  869. };
  870. /**
  871. * Checks if the client is authorized and calls the next step.
  872. */
  873. DriveClient.prototype.redirectToNewApp = function(error, fileId)
  874. {
  875. this.ui.spinner.stop();
  876. if (!this.redirectDialogShowing)
  877. {
  878. this.redirectDialogShowing = true;
  879. var url = window.location.protocol + '//' + this.newAppHostname + '/' + this.ui.getSearch(
  880. ['create', 'title', 'mode', 'url', 'drive', 'splash']) + '#G' + fileId;
  881. if (error != null)
  882. {
  883. this.ui.confirm(mxResources.get('redirectToNewApp'), mxUtils.bind(this, function()
  884. {
  885. this.redirectDialogShowing = false;
  886. window.location.href = url;
  887. }), mxUtils.bind(this, function()
  888. {
  889. this.redirectDialogShowing = false;
  890. if (error != null)
  891. {
  892. error();
  893. }
  894. }));
  895. }
  896. else
  897. {
  898. this.ui.alert(mxResources.get('redirectToNewApp'), mxUtils.bind(this, function()
  899. {
  900. this.redirectDialogShowing = false;
  901. window.location.href = url;
  902. }));
  903. }
  904. }
  905. };
  906. /**
  907. * Translates this point by the given vector.
  908. *
  909. * @param {number} dx X-coordinate of the translation.
  910. * @param {number} dy Y-coordinate of the translation.
  911. */
  912. DriveClient.prototype.insertFile = function(title, data, folderId, success, error, mimeType, binary, allowRealtime)
  913. {
  914. mimeType = (mimeType != null) ? mimeType : this.mimeType;
  915. allowRealtime = (allowRealtime != null) ? allowRealtime : true;
  916. var metadata =
  917. {
  918. 'mimeType': mimeType,
  919. 'title': title
  920. };
  921. if (folderId != null)
  922. {
  923. metadata.parents = [{'kind': 'drive#fileLink', 'id': folderId}];
  924. }
  925. // NOTE: Cannot create thumbnail on insert since no ui has no current file
  926. this.executeRequest(this.createUploadRequest(null, metadata, data, false, binary), mxUtils.bind(this, function(resp)
  927. {
  928. if (mimeType == this.libraryMimeType)
  929. {
  930. success(new DriveLibrary(this.ui, data, resp));
  931. }
  932. else if (resp == false)
  933. {
  934. if (error != null)
  935. {
  936. error({message: mxResources.get('errorSavingFile')});
  937. }
  938. }
  939. else if (allowRealtime)
  940. {
  941. this.loadRealtime(resp, mxUtils.bind(this, function(doc)
  942. {
  943. if (this.user != null)
  944. {
  945. var file = new DriveFile(this.ui, data, resp, doc);
  946. // Avoids creating a new revision on first autosave of new files
  947. file.lastAutosaveRevision = new Date().getTime();
  948. success(file);
  949. }
  950. else if (error != null)
  951. {
  952. error({message: mxResources.get('loggedOut')});
  953. }
  954. }), error);
  955. }
  956. else
  957. {
  958. success(resp);
  959. }
  960. }), error);
  961. };
  962. /**
  963. * Translates this point by the given vector.
  964. *
  965. * @param {number} dx X-coordinate of the translation.
  966. * @param {number} dy Y-coordinate of the translation.
  967. */
  968. DriveClient.prototype.createUploadRequest = function(id, metadata, data, revision, binary)
  969. {
  970. binary = (binary != null) ? binary : false;
  971. var bd = '-------314159265358979323846';
  972. var delim = '\r\n--' + bd + '\r\n';
  973. var close = '\r\n--' + bd + '--';
  974. var ctype = 'application/octect-stream';
  975. var reqObj =
  976. {
  977. 'path': '/upload/drive/v2/files' + (id != null ? '/' + id : ''),
  978. 'method': (id != null) ? 'PUT' : 'POST',
  979. 'params': {'uploadType': 'multipart'},
  980. 'headers': {'Content-Type' : 'multipart/mixed; boundary="' + bd + '"'},
  981. 'body' : delim + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delim +
  982. 'Content-Type: ' + ctype + '\r\n' + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' +
  983. ((data != null) ? (binary) ? data : Base64.encode(data) : '') + close
  984. }
  985. if (!revision)
  986. {
  987. reqObj.params['newRevision'] = false;
  988. }
  989. return gapi.client.request(reqObj);
  990. };
  991. /**
  992. * Translates this point by the given vector.
  993. *
  994. * @param {number} dx X-coordinate of the translation.
  995. * @param {number} dy Y-coordinate of the translation.
  996. */
  997. DriveClient.prototype.pickFile = function(fn, acceptAllFiles)
  998. {
  999. this.filePickerCallback = (fn != null) ? fn : mxUtils.bind(this, function(id)
  1000. {
  1001. this.ui.loadFile('G' + id);
  1002. });
  1003. this.filePicked = mxUtils.bind(this, function(data)
  1004. {
  1005. if (data.action == google.picker.Action.PICKED)
  1006. {
  1007. this.filePickerCallback(data.docs[0].id);
  1008. }
  1009. });
  1010. if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
  1011. {
  1012. this.execute(mxUtils.bind(this, function()
  1013. {
  1014. this.ui.spinner.stop();
  1015. // Reuses picker as long as token doesn't change.
  1016. var token = gapi.auth.getToken().access_token;
  1017. var name = (acceptAllFiles) ? 'genericPicker' : 'filePicker';
  1018. // Click on background closes dialog as workaround for blocking dialog
  1019. // states such as 401 where the dialog cannot be closed and blocks UI
  1020. var exit = mxUtils.bind(this, function(evt)
  1021. {
  1022. // Workaround for click from appIcon on second call
  1023. if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
  1024. {
  1025. mxEvent.removeListener(document, 'click', exit);
  1026. this[name].setVisible(false);
  1027. }
  1028. });
  1029. if (this[name] == null || this[name + 'Token'] != token)
  1030. {
  1031. // FIXME: Dispose not working
  1032. // if (this[name] != null)
  1033. // {
  1034. // console.log(name, this[name]);
  1035. // this[name].dispose();
  1036. // }
  1037. this[name + 'Token'] = token;
  1038. // Pseudo-hierarchical directory view, see
  1039. // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
  1040. var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
  1041. .setParent('root')
  1042. .setIncludeFolders(true);
  1043. var view2 = new google.picker.DocsView();
  1044. var view3 = new google.picker.DocsUploadView()
  1045. .setIncludeFolders(true);
  1046. if (!acceptAllFiles)
  1047. {
  1048. view.setMimeTypes(this.mimeTypes);
  1049. view2.setMimeTypes(this.mimeTypes);
  1050. }
  1051. this[name] = new google.picker.PickerBuilder()
  1052. .setOAuthToken(this[name + 'Token'])
  1053. .setLocale(mxLanguage)
  1054. .setAppId(this.appId)
  1055. .addView(view)
  1056. .addView(view2)
  1057. .addView(google.picker.ViewId.RECENTLY_PICKED)
  1058. .addView(view3)
  1059. .setCallback(mxUtils.bind(this, function(data)
  1060. {
  1061. if (data.action == google.picker.Action.PICKED ||
  1062. data.action == google.picker.Action.CANCEL)
  1063. {
  1064. mxEvent.removeListener(document, 'click', exit);
  1065. }
  1066. if (data.action == google.picker.Action.PICKED)
  1067. {
  1068. this.filePicked(data);
  1069. }
  1070. })).build();
  1071. }
  1072. mxEvent.addListener(document, 'click', exit);
  1073. this[name].setVisible(true);
  1074. this.ui.movePickersToTop();
  1075. }));
  1076. }
  1077. };
  1078. /**
  1079. * Translates this point by the given vector.
  1080. *
  1081. * @param {number} dx X-coordinate of the translation.
  1082. * @param {number} dy Y-coordinate of the translation.
  1083. */
  1084. DriveClient.prototype.pickFolder = function(fn)
  1085. {
  1086. // Picker is initialized once and points to this function
  1087. // which is overridden each time to the picker is shown
  1088. this.folderPickerCallback = fn;
  1089. if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
  1090. {
  1091. this.execute(mxUtils.bind(this, function()
  1092. {
  1093. this.ui.spinner.stop();
  1094. // Reuses picker as long as token doesn't change.
  1095. var token = gapi.auth.getToken().access_token;
  1096. var name = 'folderPicker';
  1097. // Click on background closes dialog as workaround for blocking dialog
  1098. // states such as 401 where the dialog cannot be closed and blocks UI
  1099. var exit = mxUtils.bind(this, function(evt)
  1100. {
  1101. // Workaround for click from appIcon on second call
  1102. if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
  1103. {
  1104. mxEvent.removeListener(document, 'click', exit);
  1105. this[name].setVisible(false);
  1106. }
  1107. });
  1108. if (this[name] == null || this[name + 'Token'] != token)
  1109. {
  1110. // FIXME: Dispose not working
  1111. // if (this[name] != null)
  1112. // {
  1113. // console.log(name, this[name]);
  1114. // this[name].dispose();
  1115. // }
  1116. this[name + 'Token'] = token;
  1117. // Pseudo-hierarchical directory view, see
  1118. // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
  1119. var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
  1120. .setParent('root')
  1121. .setIncludeFolders(true)
  1122. .setSelectFolderEnabled(true)
  1123. .setMimeTypes('application/vnd.google-apps.folder');
  1124. var view2 = new google.picker.DocsView()
  1125. .setIncludeFolders(true)
  1126. .setSelectFolderEnabled(true)
  1127. .setMimeTypes('application/vnd.google-apps.folder');
  1128. this[name] = new google.picker.PickerBuilder()
  1129. .setSelectableMimeTypes('application/vnd.google-apps.folder')
  1130. .setOAuthToken(this[name + 'Token'])
  1131. .setLocale(mxLanguage)
  1132. .setAppId(this.appId)
  1133. .addView(view)
  1134. .addView(view2)
  1135. .addView(google.picker.ViewId.RECENTLY_PICKED)
  1136. .setTitle(mxResources.get('pickFolder'))
  1137. .setCallback(mxUtils.bind(this, function(data)
  1138. {
  1139. if (data.action == google.picker.Action.PICKED ||
  1140. data.action == google.picker.Action.CANCEL)
  1141. {
  1142. mxEvent.removeListener(document, 'click', exit);
  1143. }
  1144. this.folderPickerCallback(data);
  1145. })).build();
  1146. }
  1147. mxEvent.addListener(document, 'click', exit);
  1148. this[name].setVisible(true);
  1149. this.ui.movePickersToTop();
  1150. }));
  1151. }
  1152. };
  1153. /**
  1154. * Translates this point by the given vector.
  1155. *
  1156. * @param {number} dx X-coordinate of the translation.
  1157. * @param {number} dy Y-coordinate of the translation.
  1158. */
  1159. DriveClient.prototype.pickLibrary = function(fn)
  1160. {
  1161. this.filePickerCallback = fn;
  1162. this.filePicked = mxUtils.bind(this, function(data)
  1163. {
  1164. if (data.action == google.picker.Action.PICKED)
  1165. {
  1166. this.filePickerCallback(data.docs[0].id);
  1167. }
  1168. else if (data.action == google.picker.Action.CANCEL && this.ui.getCurrentFile() == null)
  1169. {
  1170. this.ui.showSplash();
  1171. }
  1172. });
  1173. if (this.ui.spinner.spin(document.body, mxResources.get('authorizing')))
  1174. {
  1175. this.execute(mxUtils.bind(this, function()
  1176. {
  1177. this.ui.spinner.stop();
  1178. // Click on background closes dialog as workaround for blocking dialog
  1179. // states such as 401 where the dialog cannot be closed and blocks UI
  1180. var exit = mxUtils.bind(this, function(evt)
  1181. {
  1182. // Workaround for click from appIcon on second call
  1183. if (mxEvent.getSource(evt).className == 'picker modal-dialog-bg picker-dialog-bg')
  1184. {
  1185. mxEvent.removeListener(document, 'click', exit);
  1186. this.libraryPicker.setVisible(false);
  1187. }
  1188. });
  1189. // Reuses picker as long as token doesn't change.
  1190. var token = gapi.auth.getToken().access_token;
  1191. if (this.libraryPicker == null || this.libraryPickerToken != token)
  1192. {
  1193. // FIXME: Dispose not working
  1194. // if (this[name] != null)
  1195. // {
  1196. // console.log(name, this[name]);
  1197. // this[name].dispose();
  1198. // }
  1199. this.libraryPickerToken = token;
  1200. // Pseudo-hierarchical directory view, see
  1201. // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ
  1202. var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
  1203. .setParent('root')
  1204. .setIncludeFolders(true)
  1205. .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
  1206. var view2 = new google.picker.DocsView()
  1207. .setMimeTypes(this.libraryMimeType + ',application/xml,text/plain,application/octet-stream');
  1208. var view3 = new google.picker.DocsUploadView()
  1209. .setIncludeFolders(true);
  1210. this.libraryPicker = new google.picker.PickerBuilder()
  1211. .setOAuthToken(this.libraryPickerToken)
  1212. .setLocale(mxLanguage)
  1213. .setAppId(this.appId)
  1214. .addView(view)
  1215. .addView(view2)
  1216. .addView(google.picker.ViewId.RECENTLY_PICKED)
  1217. .addView(view3)
  1218. .setCallback(mxUtils.bind(this, function(data)
  1219. {
  1220. if (data.action == google.picker.Action.PICKED ||
  1221. data.action == google.picker.Action.CANCEL)
  1222. {
  1223. mxEvent.removeListener(document, 'click', exit);
  1224. }
  1225. if (data.action == google.picker.Action.PICKED)
  1226. {
  1227. this.filePicked(data);
  1228. }
  1229. })).build();
  1230. }
  1231. mxEvent.addListener(document, 'click', exit);
  1232. this.libraryPicker.setVisible(true);
  1233. this.ui.movePickersToTop();
  1234. }));
  1235. }
  1236. };
  1237. /**
  1238. * Translates this point by the given vector.
  1239. *
  1240. * @param {number} dx X-coordinate of the translation.
  1241. * @param {number} dy Y-coordinate of the translation.
  1242. */
  1243. DriveClient.prototype.showPermissions = function(id)
  1244. {
  1245. this.checkToken(mxUtils.bind(this, function()
  1246. {
  1247. var shareClient = new gapi.drive.share.ShareClient(this.appId);
  1248. shareClient.setOAuthToken(gapi.auth.getToken().access_token);
  1249. shareClient.setItemIds([id]);
  1250. shareClient.showSettingsDialog();
  1251. }));
  1252. };