diff options
author | David Benson [draw.io] <david@jgraph.com> | 2019-09-27 13:51:03 +0300 |
---|---|---|
committer | David Benson [draw.io] <david@jgraph.com> | 2019-09-27 13:51:03 +0300 |
commit | 55312c18958b690e778a859b7c5f7d9ee9fb7db8 (patch) | |
tree | ed4de615a0b4231a81b250d04700ebbc33c59371 | |
parent | 86d0f98d4b795c13ddd46446345d257d21f800ce (diff) |
11.3.2 releasev11.3.2
68 files changed, 886 insertions, 1105 deletions
@@ -1,3 +1,5 @@ +- Fixes default grid color in dark mode + 26-SEP-2019: 11.3.2 - Updates Dell rack server stencils diff --git a/src/main/java/com/mxgraph/online/AbsAuthServlet.java b/src/main/java/com/mxgraph/online/AbsAuthServlet.java index cb6ec7b7..168f0237 100644 --- a/src/main/java/com/mxgraph/online/AbsAuthServlet.java +++ b/src/main/java/com/mxgraph/online/AbsAuthServlet.java @@ -11,6 +11,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URL; +import java.util.HashMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -20,7 +21,9 @@ import javax.servlet.http.HttpServletResponse; @SuppressWarnings("serial") abstract public class AbsAuthServlet extends HttpServlet { - + private static final boolean DEBUG = false; + private static final String SEPARATOR = "/:::/"; + static public class Config { public String DEV_CLIENT_SECRET = null, CLIENT_SECRET = null, DEV_CLIENT_ID = null, CLIENT_ID = null, @@ -32,6 +35,13 @@ abstract public class AbsAuthServlet extends HttpServlet return null; } + protected String processAuthError(String errorCode) + { + //Usually sending null is enough as it is used as a value for auth info + //If more processing is needed, override this method + return processAuthResponse("null", false); + } + protected String processAuthResponse(String authRes, boolean jsonResponse) { return ""; @@ -45,23 +55,88 @@ abstract public class AbsAuthServlet extends HttpServlet { String code = request.getParameter("code"); String refreshToken = request.getParameter("refresh_token"); + String error = request.getParameter("error"); + HashMap<String, String> stateVars = new HashMap<>(); + + try + { + String state = request.getParameter("state"); + + if (state != null) + { + String[] parts = state.split("&"); + + for (String part : parts) + { + String[] keyVal = part.split("="); + stateVars.put(keyVal[0], keyVal[1]); + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + + int configIndex = 0; + + try + { + String appIndex = stateVars.get("appIndex"); + + if (appIndex != null) + { + configIndex = Integer.parseInt(appIndex); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + Config CONFIG = getConfig(); String secret, client, redirectUri; + String[] secrets, clients; if ("127.0.0.1".equals(request.getServerName())) { - secret = CONFIG.DEV_CLIENT_SECRET; - client = CONFIG.DEV_CLIENT_ID; + secrets = CONFIG.DEV_CLIENT_SECRET.split(SEPARATOR); + clients = CONFIG.DEV_CLIENT_ID.split(SEPARATOR); redirectUri = CONFIG.DEV_REDIRECT_URI; } else { - secret = CONFIG.CLIENT_SECRET; - client = CONFIG.CLIENT_ID; + secrets = CONFIG.CLIENT_SECRET.split(SEPARATOR); + clients = CONFIG.CLIENT_ID.split(SEPARATOR); redirectUri = CONFIG.REDIRECT_URI; } - if (code == null && refreshToken == null) + secret = secrets.length > configIndex ? secrets[configIndex] : secrets[0]; + client = clients.length > configIndex ? clients[configIndex] : clients[0]; + + if (error != null) + { + try + { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + OutputStream out = response.getOutputStream(); + + PrintWriter writer = new PrintWriter(out); + + // Writes JavaScript code + writer.println(processAuthError(error)); + + writer.flush(); + writer.close(); + } + catch(Exception e) + { + e.printStackTrace(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + else if (code == null && refreshToken == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } @@ -135,7 +210,8 @@ abstract public class AbsAuthServlet extends HttpServlet catch(IOException e) { e.printStackTrace(); - + StringBuilder details = new StringBuilder(""); + if (con != null) { try @@ -148,6 +224,8 @@ abstract public class AbsAuthServlet extends HttpServlet while ((inputLine = in.readLine()) != null) { System.err.println(inputLine); + details.append(inputLine); + details.append("\n"); } in.close(); } @@ -165,6 +243,19 @@ abstract public class AbsAuthServlet extends HttpServlet { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } + + if (DEBUG) + { + OutputStream out = response.getOutputStream(); + + PrintWriter writer = new PrintWriter(out); + + e.printStackTrace(writer); + writer.println(details.toString()); + + writer.flush(); + writer.close(); + } } catch (Exception e) { diff --git a/src/main/webapp/js/diagramly/App.js b/src/main/webapp/js/diagramly/App.js index f466ba00..0f397176 100644 --- a/src/main/webapp/js/diagramly/App.js +++ b/src/main/webapp/js/diagramly/App.js @@ -279,12 +279,7 @@ App.PUSHER_URL = 'https://js.pusher.com/4.3/pusher.min.js'; * Google APIs to load. The realtime API is needed to notify collaborators of conversion * of the realtime files, but after Dec 11 it's read-only and hence no longer needed. */ -App.GOOGLE_APIS = 'client,drive-share'; - -/** - * Google Realtime API export endpoint end of life on 09/27/2019. - */ -App.GOOGLE_REALTIME_EOL = 1569535200000; +App.GOOGLE_APIS = 'drive-share'; /** * Function: authorize @@ -910,7 +905,7 @@ mxUtils.extend(App, EditorUi); /** * Executes the first step for connecting to Google Drive. */ -App.prototype.defaultUserPicture = 'https://lh3.googleusercontent.com/-HIzvXUy6QUY/AAAAAAAAAAI/AAAAAAAAAAA/giuR7PQyjEk/photo.jpg?sz=30'; +App.prototype.defaultUserPicture = 'https://lh3.googleusercontent.com/-HIzvXUy6QUY/AAAAAAAAAAI/AAAAAAAAAAA/giuR7PQyjEk/photo.jpg?sz=64'; /** * @@ -1262,42 +1257,6 @@ App.prototype.init = function() this.updateUserElement(); this.restoreLibraries(); this.checkLicense(); - - // Stop notification one day before API is disabled - if (App.GOOGLE_REALTIME_EOL - Date.now() > 86400000) - { - if (this.drive.user != null && (!isLocalStorage || mxSettings.settings == null || - mxSettings.settings.closeRealtimeWarning == null || mxSettings.settings.closeRealtimeWarning < - new Date().getTime() - (2 * 24 * 60 * 60 * 1000)) && - (!this.editor.chromeless || this.editor.editable)) - { - this.drive.checkRealtimeFiles(mxUtils.bind(this, function() - { - var footer = createFooter('You need to take action to convert legacy files. Click here.', - 'https://desk.draw.io/support/solutions/articles/16000092210', - 'geStatusAlert', - mxUtils.bind(this, function() - { - footer.parentNode.removeChild(footer); - this.hideFooter(); - - // Close permanently - if (isLocalStorage && mxSettings.settings != null) - { - mxSettings.settings.closeRealtimeWarning = Date.now(); - mxSettings.save(); - } - })); - - document.body.appendChild(footer); - - window.setTimeout(mxUtils.bind(this, function() - { - mxUtils.setPrefixedStyle(footer.style, 'transform', 'translate(-50%,0%)'); - }), 1500); - })); - } - } })) // Notifies listeners of new client @@ -1306,24 +1265,7 @@ App.prototype.init = function() if (window.DrawGapiClientCallback != null) { - gapi.load(((urlParams['picker'] != '0') ? 'picker,': '') + 'auth:' + App.GOOGLE_APIS, mxUtils.bind(this, function(resp) - { - // Starts the app without the Google Option if the API fails to load - if (gapi.client != null) - { - gapi.client.load('drive', 'v2', mxUtils.bind(this, function() - { - // Needed to avoid popup blocking for non-immediate authentication - gapi.auth.init(mxUtils.bind(this, function() - { - if (gapi.client.drive != null) - { - doInit(); - } - })); - })); - } - })); + gapi.load(((urlParams['picker'] != '0') ? 'picker,': '') + App.GOOGLE_APIS, doInit); /** * Clears any callbacks. @@ -2436,33 +2378,7 @@ App.prototype.loadGapi = function(then) { if (typeof gapi !== 'undefined') { - gapi.load(((urlParams['picker'] != '0') ? 'picker,': '') + 'auth:' + App.GOOGLE_APIS, mxUtils.bind(this, function(resp) - { - // Starts the app without the Google Option if the API fails to load - if (gapi.client == null) - { - this.drive = null; - this.mode = null; - then(); - } - else - { - gapi.client.load('drive', 'v2', mxUtils.bind(this, function() - { - // Needed to avoid popup blocking for non-immediate authentication - gapi.auth.init(mxUtils.bind(this, function() - { - if (gapi.client.drive == null) - { - this.drive = null; - this.mode = null; - } - - then(); - })); - })); - } - })); + gapi.load(((urlParams['picker'] != '0') ? 'picker,': '') + App.GOOGLE_APIS, then); } }; @@ -5823,75 +5739,140 @@ App.prototype.updateUserElement = function() if (this.drive != null) { - var driveUser = this.drive.getUser(); + var driveUsers = this.drive.getUsersList(); - if (driveUser != null) + if (driveUsers.length > 0) { - connected = true; - this.userPanel.innerHTML += '<table title="User ID: ' + driveUser.id + - '" style="font-size:10pt;padding:20px 20px 10px 10px;">' + - '<tr><td valign="middle">' + - ((driveUser.pictureUrl != null) ? - '<img width="50" height="50" style="margin-right:8px;border-radius:50%;" src="' + driveUser.pictureUrl + '"/>' : - '<img width="46" height="46" style="margin-right:4px;margin-top:0px;" src="' + this.defaultUserPicture + '"/>') + - '</td><td valign="top" style="white-space:nowrap;' + - ((driveUser.pictureUrl != null) ? 'padding-top:4px;' : '') + - '">' + mxUtils.htmlEntities(driveUser.displayName) + '<br>' + - '<small style="color:gray;">' + mxUtils.htmlEntities(driveUser.email) + - '</small><div style="margin-top:4px;"><i>' + - mxResources.get('googleDrive') + '</i></div></tr></table>'; - var div = document.createElement('div'); - div.style.textAlign = 'center'; - div.style.paddingBottom = '12px'; - div.style.whiteSpace = 'nowrap'; - // LATER: Cannot change user while file is open since close will not work with new // credentials and closing the file using fileLoaded(null) will show splash dialog. - var btn = mxUtils.button(mxResources.get('signOut'), mxUtils.bind(this, function() + var closeFile = mxUtils.bind(this, function(callback, spinnerMsg) { var file = this.getCurrentFile(); if (file != null && file.constructor == DriveFile) { - this.confirm(mxResources.get('areYouSure'), mxUtils.bind(this, function() - { - this.spinner.spin(document.body, mxResources.get('signOut')); - - this.diagramContainer.style.display = 'none'; - this.formatContainer.style.display = 'none'; - this.hsplit.style.display = 'none'; - this.sidebarContainer.style.display = 'none'; - this.sidebarFooterContainer.style.display = 'none'; + this.spinner.spin(document.body, spinnerMsg); - if (this.tabContainer != null) - { - this.tabContainer.style.display = 'none'; - } - - file.close(); - - // LATER: Use callback to wait for thumbnail update - window.setTimeout(mxUtils.bind(this, function() +// file.close(); + this.fileLoaded(null); + + // LATER: Use callback to wait for thumbnail update + window.setTimeout(mxUtils.bind(this, function() + { + this.spinner.stop(); + callback(); + }), 2000); + } + else + { + callback(); + } + }); + + var createUserRow = mxUtils.bind(this, function (user) + { + var tr = document.createElement('tr'); + tr.style.cssText = user.isCurrent? '' : 'background-color: whitesmoke; cursor: pointer'; + tr.setAttribute('title', 'User ID: ' + user.id); + tr.innerHTML = '<td valign="middle" style="height: 59px;width: 66px;' + + (user.isCurrent? '' : 'border-top: 1px solid rgb(224, 224, 224);') + '">' + + '<img width="50" height="50" style="margin: 4px 8px 0 8px;border-radius:50%;" src="' + + ((user.pictureUrl != null) ? user.pictureUrl : this.defaultUserPicture) + '"/>' + + '</td><td valign="middle" style="white-space:nowrap;' + + ((user.pictureUrl != null) ? 'padding-top:4px;' : '') + + (user.isCurrent? '' : 'border-top: 1px solid rgb(224, 224, 224);') + + '">' + mxUtils.htmlEntities(user.displayName) + '<br>' + + '<small style="color:gray;">' + mxUtils.htmlEntities(user.email) + + '</small><div style="margin-top:4px;"><i>' + + mxResources.get('googleDrive') + '</i></div>'; + + if (!user.isCurrent) + { + mxEvent.addListener(tr, 'click', mxUtils.bind(this, function(evt) + { + closeFile(mxUtils.bind(this, function() { - // Workaround to disable the splash screen before reload - this.showDialog = function() {}; - window.location.hash = ''; - this.drive.clearUserId(); - gapi.auth.signOut(); + this.stateArg = null; + this.drive.setUser(user); - // Reload page to reset client auth - window.location.reload(); - }), (file != null && file.constructor == DriveFile) ? 2000 : 0); + this.drive.authorize(true, mxUtils.bind(this, function() + { + this.setMode(App.MODE_GOOGLE); + this.hideDialog(); + this.showSplash(); + }), mxUtils.bind(this, function(resp) + { + this.handleError(resp); + }), true); //Remember is true since add account imply keeping that account + }), mxResources.get('closingFile', null, 'Closing file...')); + + mxEvent.consume(evt); })); } - else + + return tr; + }); + + connected = true; + + var driveUserTable = document.createElement('table'); + driveUserTable.style.cssText ='font-size:10pt;padding: 20px 0 0 0;min-width: 300px;border-spacing: 0;'; + + for (var i = 0; i < driveUsers.length; i++) + { + driveUserTable.appendChild(createUserRow(driveUsers[i])); + } + + this.userPanel.appendChild(driveUserTable); + + var div = document.createElement('div'); + div.style.textAlign = 'left'; + div.style.padding = '8px'; + div.style.whiteSpace = 'nowrap'; + div.style.borderTop = '1px solid rgb(224, 224, 224)'; + + var btn = mxUtils.button(mxResources.get('signOut'), mxUtils.bind(this, function() + { + this.confirm(mxResources.get('areYouSure'), mxUtils.bind(this, function() { - this.drive.clearUserId(); - this.drive.setUser(null); - gapi.auth.signOut(); - } + closeFile(mxUtils.bind(this, function() + { + this.stateArg = null; + this.drive.logout(); + this.setMode(App.MODE_GOOGLE); + this.hideDialog(); + this.showSplash(); + }), mxResources.get('signOut')); + })); })); btn.className = 'geBtn'; + btn.style.float = 'right'; + div.appendChild(btn); + + var btn = mxUtils.button(mxResources.get('addAccount', null, 'Add Account'), mxUtils.bind(this, function() + { + var authWin = this.drive.createAuthWin(); + //FIXME This doean't work to set focus back to main window until closing the file is done + authWin.blur(); + window.focus(); + + closeFile(mxUtils.bind(this, function() + { + this.stateArg = null; + + this.drive.authorize(false, mxUtils.bind(this, function() + { + this.setMode(App.MODE_GOOGLE); + this.hideDialog(); + this.showSplash(); + }), mxUtils.bind(this, function(resp) + { + this.handleError(resp); + }), true, authWin); //Remember is true since add account imply keeping that account + }), mxResources.get('closingFile', null, 'Closing file...')); + })); + btn.className = 'geBtn'; + btn.style.margin = '0px'; div.appendChild(btn); this.userPanel.appendChild(div); } @@ -5907,12 +5888,17 @@ App.prototype.updateUserElement = function() } connected = true; - this.userPanel.innerHTML += '<table style="font-size:10pt;padding:20px 20px 10px 10px;"><tr><td valign="top">' + + var userTable = document.createElement('table'); + userTable.style.cssText = 'font-size:10pt;padding:' + (connected? '10' : '20') + 'px 20px 10px 10px;'; + + userTable.innerHTML += '<tr><td valign="top">' + ((logo != null) ? '<img style="margin-right:6px;" src="' + logo + '" width="40" height="40"/></td>' : '') + '<td valign="middle" style="white-space:nowrap;">' + mxUtils.htmlEntities(user.displayName) + ((user.email != null) ? '<br><small style="color:gray;">' + mxUtils.htmlEntities(user.email) + '</small>' : '') + ((label != null) ? '<div style="margin-top:4px;"><i>' + mxUtils.htmlEntities(label) + '</i></div>' : '') + - '</td></tr></table>'; + '</td></tr>'; + + this.userPanel.appendChild(userTable); var div = document.createElement('div'); div.style.textAlign = 'center'; div.style.paddingBottom = '12px'; diff --git a/src/main/webapp/js/diagramly/Dialogs.js b/src/main/webapp/js/diagramly/Dialogs.js index 3298ba2f..48a327e1 100644 --- a/src/main/webapp/js/diagramly/Dialogs.js +++ b/src/main/webapp/js/diagramly/Dialogs.js @@ -736,7 +736,7 @@ var SplashDialog = function(editorUi) if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp) { - var driveUser = (editorUi.drive != null) ? editorUi.drive.getUser() : null; + var driveUsers = (editorUi.drive != null) ? editorUi.drive.getUsersList() : []; function addLogout(logout) { @@ -763,48 +763,68 @@ var SplashDialog = function(editorUi) buttons.appendChild(link); }; - if (editorUi.mode == App.MODE_GOOGLE && driveUser != null) + if (editorUi.mode == App.MODE_GOOGLE && driveUsers.length > 0) { - btn.style.marginBottom = '24px'; - - var link = document.createElement('a'); - link.setAttribute('href', 'javascript:void(0)'); - link.style.display = 'inline-block'; - link.style.marginTop = '6px'; - mxUtils.write(link, mxResources.get('changeUser') + ' (' + driveUser.displayName + ')'); + var title = document.createElement('span'); + title.style.marginTop = '6px'; + mxUtils.write(title, mxResources.get('changeUser') + ':'); // Makes room after last big buttons btn.style.marginBottom = '16px'; buttons.style.paddingBottom = '18px'; - mxEvent.addListener(link, 'click', function() + buttons.appendChild(title); + + var usersSelect = document.createElement('select'); + usersSelect.style.marginLeft = '4px'; + usersSelect.style.width = '200px'; + + for (var i = 0; i < driveUsers.length; i++) { - editorUi.hideDialog(); - editorUi.drive.clearUserId(); - editorUi.drive.setUser(null); - gapi.auth.signOut(); - - // Restores current dialog after clearing user - editorUi.setMode(App.MODE_GOOGLE); - editorUi.hideDialog(); - editorUi.showSplash(); + var option = document.createElement('option'); + mxUtils.write(option, driveUsers[i].displayName); + option.value = i; + usersSelect.appendChild(option); + //More info (email) about the user in a disabled option + option = document.createElement('option'); + option.innerHTML = ' '; + mxUtils.write(option, '<' + driveUsers[i].email + '>'); + option.setAttribute('disabled', 'disabled'); + usersSelect.appendChild(option); + } + + //Add account option + var option = document.createElement('option'); + mxUtils.write(option, mxResources.get('addAccount')); + option.value = driveUsers.length; + usersSelect.appendChild(option); + + mxEvent.addListener(usersSelect, 'change', function() + { + var userIndex = usersSelect.value; + var existingAccount = driveUsers.length != userIndex; - // FIXME: Does not force showing the auth dialog if only one user is logged in - editorUi.drive.authorize(false, mxUtils.bind(this, mxUtils.bind(this, function() + if (existingAccount) { + editorUi.drive.setUser(driveUsers[userIndex]); + } + + editorUi.drive.authorize(existingAccount, function() + { + editorUi.setMode(App.MODE_GOOGLE); editorUi.hideDialog(); editorUi.showSplash(); - })), mxUtils.bind(this, function(resp) + }, function(resp) { editorUi.handleError(resp, null, function() { editorUi.hideDialog(); editorUi.showSplash(); }); - })); + }, true); }); - - buttons.appendChild(link); + + buttons.appendChild(usersSelect); } else if (editorUi.mode == App.MODE_ONEDRIVE && editorUi.oneDrive != null) { @@ -1980,11 +2000,10 @@ var BackgroundImageDialog = function(editorUi, applyFn) // Creates one picker and reuses it to avoid polluting the DOM if (editorUi.photoPicker == null) { - var token = gapi.auth.getToken().access_token; var picker = new google.picker.PickerBuilder() .setAppId(editorUi.drive.appId) .setLocale(mxLanguage) - .setOAuthToken(token) + .setOAuthToken(editorUi.drive.token) .addView(google.picker.ViewId.PHOTO_UPLOAD); editorUi.photoPicker = picker.setCallback(function(data) @@ -4541,11 +4560,10 @@ var ImageDialog = function(editorUi, title, initialValue, fn, ignoreExisting, co // Creates one picker and reuses it to avoid polluting the DOM if (editorUi.photoPicker == null) { - var token = gapi.auth.getToken().access_token; var picker = new google.picker.PickerBuilder() .setAppId(editorUi.drive.appId) .setLocale(mxLanguage) - .setOAuthToken(token) + .setOAuthToken(editorUi.drive.token) .addView(google.picker.ViewId.PHOTO_UPLOAD); editorUi.photoPicker = picker.setCallback(function(data) @@ -4931,7 +4949,6 @@ var LinkDialog = function(editorUi, initialValue, btnLabel, fn, showPages) // Creates one picker and reuses it to avoid polluting the DOM if (editorUi.linkPicker == null) { - var token = gapi.auth.getToken().access_token; var view = new google.picker.DocsView(google.picker.ViewId.FOLDERS) .setParent('root') .setIncludeFolders(true) @@ -4946,7 +4963,7 @@ var LinkDialog = function(editorUi, initialValue, btnLabel, fn, showPages) var picker = new google.picker.PickerBuilder() .setAppId(editorUi.drive.appId) .setLocale(mxLanguage) - .setOAuthToken(token) + .setOAuthToken(editorUi.drive.token) .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES) .addView(view) .addView(view2) diff --git a/src/main/webapp/js/diagramly/DriveClient.js b/src/main/webapp/js/diagramly/DriveClient.js index fa299f4f..e753a555 100644 --- a/src/main/webapp/js/diagramly/DriveClient.js +++ b/src/main/webapp/js/diagramly/DriveClient.js @@ -6,6 +6,25 @@ DriveClient = function(editorUi) { mxEventSource.call(this); + DrawioClient.call(this, editorUi, 'gDriveAuthInfo'); + + var authInfo = JSON.parse(this.token); + + if (authInfo != null && authInfo.current != null) + { + authInfo = authInfo.current; + + this.userId = authInfo.userId; + this.token = authInfo.access_token; + + var remainingTime = (authInfo.expires - Date.now()) / 1000; + + authInfo.expires_in = remainingTime < 600? 1 : remainingTime; //10 min tolerance window in case of any rounding errors + this.resetTokenRefresh(authInfo); + + this.authCalled = false; + } + /** * Holds a reference to the UI. Needed for the sharing client. */ @@ -22,11 +41,13 @@ DriveClient = function(editorUi) this.clientId = window.DRAWIO_GOOGLE_VIEWER_CLIENT_ID || '850530949725.apps.googleusercontent.com'; this.scopes = ['https://www.googleapis.com/auth/drive.readonly', 'https://www.googleapis.com/auth/userinfo.profile']; + this.appIndex = 0; } else { this.appId = window.DRAWIO_GOOGLE_APP_ID || '671128082532'; this.clientId = window.DRAWIO_GOOGLE_CLIENT_ID || '671128082532-jhphbq6d0e1gnsus9mn7vf8a6fjn10mp.apps.googleusercontent.com'; + this.appIndex = 1; } this.mimeTypes = this.xmlMimeType + 'application/mxe,application/mxr,' + @@ -41,6 +62,12 @@ DriveClient = function(editorUi) // Extends mxEventSource mxUtils.extend(DriveClient, mxEventSource); +// Extends DrawioClient +mxUtils.extend(DriveClient, DrawioClient); + +DriveClient.prototype.redirectUri = 'https://' + window.location.hostname + '/google'; +DriveClient.prototype.GDriveBaseUrl = 'https://www.googleapis.com/drive/v2'; + /** * OAuth 2.0 scopes for installing Drive Apps. */ @@ -142,111 +169,72 @@ DriveClient.prototype.setUser = function(user) { this.user = user; - if (this.user == null && this.tokenRefreshThread != null) + if (this.user == null) { - window.clearTimeout(this.tokenRefreshThread); - this.tokenRefreshThread = null; + this.userId = null; + + if (this.tokenRefreshThread != null) + { + window.clearTimeout(this.tokenRefreshThread); + this.tokenRefreshThread = null; + } + } + else + { + this.userId = user.id; } this.fireEvent(new mxEventObject('userChanged')); }; -/** - * Authorizes the client, gets the userId and calls <open>. - */ -DriveClient.prototype.getUser = function() -{ - return this.user; -}; - -/** - * Authorizes the client, gets the userId and calls <open>. - */ -DriveClient.prototype.setUserId = function(userId, remember) +DriveClient.prototype.setUserId = function(userId) { - if (remember) + this.userId = userId; + + if (this.user != null && this.user.id != this.userId) { - if (isLocalStorage) - { - localStorage.setItem('.guid', userId); - } - else if (typeof(Storage) != 'undefined') - { - try - { - var expiry = new Date(); - expiry.setYear(expiry.getFullYear() + 1); - document.cookie = 'GUID=' + userId + '; expires=' + expiry.toUTCString(); - } - catch (e) - { - // any errors for storing the user ID can be safely ignored - } - } + this.user = null; } }; - /** * Authorizes the client, gets the userId and calls <open>. */ -DriveClient.prototype.clearUserId = function() +DriveClient.prototype.getUser = function() { - if (isLocalStorage) - { - localStorage.removeItem('.guid'); - } - else if (typeof(Storage) != 'undefined') - { - var expiry = new Date(); - expiry.setYear(expiry.getFullYear() - 1); - document.cookie = 'GUID=; expires=' + expiry.toUTCString(); - } + return this.user; }; -/** - * Authorizes the client, gets the userId and calls <open>. - */ -DriveClient.prototype.getUserId = function() +DriveClient.prototype.getUsersList = function() { - var uid = null; - - if (this.user != null) - { - uid = this.user.id; - } + var users = []; + var authInfo = JSON.parse(this.getPersistentToken(true)); + var curUserId = null; - if (uid == null && isLocalStorage) + if (authInfo != null) { - uid = localStorage.getItem('.guid'); - } - - if (uid == null && typeof(Storage) != 'undefined') - { - var cookies = document.cookie.split(";"); - - for (var i = 0; i < cookies.length; i++) + if (authInfo.current != null) { - // Removes spaces around cookie - var cookie = mxUtils.trim(cookies[i]); + curUserId = authInfo.current.userId; + users.push(authInfo[curUserId].user); + users[0].isCurrent = true; - if (cookie.substring(0, 5) == 'GUID=') - { - uid = cookie.substring(5); - break; - } } - if (uid != null && isLocalStorage) + for (var id in authInfo) { - // Moves to local storage - var expiry = new Date(); - expiry.setYear(expiry.getFullYear() - 1); - document.cookie = 'GUID=; expires=' + expiry.toUTCString(); - localStorage.setItem('.guid', uid); + if (id == 'current' || id == curUserId) continue; + + users.push(authInfo[id].user); } } - - return uid; + return users; +}; + +DriveClient.prototype.logout = function() +{ + this.clearPersistentToken(); + this.setUser(null); + this.token = null; }; /** @@ -286,9 +274,7 @@ DriveClient.prototype.execute = function(fn) } } - this.ui.drive.clearUserId(); - this.ui.drive.setUser(null); - gapi.auth.signOut(); + this.logout(); this.ui.showError(mxResources.get('error'), msg, mxResources.get('help'), mxUtils.bind(this, function() { @@ -305,7 +291,7 @@ DriveClient.prototype.execute = function(fn) /** * Executes the given request. */ -DriveClient.prototype.executeRequest = function(req, success, error) +DriveClient.prototype.executeRequest = function(reqObj, success, error) { try { @@ -324,7 +310,7 @@ DriveClient.prototype.executeRequest = function(req, success, error) try { this.requestThread = null; - this.currentRequest = req; + this.currentRequest = reqObj; if (timeoutThread != null) { @@ -341,7 +327,50 @@ DriveClient.prototype.executeRequest = function(req, success, error) } }), this.ui.timeout); - req.execute(mxUtils.bind(this, function(resp) + var params = null; + var isJSON = false; + + if (typeof reqObj.params === 'string') + { + params = reqObj.params; + } + else if (reqObj.params != null) + { + params = JSON.stringify(reqObj.params); + isJSON = true; + } + + var url = reqObj.fullUrl || (this.GDriveBaseUrl + reqObj.url); + + if (isJSON) + { + url += (url.indexOf('?') > 0 ? '&' : '?') + 'alt=json'; + } + + var req = new mxXmlRequest(url, params, reqObj.method || 'GET'); + + req.setRequestHeaders = mxUtils.bind(this, function(request, params) + { + if (reqObj.headers != null) + { + for (var key in reqObj.headers) + { + request.setRequestHeader(key, reqObj.headers[key]); + } + } + else if (reqObj.contentType != null) + { + request.setRequestHeader('Content-Type', reqObj.contentType); + } + else if (isJSON) + { + request.setRequestHeader('Content-Type', 'application/json'); + } + + request.setRequestHeader('Authorization', 'Bearer ' + this.token); + }); + + req.send(mxUtils.bind(this, function(req) { try { @@ -349,7 +378,18 @@ DriveClient.prototype.executeRequest = function(req, success, error) if (acceptResponse) { - if (resp != null && resp.error == null) + var resp; + + try + { + resp = JSON.parse(req.getText()); + } + catch(e) + { + resp = null; + } + + if (req.getStatus() >= 200 && req.getStatus() <= 299) { if (success != null) { @@ -376,23 +416,26 @@ DriveClient.prototype.executeRequest = function(req, success, error) else if (resp != null && resp.error != null && (resp.error.code == 401 || (resp.error.code == 403 && reason != 'rateLimitExceeded'))) { - // Shows an error if we're authenticated but the server still doesn't allow it - if ((resp.error.code == 403 && this.user != null) || - (resp.error.code == 401 && this.user != null && reason == 'authError')) + // Shows an error if re-authenticated but the server still doesn't allow it + if ((resp.error.code == 403 && this.retryAuth) || + (resp.error.code == 401 && this.retryAuth && reason == 'authError')) { if (error != null) { error(resp); } + + this.retryAuth = false; } else { + this.retryAuth = true; this.execute(fn); } } // Schedules a retry if no new request was executed else if (resp != null && resp.error != null && resp.error.code != 412 && resp.error.code != 404 && - resp.error.code != 400 && this.currentRequest == req && retryCount < this.maxRetries) + resp.error.code != 400 && this.currentRequest == reqObj && retryCount < this.maxRetries) { retryCount++; var jitter = 1 + 0.1 * (Math.random() - 0.5); @@ -434,7 +477,7 @@ DriveClient.prototype.executeRequest = function(req, success, error) }); // Must get token before first request in this case - if (gapi.auth.getToken() == null) + if (this.token == null || !this.authCalled) { this.execute(fn); } @@ -454,25 +497,106 @@ DriveClient.prototype.executeRequest = function(req, success, error) throw e; } } -}, +}; + +DriveClient.prototype.createAuthWin = function(url) +{ + var width = 525, + height = 525, + screenX = window.screenX, + screenY = window.screenY, + outerWidth = window.outerWidth, + outerHeight = window.outerHeight; + + var left = screenX + Math.max(outerWidth - width, 0) / 2; + var top = screenY + Math.max(outerHeight - height, 0) / 2; + + var features = ['width=' + width, 'height=' + height, + 'top=' + top, 'left=' + left, + 'status=no', 'resizable=yes', + 'toolbar=no', 'menubar=no', + 'scrollbars=yes']; + return window.open(url? url : 'about:blank', 'gdauth', features.join(',')); +}; /** * Authorizes the client, gets the userId and calls <open>. */ -DriveClient.prototype.authorize = function(immediate, success, error, remember) +DriveClient.prototype.authorize = function(immediate, success, error, remember, popup) { - try + var updateAuthInfo = mxUtils.bind(this, function (newAuthInfo, remember, forceUserUpdate) { - var userId = this.getUserId(); + this.token = newAuthInfo.access_token; + newAuthInfo.expires = Date.now() + parseInt(newAuthInfo.expires_in) * 1000; + newAuthInfo.remember = remember; + + this.resetTokenRefresh(newAuthInfo); + this.authCalled = true; + if (forceUserUpdate || this.user == null) + { + //IE/Edge security doesn't allow access to newAuthInfo in a callback function (outside this function scope) + //So, stringify the object and restore it (parse) in the callback + var strAuthInfo = JSON.stringify(newAuthInfo); + + this.updateUser(mxUtils.bind(this, function() + { + //Restore the auth info object to bypass IE/Edge security + var resAuthInfo = JSON.parse(strAuthInfo); + //Save user and new token + this.setPersistentToken(resAuthInfo, !remember); + + if (success != null) + { + success(); + } + }), error); + } + else if (success != null) + { + this.setPersistentToken(newAuthInfo, !remember); + success(); + } + }); + + try + { // Takes userId from state URL parameter if (this.ui.stateArg != null && this.ui.stateArg.userId != null) { - userId = this.ui.stateArg.userId; + this.userId = this.ui.stateArg.userId; + + if (this.user != null && this.user.id != this.userId) + { + this.user = null; + } } - // Immediate only possible with userId - if (immediate && userId == null) + //Retry request with refreshed token + var authInfo = JSON.parse(this.getPersistentToken(true)); + + if (authInfo != null) + { + if (this.userId == null) + { + if (authInfo.current != null) + { + this.userId = authInfo.current.userId; + authInfo = authInfo[this.userId]; + } + else + { + authInfo = null; + } + } + else + { + authInfo = authInfo[this.userId]; //If user id is new, authInfo will be null + } + } + + // Immediate only possible with a refresh token + if (immediate && (authInfo == null || authInfo.refresh_token == null)) { if (error != null) { @@ -481,58 +605,88 @@ DriveClient.prototype.authorize = function(immediate, success, error, remember) } else { - var params = + if (immediate) //Note, we checked refresh token is not null above { - scope: this.scopes, - client_id: this.clientId - }; - - if (immediate && userId != null) - { - params.immediate = true; - params.user_id = userId; + //state is used to identify which app is used + var req = new mxXmlRequest(this.redirectUri + '?state=appIndex%3D' + this.appIndex + '&refresh_token=' + authInfo.refresh_token, null, 'GET'); + + req.send(mxUtils.bind(this, function(req) + { + if (req.getStatus() >= 200 && req.getStatus() <= 299) + { + var newAuthInfo = JSON.parse(req.getText()); + newAuthInfo.refresh_token = authInfo.refresh_token; //Refresh token is not returned in the new auth info + + updateAuthInfo(newAuthInfo, true); //We set remember to true since we can only have a refresh token if user initially selected remember + } + else + { + this.logout(); + + if (error != null) + { + error(req); //TODO review this code path and how error is handled + } + } + }), error); } else { - params.immediate = false; - params.authuser = -1; - } - - gapi.auth.authorize(params, mxUtils.bind(this, function(resp) - { - try + var url = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + this.clientId + + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + + '&response_type=code&include_granted_scopes=true' + + (remember? '&access_type=offline&prompt=consent%20select_account' : '') + //Ask for consent again to get a new refresh token + '&scope=' + encodeURIComponent(this.scopes.join(' ')) + + '&state=appIndex%3D' + this.appIndex; //To identify which app is used + + if (popup == null) + { + popup = this.createAuthWin(url); + } + else { - // Updates the current user info - if (resp != null && resp.error == null) + popup.location = url; + } + + if (popup != null) + { + window.onGoogleDriveCallback = mxUtils.bind(this, function(newAuthInfo, authWindow) { - if (this.user == null || !immediate || this.user.id != userId) + window.onGoogleDriveCallback = null; + + try { - this.updateUser(success, error, remember); + if (newAuthInfo == null) + { + if (error != null) + { + error({message: mxResources.get('accessDenied')}); //TODO Check this error handling is correct + } + } + else + { + updateAuthInfo(newAuthInfo, remember, true); + } } - else if (success != null) + catch (e) { - success(); + if (error != null) + { + error(e); + } } - } - else if (error != null) - { - error(resp); - } - - this.resetTokenRefresh(resp); - } - catch (e) - { - if (error != null) - { - error(e); - } - else - { - throw e; - } + finally + { + if (authWindow != null) + { + authWindow.close(); + } + } + }); + + popup.focus(); } - })); + } } } catch (e) @@ -569,10 +723,10 @@ DriveClient.prototype.resetTokenRefresh = function(resp) { this.authorize(true, mxUtils.bind(this, function() { - //console.log('tokenRefresh: refreshed', gapi.auth.getToken()); + //console.log('tokenRefresh: refreshed', this.token); }), mxUtils.bind(this, function() { - //console.log('tokenRefresh: error refreshing', gapi.auth.getToken()); + //console.log('tokenRefresh: error refreshing', this.token); })); }), resp.expires_in * 900); } @@ -608,19 +762,18 @@ DriveClient.prototype.checkToken = function(fn) /** * Checks if the client is authorized and calls the next step. */ -DriveClient.prototype.updateUser = function(success, error, remember) +DriveClient.prototype.updateUser = function(success, error) { try { - var token = gapi.auth.getToken().access_token; - var url = 'https://www.googleapis.com/oauth2/v2/userinfo?alt=json&access_token=' + token; + var url = 'https://www.googleapis.com/oauth2/v2/userinfo?alt=json&access_token=' + this.token; this.ui.loadUrl(url, mxUtils.bind(this, function(data) { var info = JSON.parse(data); // Requests more information about the user (email address is sometimes not in info) - this.executeRequest(gapi.client.drive.about.get(), mxUtils.bind(this, function(resp) + this.executeRequest({url: '/about'}, mxUtils.bind(this, function(resp) { var email = mxResources.get('notAvailable'); var name = email; @@ -634,7 +787,7 @@ DriveClient.prototype.updateUser = function(success, error, remember) } this.setUser(new DrawioUser(info.id, email, name, pic, info.locale)); - this.setUserId(info.id, remember); + this.userId = info.id; if (success != null) { @@ -666,11 +819,12 @@ DriveClient.prototype.copyFile = function(id, title, success, error) { if (id != null && title != null) { - this.executeRequest(gapi.client.drive.files.copy({'fileId': id, - 'fields': this.allFields, 'supportsTeamDrives': true, - 'resource': {'title': title, 'properties': - [{'key': 'channel', 'value': Editor.guid()}]}}), - success, error); + this.executeRequest({url: '/files/' + id + '/copy?fields=' + encodeURIComponent(this.allFields) + + '&supportsTeamDrives=true', //&alt=json + method: 'POST', + params: {'title': title, 'properties': + [{'key': 'channel', 'value': Editor.guid()}]} + }, success, error); } }; @@ -712,13 +866,12 @@ DriveClient.prototype.moveFile = function(id, folderId, success, error) */ DriveClient.prototype.createDriveRequest = function(id, body) { - return gapi.client.request({ - 'path': '/drive/v2/files/' + id, + return { + 'url': '/files/' + id + '?uploadType=multipart&supportsTeamDrives=true', 'method': 'PUT', - 'params': {'uploadType' : 'multipart', 'supportsTeamDrives': true}, - 'headers': {'Content-Type': 'application/json; charset=UTF-8'}, - 'body': JSON.stringify(body) - }); + 'contentType': 'application/json; charset=UTF-8', + 'params': body + }; }; /** @@ -734,9 +887,9 @@ DriveClient.prototype.getLibrary = function(id, success, error) */ DriveClient.prototype.loadDescriptor = function(id, success, error, fields) { - this.executeRequest(gapi.client.drive.files.get({'fileId': id, - 'fields': (fields != null) ? fields : this.allFields, - 'supportsTeamDrives': true}), success, error); + this.executeRequest({ + url: '/files/' + id + '?supportsTeamDrives=true&fields=' + (fields != null ? fields : this.allFields) + }, success, error); }; /** @@ -775,8 +928,9 @@ DriveClient.prototype.getFile = function(id, success, error, readXml, readLibrar if (urlParams['rev'] != null) { - this.executeRequest(gapi.client.drive.revisions.get({'fileId': id, - 'revisionId': urlParams['rev'], 'supportsTeamDrives': true}), + this.executeRequest({ + url: '/files/' + id + '/revisions/' + urlParams['rev'] + '?supportsTeamDrives=true' + }, mxUtils.bind(this, function(resp) { // Redirects title to originalFilename to @@ -805,7 +959,7 @@ DriveClient.prototype.getFile = function(id, success, error, readXml, readLibrar if (/\.v(dx|sdx?)$/i.test(resp.title) || /\.gliffy$/i.test(resp.title) || (!this.ui.useCanvasForExport && binary)) { - var url = resp.downloadUrl + '&access_token=' + gapi.auth.getToken().access_token; + var url = resp.downloadUrl + '&access_token=' + this.token; this.ui.convertFile(url, resp.title, resp.mimeType, this.extension, success, error); } else @@ -818,14 +972,7 @@ DriveClient.prototype.getFile = function(id, success, error, readXml, readLibrar } else { - if (this.isGoogleRealtimeMimeType(resp.mimeType)) - { - this.convertRealtimeFile(resp, success, error); - } - else - { - this.getXmlFile(resp, success, error); - } + this.getXmlFile(resp, success, error); } } } @@ -858,52 +1005,6 @@ DriveClient.prototype.isGoogleRealtimeMimeType = function(mimeType) }; /** - * Checks if the client is authorized and calls the next step. - */ -DriveClient.prototype.getRealtimeData = function(id, success, error, retryCount) -{ - if (App.GOOGLE_REALTIME_EOL - Date.now() < 0) - { - error({message: 'Google Realtime API export endpoint no longer available'}); - } - else - { - this.executeRequest(gapi.client.drive.realtime.get({'fileId': id, - 'supportsTeamDrives': true}), mxUtils.bind(this, function(resp) - { - var json = (resp.result != null) ? resp.result.data : null; - - if (json != null && json.value != null && json.value.diagrams != null) - { - success(json); - } - else if (error != null) - { - error({message: 'realtime.get returned invalid data for ' + id}); - } - }), mxUtils.bind(this, function(resp) - { - if (retryCount == null) - { - retryCount = 0; - } - - if (retryCount < 3) - { - window.setTimeout(mxUtils.bind(this, function() - { - this.getRealtimeData(id, success, error, retryCount + 1); - }), (retryCount + 1) * 100); - } - else if (error != null) - { - error({message: 'realtime.get failed for ' + id}); - } - })); - } -}; - -/** * Checks if the client is authorized and calls the next step. The ignoreMime argument is * used for import via getFile. Default is false. The optional * readLibrary argument is used for reading libraries. Default is false. @@ -912,8 +1013,7 @@ DriveClient.prototype.getXmlFile = function(resp, success, error, ignoreMime, re { try { - var token = gapi.auth.getToken().access_token; - var url = resp.downloadUrl + '&access_token=' + token; + var url = resp.downloadUrl + '&access_token=' + this.token; // Loads XML to initialize realtime document if realtime is empty this.ui.loadUrl(url, mxUtils.bind(this, function(data) @@ -1290,21 +1390,17 @@ DriveClient.prototype.saveFile = function(file, revision, success, errFn, noChec if (prevDesc != null) { // Pins previous revision - this.executeRequest(gapi.client.drive.revisions.get( - { - 'fileId': prevDesc.id, - 'revisionId': prevDesc.headRevisionId, - 'supportsTeamDrives': true - }), mxUtils.bind(this, mxUtils.bind(this, function(resp) + this.executeRequest({ + url: '/files/' + prevDesc.id + '/revisions/' + prevDesc.headRevisionId + '?supportsTeamDrives=true' + }, mxUtils.bind(this, mxUtils.bind(this, function(resp) { resp.pinned = true; - this.executeRequest(gapi.client.drive.revisions.update( - { - 'fileId': prevDesc.id, - 'revisionId': prevDesc.headRevisionId, - 'resource': resp - })); + this.executeRequest({ + url: '/files/' + prevDesc.id + '/revisions/' + prevDesc.headRevisionId, + method: 'PUT', + params: resp + }); }))); // Logs conversion @@ -1412,9 +1508,10 @@ DriveClient.prototype.saveFile = function(file, revision, success, errFn, noChec { // Check for stale etag which can happen if a file is being saved or if // the etag simply isn't change but system still returns a 412 error (stale) - this.executeRequest(gapi.client.drive.files.get({'fileId': file.getId(), - 'fields': this.catchupFields, 'supportsTeamDrives': true}), - mxUtils.bind(this, function(resp) + this.executeRequest({ + url: '/files/' + file.getId() + '?supportsTeamDrives=true&fields=' + this.catchupFields + }, + mxUtils.bind(this, function(resp) { file.saveLevel = 7; @@ -1655,8 +1752,9 @@ DriveClient.prototype.verifyMimeType = function(fileId, fn, force, error) { this.checkingMimeType = true; - this.executeRequest(gapi.client.drive.files.get({'fileId': fileId, 'fields': 'mimeType', - 'supportsTeamDrives': true}), mxUtils.bind(this, function(resp) + this.executeRequest({ + url: '/files/' + fileId + '?supportsTeamDrives=true&fields=mimeType' + }, mxUtils.bind(this, function(resp) { this.checkingMimeType = false; @@ -1785,29 +1883,25 @@ DriveClient.prototype.createUploadRequest = function(id, metadata, data, revisio var reqObj = { - 'path': '/upload/drive/v2/files' + (id != null ? '/' + id : ''), + 'fullUrl': 'https://content.googleapis.com/upload/drive/v2/files' + (id != null ? '/' + id : '') + '?uploadType=multipart&supportsTeamDrives=true&fields=' + this.allFields, 'method': (id != null) ? 'PUT' : 'POST', - 'params': {'uploadType': 'multipart'}, 'headers': headers, - 'body': delim + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delim + + 'params': delim + 'Content-Type: application/json\r\n\r\n' + JSON.stringify(metadata) + delim + 'Content-Type: ' + ctype + '\r\n' + 'Content-Transfer-Encoding: base64\r\n' + '\r\n' + ((data != null) ? (binary) ? data : Base64.encode(data) : '') + close } if (!revision) { - reqObj.params['newRevision'] = false; + reqObj.url += '&newRevision=false'; } if (pinned) { - reqObj.params['pinned'] = true; + reqObj.url += '&pinned=true'; } - reqObj.params['supportsTeamDrives'] = true; - reqObj.params['fields'] = this.allFields; - - return gapi.client.request(reqObj); + return reqObj; }; /** @@ -1840,7 +1934,6 @@ DriveClient.prototype.pickFile = function(fn, acceptAllFiles) this.ui.spinner.stop(); // Reuses picker as long as token doesn't change. - var token = gapi.auth.getToken().access_token; var name = (acceptAllFiles) ? 'genericPicker' : 'filePicker'; // Click on background closes dialog as workaround for blocking dialog @@ -1855,7 +1948,7 @@ DriveClient.prototype.pickFile = function(fn, acceptAllFiles) } }); - if (this[name] == null || this[name + 'Token'] != token) + if (this[name] == null || this[name + 'Token'] != this.token) { // FIXME: Dispose not working // if (this[name] != null) @@ -1864,7 +1957,7 @@ DriveClient.prototype.pickFile = function(fn, acceptAllFiles) // this[name].dispose(); // } - this[name + 'Token'] = token; + this[name + 'Token'] = this.token; // Pseudo-hierarchical directory view, see // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ @@ -1905,6 +1998,7 @@ DriveClient.prototype.pickFile = function(fn, acceptAllFiles) .addView(view3) .addView(google.picker.ViewId.RECENTLY_PICKED) .addView(view4) +// .setOrigin(window.location.protocol + '//' + window.location.host) //TODO Still there is an error in console about incorrect origin!, it also causes the picker to hang (has a blocking empty iframe on top!) .setCallback(mxUtils.bind(this, function(data) { if (data.action == google.picker.Action.PICKED || @@ -1957,7 +2051,6 @@ DriveClient.prototype.pickFolder = function(fn, force) this.ui.spinner.stop(); // Reuses picker as long as token doesn't change. - var token = gapi.auth.getToken().access_token; var name = 'folderPicker'; // Click on background closes dialog as workaround for blocking dialog @@ -1972,7 +2065,7 @@ DriveClient.prototype.pickFolder = function(fn, force) } }); - if (this[name] == null || this[name + 'Token'] != token) + if (this[name] == null || this[name + 'Token'] != this.token) { // FIXME: Dispose not working // if (this[name] != null) @@ -1981,7 +2074,7 @@ DriveClient.prototype.pickFolder = function(fn, force) // this[name].dispose(); // } - this[name + 'Token'] = token; + this[name + 'Token'] = this.token; // Pseudo-hierarchical directory view, see // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ @@ -2013,6 +2106,7 @@ DriveClient.prototype.pickFolder = function(fn, force) .addView(view3) .addView(google.picker.ViewId.RECENTLY_PICKED) .setTitle(mxResources.get('pickFolder')) +// .setOrigin(window.location.protocol + '//' + window.location.host) //TODO Still there is an error in console about incorrect origin!, it also causes the picker to hang (has a blocking empty iframe on top!) .setCallback(mxUtils.bind(this, function(data) { if (data.action == google.picker.Action.PICKED || @@ -2102,9 +2196,8 @@ DriveClient.prototype.pickLibrary = function(fn) }); // Reuses picker as long as token doesn't change - var token = gapi.auth.getToken().access_token; - if (this.libraryPicker == null || this.libraryPickerToken != token) + if (this.libraryPicker == null || this.libraryPickerToken != this.token) { // FIXME: Dispose not working // if (this[name] != null) @@ -2113,7 +2206,7 @@ DriveClient.prototype.pickLibrary = function(fn) // this[name].dispose(); // } - this.libraryPickerToken = token; + this.libraryPickerToken = this.token; // Pseudo-hierarchical directory view, see // https://groups.google.com/forum/#!topic/google-picker-api/FSFcuJe7icQ @@ -2144,6 +2237,7 @@ DriveClient.prototype.pickLibrary = function(fn) .addView(view3) .addView(google.picker.ViewId.RECENTLY_PICKED) .addView(view4) +// .setOrigin(window.location.protocol + '//' + window.location.host) //TODO Still there is an error in console about incorrect origin!, it also causes the picker to hang (has a blocking empty iframe on top!) .setCallback(mxUtils.bind(this, function(data) { if (data.action == google.picker.Action.PICKED || @@ -2200,7 +2294,7 @@ DriveClient.prototype.showPermissions = function(id) try { var shareClient = new gapi.drive.share.ShareClient(this.appId); - shareClient.setOAuthToken(gapi.auth.getToken().access_token); + shareClient.setOAuthToken(this.token); shareClient.setItemIds([id]); shareClient.showSettingsDialog(); @@ -2262,624 +2356,35 @@ DriveClient.prototype.showPermissions = function(id) } }; -/** - * Converts the given file from realtime to XML. - */ -DriveClient.prototype.getRealtimeAge = function(desc, json) +DriveClient.prototype.clearPersistentToken = function() { - var mod = (json != null && json.value != null && json.value.modifiedDate != null) ? - json.value.modifiedDate.json : null; - var result = 0; - - if (mod != null && mod > 0) - { - var ts = new Date(desc.modifiedDate); - var rt = new Date(mod); - result = ts.getTime() - rt.getTime(); - } + //Since we have multiple accounts now, full deletion is not possible + var authInfo = JSON.parse(this.getPersistentToken(true)) || {}; - return result; -}; - -/** - * Converts the given file from realtime to XML. - */ -DriveClient.prototype.convertRealtimeFile = function(desc, success, error) -{ - var xmlSuccess = mxUtils.bind(this, function(file) - { - file.convertedFrom = 'xml'; - success(file); - }); + //Delete current user info + delete authInfo.current; + delete authInfo[this.userId]; - var jsonSuccess = mxUtils.bind(this, function(file) - { - file.convertedFrom = 'json'; - success(file); - }); - - this.getRealtimeData(desc.id, mxUtils.bind(this, function(json) + //Set the next user as current + for (var id in authInfo) { - try - { - var age = this.getRealtimeAge(desc, json); - - // Uses realtime if newer or less than 5 minutes old - if (age < 300000) - { - jsonSuccess(new DriveFile(this.ui, mxUtils.getXml( - this.convertJsonToXml(json)), desc)); - } - else - { - this.getXmlFile(desc, xmlSuccess, mxUtils.bind(this, function() - { - try - { - jsonSuccess(new DriveFile(this.ui, mxUtils.getXml( - this.convertJsonToXml(json)), desc)); - - } - catch (e) - { - this.getXmlFile(desc, xmlSuccess, error); - } - })); - } - } - catch (e) - { - this.getXmlFile(desc, xmlSuccess, error); - } - }), mxUtils.bind(this, function() - { - this.getXmlFile(desc, xmlSuccess, error); - })); -}; - -/** - * Returns the location as a new object. - */ -DriveClient.prototype.convertJsonToXml = function(json, uncompressed) -{ - if (json.value == null || json.value.diagrams == null) - { - throw Error('Invalid JSON: no diagrams in root map'); - } - else - { - var node = mxUtils.createXmlDocument().createElement('mxfile'); - var diagrams = json.value.diagrams.value; - - for (var i = 0; i < diagrams.length; i++) - { - try - { - var diagramNode = this.decodeJsonPage(diagrams[i].value, - node.ownerDocument.createElement('diagram'), - uncompressed); - - //if (diagramNode.getAttribute('name') == null) - //{ - // TODO: Should only use when converting but not when comparing - //diagramNode.setAttribute('name', mxResources.get('pageWithNumber', [i + 1])); - //} - - node.appendChild(diagramNode); - } - catch (e) - { - throw Error('Error on page ' + i + ': ' + e.stack); - } - } - - //console.log('leaving convertJson', mxUtils.getPrettyXml(node)); - - return node; - } -}; - -/** - * Returns true if copy, export and print are not allowed for this file. - */ -DriveClient.prototype.decodeJsonPage = function(json, node, uncompressed) -{ - if (json == null) - { - throw Error('Invalid JSON: json for page is null'); - } - else - { - var codec = new mxCodec(); - var root = this.createJsonCell(json.root, codec); - - if (root == null) - { - throw Error('Invalid JSON: no root cell for page'); - } - else - { - // Dummy model for encoding - var modelNode = codec.encode(new mxGraphModel(root)); - this.decodeJsonViewState(json, modelNode); - - if (uncompressed) - { - node.appendChild(modelNode); - } - else - { - mxUtils.setTextContent(node, Graph.compressNode(modelNode)); - } - - // Adds attributes to diagram node - if (json.id != null) - { - node.setAttribute('id', json.id.json); - } - else - { - // Workaround for missing page ID in JSON - node.setAttribute('id', Editor.guid()); - } - - if (json.name != null) - { - node.setAttribute('name', json.name.json); - } - } - } - - //console.log('decoded json page', json, node); - - return node; -}; - -/** - * Writes the view state to the given node. - */ -DriveClient.prototype.decodeJsonViewState = function(json, node) -{ - // Page format is stored as "width,height" - var pf = (json.pageFormat != null) ? json.pageFormat.json : null; - - if (pf != null && pf.length > 0) - { - var values = pf.split(','); - - if (values.length > 1) - { - node.setAttribute('pageWidth', values[0]); - node.setAttribute('pageHeight', values[1]); - } + authInfo.current = {userId: id, expires: 0}; //An expired token + break; } - - var bg = (json.backgroundColor != null) ? json.backgroundColor.json : null; - if (bg != null && bg.length > 0) - { - node.setAttribute('background', bg); - } - - var img = (json.backgroundImage != null) ? json.backgroundImage.json : null; - - if (img != null && img.length > 0) - { - node.setAttribute('backgroundImage', img); - } - - node.setAttribute('fold', (json.foldingEnabled != null) ? json.foldingEnabled.json : '0'); - node.setAttribute('pageScale', (json.pageScale != null) ? json.pageScale.json : mxGraph.prototype.pageScale); - node.setAttribute('math', (json.mathEnabled != null) ? json.mathEnabled.json : '0'); - node.setAttribute('shadow', (json.shadowVisible != null) ? json.shadowVisible.json : '0'); - - return node; + DrawioClient.prototype.setPersistentToken.call(this, JSON.stringify(authInfo)); }; -/** - * Syncs initial state from collab model to graph model. - */ -DriveClient.prototype.createJsonCell = function(json, codec) +DriveClient.prototype.setPersistentToken = function(userAuthInfo, sessionOnly) { - if (json != null && json.id != null) - { - var val = json.value; - var cell = this.jsonToCell(val, codec); - codec.putObject(json.id, cell); - - cell.source = (val.source != null) ? this.createJsonCell(val.source, codec) : null; - cell.target = (val.target != null) ? this.createJsonCell(val.target, codec) : null; - - // Cells can be serialized as parents of terminals - this.createJsonCell(val.parent, codec) - - for (var i = 0; i < val.children.value.length; i++) - { - var child = this.createJsonCell(val.children.value[i], codec); - - if (child != null) - { - cell.insert(child); - } - else - { - throw Error('Invalid JSON: no child ' + i + ' for cell ' + json.id); - } - } - - return cell; - } - else if (json != null && json.ref != null) - { - return codec.objects[json.ref]; - } - else - { - return null; - } -}; - -/** - * Adds the listener for automatically saving the diagram for local changes. - */ -DriveClient.prototype.jsonToCell = function(val, codec) -{ - var cell = new mxCell(); - - cell.id = val.cellId.json; - cell.vertex = val.type.json == 'vertex'; - cell.edge = val.type.json == 'edge'; - cell.connectable = val.connectable.json != '0'; - cell.collapsed = val.collapsed.json == '1'; - cell.visible = val.visible.json != '0'; - cell.style = (val.style != null) ? val.style.json : null; - cell.value = (val.xmlValue != null) ? - mxUtils.parseXml(val.xmlValue.json).documentElement : - ((val.value != null) ? val.value.json : null); - cell.geometry = (val.geometry != null) ? - codec.decode(mxUtils.parseXml(val.geometry.json).documentElement) : null; - - return cell; -}; - -/** - * Invokes the given function if the user has writeable realtime files - * that must be converted. - */ -DriveClient.prototype.checkRealtimeFiles = function(fn) -{ - var email = (this.user != null && this.user.email != null) ? this.user.email : null; - - this.executeRequest(gapi.client.drive.files.list({'maxResults': 1, 'q': - 'mimeType=\'application/vnd.jgraph.mxfile.realtime\'' + - ((email != null) ? ' and \'' + email + '\' in writers' : ''), - 'includeTeamDriveItems': true, 'supportsTeamDrives': true}), mxUtils.bind(this, function(res) - { - if (res != null && (res.nextPageToken != null || (res.items != null && res.items.length > 0))) - { - fn(); - } - })); -}; - -/** - * Converts all old realtime files. Invoke this using - * https://www.draw.io/?mode=google&convert-realtime=1 - */ -DriveClient.prototype.convertRealtimeFiles = function() -{ - var output = document.createElement('div'); - output.style.cssText = 'position:absolute;top:0px;left:0px;right:0px;bottom:0px;padding:8px;' + - 'background:#ffffff;z-index:2;overflow:auto;white-space:nowrap;line-height:1.5em;'; - document.body.appendChild(output); - var t0 = Date.now(); - - var print = mxUtils.bind(this, function(msg, noBr) - { - output.innerHTML += msg + ((noBr) ? '' : '<br>'); - output.scrollTop = output.scrollHeight; - }); - - if (App.GOOGLE_REALTIME_EOL - Date.now() < 0) - { - print('draw.io (' + EditorUi.VERSION + '): Google Realtime API export endpoint no longer available'); - } - else - { - print('draw.io (' + EditorUi.VERSION + ') is searching files to be converted...'); - print('<a href="https://desk.draw.io/support/solutions/articles/16000092210" target="_blank">Click here for help</a>'); - - if (this.ui.spinner.spin(document.body, 'Searching files...')) - { - this.checkToken(mxUtils.bind(this, function() - { - var convertDelay = 2000; - var convertedIds = {}; - var converted = 0; - var fromJson = 0; - var fromXml = 0; - var loadFail = 0; - var invalid = 0; - var saveFail = 0; - var failed = 0; - var total = 0; - var queryFail = 0; + var authInfo = JSON.parse(this.getPersistentToken(true)) || {}; - var email = (this.user != null && this.user.email != null) ? this.user.email : null; - var q = 'mimeType=\'application/vnd.jgraph.mxfile.realtime\'' + - ((email != null) ? ' and \'' + email + '\' in writers' : ''); - - var done = mxUtils.bind(this, function() - { - this.ui.spinner.stop(); - print('<br>Conversion complete. Successfully converted ' + converted + ' file(s).', true); - - if (failed > 0) - { - print(' Failed to convert ' + failed + ' file(s).<br><br><b>ACTION REQUIRED:</b><br><ul><li>Click ' + - '<a target="_blank" href="https://drive.google.com/drive/u/0/search?q=type:application/vnd.jgraph.mxfile.realtime">here</a> ' + - 'to list all affected files</li><li>Open each file in turn by right-clicking the file and selecting open with draw.io</li>' + - '<li>Open each file in turn. When loaded, select File->Save</li></ul>'); - } - else - { - print('<br><br>This window can now be closed.') - } - - try - { - var dt = Date.now() - t0; - - // Logs conversion - EditorUi.logEvent({category: 'AUTO-CONVERT', - action: 'total_' + total + '-done_' + converted + - '-fail_' + failed + '-xml_' + fromXml + '-json_' + fromJson + - '-load_' + loadFail + '-save_' + saveFail + - '-invalid_' + invalid + '-dt-' + Math.round(dt / 1000), - label: (this.user != null) ? ('user_' + this.user.id) : '-nouser'}); - } - catch (e) - { - // ignore - } - }); - - var getMessage = function(err) - { - return (err == null) ? '' : ((err.message != null) ? err.message : ((err.error != null && - err.error.message != null) ? err.error.message : '')); - }; - - var doConvert = mxUtils.bind(this, function() - { - if (this.ui.spinner.spin(document.body, 'Converting ' + total + ' file(s)')) - { - print('Found ' + total + ' file(s). This will take up to ' + Math.ceil((total * (convertDelay + 3000)) / 60000) + - ' minute(s). <b>Please do not close this window!</b><br>'); - var counter = 0; + userAuthInfo.userId = this.userId; + authInfo.current = userAuthInfo; + authInfo[this.userId] = { + refresh_token: userAuthInfo.refresh_token, + user: this.user + }; - // Does not show picker if there are no folders in the root - var nextPage = mxUtils.bind(this, function(token, delay) - { - var query = {'maxResults': 1, 'q': q, 'includeTeamDriveItems': true, 'supportsTeamDrives': true}; - - if (token != null) - { - query.pageToken = token; - } - - var acceptResponse = true; - - var timeoutThread = window.setTimeout(mxUtils.bind(this, function() - { - acceptResponse = false; - nextPage(token, delay); - }), this.ui.timeout); - - this.executeRequest(gapi.client.drive.files.list(query), mxUtils.bind(this, function(res) - { - window.clearTimeout(timeoutThread); - - if (acceptResponse) - { - var doNextPage = mxUtils.bind(this, function() - { - if (res.nextPageToken != null) - { - nextPage(res.nextPageToken); - } - else - { - done(); - } - }); - - if (res != null && res.error != null) - { - queryFail++; - - if (queryFail < 4) - { - nextPage(token, delay); - } - else - { - this.ui.spinner.stop(); - print('Query for next file failed multiple times. Exiting.<br><br>This window can now be closed.'); - } - } - else if (res != null && (res.items == null || res.items.length == 0) && - res.nextPageToken != null) - { - // Next page can still contain results, see - // https://stackoverflow.com/questions/23741845 - nextPage(res.nextPageToken, 10000); - } - else if (res != null && res.items != null && res.items.length > 0) - { - var fileId = res.items[0].id; - this.ui.spinner.stop(); - counter++; - - if (this.ui.spinner.spin(document.body, 'Converting file ' + counter + ' of ' + total)) - { - print('Converting ' + counter + ' of ' + total + ': "' + mxUtils.htmlEntities(res.items[0].title) + - '" (<a href="https://drive.google.com/open?id=' + fileId + '" target="_blank">' + fileId + '</a>)... ', true); - - window.setTimeout(mxUtils.bind(this, function() - { - // Exits if same file is returned twice - if (convertedIds[fileId] == null) - { - convertedIds[fileId] = true; - - acceptResponse = true; - - timeoutThread = window.setTimeout(mxUtils.bind(this, function() - { - acceptResponse = false; - - failed++; - loadFail++; - print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> Timeout'); - doNextPage(); - }), this.ui.timeout); - - this.getFile(fileId, mxUtils.bind(this, function(file) - { - window.clearTimeout(timeoutThread); - - if (acceptResponse) - { - if (file.constructor == DriveFile) - { - if (file.convertedFrom == 'json') - { - fromJson++; - } - else - { - fromXml++; - } - - acceptResponse = true; - - timeoutThread = window.setTimeout(mxUtils.bind(this, function() - { - acceptResponse = false; - - failed++; - saveFail++; - print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> Timeout'); - doNextPage(); - }), this.ui.timeout); - - this.saveFile(file, null, mxUtils.bind(this, function() - { - window.clearTimeout(timeoutThread); - - if (acceptResponse) - { - converted++; - print('OK <img src="' + Editor.checkmarkImage + '" border="0" valign="middle"/>'); - doNextPage(); - } - }), mxUtils.bind(this, function(err) - { - window.clearTimeout(timeoutThread); - - if (acceptResponse) - { - var msg = getMessage(err); - failed++; - saveFail++; - print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> ' + msg); - doNextPage(); - } - })); - } - else - { - failed++; - invalid++; - print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> Invalid file'); - doNextPage(); - } - } - }), mxUtils.bind(this, function(err) - { - window.clearTimeout(timeoutThread); - - if (acceptResponse) - { - var msg = getMessage(err); - failed++; - loadFail++; - print('<img src="' + this.ui.editor.graph.warningImage.src + '" border="0" valign="absmiddle"/> ' + msg); - doNextPage(); - } - })); - } - else - { - this.ui.spinner.stop(); - print('Search returned duplicate file ' + fileId + '. Exiting.<br><br>This window can now be closed.'); - } - }), (delay != null) ? delay : convertDelay) - } - } - else - { - done(); - } - } - })); - }); - - nextPage(); - } - }); - - var totals = {'maxResults': 10000, 'q': q, 'includeTeamDriveItems': true, 'supportsTeamDrives': true}; - - var count = mxUtils.bind(this, function(token) - { - if (token != null) - { - totals.pageToken = token; - } - - this.executeRequest(gapi.client.drive.files.list(totals), mxUtils.bind(this, function(res) - { - total += (res != null && res.items != null) ? res.items.length : 0; - - if (res.nextPageToken != null) - { - count(res.nextPageToken); - } - else - { - this.ui.spinner.stop(); - - this.ui.showError('Confirm', 'You are about to convert ' + total + ' file(s)', - 'Cancel', mxUtils.bind(this, function() - { - print('Cancelled by user.<br><br>This window can now be closed.'); - }), null, 'OK', doConvert, 'Help', function() - { - window.open('https://desk.draw.io/support/solutions/articles/16000092210'); - }, 340, 120); - } - })); - }); - - count(); - })); - } - else - { - this.ui.spinner.stop(); - print('Busy. <br><br>This window can now be closed.'); - } - } + DrawioClient.prototype.setPersistentToken.call(this, JSON.stringify(authInfo), sessionOnly); }; diff --git a/src/main/webapp/js/diagramly/DriveComment.js b/src/main/webapp/js/diagramly/DriveComment.js index 829d98f6..c716eef1 100644 --- a/src/main/webapp/js/diagramly/DriveComment.js +++ b/src/main/webapp/js/diagramly/DriveComment.js @@ -20,12 +20,12 @@ DriveComment.prototype.addReply = function(reply, success, error, doResolve, doR body.verb = 'reopen'; } - this.file.ui.drive.executeRequest(gapi.client.drive.replies.insert( + this.file.ui.drive.executeRequest( { - 'fileId': this.file.getId(), - 'commentId': this.id, - 'resource': body - }), + url: '/files/' + this.file.getId() + '/comments/' + this.id + '/replies', + params: body, + method: 'POST' + }, mxUtils.bind(this, function(resp) { success(resp.replyId); //pass comment id @@ -39,17 +39,16 @@ DriveComment.prototype.editComment = function(newContent, success, error) this.file.ui.drive.executeRequest( this.pCommentId? - gapi.client.drive.replies.patch({ - 'fileId': this.file.getId(), - 'commentId': this.pCommentId, - 'replyId': this.id, - 'resource': body - }) : - gapi.client.drive.comments.patch({ - 'fileId': this.file.getId(), - 'commentId': this.id, - 'resource': body - }), + { + url: '/files/' + this.file.getId() + '/comments/' + this.pCommentId + '/replies/' + this.id, + params: body, + method: 'PATCH' + } : + { + url: '/files/' + this.file.getId() + '/comments/' + this.id, + params: body, + method: 'PATCH' + }, success, error); }; @@ -57,14 +56,13 @@ DriveComment.prototype.deleteComment = function(success, error) { this.file.ui.drive.executeRequest( this.pCommentId? - gapi.client.drive.replies.delete({ - 'fileId': this.file.getId(), - 'commentId': this.pCommentId, - 'replyId': this.id - }): - gapi.client.drive.comments.delete({ - 'fileId': this.file.getId(), - 'commentId': this.id - }), + { + url: '/files/' + this.file.getId() + '/comments/' + this.pCommentId + '/replies/' + this.id, + method: 'DELETE' + }: + { + url: '/files/' + this.file.getId() + '/comments/' + this.id, + method: 'DELETE' + }, success, error); }; diff --git a/src/main/webapp/js/diagramly/DriveFile.js b/src/main/webapp/js/diagramly/DriveFile.js index 1392681f..cf89e1f3 100644 --- a/src/main/webapp/js/diagramly/DriveFile.js +++ b/src/main/webapp/js/diagramly/DriveFile.js @@ -71,10 +71,10 @@ DriveFile.prototype.getMode = function() */ DriveFile.prototype.getPublicUrl = function(fn) { - gapi.client.drive.permissions.list( - { - 'fileId': this.desc.id - }).execute(mxUtils.bind(this, function(resp) + this.ui.drive.executeRequest({ + url: '/files/' + this.desc.id + '/permissions' + }, + mxUtils.bind(this, function(resp) { if (resp != null && resp.items != null) { @@ -536,8 +536,11 @@ DriveFile.prototype.isRevisionHistorySupported = function() */ DriveFile.prototype.getRevisions = function(success, error) { - this.ui.drive.executeRequest(gapi.client.drive.revisions.list({'fileId': this.getId()}), - mxUtils.bind(this, function(resp) + this.ui.drive.executeRequest( + { + url: '/files/' + this.getId() + '/revisions' + }, + mxUtils.bind(this, function(resp) { for (var i = 0; i < resp.items.length; i++) { @@ -652,9 +655,11 @@ DriveFile.prototype.setDescriptorEtag = function(desc, etag) */ DriveFile.prototype.loadPatchDescriptor = function(success, error) { - this.ui.drive.executeRequest(gapi.client.drive.files.get({'fileId': this.getId(), - 'fields': this.ui.drive.catchupFields, 'supportsTeamDrives': true}), - mxUtils.bind(this, function(desc) + this.ui.drive.executeRequest( + { + url: '/files/' + this.getId() + '?supportsTeamDrives=true&fields=' + this.ui.drive.catchupFields + }, + mxUtils.bind(this, function(desc) { success(desc); }), error); @@ -712,8 +717,11 @@ DriveFile.prototype.getComments = function(success, error) return comment; }; - this.ui.drive.executeRequest(gapi.client.drive.comments.list({'fileId': this.getId()}), - mxUtils.bind(this, function(resp) + this.ui.drive.executeRequest( + { + url: '/files/' + this.getId() + '/comments' + }, + mxUtils.bind(this, function(resp) { var comments = []; @@ -735,8 +743,13 @@ DriveFile.prototype.addComment = function(comment, success, error) { var body = {'content': comment.content}; - this.ui.drive.executeRequest(gapi.client.drive.comments.insert({'fileId': this.getId(), 'resource': body}), - mxUtils.bind(this, function(resp) + this.ui.drive.executeRequest( + { + url: '/files/' + this.getId() + '/comments', + method: 'POST', + params: body + }, + mxUtils.bind(this, function(resp) { success(resp.commentId); //pass comment id }), error); diff --git a/src/main/webapp/js/diagramly/EditorUi.js b/src/main/webapp/js/diagramly/EditorUi.js index 5d92b17e..648d5d36 100644 --- a/src/main/webapp/js/diagramly/EditorUi.js +++ b/src/main/webapp/js/diagramly/EditorUi.js @@ -14,6 +14,17 @@ */ EditorUi.compactUi = uiTheme != 'atlas'; + + /** + * Overrides default grid color for dark mode + */ + mxGraphView.prototype.defaultDarkGridColor = '#6e6e6e'; + + if (uiTheme == 'dark') + { + mxGraphView.prototype.gridColor = mxGraphView.prototype.defaultDarkGridColor; + } + /** * Switch to disable logging for mode and search terms. */ @@ -3660,14 +3671,77 @@ this.handleError(resp, title, fn, invokeFnOnClose, notFoundMessage) }), retry, mxResources.get('changeUser'), mxUtils.bind(this, function() { - if (this.spinner.spin(document.body, mxResources.get('loading'))) + var driveUsers = this.drive.getUsersList(); + + var div = document.createElement('div'); + + var title = document.createElement('span'); + title.style.marginTop = '6px'; + mxUtils.write(title, mxResources.get('changeUser') + ': '); + + div.appendChild(title); + + var usersSelect = document.createElement('select'); + usersSelect.style.width = '200px'; + + //TODO This code is similar to Dialogs.js change user part in SplashDialog + function fillUsersSelect() { - this.drive.clearUserId(); - gapi.auth.signOut(); + usersSelect.innerHTML = ''; + + for (var i = 0; i < driveUsers.length; i++) + { + var option = document.createElement('option'); + mxUtils.write(option, driveUsers[i].displayName); + option.value = i; + usersSelect.appendChild(option); + //More info (email) about the user in a disabled option + option = document.createElement('option'); + option.innerHTML = ' '; + mxUtils.write(option, '<' + driveUsers[i].email + '>'); + option.setAttribute('disabled', 'disabled'); + usersSelect.appendChild(option); + } - // Reload page to reset client auth - window.location.reload(); + //Add account option + var option = document.createElement('option'); + mxUtils.write(option, mxResources.get('addAccount')); + option.value = driveUsers.length; + usersSelect.appendChild(option); } + + fillUsersSelect(); + + mxEvent.addListener(usersSelect, 'change', mxUtils.bind(this, function() + { + var userIndex = usersSelect.value; + var existingAccount = driveUsers.length != userIndex; + + if (existingAccount) + { + this.drive.setUser(driveUsers[userIndex]); + } + + this.drive.authorize(existingAccount, mxUtils.bind(this, function() + { + if (!existingAccount) + { + driveUsers = this.drive.getUsersList(); + fillUsersSelect(); + } + }), mxUtils.bind(this, function(resp) + { + this.handleError(resp); + }), true); + })); + + div.appendChild(usersSelect); + + var dlg = new CustomDialog(this, div, mxUtils.bind(this, function() + { + this.loadFile(window.location.hash.substr(1), true); + })); + this.showDialog(dlg.container, 300, 75, true, true); }), mxResources.get('cancel'), mxUtils.bind(this, function() { window.location.hash = ''; @@ -9497,6 +9571,30 @@ // Gets recent colors from settings ColorDialog.recentColors = mxSettings.getRecentColors(); + // Avoids overridden values for changes in + // multiple windows and updates shared values + if (isLocalStorage) + { + try + { + window.addEventListener('storage', mxUtils.bind(this, function(evt) + { + if (evt.key == mxSettings.key) + { + mxSettings.load(); + + // Updates values + ColorDialog.recentColors = mxSettings.getRecentColors(); + this.menus.customFonts = mxSettings.getCustomFonts(); + } + }), false); + } + catch (e) + { + // ignore + } + } + // Updates UI to reflect current edge style this.fireEvent(new mxEventObject('styleChanged', 'keys', [], 'values', [], 'cells', [])); @@ -9537,11 +9635,13 @@ /** * Persists default grid color. */ - this.editor.graph.view.gridColor = mxSettings.getGridColor(); + this.editor.graph.view.gridColor = mxSettings.getGridColor(uiTheme == 'dark'); this.addListener('gridColorChanged', mxUtils.bind(this, function(sender, evt) { - mxSettings.setGridColor(this.editor.graph.view.gridColor); + console.log('gridColorChanged', this.editor.graph.view.gridColor); + + mxSettings.setGridColor(this.editor.graph.view.gridColor, uiTheme == 'dark'); mxSettings.save(); })); diff --git a/src/main/webapp/js/diagramly/GitHubClient.js b/src/main/webapp/js/diagramly/GitHubClient.js index 55230735..65fbaade 100644 --- a/src/main/webapp/js/diagramly/GitHubClient.js +++ b/src/main/webapp/js/diagramly/GitHubClient.js @@ -769,7 +769,7 @@ GitHubClient.prototype.showGitHubDialog = function(showFiles, fn) var content = document.createElement('div'); content.style.whiteSpace = 'nowrap'; content.style.overflow = 'hidden'; - content.style.height = '224px'; + content.style.height = '304px'; var hd = document.createElement('h3'); mxUtils.write(hd, mxResources.get((showFiles) ? 'selectFile' : 'selectFolder')); @@ -783,7 +783,7 @@ GitHubClient.prototype.showGitHubDialog = function(showFiles, fn) div.style.padding = '4px'; div.style.overflow = 'auto'; div.style.lineHeight = '1.2em'; - div.style.height = '194px'; + div.style.height = '274px'; content.appendChild(div); var listItem = document.createElement('div'); @@ -797,7 +797,7 @@ GitHubClient.prototype.showGitHubDialog = function(showFiles, fn) { fn(org + '/' + repo + '/' + encodeURIComponent(ref) + '/' + path); })); - this.ui.showDialog(dlg.container, 340, 270, true, true); + this.ui.showDialog(dlg.container, 420, 360, true, true); if (showFiles) { @@ -808,9 +808,10 @@ GitHubClient.prototype.showGitHubDialog = function(showFiles, fn) { var link = document.createElement('a'); link.setAttribute('href', 'javascript:void(0);'); + link.setAttribute('title', label); mxUtils.write(link, label); mxEvent.addListener(link, 'click', fn); - + if (padding != null) { var temp = listItem.cloneNode(); diff --git a/src/main/webapp/js/diagramly/GitLabClient.js b/src/main/webapp/js/diagramly/GitLabClient.js index 3462b14f..b25fd180 100644 --- a/src/main/webapp/js/diagramly/GitLabClient.js +++ b/src/main/webapp/js/diagramly/GitLabClient.js @@ -649,7 +649,7 @@ GitLabClient.prototype.showGitLabDialog = function(showFiles, fn) var content = document.createElement('div'); content.style.whiteSpace = 'nowrap'; content.style.overflow = 'hidden'; - content.style.height = '224px'; + content.style.height = '304px'; var hd = document.createElement('h3'); mxUtils.write(hd, mxResources.get((showFiles) ? 'selectFile' : 'selectFolder')); @@ -663,7 +663,7 @@ GitLabClient.prototype.showGitLabDialog = function(showFiles, fn) div.style.padding = '4px'; div.style.overflow = 'auto'; div.style.lineHeight = '1.2em'; - div.style.height = '194px'; + div.style.height = '274px'; content.appendChild(div); var listItem = document.createElement('div'); @@ -677,7 +677,7 @@ GitLabClient.prototype.showGitLabDialog = function(showFiles, fn) { fn(org + '/' + repo + '/' + encodeURIComponent(ref) + '/' + path); })); - this.ui.showDialog(dlg.container, 340, 270, true, true); + this.ui.showDialog(dlg.container, 420, 360, true, true); if (showFiles) { @@ -688,6 +688,7 @@ GitLabClient.prototype.showGitLabDialog = function(showFiles, fn) { var link = document.createElement('a'); link.setAttribute('href', 'javascript:void(0);'); + link.setAttribute('title', label); mxUtils.write(link, label); mxEvent.addListener(link, 'click', fn); diff --git a/src/main/webapp/js/diagramly/Init.js b/src/main/webapp/js/diagramly/Init.js index cb600afe..990e2ff1 100644 --- a/src/main/webapp/js/diagramly/Init.js +++ b/src/main/webapp/js/diagramly/Init.js @@ -309,6 +309,7 @@ if (urlParams['offline'] == '1' || urlParams['demo'] == '1' || urlParams['stealt urlParams['db'] = '0'; urlParams['od'] = '0'; urlParams['gh'] = '0'; + urlParams['gl'] = '0'; urlParams['tr'] = '0'; } diff --git a/src/main/webapp/js/diagramly/Menus.js b/src/main/webapp/js/diagramly/Menus.js index bdc8cad5..61cfdb30 100644 --- a/src/main/webapp/js/diagramly/Menus.js +++ b/src/main/webapp/js/diagramly/Menus.js @@ -1347,8 +1347,7 @@ req.setRequestHeaders = function(request) { mxXmlRequest.prototype.setRequestHeaders.apply(this, arguments); - var token = gapi.auth.getToken().access_token; - request.setRequestHeader('authorization', 'Bearer ' + token); + request.setRequestHeader('authorization', 'Bearer ' + editorUi.drive.token); }; req.send(function(req) diff --git a/src/main/webapp/js/diagramly/Pages.js b/src/main/webapp/js/diagramly/Pages.js index 42f05137..4e24b7c2 100644 --- a/src/main/webapp/js/diagramly/Pages.js +++ b/src/main/webapp/js/diagramly/Pages.js @@ -500,7 +500,7 @@ Graph.prototype.createViewState = function(node) return { gridEnabled: node.getAttribute('grid') != '0', - //gridColor: node.getAttribute('gridColor') || mxSettings.getGridColor(), + //gridColor: node.getAttribute('gridColor') || mxSettings.getGridColor(uiTheme == 'dark'), gridSize: parseFloat(node.getAttribute('gridSize')) || mxGraph.prototype.gridSize, guidesEnabled: node.getAttribute('guides') != '0', foldingEnabled: node.getAttribute('fold') != '0', diff --git a/src/main/webapp/js/diagramly/Settings.js b/src/main/webapp/js/diagramly/Settings.js index d2cd2e50..e834b888 100644 --- a/src/main/webapp/js/diagramly/Settings.js +++ b/src/main/webapp/js/diagramly/Settings.js @@ -41,13 +41,20 @@ var mxSettings = { mxSettings.settings.showStartScreen = showStartScreen; }, - getGridColor: function() + getGridColor: function(darkMode) { - return mxSettings.settings.gridColor; + return (darkMode) ? mxSettings.settings.darkGridColor : mxSettings.settings.gridColor; }, - setGridColor: function(gridColor) + setGridColor: function(gridColor, darkMode) { - mxSettings.settings.gridColor = gridColor; + if (darkMode) + { + mxSettings.settings.darkGridColor = gridColor; + } + else + { + mxSettings.settings.gridColor = gridColor; + } }, getAutosave: function() { @@ -192,7 +199,8 @@ var mxSettings = pageFormat: mxGraph.prototype.pageFormat, search: true, showStartScreen: true, - gridColor: mxGraphView.prototype.gridColor, + gridColor: mxGraphView.prototype.defaultGridColor, + darkGridColor: mxGraphView.prototype.defaultDarkGridColor, autosave: true, resizeImages: null, openCounter: 0, @@ -308,7 +316,12 @@ var mxSettings = if (mxSettings.settings.gridColor == null) { - mxSettings.settings.gridColor = mxGraphView.prototype.gridColor; + mxSettings.settings.gridColor = mxGraphView.prototype.defaultGridColor; + } + + if (mxSettings.settings.darkGridColor == null) + { + mxSettings.settings.darkGridColor = mxGraphView.prototype.defaultDarkGridColor; } if (mxSettings.settings.autosave == null) diff --git a/src/main/webapp/resources/dia.txt b/src/main/webapp/resources/dia.txt index 72824bf8..a0cf7a7a 100644 --- a/src/main/webapp/resources/dia.txt +++ b/src/main/webapp/resources/dia.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_am.txt b/src/main/webapp/resources/dia_am.txt index ccd93eae..962470a9 100644 --- a/src/main/webapp/resources/dia_am.txt +++ b/src/main/webapp/resources/dia_am.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_ar.txt b/src/main/webapp/resources/dia_ar.txt index 1257410f..189037f5 100644 --- a/src/main/webapp/resources/dia_ar.txt +++ b/src/main/webapp/resources/dia_ar.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=الحجم الحقيقي add=إضافة +addAccount=Add account addedFile=Added {1} addImages=إضافة صور addImageUrl=إضافة رابط صورة diff --git a/src/main/webapp/resources/dia_bg.txt b/src/main/webapp/resources/dia_bg.txt index 5ea389e3..d8d6ee29 100644 --- a/src/main/webapp/resources/dia_bg.txt +++ b/src/main/webapp/resources/dia_bg.txt @@ -5,6 +5,7 @@ accessDenied=Достъпът отказан action=Action actualSize=Действителен размер add=Добавяне +addAccount=Add account addedFile=Добавен {1} addImages=Добавяне на изображения addImageUrl=Добавяне на URL на изображението diff --git a/src/main/webapp/resources/dia_bn.txt b/src/main/webapp/resources/dia_bn.txt index 6f08a26c..91e3e050 100644 --- a/src/main/webapp/resources/dia_bn.txt +++ b/src/main/webapp/resources/dia_bn.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_bs.txt b/src/main/webapp/resources/dia_bs.txt index d7460cd9..67726a20 100644 --- a/src/main/webapp/resources/dia_bs.txt +++ b/src/main/webapp/resources/dia_bs.txt @@ -5,6 +5,7 @@ accessDenied=Pristup odbijen action=Action actualSize=Prirodna veličina add=Dodaj +addAccount=Add account addedFile=Dodato {1} addImages=Dodaj slike addImageUrl=Dodaj URL slike diff --git a/src/main/webapp/resources/dia_ca.txt b/src/main/webapp/resources/dia_ca.txt index 3707396c..d7c91294 100644 --- a/src/main/webapp/resources/dia_ca.txt +++ b/src/main/webapp/resources/dia_ca.txt @@ -5,6 +5,7 @@ accessDenied=Accés denegat action=Action actualSize=Mida real add=Afegeix +addAccount=Add account addedFile=Afegit {1} addImages=Afegeix imatges addImageUrl=Afegeix l'URL de la imatge diff --git a/src/main/webapp/resources/dia_cs.txt b/src/main/webapp/resources/dia_cs.txt index 29f7f699..1dc7ce95 100644 --- a/src/main/webapp/resources/dia_cs.txt +++ b/src/main/webapp/resources/dia_cs.txt @@ -5,6 +5,7 @@ accessDenied=Přístup odepřen action=Action actualSize=Skutečná velikost add=Přidat +addAccount=Add account addedFile=Přidáno: {1} addImages=Přidat obrázky addImageUrl=Přidat URL obrázku diff --git a/src/main/webapp/resources/dia_da.txt b/src/main/webapp/resources/dia_da.txt index 61ba4dc5..402f524a 100644 --- a/src/main/webapp/resources/dia_da.txt +++ b/src/main/webapp/resources/dia_da.txt @@ -5,6 +5,7 @@ accessDenied=Adgang nægtet action=Action actualSize=Faktisk størrelse add=Tilføj +addAccount=Add account addedFile=Tilføjet {1} addImages=Tilføj billeder addImageUrl=Tilføj billed-URL diff --git a/src/main/webapp/resources/dia_de.txt b/src/main/webapp/resources/dia_de.txt index f5691e9c..d9b0040d 100644 --- a/src/main/webapp/resources/dia_de.txt +++ b/src/main/webapp/resources/dia_de.txt @@ -5,6 +5,7 @@ accessDenied=Zugriff verweigert action=Aktion actualSize=Tatsächliche Größe add=Einfügen +addAccount=Konto hinzufügen addedFile={1} eingefügt addImages=Bilder hinzufügen addImageUrl=Bild URL hinzufügen diff --git a/src/main/webapp/resources/dia_el.txt b/src/main/webapp/resources/dia_el.txt index 23d9b6e7..758b91c0 100644 --- a/src/main/webapp/resources/dia_el.txt +++ b/src/main/webapp/resources/dia_el.txt @@ -5,6 +5,7 @@ accessDenied=Απαγόρευση εισόδου action=Action actualSize=Πραγματικό Μέγεθος add=Προσθήκη +addAccount=Add account addedFile=Προστέθηκε {1} addImages=Προσθήκη εικόνων addImageUrl=Προσθήκη εικόνας URL diff --git a/src/main/webapp/resources/dia_eo.txt b/src/main/webapp/resources/dia_eo.txt index 18726519..e35fff6e 100644 --- a/src/main/webapp/resources/dia_eo.txt +++ b/src/main/webapp/resources/dia_eo.txt @@ -5,6 +5,7 @@ accessDenied=Aliro rifuzita action=Ago actualSize=Vera grandeco add=Aldoni +addAccount=Add account addedFile={1} aldonita addImages=Aldoni bildojn addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_es.txt b/src/main/webapp/resources/dia_es.txt index fc873c11..cff12e4e 100644 --- a/src/main/webapp/resources/dia_es.txt +++ b/src/main/webapp/resources/dia_es.txt @@ -5,6 +5,7 @@ accessDenied=Acceso denegado action=Action actualSize=Tamaño real add=Agregar +addAccount=Add account addedFile=Agregado {1} addImages=Agregar imágenes addImageUrl=Añadir URL de la imagen diff --git a/src/main/webapp/resources/dia_et.txt b/src/main/webapp/resources/dia_et.txt index 3a447840..bc618988 100644 --- a/src/main/webapp/resources/dia_et.txt +++ b/src/main/webapp/resources/dia_et.txt @@ -5,6 +5,7 @@ accessDenied=Ligipääs keelatud action=Tegevus actualSize=Tegelik suurus add=Lisa +addAccount=Add account addedFile=Lisatud {1} addImages=Lisa pilte addImageUrl=Lisa pildi URL diff --git a/src/main/webapp/resources/dia_fa.txt b/src/main/webapp/resources/dia_fa.txt index 2aa0f17d..b7d16378 100644 --- a/src/main/webapp/resources/dia_fa.txt +++ b/src/main/webapp/resources/dia_fa.txt @@ -5,6 +5,7 @@ accessDenied=دسترسی مجاز نیست. action=Action actualSize=سایز واقعی add=افزودن +addAccount=Add account addedFile=اضافه شد {1} addImages=افزودن تصاویر addImageUrl=افزودن URL تصویر diff --git a/src/main/webapp/resources/dia_fi.txt b/src/main/webapp/resources/dia_fi.txt index b697b81d..b45c8657 100644 --- a/src/main/webapp/resources/dia_fi.txt +++ b/src/main/webapp/resources/dia_fi.txt @@ -5,6 +5,7 @@ accessDenied=Pääsy kielletty action=Action actualSize=Oikea koko add=Lisää +addAccount=Add account addedFile=Lisätty tiedosto addImages=Lisää kuvia addImageUrl=Lisää kuvan URL diff --git a/src/main/webapp/resources/dia_fil.txt b/src/main/webapp/resources/dia_fil.txt index 00dec396..3edfb3fe 100644 --- a/src/main/webapp/resources/dia_fil.txt +++ b/src/main/webapp/resources/dia_fil.txt @@ -5,6 +5,7 @@ accessDenied=Tinanggihan ang pagpasok action=Action actualSize=Aktwal na sukat add=Magdagdag +addAccount=Add account addedFile=Idinagdag addImages=Magdagdag ng mga imahe addImageUrl=Magdagdag ng URL ng imahe diff --git a/src/main/webapp/resources/dia_fr.txt b/src/main/webapp/resources/dia_fr.txt index 89a04e83..dfa2a357 100644 --- a/src/main/webapp/resources/dia_fr.txt +++ b/src/main/webapp/resources/dia_fr.txt @@ -5,6 +5,7 @@ accessDenied=Accès refusé action=Action actualSize=Taille réelle add=Ajouter +addAccount=Add account addedFile=Ajouté(e) {1} addImages=Ajouter des images addImageUrl=Ajouter l'URL d'une image diff --git a/src/main/webapp/resources/dia_gu.txt b/src/main/webapp/resources/dia_gu.txt index 904a1f01..15a9a3cd 100644 --- a/src/main/webapp/resources/dia_gu.txt +++ b/src/main/webapp/resources/dia_gu.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_he.txt b/src/main/webapp/resources/dia_he.txt index fc60e4c2..3536e802 100644 --- a/src/main/webapp/resources/dia_he.txt +++ b/src/main/webapp/resources/dia_he.txt @@ -5,6 +5,7 @@ accessDenied=הגישה דחתה action=Action actualSize=גודל אמיתי add=הוסף +addAccount=Add account addedFile=נוסף {1} addImages=הוסף תמונות addImageUrl=הוסף קישור לתמונה diff --git a/src/main/webapp/resources/dia_hi.txt b/src/main/webapp/resources/dia_hi.txt index 934596de..b01d8b1d 100644 --- a/src/main/webapp/resources/dia_hi.txt +++ b/src/main/webapp/resources/dia_hi.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_hr.txt b/src/main/webapp/resources/dia_hr.txt index 91d3315d..16cdffcb 100644 --- a/src/main/webapp/resources/dia_hr.txt +++ b/src/main/webapp/resources/dia_hr.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_hu.txt b/src/main/webapp/resources/dia_hu.txt index 421a028d..d062c2d4 100644 --- a/src/main/webapp/resources/dia_hu.txt +++ b/src/main/webapp/resources/dia_hu.txt @@ -5,6 +5,7 @@ accessDenied=Hozzáférés elutasítva action=Action actualSize=Aktuális méret add=Hozzáad +addAccount=Add account addedFile={1} hozzáadva addImages=Képet hozzáad addImageUrl=Kép URL hozzáadás diff --git a/src/main/webapp/resources/dia_i18n.txt b/src/main/webapp/resources/dia_i18n.txt index 5888c46e..31f557e4 100644 --- a/src/main/webapp/resources/dia_i18n.txt +++ b/src/main/webapp/resources/dia_i18n.txt @@ -5,6 +5,7 @@ accessDenied=accessDenied action=action actualSize=actualSize add=add +addAccount=addAccount addedFile=addedFile addImages=addImages addImageUrl=addImageUrl diff --git a/src/main/webapp/resources/dia_id.txt b/src/main/webapp/resources/dia_id.txt index 3d097afc..747d771c 100644 --- a/src/main/webapp/resources/dia_id.txt +++ b/src/main/webapp/resources/dia_id.txt @@ -5,6 +5,7 @@ accessDenied=Akses Ditolak action=Action actualSize=Ukuran Aktual add=Tambahkan +addAccount=Add account addedFile=Ditambahkan addImages=Tambahkan Gambar addImageUrl=Tambahkan URL Gambar diff --git a/src/main/webapp/resources/dia_it.txt b/src/main/webapp/resources/dia_it.txt index a4b97092..f8cda286 100644 --- a/src/main/webapp/resources/dia_it.txt +++ b/src/main/webapp/resources/dia_it.txt @@ -5,6 +5,7 @@ accessDenied=Accesso negato action=Action actualSize=Dimensioni attuali add=Aggiungi +addAccount=Add account addedFile=Aggiunto addImages=Aggiungi immagini addImageUrl=Aggiungi immagine da URL diff --git a/src/main/webapp/resources/dia_ja.txt b/src/main/webapp/resources/dia_ja.txt index d7bcc0d4..2110d098 100644 --- a/src/main/webapp/resources/dia_ja.txt +++ b/src/main/webapp/resources/dia_ja.txt @@ -5,6 +5,7 @@ accessDenied=アクセス拒否されました action=Action actualSize=実寸 add=追加する +addAccount=Add account addedFile=追加された {1} addImages=画像を追加する addImageUrl=画像のURLを追加する diff --git a/src/main/webapp/resources/dia_kn.txt b/src/main/webapp/resources/dia_kn.txt index aad20106..2fb85d9f 100644 --- a/src/main/webapp/resources/dia_kn.txt +++ b/src/main/webapp/resources/dia_kn.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_ko.txt b/src/main/webapp/resources/dia_ko.txt index 9c41c467..be19e2ef 100644 --- a/src/main/webapp/resources/dia_ko.txt +++ b/src/main/webapp/resources/dia_ko.txt @@ -5,6 +5,7 @@ accessDenied=접근거부 action=Action actualSize=실제 크기 add=추가 +addAccount=Add account addedFile=추가된 {1} addImages=이미지 추가 addImageUrl=이미지 URL 추가 diff --git a/src/main/webapp/resources/dia_lt.txt b/src/main/webapp/resources/dia_lt.txt index 6ea78045..88d53af4 100644 --- a/src/main/webapp/resources/dia_lt.txt +++ b/src/main/webapp/resources/dia_lt.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_lv.txt b/src/main/webapp/resources/dia_lv.txt index f6f49b8a..a6fde381 100644 --- a/src/main/webapp/resources/dia_lv.txt +++ b/src/main/webapp/resources/dia_lv.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_ml.txt b/src/main/webapp/resources/dia_ml.txt index 99f2ece5..f151e3c6 100644 --- a/src/main/webapp/resources/dia_ml.txt +++ b/src/main/webapp/resources/dia_ml.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_mr.txt b/src/main/webapp/resources/dia_mr.txt index 63715e18..d07f30cc 100644 --- a/src/main/webapp/resources/dia_mr.txt +++ b/src/main/webapp/resources/dia_mr.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_ms.txt b/src/main/webapp/resources/dia_ms.txt index a2c888a4..dd7809d5 100644 --- a/src/main/webapp/resources/dia_ms.txt +++ b/src/main/webapp/resources/dia_ms.txt @@ -5,6 +5,7 @@ accessDenied=Akses Ditolak action=Action actualSize=Saiz Sebenar add=Tambah +addAccount=Add account addedFile=Tambah {1} addImages=Tambah Imej addImageUrl=Tambah URL Imej diff --git a/src/main/webapp/resources/dia_nl.txt b/src/main/webapp/resources/dia_nl.txt index 4620386b..f580d090 100644 --- a/src/main/webapp/resources/dia_nl.txt +++ b/src/main/webapp/resources/dia_nl.txt @@ -5,6 +5,7 @@ accessDenied=Toegang geweigerd action=Actie actualSize=Ware grootte add=Toevoegen +addAccount=Add account addedFile={1} toegevoegd addImages=Afbeeldingen toevoegen addImageUrl=Afbeelding-URL toevoegen diff --git a/src/main/webapp/resources/dia_no.txt b/src/main/webapp/resources/dia_no.txt index e2989bb9..bfbd402d 100644 --- a/src/main/webapp/resources/dia_no.txt +++ b/src/main/webapp/resources/dia_no.txt @@ -5,6 +5,7 @@ accessDenied=Tilgang nektet action=Action actualSize=Faktisk størrelse add=Legg til +addAccount=Add account addedFile=Fil lagt til addImages=Legg til bilder addImageUrl=Legg til bilde-URL diff --git a/src/main/webapp/resources/dia_pl.txt b/src/main/webapp/resources/dia_pl.txt index d0cf1a37..758f9d1b 100644 --- a/src/main/webapp/resources/dia_pl.txt +++ b/src/main/webapp/resources/dia_pl.txt @@ -5,6 +5,7 @@ accessDenied=Brak dostępu action=Action actualSize=Rozmiar rzeczywisty add=Dodaj +addAccount=Add account addedFile=Dodano {1} addImages=Dodaj obrazy addImageUrl=Dodaj adres URL obrazu diff --git a/src/main/webapp/resources/dia_pt-br.txt b/src/main/webapp/resources/dia_pt-br.txt index 76da99c6..d492f36e 100644 --- a/src/main/webapp/resources/dia_pt-br.txt +++ b/src/main/webapp/resources/dia_pt-br.txt @@ -5,6 +5,7 @@ accessDenied=Acesso Negado action=Action actualSize=Tamanho real add=Adicionar +addAccount=Add account addedFile=Adicionado {1} addImages=Adicionar imagens addImageUrl=Adicionar URL da imagem diff --git a/src/main/webapp/resources/dia_pt.txt b/src/main/webapp/resources/dia_pt.txt index cad34c76..c1b217d4 100644 --- a/src/main/webapp/resources/dia_pt.txt +++ b/src/main/webapp/resources/dia_pt.txt @@ -5,6 +5,7 @@ accessDenied=Acesso Negado action=Action actualSize=Tamanho real add=Adicionar +addAccount=Add account addedFile=Adicionado {1} addImages=Adicionar imagens addImageUrl=Adicionar URL da imagem diff --git a/src/main/webapp/resources/dia_ro.txt b/src/main/webapp/resources/dia_ro.txt index 38e02fd3..5e97fc21 100644 --- a/src/main/webapp/resources/dia_ro.txt +++ b/src/main/webapp/resources/dia_ro.txt @@ -5,6 +5,7 @@ accessDenied=Acces interzis action=Action actualSize=Dimensiunea reală add=Adaugă +addAccount=Add account addedFile=Adăugat {1} addImages=Adaugă imagini addImageUrl=Adaugă imagine URL diff --git a/src/main/webapp/resources/dia_ru.txt b/src/main/webapp/resources/dia_ru.txt index d072017c..821736f3 100644 --- a/src/main/webapp/resources/dia_ru.txt +++ b/src/main/webapp/resources/dia_ru.txt @@ -5,6 +5,7 @@ accessDenied=Доступ запрещён action=Действие actualSize=Фактический размер add=Добавить +addAccount=Add account addedFile=Добавлено {1} addImages=Добавить изображения addImageUrl=Добавить ссылку на изображение diff --git a/src/main/webapp/resources/dia_sk.txt b/src/main/webapp/resources/dia_sk.txt index f957e670..dcb34886 100644 --- a/src/main/webapp/resources/dia_sk.txt +++ b/src/main/webapp/resources/dia_sk.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_sl.txt b/src/main/webapp/resources/dia_sl.txt index f8130682..a11ce0bb 100644 --- a/src/main/webapp/resources/dia_sl.txt +++ b/src/main/webapp/resources/dia_sl.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_sr.txt b/src/main/webapp/resources/dia_sr.txt index fcb60c6a..3f389c8f 100644 --- a/src/main/webapp/resources/dia_sr.txt +++ b/src/main/webapp/resources/dia_sr.txt @@ -5,6 +5,7 @@ accessDenied=Pristup je odbijen action=Action actualSize=Prirodna veličina add=Dodaj +addAccount=Add account addedFile=Dodat {1} addImages=Dodaj slike addImageUrl=Dodaj URL slike diff --git a/src/main/webapp/resources/dia_sv.txt b/src/main/webapp/resources/dia_sv.txt index d9647c67..be002aeb 100644 --- a/src/main/webapp/resources/dia_sv.txt +++ b/src/main/webapp/resources/dia_sv.txt @@ -5,6 +5,7 @@ accessDenied=Åtkomst nekad action=Action actualSize=Verklig storlek add=Lägg till +addAccount=Add account addedFile=Tillagd addImages=Lägg till bilder addImageUrl=Lägg till URL för bild diff --git a/src/main/webapp/resources/dia_sw.txt b/src/main/webapp/resources/dia_sw.txt index 19eb04d5..dbdb0458 100644 --- a/src/main/webapp/resources/dia_sw.txt +++ b/src/main/webapp/resources/dia_sw.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_ta.txt b/src/main/webapp/resources/dia_ta.txt index 762365b5..43a1e06b 100644 --- a/src/main/webapp/resources/dia_ta.txt +++ b/src/main/webapp/resources/dia_ta.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_te.txt b/src/main/webapp/resources/dia_te.txt index 9bcd028a..3d7a1cd0 100644 --- a/src/main/webapp/resources/dia_te.txt +++ b/src/main/webapp/resources/dia_te.txt @@ -5,6 +5,7 @@ accessDenied=Access Denied action=Action actualSize=Actual Size add=Add +addAccount=Add account addedFile=Added {1} addImages=Add Images addImageUrl=Add Image URL diff --git a/src/main/webapp/resources/dia_th.txt b/src/main/webapp/resources/dia_th.txt index b68de385..b4fc66dd 100644 --- a/src/main/webapp/resources/dia_th.txt +++ b/src/main/webapp/resources/dia_th.txt @@ -5,6 +5,7 @@ accessDenied=ปฏิเสธการเข้าถึง action=Action actualSize=ขนาดปกติ add=เพิ่ม +addAccount=Add account addedFile=เพิ่ม {1} แล้ว addImages=เพิ่มรูปภาพ addImageUrl=เพิ่ม URL รูปภาพ diff --git a/src/main/webapp/resources/dia_tr.txt b/src/main/webapp/resources/dia_tr.txt index cf772756..21530447 100644 --- a/src/main/webapp/resources/dia_tr.txt +++ b/src/main/webapp/resources/dia_tr.txt @@ -5,6 +5,7 @@ accessDenied=erişim engellendi action=Action actualSize=Gerçek boyut add=Ekle +addAccount=Add account addedFile=Eklendi addImages=Resim ekle addImageUrl=Resim URL'si ekle diff --git a/src/main/webapp/resources/dia_uk.txt b/src/main/webapp/resources/dia_uk.txt index a84ff2b7..9ec7672f 100644 --- a/src/main/webapp/resources/dia_uk.txt +++ b/src/main/webapp/resources/dia_uk.txt @@ -5,6 +5,7 @@ accessDenied=Доступ заборонено action=Action actualSize=Фактичний розмір add=Додати +addAccount=Add account addedFile=Додано {1} addImages=Додати малюнки addImageUrl=Додати URL-адресу малюнка diff --git a/src/main/webapp/resources/dia_vi.txt b/src/main/webapp/resources/dia_vi.txt index 8738ba74..a1074688 100644 --- a/src/main/webapp/resources/dia_vi.txt +++ b/src/main/webapp/resources/dia_vi.txt @@ -5,6 +5,7 @@ accessDenied=Truy cập bị từ chối action=Action actualSize=Kích thước thực tế add=Thêm +addAccount=Add account addedFile=Đã thêm {1} addImages=Thêm hình ảnh addImageUrl=Thêm đường dẫn hình ảnh diff --git a/src/main/webapp/resources/dia_zh-tw.txt b/src/main/webapp/resources/dia_zh-tw.txt index 5c370576..87afb1d8 100644 --- a/src/main/webapp/resources/dia_zh-tw.txt +++ b/src/main/webapp/resources/dia_zh-tw.txt @@ -5,6 +5,7 @@ accessDenied=拒絕存取 action=Action actualSize=實際尺寸 add=新增 +addAccount=Add account addedFile=已新增{1} addImages=新增圖片 addImageUrl=新增圖片URL diff --git a/src/main/webapp/resources/dia_zh.txt b/src/main/webapp/resources/dia_zh.txt index 8a960521..e2a004f8 100644 --- a/src/main/webapp/resources/dia_zh.txt +++ b/src/main/webapp/resources/dia_zh.txt @@ -5,6 +5,7 @@ accessDenied=访问被拒 action=Action actualSize=实际尺寸 add=添加 +addAccount=Add account addedFile=已添加 {1} addImages=添加图片 addImageUrl=添加图片链接 |