diff --git a/public/res/constants.js b/public/res/constants.js
index ece21fb4..6eb70f29 100644
--- a/public/res/constants.js
+++ b/public/res/constants.js
@@ -28,7 +28,7 @@ define([], function() {
constants.SSH_PROXY_URL = "https://stackedit-ssh-proxy.herokuapp.com/";
constants.HTMLTOPDF_URL = "https://stackedit-htmltopdf.herokuapp.com/";
- // Site dependent
+ // Site dependent
constants.BASE_URL = "http://localhost/";
constants.GOOGLE_CLIENT_ID = '241271498917-lev37kef013q85avc91am1gccg5g8lrb.apps.googleusercontent.com';
constants.GITHUB_CLIENT_ID = 'e47fef6055344579799d';
diff --git a/public/res/editor.js b/public/res/editor.js
index 454b4345..2510a7a4 100644
--- a/public/res/editor.js
+++ b/public/res/editor.js
@@ -175,12 +175,10 @@ define([
this.updateSelectionRange = function() {
var min = Math.min(this.selectionStart, this.selectionEnd);
var max = Math.max(this.selectionStart, this.selectionEnd);
- var range = this.createRange(min, max);
+ var range = this.createRange(min, max);
var selection = rangy.getSelection();
selection.removeAllRanges();
selection.addRange(range, this.selectionStart > this.selectionEnd);
- selection.detach();
- range.detach();
};
this.setSelectionStartEnd = function(start, end) {
if(start === undefined) {
@@ -232,13 +230,12 @@ define([
selectionEnd = offset + (range + '').length;
}
}
- selectionRange.detach();
}
- selection.detach();
self.setSelectionStartEnd(selectionStart, selectionEnd);
}
undoMgr.saveSelectionState();
}
+
var nextTickAdjustScroll = false;
var debouncedSave = utils.debounce(function() {
save();
@@ -353,7 +350,7 @@ define([
var range = selectionMgr.createRange(startOffset, textContent.length - endOffset);
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
- range.detach();
+ range.detach();
}
editor.setValue = setValue;
@@ -369,7 +366,7 @@ define([
}
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
- range.detach();
+ range.detach();
offset = offset - text.length + replacement.length;
selectionMgr.setSelectionStartEnd(offset, offset);
selectionMgr.updateSelectionRange();
@@ -759,9 +756,18 @@ define([
}, 0);
})
.on('mouseup', _.bind(selectionMgr.saveSelectionState, selectionMgr, true, false))
- .on('paste', function() {
+ .on('paste', function(evt) {
undoMgr.currentMode = 'paste';
adjustCursorPosition();
+ try {
+ var data = evt.originalEvent.clipboardData.getData("text/plain");
+ if(data) {
+ evt.preventDefault();
+ document.execCommand("insertHTML", false, data);
+ }
+ }
+ catch(e) {
+ }
})
.on('cut', function() {
undoMgr.currentMode = 'cut';
diff --git a/public/res/extensions/umlDiagrams.js b/public/res/extensions/umlDiagrams.js
index 177cc18f..ccf3150f 100644
--- a/public/res/extensions/umlDiagrams.js
+++ b/public/res/extensions/umlDiagrams.js
@@ -40,7 +40,9 @@ define([
});
preElt.parentNode.replaceChild(containerElt, preElt);
chart.drawSVG(containerElt, {
- 'line-width': 2
+ 'line-width': 2,
+ 'font-family': 'sans-serif',
+ 'font-weight': 'normal'
});
}
catch(e) {
diff --git a/public/res/helpers/googleHelper.js b/public/res/helpers/googleHelper.js
index f1d46ebf..c2f52df1 100644
--- a/public/res/helpers/googleHelper.js
+++ b/public/res/helpers/googleHelper.js
@@ -1,984 +1,981 @@
/*global gapi, google */
define([
- "underscore",
- "jquery",
- "constants",
- "core",
- "utils",
- "storage",
- "logger",
- "settings",
- "eventMgr",
- "classes/AsyncTask",
+ "underscore",
+ "jquery",
+ "constants",
+ "core",
+ "utils",
+ "storage",
+ "logger",
+ "settings",
+ "eventMgr",
+ "classes/AsyncTask"
], function(_, $, constants, core, utils, storage, logger, settings, eventMgr, AsyncTask) {
- var connected = false;
- var authorizationMgrMap = {};
- function AuthorizationMgr(accountId) {
- var permissionList = {
- profile: true
- };
- var refreshFlag = true;
- _.each((storage[accountId + '.permissions'] || '').split(';'), function(permission) {
- permission && (permissionList[permission] = true);
- });
- this.setRefreshFlag = function() {
- refreshFlag = true;
- };
- this.isAuthorized = function(permission) {
- return refreshFlag === false && _.has(permissionList, permission);
- };
- this.add = function(permission) {
- permissionList[permission] = true;
- storage[accountId + '.permissions'] = _.keys(permissionList).join(';');
- refreshFlag = false;
- };
- this.getListWithNew = function(permission) {
- var result = _.keys(permissionList);
- if(!_.has(permissionList, permission)) {
- result.push(permission);
- }
- return result;
- };
- var userId = storage[accountId + '.userId'];
- this.setUserId = function(value) {
- userId = value;
- storage[accountId + '.userId'] = userId;
- };
- this.getUserId = function() {
- return userId;
- };
- }
+ var connected = false;
+ var authorizationMgrMap = {};
- var googleHelper = {};
+ function AuthorizationMgr(accountId) {
+ var permissionList = {
+ profile: true
+ };
+ var refreshFlag = true;
+ _.each((storage[accountId + '.permissions'] || '').split(';'), function(permission) {
+ permission && (permissionList[permission] = true);
+ });
+ this.setRefreshFlag = function() {
+ refreshFlag = true;
+ };
+ this.isAuthorized = function(permission) {
+ return refreshFlag === false && _.has(permissionList, permission);
+ };
+ this.add = function(permission) {
+ permissionList[permission] = true;
+ storage[accountId + '.permissions'] = _.keys(permissionList).join(';');
+ refreshFlag = false;
+ };
+ this.getListWithNew = function(permission) {
+ var result = _.keys(permissionList);
+ if(!_.has(permissionList, permission)) {
+ result.push(permission);
+ }
+ return result;
+ };
+ var userId = storage[accountId + '.userId'];
+ this.setUserId = function(value) {
+ userId = value;
+ storage[accountId + '.userId'] = userId;
+ };
+ this.getUserId = function() {
+ return userId;
+ };
+ }
- // Listen to offline status changes
- var isOffline = false;
- eventMgr.addListener("onOfflineChanged", function(isOfflineParam) {
- isOffline = isOfflineParam;
- });
+ var googleHelper = {};
- // Try to connect by downloading client.js
- function connect(task) {
- task.onRun(function() {
- if(isOffline === true) {
- connected = false;
- task.error(new Error("Operation not available in offline mode.|stopPublish"));
- return;
- }
- if(connected === true) {
- task.chain();
- return;
- }
- window.delayedFunction = function() {
- gapi.load("client", function() {
- gapi.client.load('drive', 'v2', function() {
- connected = true;
- task.chain();
- });
- });
- };
- $.ajax({
- url: "https://apis.google.com/js/api.js?onload=runDelayedFunction",
- dataType: "script",
- timeout: constants.AJAX_TIMEOUT
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- handleError(error, task);
- });
- });
- }
+ // Listen to offline status changes
+ var isOffline = false;
+ eventMgr.addListener("onOfflineChanged", function(isOfflineParam) {
+ isOffline = isOfflineParam;
+ });
- // Try to authenticate with Oauth
- var scopeMap = {
- profile: [
- 'https://www.googleapis.com/auth/userinfo.profile'
- ],
- gdrive: [
- 'https://www.googleapis.com/auth/drive.install',
- settings.gdriveFullAccess === true ? 'https://www.googleapis.com/auth/drive' : 'https://www.googleapis.com/auth/drive.file'
- ],
- blogger: [
- 'https://www.googleapis.com/auth/blogger'
- ],
- picasa: [
- 'https://www.googleapis.com/auth/photos'
- ]
- };
- var oauthIframes = [];
- function authenticate(task, permission, accountId) {
- var authorizationMgr = authorizationMgrMap[accountId];
- if(!authorizationMgr) {
- authorizationMgr = new AuthorizationMgr(accountId);
- authorizationMgrMap[accountId] = authorizationMgr;
- }
- task.onRun(function() {
- var currentToken = gapi.auth.getToken();
- var newToken;
- function getTokenInfo() {
- $.ajax({
- url: 'https://www.googleapis.com/oauth2/v1/tokeninfo',
- data: {
- access_token: newToken.access_token
- },
- timeout: constants.AJAX_TIMEOUT,
- type: "GET"
- }).done(function(data) {
- if(authorizationMgr.getUserId() && authorizationMgr.getUserId() != data.user_id) {
- // Wrong user id, try again
- startAuthenticate();
- }
- else {
- authorizationMgr.setUserId(data.user_id);
- authorizationMgr.add(permission);
- authorizationMgr.token = newToken;
- task.chain();
- }
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- handleError(error, task);
- });
- }
- var authuser = 0;
- var immediate;
- function localAuthenticate() {
- if(authuser > 5) {
- task.error(new Error('Unable to authenticate user ' + authorizationMgr.getUserId() + ', please sign in with Google.'));
- return;
- }
- if(immediate === false) {
- task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT;
- }
- var scopeList = _.chain(scopeMap).pick(authorizationMgr.getListWithNew(permission)).flatten().value();
- gapi.auth.authorize({
- client_id: constants.GOOGLE_CLIENT_ID,
- scope: scopeList,
- immediate: immediate,
- authuser: immediate === false ? '' : authuser
- }, function(authResult) {
+ // Try to connect by downloading client.js
+ function connect(task) {
+ task.onRun(function() {
+ if(isOffline === true) {
+ connected = false;
+ return task.error(new Error("Operation not available in offline mode.|stopPublish"));
+ }
+ if(connected === true) {
+ return task.chain();
+ }
+ window.delayedFunction = function() {
+ gapi.load("client", function() {
+ gapi.client.load('drive', 'v2', function() {
+ connected = true;
+ task.chain();
+ });
+ });
+ };
+ $.ajax({
+ url: "https://apis.google.com/js/api.js?onload=runDelayedFunction",
+ dataType: "script",
+ timeout: constants.AJAX_TIMEOUT
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ handleError(error, task);
+ });
+ });
+ }
- // Hack to clean window from old oauth iframes
- authorizationMgr.$oauthIframe && authorizationMgr.$oauthIframe.remove();
- var currentOauthIframes = _.filter(document.querySelectorAll('iframe'), function(iframe) {
- var src = iframe.getAttribute('src');
- return src && src.indexOf('https://accounts.google.com/o/oauth2/auth') === 0;
- });
- authorizationMgr.$oauthIframe = $(_.difference(currentOauthIframes, oauthIframes));
- oauthIframes = currentOauthIframes;
+ // Try to authenticate with Oauth
+ var scopeMap = {
+ profile: [
+ 'https://www.googleapis.com/auth/userinfo.profile'
+ ],
+ gdrive: [
+ 'https://www.googleapis.com/auth/drive.install',
+ settings.gdriveFullAccess === true ? 'https://www.googleapis.com/auth/drive' : 'https://www.googleapis.com/auth/drive.file'
+ ],
+ blogger: [
+ 'https://www.googleapis.com/auth/blogger'
+ ],
+ picasa: [
+ 'https://www.googleapis.com/auth/photos'
+ ]
+ };
- newToken = gapi.auth.getToken();
- gapi.auth.setToken(currentToken);
- if(!authResult || authResult.error) {
- if(connected === true && immediate === true) {
- // If immediate did not work retry without immediate
- // flag
- immediate = false;
- task.chain(oauthRedirect);
- }
- else {
- // Error
- task.error(new Error("Access to Google account is not authorized."));
- }
- }
- else {
- // Success but we need to check the user id
- immediate === true && authuser++;
- task.chain(getTokenInfo);
- }
- });
- }
- function oauthRedirect() {
- if(immediate === true) {
- task.chain(localAuthenticate);
- return;
- }
- utils.redirectConfirm('You are being redirected to Google authorization page.', function() {
- task.chain(localAuthenticate);
- }, function() {
- task.error(new Error('Operation canceled.'));
- });
- }
- function startAuthenticate() {
- immediate = true;
- if(authorizationMgr.token && authorizationMgr.isAuthorized(permission)) {
- task.chain();
- return;
- }
- if(!authorizationMgr.getUserId()) {
- immediate = false;
- }
- task.chain(oauthRedirect);
- }
- startAuthenticate();
- });
- }
- googleHelper.refreshGdriveToken = function(accountId) {
- var task = new AsyncTask();
- connect(task);
- var authorizationMgr = authorizationMgrMap[accountId];
- authorizationMgr && authorizationMgr.setRefreshFlag();
- authenticate(task, 'gdrive', accountId);
- task.enqueue();
- };
+ function authenticate(task, permission, accountId) {
+ var authorizationMgr = authorizationMgrMap[accountId];
+ if(!authorizationMgr) {
+ authorizationMgr = new AuthorizationMgr(accountId);
+ authorizationMgrMap[accountId] = authorizationMgr;
+ }
+ task.onRun(function() {
+ var currentToken = gapi.auth.getToken();
+ var newToken;
- function runWithToken(accountId, functionToRun) {
- var currentToken = gapi.auth.getToken();
- var authorizationMgr = authorizationMgrMap[accountId];
- gapi.auth.setToken(authorizationMgr.token);
- functionToRun();
- gapi.auth.setToken(currentToken);
- }
+ function getTokenInfo() {
+ $.ajax({
+ url: 'https://www.googleapis.com/oauth2/v1/tokeninfo',
+ data: {
+ access_token: newToken.access_token
+ },
+ timeout: constants.AJAX_TIMEOUT,
+ type: "GET"
+ }).done(function(data) {
+ if(authorizationMgr.getUserId() && authorizationMgr.getUserId() != data.user_id) {
+ // Wrong user id, try again
+ startAuthenticate();
+ }
+ else {
+ authorizationMgr.setUserId(data.user_id);
+ authorizationMgr.add(permission);
+ authorizationMgr.token = newToken;
+ task.chain();
+ }
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ handleError(error, task);
+ });
+ }
- googleHelper.upload = function(fileId, parentId, title, content, contentType, etag, accountId, callback) {
- var result;
- var task = new AsyncTask();
- connect(task);
- authenticate(task, 'gdrive', accountId);
- task.onRun(function() {
- var boundary = '-------314159265358979323846';
- var delimiter = "\r\n--" + boundary + "\r\n";
- var close_delim = "\r\n--" + boundary + "--";
- contentType = contentType || settings.markdownMimeType;
- var metadata = {
- title: title,
- mimeType: contentType
- };
- if(parentId) {
- // Specify the directory
- metadata.parents = [
- {
- kind: 'drive#fileLink',
- id: parentId
- }
- ];
- }
- var path = '/upload/drive/v2/files';
- var method = 'POST';
- if(fileId) {
- // If it's an update
- path += "/" + fileId;
- method = 'PUT';
- }
- var headers = {
- 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"',
- };
+ var authuser = 0;
+ var immediate;
- var base64Data = utils.encodeBase64(content);
- var multipartRequestBody = [
- delimiter,
- 'Content-Type: application/json\r\n\r\n',
- JSON.stringify(metadata),
- delimiter,
- 'Content-Type: ',
- contentType,
- '\r\n',
- 'Content-Transfer-Encoding: base64\r\n',
- '\r\n',
- base64Data,
- close_delim
- ].join("");
+ function localAuthenticate() {
+ if(authuser > 5) {
+ return task.error(new Error('Unable to authenticate user ' + authorizationMgr.getUserId() + ', please sign in with Google.'));
+ }
+ if(immediate === false) {
+ task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT;
+ }
+ var scopeList = _.chain(scopeMap).pick(authorizationMgr.getListWithNew(permission)).flatten().value();
+ gapi.auth.authorize({
+ client_id: constants.GOOGLE_CLIENT_ID,
+ scope: scopeList,
+ immediate: immediate,
+ authuser: immediate === false ? '' : authuser
+ }, function(authResult) {
+ newToken = gapi.auth.getToken();
+ gapi.auth.setToken(currentToken);
+ if(!authResult || authResult.error) {
+ if(connected === true && immediate === true) {
+ // If immediate did not work retry without immediate
+ // flag
+ immediate = false;
+ task.chain(oauthRedirect);
+ }
+ else {
+ // Error
+ task.error(new Error("Access to Google account is not authorized."));
+ }
+ }
+ else {
+ // Success but we need to check the user id
+ immediate === true && authuser++;
+ task.chain(getTokenInfo);
+ }
+ });
+ }
- runWithToken(accountId, function() {
- var request = gapi.client.request({
- 'path': path,
- 'method': method,
- 'params': {
- 'uploadType': 'multipart',
- },
- 'headers': headers,
- 'body': multipartRequestBody,
- });
- request.execute(function(response) {
- if(response && response.id) {
- // Upload success
- result = response;
- result.content = content;
- task.chain();
- return;
- }
- var error = response.error;
- // Handle error
- if(error !== undefined && fileId !== undefined) {
- if(error.code === 404) {
- error = 'File ID "' + fileId + '" not found on Google Drive.|removePublish';
- }
- else if(error.code === 412) {
- // We may have missed a file update
- storage.removeItem(accountId + ".gdrive.lastChangeId");
- error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.';
- }
- }
- handleError(error, task);
- });
- });
- });
- task.onSuccess(function() {
- callback(undefined, result);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ function oauthRedirect() {
+ if(immediate === true) {
+ return task.chain(localAuthenticate);
+ }
+ utils.redirectConfirm('You are being redirected to Google authorization page.', function() {
+ task.chain(localAuthenticate);
+ }, function() {
+ task.error(new Error('Operation canceled.'));
+ });
+ }
- googleHelper.rename = function(fileId, title, accountId, callback) {
- var result;
- var task = new AsyncTask();
- connect(task);
- authenticate(task, 'gdrive', accountId);
- task.onRun(function() {
- var body = {'title': title};
- runWithToken(accountId, function() {
- var request = gapi.client.drive.files.patch({
- 'fileId': fileId,
- 'resource': body
- });
- request.execute(function(response) {
- if(response && response.id) {
- // Rename success
- result = response;
- task.chain();
- return;
- }
- var error = response.error;
- // Handle error
- if(error !== undefined && fileId !== undefined) {
- if(error.code === 404) {
- error = 'File ID "' + fileId + '" not found on Google Drive.|removePublish';
- }
- }
- handleError(error, task);
- });
- });
- });
- task.onSuccess(function() {
- callback(undefined, result);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ function startAuthenticate() {
+ immediate = true;
+ if(authorizationMgr.token && authorizationMgr.isAuthorized(permission)) {
+ task.chain();
+ return;
+ }
+ if(!authorizationMgr.getUserId()) {
+ immediate = false;
+ }
+ task.chain(oauthRedirect);
+ }
- googleHelper.checkChanges = function(lastChangeId, accountId, callback) {
- var changes = [];
- var newChangeId = lastChangeId || 0;
- var task = new AsyncTask();
- connect(task);
- authenticate(task, 'gdrive', accountId);
- task.onRun(function() {
- var nextPageToken;
- function retrievePageOfChanges() {
- runWithToken(accountId, function() {
- var request;
- if(nextPageToken === undefined) {
- request = gapi.client.drive.changes.list({
- 'startChangeId': newChangeId + 1
- });
- }
- else {
- request = gapi.client.drive.changes.list({
- 'pageToken': nextPageToken
- });
- }
+ startAuthenticate();
+ });
+ }
- request.execute(function(response) {
- if(!response || !response.largestChangeId) {
- // Handle error
- handleError(response.error, task);
- return;
- }
- // Retrieve success
- newChangeId = response.largestChangeId;
- nextPageToken = response.nextPageToken;
- if(response.items !== undefined) {
- changes = changes.concat(response.items);
- }
- if(nextPageToken !== undefined) {
- task.chain(retrievePageOfChanges);
- }
- else {
- task.chain();
- }
- });
- });
- }
- task.chain(retrievePageOfChanges);
- });
- task.onSuccess(function() {
- callback(undefined, changes, newChangeId);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ googleHelper.refreshGdriveToken = function(accountId) {
+ var task = new AsyncTask();
+ connect(task);
+ var authorizationMgr = authorizationMgrMap[accountId];
+ authorizationMgr && authorizationMgr.setRefreshFlag();
+ authenticate(task, 'gdrive', accountId);
+ task.enqueue();
+ };
- googleHelper.downloadMetadata = function(ids, accountId, callback, skipAuth) {
- var result = [];
- var task = new AsyncTask();
- connect(task);
- if(!skipAuth) {
- authenticate(task, 'gdrive', accountId);
- }
- task.onRun(function() {
- function recursiveDownloadMetadata() {
- if(ids.length === 0) {
- task.chain();
- return;
- }
- var id = ids[0];
- var headers = {};
- var authorizationMgr = authorizationMgrMap[accountId];
- if(authorizationMgr && authorizationMgr.token) {
- headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
- }
- $.ajax({
- url: "https://www.googleapis.com/drive/v2/files/" + id,
- headers: headers,
- data: {
- key: constants.GOOGLE_API_KEY
- },
- dataType: "json",
- timeout: constants.AJAX_TIMEOUT
- }).done(function(data) {
- result.push(data);
- ids.shift();
- task.chain(recursiveDownloadMetadata);
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- if(error.code === 404) {
- error = 'File ID "' + id + '" not found on Google Drive.';
- }
- handleError(error, task);
- });
- }
- task.chain(recursiveDownloadMetadata);
- });
- task.onSuccess(function() {
- callback(undefined, result);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ function runWithToken(accountId, functionToRun) {
+ var currentToken = gapi.auth.getToken();
+ var authorizationMgr = authorizationMgrMap[accountId];
+ gapi.auth.setToken(authorizationMgr.token);
+ functionToRun();
+ gapi.auth.setToken(currentToken);
+ }
- googleHelper.downloadContent = function(objects, accountId, callback, skipAuth) {
- var result = [];
- var task = new AsyncTask();
- // Add some time for user to choose his files
- task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT;
- connect(task);
- if(!skipAuth) {
- authenticate(task, 'gdrive', accountId);
- }
- task.onRun(function() {
- function recursiveDownloadContent() {
- if(objects.length === 0) {
- task.chain();
- return;
- }
- var object = objects[0];
- result.push(object);
- var file;
- // object may be a file
- if(object.kind == "drive#file") {
- file = object;
- }
- // object may be a change
- else if(object.kind == "drive#change") {
- file = object.file;
- }
- if(!file) {
- objects.shift();
- task.chain(recursiveDownloadContent);
- return;
- }
- var url = file.downloadUrl;
- // if file is a real time document
- if(file.mimeType.indexOf("application/vnd.google-apps.drive-sdk") === 0) {
- file.isRealtime = true;
- url = 'https://www.googleapis.com/drive/v2/files/' + file.id + '/realtime';
- }
- var headers = {};
- var authorizationMgr = authorizationMgrMap[accountId];
- if(authorizationMgr && authorizationMgr.token) {
- headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
- }
- $.ajax({
- url: url,
- headers: headers,
- data: {
- key: constants.GOOGLE_API_KEY
- },
- dataType: file.isRealtime ? 'json' : 'text',
- timeout: constants.AJAX_TIMEOUT
- }).done(function(data) {
- file.content = file.isRealtime ? data.data.value.content.value : data;
- objects.shift();
- task.chain(recursiveDownloadContent);
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- handleError(error, task);
- });
- }
- task.chain(recursiveDownloadContent);
- });
- task.onSuccess(function() {
- callback(undefined, result);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ googleHelper.upload = function(fileId, parentId, title, content, contentType, etag, accountId, callback) {
+ var result;
+ var task = new AsyncTask();
+ connect(task);
+ authenticate(task, 'gdrive', accountId);
+ task.onRun(function() {
+ var boundary = '-------314159265358979323846';
+ var delimiter = "\r\n--" + boundary + "\r\n";
+ var close_delim = "\r\n--" + boundary + "--";
+ contentType = contentType || settings.markdownMimeType;
+ var metadata = {
+ title: title,
+ mimeType: contentType
+ };
+ if(parentId) {
+ // Specify the directory
+ metadata.parents = [
+ {
+ kind: 'drive#fileLink',
+ id: parentId
+ }
+ ];
+ }
+ var path = '/upload/drive/v2/files';
+ var method = 'POST';
+ if(fileId) {
+ // If it's an update
+ path += "/" + fileId;
+ method = 'PUT';
+ }
+ var headers = {
+ 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
+ };
- googleHelper.uploadImg = function(name, content, albumId, callback) {
- var accountId = 'google.picasa0';
- var result;
- var task = new AsyncTask();
- connect(task);
- authenticate(task, 'picasa', accountId);
- task.onRun(function() {
- var headers = {
- "Slug": name
- };
- if(name.match(/.jpe?g$/i)) {
- headers["Content-Type"] = "image/jpeg";
- }
- else if(name.match(/.png$/i)) {
- headers["Content-Type"] = "image/png";
- }
- else if(name.match(/.gif$/i)) {
- headers["Content-Type"] = "image/gif";
- }
- var authorizationMgr = authorizationMgrMap[accountId];
- if(authorizationMgr && authorizationMgr.token) {
- headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
- }
+ var base64Data = utils.encodeBase64(content);
+ var multipartRequestBody = [
+ delimiter,
+ 'Content-Type: application/json\r\n\r\n',
+ JSON.stringify(metadata),
+ delimiter,
+ 'Content-Type: ',
+ contentType,
+ '\r\n',
+ 'Content-Transfer-Encoding: base64\r\n',
+ '\r\n',
+ base64Data,
+ close_delim
+ ].join("");
- $.ajax({
- url: constants.PICASA_PROXY_URL + "upload/" + albumId,
- headers: headers,
- data: content,
- processData: false,
- dataType: "xml",
- timeout: constants.AJAX_TIMEOUT,
- type: "POST"
- }).done(function(data) {
- result = data;
- task.chain();
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- if(error.code == 200) {
- error.message = jqXHR.responseText;
- }
- handleError(error, task);
- });
- });
- task.onSuccess(function() {
- callback(undefined, result);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ runWithToken(accountId, function() {
+ var request = gapi.client.request({
+ 'path': path,
+ 'method': method,
+ 'params': {
+ 'uploadType': 'multipart'
+ },
+ 'headers': headers,
+ 'body': multipartRequestBody
+ });
+ request.execute(function(response) {
+ if(response && response.id) {
+ // Upload success
+ result = response;
+ result.content = content;
+ return task.chain();
+ }
+ var error = response.error;
+ // Handle error
+ if(error !== undefined && fileId !== undefined) {
+ if(error.code === 404) {
+ error = 'File ID "' + fileId + '" not found on Google Drive.|removePublish';
+ }
+ else if(error.code === 412) {
+ // We may have missed a file update
+ storage.removeItem(accountId + ".gdrive.lastChangeId");
+ error = 'Conflict on file ID "' + fileId + '". Please restart the synchronization.';
+ }
+ }
+ handleError(error, task);
+ });
+ });
+ });
+ task.onSuccess(function() {
+ callback(undefined, result);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
- function handleError(error, task) {
- var errorMsg;
- if(error) {
- logger.error(error);
- // Try to analyze the error
- if(typeof error === "string") {
- errorMsg = error;
- }
- else {
- errorMsg = "Google error (" + error.code + ": " + error.message + ").";
- if(error.code >= 500 && error.code < 600) {
- // Retry as described in Google's best practices
- task.retry(new Error(errorMsg));
- return;
- }
- else if(error.code === 401 || error.code === 403 || error.code == "token_refresh_required") {
- _.each(authorizationMgrMap, function(authorizationMgr) {
- authorizationMgr.setRefreshFlag();
- });
- errorMsg = "Access to Google account is not authorized.";
- task.retry(new Error(errorMsg), 1);
- return;
- }
- else if(error.code === 0 || error.code === -1) {
- connected = false;
- _.each(authorizationMgrMap, function(authorizationMgr) {
- authorizationMgr.setRefreshFlag();
- });
- core.setOffline();
- errorMsg = "|stopPublish";
- }
- }
- }
- task.error(new Error(errorMsg));
- }
+ googleHelper.rename = function(fileId, title, accountId, callback) {
+ var result;
+ var task = new AsyncTask();
+ connect(task);
+ authenticate(task, 'gdrive', accountId);
+ task.onRun(function() {
+ var body = {'title': title};
+ runWithToken(accountId, function() {
+ var request = gapi.client.drive.files.patch({
+ 'fileId': fileId,
+ 'resource': body
+ });
+ request.execute(function(response) {
+ if(response && response.id) {
+ // Rename success
+ result = response;
+ return task.chain();
+ }
+ var error = response.error;
+ // Handle error
+ if(error !== undefined && fileId !== undefined) {
+ if(error.code === 404) {
+ error = 'File ID "' + fileId + '" not found on Google Drive.|removePublish';
+ }
+ }
+ handleError(error, task);
+ });
+ });
+ });
+ task.onSuccess(function() {
+ callback(undefined, result);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
- var pickerLoaded = false;
- function loadPicker(task) {
- task.onRun(function() {
- if(pickerLoaded === true) {
- task.chain();
- return;
- }
- $.ajax({
- url: "//www.google.com/jsapi",
- data: {
- key: constants.GOOGLE_API_KEY
- },
- dataType: "script",
- timeout: constants.AJAX_TIMEOUT
- }).done(function() {
- google.load('picker', '1', {
- callback: function() {
- task.chain();
- }
- });
- pickerLoaded = true;
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- handleError(error, task);
- });
- });
- }
+ googleHelper.checkChanges = function(lastChangeId, accountId, callback) {
+ var changes = [];
+ var newChangeId = lastChangeId || 0;
+ var task = new AsyncTask();
+ connect(task);
+ authenticate(task, 'gdrive', accountId);
+ task.onRun(function() {
+ var nextPageToken;
- googleHelper.picker = function(callback, pickerType, accountId) {
- var docs = [];
- var picker;
- function hidePicker() {
- if(picker !== undefined) {
- picker.setVisible(false);
- $(".modal-backdrop, .picker").remove();
- }
- }
- var task = new AsyncTask();
- // Add some time for user to choose his files
- task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT;
- connect(task);
- if(pickerType == 'doc' || pickerType == 'folder') {
- authenticate(task, 'gdrive', accountId);
- }
- else {
- accountId = 'google.picasa0';
- authenticate(task, 'picasa', accountId);
- }
- loadPicker(task);
- task.onRun(function() {
- var authorizationMgr = authorizationMgrMap[accountId];
- var pickerBuilder = new google.picker.PickerBuilder();
- pickerBuilder.setAppId(constants.GOOGLE_DRIVE_APP_ID);
- var view;
- if(pickerType == 'doc') {
- view = new google.picker.DocsView(google.picker.ViewId.DOCS);
- view.setParent('root');
- view.setIncludeFolders(true);
- view.setMimeTypes([
- "text/x-markdown",
- "text/plain",
- "application/octet-stream",
- "application/vnd.google-apps.drive-sdk." + constants.GOOGLE_DRIVE_APP_ID
- ].join(","));
- pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
- pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
- pickerBuilder.addView(view);
- authorizationMgr && authorizationMgr.token && pickerBuilder.setOAuthToken(authorizationMgr.token.access_token);
- }
- else if(pickerType == 'folder') {
- view = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
- view.setParent('root');
- view.setIncludeFolders(true);
- view.setSelectFolderEnabled(true);
- view.setMimeTypes('application/vnd.google-apps.folder');
- pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
- pickerBuilder.addView(view);
- authorizationMgr && authorizationMgr.token && pickerBuilder.setOAuthToken(authorizationMgr.token.access_token);
- }
- else if(pickerType == 'img') {
- view = new google.picker.PhotosView();
- view.setType('flat');
- pickerBuilder.addView(view);
- view = new google.picker.PhotosView();
- view.setType('ofuser');
- pickerBuilder.addView(view);
- pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
- authorizationMgr && authorizationMgr.token && pickerBuilder.setOAuthToken(authorizationMgr.token.access_token);
- }
- pickerBuilder.setCallback(function(data) {
- if(data.action == google.picker.Action.PICKED || data.action == google.picker.Action.CANCEL) {
- if(data.action == google.picker.Action.PICKED) {
- docs = data.docs;
- }
- hidePicker();
- task.chain();
- }
- });
- picker = pickerBuilder.build();
- $(utils.createBackdrop()).click(function() {
- hidePicker();
- task.chain();
- });
- picker.setVisible(true);
- });
- task.onSuccess(function() {
- callback(undefined, docs);
- });
- task.onError(function(error) {
- hidePicker();
- callback(error);
- });
- task.enqueue();
- };
+ function retrievePageOfChanges() {
+ runWithToken(accountId, function() {
+ var request;
+ if(nextPageToken === undefined) {
+ request = gapi.client.drive.changes.list({
+ 'startChangeId': newChangeId + 1
+ });
+ }
+ else {
+ request = gapi.client.drive.changes.list({
+ 'pageToken': nextPageToken
+ });
+ }
- googleHelper.uploadBlogger = function(blogUrl, blogId, postId, labelList, isDraft, publishDate, title, content, callback) {
- var accountId = 'google.blogger0';
- var task = new AsyncTask();
- connect(task);
- authenticate(task, 'blogger', accountId);
- task.onRun(function() {
- var headers = {};
- var authorizationMgr = authorizationMgrMap[accountId];
- if(authorizationMgr && authorizationMgr.token) {
- headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
- }
- function uploadPost() {
- var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/";
- var data = {
- kind: "blogger#post",
- blog: {
- id: blogId
- },
- labels: labelList,
- title: title,
- content: content
- };
- if(publishDate) {
- data.published = publishDate.toISOString();
- }
- var type = "POST";
- // If it's an update
- if(postId !== undefined) {
- url += postId;
- data.id = postId;
- type = "PUT";
- }
- $.ajax({
- url: url,
- data: JSON.stringify(data),
- headers: headers,
- type: type,
- contentType: "application/json",
- dataType: "json",
- timeout: constants.AJAX_TIMEOUT
- }).done(function(post) {
- postId = post.id;
- task.chain(publish);
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- if(error.code === 404 && postId !== undefined) {
- error = 'Post ' + postId + ' not found on Blogger.|removePublish';
- }
- handleError(error, task);
- });
- }
- function publish() {
- var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/" + postId;
- if(isDraft) {
- url += "/revert";
- }
- else {
- url += "/publish";
- if(publishDate) {
- url += '?publishDate=' + publishDate.toISOString();
- }
- }
- $.ajax({
- url: url,
- headers: headers,
- type: 'POST',
- dataType: "json",
- timeout: constants.AJAX_TIMEOUT
- }).done(function() {
- task.chain();
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- if(error.code === 404) {
- error = 'Post ' + postId + ' not found on Blogger.|removePublish';
- }
- handleError(error, task);
- });
- }
- function getBlogId() {
- if(blogId !== undefined) {
- task.chain(uploadPost);
- return;
- }
- $.ajax({
- url: "https://www.googleapis.com/blogger/v3/blogs/byurl",
- data: {
- url: blogUrl
- },
- headers: headers,
- dataType: "json",
- timeout: constants.AJAX_TIMEOUT
- }).done(function(blog) {
- blogId = blog.id;
- task.chain(uploadPost);
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- if(error.code === 404) {
- error = 'Blog "' + blogUrl + '" not found on Blogger.|removePublish';
- }
- handleError(error, task);
- });
- }
- task.chain(getBlogId);
- });
- task.onSuccess(function() {
- callback(undefined, blogId, postId);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ request.execute(function(response) {
+ if(!response || !response.largestChangeId) {
+ // Handle error
+ return handleError(response.error, task);
+ }
+ // Retrieve success
+ newChangeId = response.largestChangeId;
+ nextPageToken = response.nextPageToken;
+ if(response.items !== undefined) {
+ changes = changes.concat(response.items);
+ }
+ if(nextPageToken !== undefined) {
+ task.chain(retrievePageOfChanges);
+ }
+ else {
+ task.chain();
+ }
+ });
+ });
+ }
- googleHelper.uploadBloggerPage = function(blogUrl, blogId, pageId, isDraft, publishDate, title, content, callback) {
- var accountId = 'google.blogger0';
- var task = new AsyncTask();
- connect(task);
- authenticate(task, 'blogger', accountId);
- task.onRun(function() {
- var headers = {};
- var authorizationMgr = authorizationMgrMap[accountId];
- if(authorizationMgr && authorizationMgr.token) {
- headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
- }
- function uploadPage() {
- var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/pages/";
- var data = {
- kind: "blogger#page",
- blog: {
- id: blogId
- },
- title: title,
- content: content
- };
- var type = "POST";
- // If it's an update
- if(pageId !== undefined) {
- url += pageId;
- data.id = pageId;
- type = "PUT";
- }
- $.ajax({
- url: url,
- data: JSON.stringify(data),
- headers: headers,
- type: type,
- contentType: "application/json",
- dataType: "json",
- timeout: constants.AJAX_TIMEOUT
- }).done(function(page) {
- pageId = page.id;
- task.chain();
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- if(error.code === 404 && pageId !== undefined) {
- error = 'Page ' + pageId + ' not found on Blogger.|removePublish';
- }
- handleError(error, task);
- });
- }
- function getBlogId() {
- if(blogId !== undefined) {
- task.chain(uploadPage);
- return;
- }
- $.ajax({
- url: "https://www.googleapis.com/blogger/v3/blogs/byurl",
- data: {
- url: blogUrl
- },
- headers: headers,
- dataType: "json",
- timeout: constants.AJAX_TIMEOUT
- }).done(function(blog) {
- blogId = blog.id;
- task.chain(uploadPage);
- }).fail(function(jqXHR) {
- var error = {
- code: jqXHR.status,
- message: jqXHR.statusText
- };
- // Handle error
- if(error.code === 404) {
- error = 'Blog "' + blogUrl + '" not found on Blogger.|removePublish';
- }
- handleError(error, task);
- });
- }
- task.chain(getBlogId);
- });
- task.onSuccess(function() {
- callback(undefined, blogId, pageId);
- });
- task.onError(function(error) {
- callback(error);
- });
- task.enqueue();
- };
+ task.chain(retrievePageOfChanges);
+ });
+ task.onSuccess(function() {
+ callback(undefined, changes, newChangeId);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
- // Use by Google's client.js
- window.delayedFunction = undefined;
- window.runDelayedFunction = function() {
- if(window.delayedFunction !== undefined) {
- window.delayedFunction();
- }
- };
+ googleHelper.downloadMetadata = function(ids, accountId, callback, skipAuth) {
+ var result = [];
+ var task = new AsyncTask();
+ connect(task);
+ if(!skipAuth) {
+ authenticate(task, 'gdrive', accountId);
+ }
+ task.onRun(function() {
+ function recursiveDownloadMetadata() {
+ if(ids.length === 0) {
+ return task.chain();
+ }
+ var id = ids[0];
+ var headers = {};
+ var authorizationMgr = authorizationMgrMap[accountId];
+ if(authorizationMgr && authorizationMgr.token) {
+ headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
+ }
+ $.ajax({
+ url: "https://www.googleapis.com/drive/v2/files/" + id,
+ headers: headers,
+ data: {
+ key: constants.GOOGLE_API_KEY
+ },
+ dataType: "json",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function(data) {
+ result.push(data);
+ ids.shift();
+ task.chain(recursiveDownloadMetadata);
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ if(error.code === 404) {
+ error = 'File ID "' + id + '" not found on Google Drive.';
+ }
+ handleError(error, task);
+ });
+ }
- return googleHelper;
+ task.chain(recursiveDownloadMetadata);
+ });
+ task.onSuccess(function() {
+ callback(undefined, result);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
+
+ googleHelper.downloadContent = function(objects, accountId, callback, skipAuth) {
+ var result = [];
+ var task = new AsyncTask();
+ // Add some time for user to choose his files
+ task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT;
+ connect(task);
+ if(!skipAuth) {
+ authenticate(task, 'gdrive', accountId);
+ }
+ task.onRun(function() {
+ function recursiveDownloadContent() {
+ if(objects.length === 0) {
+ return task.chain();
+ }
+ var object = objects[0];
+ result.push(object);
+ var file;
+ // object may be a file
+ if(object.kind == "drive#file") {
+ file = object;
+ }
+ // object may be a change
+ else if(object.kind == "drive#change") {
+ file = object.file;
+ }
+ if(!file) {
+ objects.shift();
+ return task.chain(recursiveDownloadContent);
+ }
+ var url = file.downloadUrl;
+ // if file is a real time document
+ if(file.mimeType.indexOf("application/vnd.google-apps.drive-sdk") === 0) {
+ file.isRealtime = true;
+ url = 'https://www.googleapis.com/drive/v2/files/' + file.id + '/realtime';
+ }
+ var headers = {};
+ var authorizationMgr = authorizationMgrMap[accountId];
+ if(authorizationMgr && authorizationMgr.token) {
+ headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
+ }
+ $.ajax({
+ url: url,
+ headers: headers,
+ data: {
+ key: constants.GOOGLE_API_KEY
+ },
+ dataType: file.isRealtime ? 'json' : 'text',
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function(data) {
+ file.content = file.isRealtime ? data.data.value.content.value : data;
+ objects.shift();
+ task.chain(recursiveDownloadContent);
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ handleError(error, task);
+ });
+ }
+
+ task.chain(recursiveDownloadContent);
+ });
+ task.onSuccess(function() {
+ callback(undefined, result);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
+
+ googleHelper.uploadImg = function(name, content, albumId, callback) {
+ var accountId = 'google.picasa0';
+ var result;
+ var task = new AsyncTask();
+ connect(task);
+ authenticate(task, 'picasa', accountId);
+ task.onRun(function() {
+ var headers = {
+ "Slug": name
+ };
+ if(name.match(/.jpe?g$/i)) {
+ headers["Content-Type"] = "image/jpeg";
+ }
+ else if(name.match(/.png$/i)) {
+ headers["Content-Type"] = "image/png";
+ }
+ else if(name.match(/.gif$/i)) {
+ headers["Content-Type"] = "image/gif";
+ }
+ var authorizationMgr = authorizationMgrMap[accountId];
+ if(authorizationMgr && authorizationMgr.token) {
+ headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
+ }
+
+ $.ajax({
+ url: constants.PICASA_PROXY_URL + "upload/" + albumId,
+ headers: headers,
+ data: content,
+ processData: false,
+ dataType: "xml",
+ timeout: constants.AJAX_TIMEOUT,
+ type: "POST"
+ }).done(function(data) {
+ result = data;
+ task.chain();
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ if(error.code == 200) {
+ error.message = jqXHR.responseText;
+ }
+ handleError(error, task);
+ });
+ });
+ task.onSuccess(function() {
+ callback(undefined, result);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
+
+ function handleError(error, task) {
+ var errorMsg;
+ if(error) {
+ logger.error(error);
+ // Try to analyze the error
+ if(typeof error === "string") {
+ errorMsg = error;
+ }
+ else {
+ errorMsg = "Google error (" + error.code + ": " + error.message + ").";
+ if(error.code >= 500 && error.code < 600) {
+ // Retry as described in Google's best practices
+ return task.retry(new Error(errorMsg));
+ }
+ else if(error.code === 401 || error.code === 403 || error.code == "token_refresh_required") {
+ _.each(authorizationMgrMap, function(authorizationMgr) {
+ authorizationMgr.setRefreshFlag();
+ });
+ errorMsg = "Access to Google account is not authorized.";
+ return task.retry(new Error(errorMsg), 1);
+ }
+ else if(error.code === 0 || error.code === -1) {
+ connected = false;
+ _.each(authorizationMgrMap, function(authorizationMgr) {
+ authorizationMgr.setRefreshFlag();
+ });
+ core.setOffline();
+ errorMsg = "|stopPublish";
+ }
+ }
+ }
+ task.error(new Error(errorMsg));
+ }
+
+ var pickerLoaded = false;
+
+ function loadPicker(task) {
+ task.onRun(function() {
+ if(pickerLoaded === true) {
+ return task.chain();
+ }
+ $.ajax({
+ url: "//www.google.com/jsapi",
+ data: {
+ key: constants.GOOGLE_API_KEY
+ },
+ dataType: "script",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function() {
+ google.load('picker', '1', {
+ callback: function() {
+ task.chain();
+ }
+ });
+ pickerLoaded = true;
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ handleError(error, task);
+ });
+ });
+ }
+
+ googleHelper.picker = function(callback, pickerType, accountId) {
+ var docs = [];
+ var picker;
+
+ function hidePicker() {
+ if(picker !== undefined) {
+ picker.setVisible(false);
+ $(".modal-backdrop, .picker").remove();
+ }
+ }
+
+ var task = new AsyncTask();
+ // Add some time for user to choose his files
+ task.timeout = constants.ASYNC_TASK_LONG_TIMEOUT;
+ connect(task);
+ if(pickerType == 'doc' || pickerType == 'folder') {
+ authenticate(task, 'gdrive', accountId);
+ }
+ else {
+ accountId = 'google.picasa0';
+ authenticate(task, 'picasa', accountId);
+ }
+ loadPicker(task);
+ task.onRun(function() {
+ var authorizationMgr = authorizationMgrMap[accountId];
+ var pickerBuilder = new google.picker.PickerBuilder();
+ pickerBuilder.setAppId(constants.GOOGLE_DRIVE_APP_ID);
+ var view;
+ if(pickerType == 'doc') {
+ view = new google.picker.DocsView(google.picker.ViewId.DOCS);
+ view.setParent('root');
+ view.setIncludeFolders(true);
+ view.setMimeTypes([
+ "text/x-markdown",
+ "text/plain",
+ "application/octet-stream",
+ "application/vnd.google-apps.drive-sdk." + constants.GOOGLE_DRIVE_APP_ID
+ ].join(","));
+ pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
+ pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
+ pickerBuilder.addView(view);
+ authorizationMgr && authorizationMgr.token && pickerBuilder.setOAuthToken(authorizationMgr.token.access_token);
+ }
+ else if(pickerType == 'folder') {
+ view = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
+ view.setParent('root');
+ view.setIncludeFolders(true);
+ view.setSelectFolderEnabled(true);
+ view.setMimeTypes('application/vnd.google-apps.folder');
+ pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
+ pickerBuilder.addView(view);
+ authorizationMgr && authorizationMgr.token && pickerBuilder.setOAuthToken(authorizationMgr.token.access_token);
+ }
+ else if(pickerType == 'img') {
+ view = new google.picker.PhotosView();
+ view.setType('flat');
+ pickerBuilder.addView(view);
+ view = new google.picker.PhotosView();
+ view.setType('ofuser');
+ pickerBuilder.addView(view);
+ pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
+ authorizationMgr && authorizationMgr.token && pickerBuilder.setOAuthToken(authorizationMgr.token.access_token);
+ }
+ pickerBuilder.setCallback(function(data) {
+ if(data.action == google.picker.Action.PICKED || data.action == google.picker.Action.CANCEL) {
+ if(data.action == google.picker.Action.PICKED) {
+ docs = data.docs;
+ }
+ hidePicker();
+ task.chain();
+ }
+ });
+ picker = pickerBuilder.build();
+ $(utils.createBackdrop()).on('click.backdrop', function() {
+ hidePicker();
+ task.chain();
+ });
+ picker.setVisible(true);
+ });
+ task.onSuccess(function() {
+ callback(undefined, docs);
+ });
+ task.onError(function(error) {
+ hidePicker();
+ callback(error);
+ });
+ task.enqueue();
+ };
+
+ googleHelper.uploadBlogger = function(blogUrl, blogId, postId, labelList, isDraft, publishDate, title, content, callback) {
+ var accountId = 'google.blogger0';
+ var task = new AsyncTask();
+ connect(task);
+ authenticate(task, 'blogger', accountId);
+ task.onRun(function() {
+ var headers = {};
+ var authorizationMgr = authorizationMgrMap[accountId];
+ if(authorizationMgr && authorizationMgr.token) {
+ headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
+ }
+ function uploadPost() {
+ var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/";
+ var data = {
+ kind: "blogger#post",
+ blog: {
+ id: blogId
+ },
+ labels: labelList,
+ title: title,
+ content: content
+ };
+ if(publishDate) {
+ data.published = publishDate.toISOString();
+ }
+ var type = "POST";
+ // If it's an update
+ if(postId !== undefined) {
+ url += postId;
+ data.id = postId;
+ type = "PUT";
+ }
+ $.ajax({
+ url: url,
+ data: JSON.stringify(data),
+ headers: headers,
+ type: type,
+ contentType: "application/json",
+ dataType: "json",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function(post) {
+ postId = post.id;
+ task.chain(publish);
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ if(error.code === 404 && postId !== undefined) {
+ error = 'Post ' + postId + ' not found on Blogger.|removePublish';
+ }
+ handleError(error, task);
+ });
+ }
+
+ function publish() {
+ var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/" + postId;
+ if(isDraft) {
+ url += "/revert";
+ }
+ else {
+ url += "/publish";
+ if(publishDate) {
+ url += '?publishDate=' + publishDate.toISOString();
+ }
+ }
+ $.ajax({
+ url: url,
+ headers: headers,
+ type: 'POST',
+ dataType: "json",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function() {
+ task.chain();
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ if(error.code === 404) {
+ error = 'Post ' + postId + ' not found on Blogger.|removePublish';
+ }
+ handleError(error, task);
+ });
+ }
+
+ function getBlogId() {
+ if(blogId !== undefined) {
+ task.chain(uploadPost);
+ return;
+ }
+ $.ajax({
+ url: "https://www.googleapis.com/blogger/v3/blogs/byurl",
+ data: {
+ url: blogUrl
+ },
+ headers: headers,
+ dataType: "json",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function(blog) {
+ blogId = blog.id;
+ task.chain(uploadPost);
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ if(error.code === 404) {
+ error = 'Blog "' + blogUrl + '" not found on Blogger.|removePublish';
+ }
+ handleError(error, task);
+ });
+ }
+
+ task.chain(getBlogId);
+ });
+ task.onSuccess(function() {
+ callback(undefined, blogId, postId);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
+
+ googleHelper.uploadBloggerPage = function(blogUrl, blogId, pageId, isDraft, publishDate, title, content, callback) {
+ var accountId = 'google.blogger0';
+ var task = new AsyncTask();
+ connect(task);
+ authenticate(task, 'blogger', accountId);
+ task.onRun(function() {
+ var headers = {};
+ var authorizationMgr = authorizationMgrMap[accountId];
+ if(authorizationMgr && authorizationMgr.token) {
+ headers.Authorization = "Bearer " + authorizationMgr.token.access_token;
+ }
+ function uploadPage() {
+ var url = "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/pages/";
+ var data = {
+ kind: "blogger#page",
+ blog: {
+ id: blogId
+ },
+ title: title,
+ content: content
+ };
+ var type = "POST";
+ // If it's an update
+ if(pageId !== undefined) {
+ url += pageId;
+ data.id = pageId;
+ type = "PUT";
+ }
+ $.ajax({
+ url: url,
+ data: JSON.stringify(data),
+ headers: headers,
+ type: type,
+ contentType: "application/json",
+ dataType: "json",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function(page) {
+ pageId = page.id;
+ task.chain();
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ if(error.code === 404 && pageId !== undefined) {
+ error = 'Page ' + pageId + ' not found on Blogger.|removePublish';
+ }
+ handleError(error, task);
+ });
+ }
+
+ function getBlogId() {
+ if(blogId !== undefined) {
+ task.chain(uploadPage);
+ return;
+ }
+ $.ajax({
+ url: "https://www.googleapis.com/blogger/v3/blogs/byurl",
+ data: {
+ url: blogUrl
+ },
+ headers: headers,
+ dataType: "json",
+ timeout: constants.AJAX_TIMEOUT
+ }).done(function(blog) {
+ blogId = blog.id;
+ task.chain(uploadPage);
+ }).fail(function(jqXHR) {
+ var error = {
+ code: jqXHR.status,
+ message: jqXHR.statusText
+ };
+ // Handle error
+ if(error.code === 404) {
+ error = 'Blog "' + blogUrl + '" not found on Blogger.|removePublish';
+ }
+ handleError(error, task);
+ });
+ }
+
+ task.chain(getBlogId);
+ });
+ task.onSuccess(function() {
+ callback(undefined, blogId, pageId);
+ });
+ task.onError(function(error) {
+ callback(error);
+ });
+ task.enqueue();
+ };
+
+ // Use by Google's client.js
+ window.delayedFunction = undefined;
+ window.runDelayedFunction = function() {
+ if(window.delayedFunction !== undefined) {
+ window.delayedFunction();
+ }
+ };
+
+ return googleHelper;
});
diff --git a/public/res/layout.js b/public/res/layout.js
index 1ec59696..24d055d7 100644
--- a/public/res/layout.js
+++ b/public/res/layout.js
@@ -117,7 +117,7 @@ define([
};
DomObject.prototype.createToggler = function(backdrop) {
- var backdropElt;
+ var $backdropElt;
var pushedEvents = 0;
this.toggle = function(show) {
if(show === this.isOpen) {
@@ -127,7 +127,7 @@ define([
if(this.isOpen) {
this.$elt.addClass('panel-open').trigger('show.layout.toggle');
if(backdrop) {
- $(backdropElt = utils.createBackdrop(wrapperL1.elt)).click(_.bind(function() {
+ $backdropElt = $(utils.createBackdrop(wrapperL1.elt)).on('click.backdrop', _.bind(function() {
this.toggle(false);
}, this));
this.$elt.addClass('bring-to-front');
@@ -140,8 +140,11 @@ define([
}
else {
this.$elt.trigger('hide.layout.toggle');
- backdropElt && backdropElt.removeBackdrop();
- backdropElt = undefined;
+ if($backdropElt) {
+ $backdropElt.off('click.backdrop');
+ $backdropElt[0].removeBackdrop();
+ $backdropElt = undefined;
+ }
transitionEndCallbacks.push(_.bind(function() {
if(--pushedEvents === 0) {
!this.isOpen && this.$elt.removeClass('panel-open bring-to-front').trigger('hidden.layout.toggle');
diff --git a/public/res/libs/prism-markdown.js b/public/res/libs/prism-markdown.js
index 24fc055b..c5997f5e 100644
--- a/public/res/libs/prism-markdown.js
+++ b/public/res/libs/prism-markdown.js
@@ -1,8 +1,10 @@
// Credit to https://editorially.com/
Prism.languages.md = (function () {
- var urlPattern = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>\[\]'"]+|\([^\s()<>\[\]'"]*\))+(?:\([^\s()<>\[\]'"]*\)|[^\s`!()\[\]{}:'".,<>?«»“”‘’]))/gi;
- var emailPattern = /[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum)\b/gi;
+ var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]",
+ charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]";
+ var urlPattern = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi");
+ var emailPattern = /(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)/gi;
var latex = Prism.languages.latex;
@@ -241,9 +243,6 @@ Prism.languages.md = (function () {
}
}
};
- md.email = {
- pattern: emailPattern
- };
md.code = {
pattern: /(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/g,
lookbehind: true,
@@ -297,8 +296,11 @@ Prism.languages.md = (function () {
conflict: /⧸⧸/g,
comment: Prism.languages.markup.comment,
tag: Prism.languages.markup.tag,
- entity: Prism.languages.markup.entity
+ entity: Prism.languages.markup.entity,
+ url: urlPattern,
+ email: emailPattern
};
+
for (var c = 6; c >= 1; c--) {
md["h" + c].inside.rest = rest;
}
diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js
index 84cd5faa..b0f5e575 100644
--- a/public/res/providers/gdriveProviderBuilder.js
+++ b/public/res/providers/gdriveProviderBuilder.js
@@ -91,8 +91,7 @@ define([
var syncIndex = createSyncIndex(doc.id);
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
if(fileDesc !== undefined) {
- eventMgr.onError('"' + fileDesc.title + '" was already imported.');
- return;
+ return eventMgr.onError('"' + fileDesc.title + '" was already imported.');
}
importIds.push(doc.id);
});
@@ -109,16 +108,14 @@ define([
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
if(fileDesc !== undefined) {
eventMgr.onError('File ID is already synchronized with "' + fileDesc.title + '".');
- callback(true);
- return;
+ return callback(true);
}
}
var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid');
var data = gdriveProvider.serializeContent(content, discussionListJSON);
googleHelper.upload(fileId, parentId, title, data, undefined, undefined, accountId, function(error, result) {
if(error) {
- callback(error);
- return;
+ return callback(error);
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
@@ -128,7 +125,7 @@ define([
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
- (syncAttributes.titleCRC == titleCRC) && // Content CRC hasn't changed
+ (syncAttributes.titleCRC == titleCRC) && // Title CRC hasn't changed
(syncAttributes.discussionListCRC == discussionListCRC) // Discussion list CRC hasn't changed
) {
return callback(undefined, false);
@@ -144,8 +141,7 @@ define([
var data = gdriveProvider.serializeContent(content, discussionList);
googleHelper.upload(syncAttributes.id, undefined, title, data, undefined, syncAttributes.etag, accountId, function(error, result) {
if(error) {
- callback(error, true);
- return;
+ return callback(error, true);
}
syncAttributes.etag = result.etag;
// Remove this deprecated flag if any
@@ -167,8 +163,7 @@ define([
var lastChangeId = parseInt(storage[accountId + ".gdrive.lastChangeId"], 10);
googleHelper.checkChanges(lastChangeId, accountId, function(error, changes, newChangeId) {
if(error) {
- callback(error);
- return;
+ return callback(error);
}
var interestingChanges = [];
_.each(changes, function(change) {
diff --git a/public/res/styles/base.less b/public/res/styles/base.less
index 8c336a0b..16ee0829 100644
--- a/public/res/styles/base.less
+++ b/public/res/styles/base.less
@@ -103,6 +103,7 @@ li.L9 { background: #eee }
@line-height-base: 1.45;
@p-margin: 1.1em;
@headings-font-family: inherit;
+@headings-font-weight: 300;
@font-face {
font-family: 'fontello';
@@ -183,6 +184,7 @@ li.L9 { background: #eee }
.container {
margin-bottom: 180px;
+ text-align: justify;
}
a code {
@@ -229,6 +231,9 @@ hr {
[stroke="#000000"] {
stroke: @text-color;
}
+ text[stroke="#000000"] {
+ stroke: none;
+ }
[fill="#000"], [fill="#000000"], [fill="black"] {
fill: @text-color;
}
diff --git a/public/res/styles/main.less b/public/res/styles/main.less
index 4c14a2de..195d4b2a 100644
--- a/public/res/styles/main.less
+++ b/public/res/styles/main.less
@@ -130,18 +130,19 @@
@popover-arrow-outer-color: @secondary-border-color;
@popover-title-bg: @transparent;
@alert-border-radius: 0;
-@label-warning-bg: spin(darken(@logo-yellow, 4%), -6);
-@label-danger-bg: spin(darken(@logo-orange, 4%), -4);
-@state-warning-text: spin(darken(@logo-yellow, 14%), -6);
-@state-warning-bg: fade(spin(@logo-yellow, -6), 12%);
-@state-warning-border: fade(spin(@logo-yellow, -6), 24%);
-@state-danger-text: spin(darken(@logo-orange, 18%), -4);
-@state-danger-bg: fade(spin(@logo-orange, -4), 10%);
-@state-danger-border: fade(spin(@logo-orange, -4), 20%);
+@label-warning-bg: spin(darken(@logo-yellow, 4%), -10);
+@state-warning-text: spin(darken(@logo-yellow, 14%), -10);
+@state-warning-bg: fade(spin(@logo-yellow, -10), 12%);
+@state-warning-border: fade(spin(@logo-yellow, -10), 24%);
+@label-danger-bg: spin(darken(@logo-orange, 4%), -8);
+@state-danger-text: spin(darken(@logo-orange, 18%), -8);
+@state-danger-bg: fade(spin(@logo-orange, -8), 10%);
+@state-danger-border: fade(spin(@logo-orange, -8), 20%);
body {
tab-size: 4;
+ text-align: start;
}
* {
@@ -192,18 +193,27 @@ body {
.translate(0, 0);
}
- .modal-content {
- background-color: @secondary-bg-light;
- }
+}
- .modal-body {
- background-color: @secondary-bg-lighter;
- padding-bottom: 30px;
- }
+.modal-content {
+ background-color: @secondary-bg-light;
+}
- .modal-footer {
- margin-top: 0;
- }
+.modal-body {
+ background-color: @secondary-bg-lighter;
+ padding-bottom: 30px;
+}
+
+.modal-footer {
+ background-color: @secondary-bg-light;
+ margin-top: 0;
+}
+
+.modal-iframe {
+ display: block;
+ margin: 30px auto 0;
+ z-index: 1040;
+ border-radius: 0;
}
a {
@@ -244,6 +254,8 @@ a {
.list-group-item {
padding: 10px 15px;
+ border-left-width: 0;
+ border-right-width: 0;
margin-bottom: 0;
.list-group & {
border-radius: 0;
@@ -251,9 +263,24 @@ a {
a&:hover,
a&:focus {
color: darken(@secondary, 30%);
- border-top-color: @list-group-hover-border-color;
- border-bottom-color: @list-group-hover-border-color;
+ border-color: @list-group-hover-border-color;
}
+ .checkbox {
+ float: right;
+ margin: 0;
+ padding: 0;
+ cursor: pointer;
+ input {
+ cursor: pointer;
+ margin: 0 16px;
+ height: @input-height-slim;
+ }
+ }
+}
+
+.form-group {
+ margin-bottom: 1px;
+ padding: 5px;
}
.text-danger:hover {
@@ -765,17 +792,6 @@ a {
font-size: 15px;
border-top-color: @modal-content-separator-color;
}
- .checkbox {
- float: right;
- margin: 0;
- padding: 0;
- cursor: pointer;
- input {
- cursor: pointer;
- margin: 0 16px;
- height: @input-height-slim;
- }
- }
.input-rename {
width: 220px;
height: @input-height-slim;
@@ -922,7 +938,7 @@ a {
width: 220px;
margin: 10px auto 20px;
.btn {
- text-align: initial;
+ text-align: start;
padding-left: 15px;
}
}
@@ -1223,11 +1239,6 @@ a {
.h6 { font-size: 0.9em; }
- .url,
- .email {
- color: @tertiary-color-light;
- }
-
.md, .hr {
color: @tertiary-color-light;
font-style: normal;
@@ -1248,11 +1259,17 @@ a {
text-decoration: line-through
}
+ .url,
+ .email,
.md-underlined-text {
text-decoration: underline;
}
- .img,
+ .linkdef .url {
+ color: @tertiary-color-light;
+ }
+
+ .img,
.imgref {
padding: 0.2em 0.4em;
padding-right: 0;
diff --git a/public/res/utils.js b/public/res/utils.js
index 9b435106..710453f0 100644
--- a/public/res/utils.js
+++ b/public/res/utils.js
@@ -349,6 +349,36 @@ define([
].join(""));
};
+ var $windowElt = $(window);
+ utils.iframe = function(url, width, height) {
+ var $backdropElt = $(utils.createBackdrop());
+ var result = crel('iframe', {
+ src: url,
+ frameborder: 0,
+ class: 'modal-content modal-iframe'
+ });
+ document.body.appendChild(result);
+ function placeIframe() {
+ var actualWidth = window.innerWidth - 20;
+ actualWidth > width && (actualWidth = width);
+ var actualHeight = window.innerHeight - 50;
+ actualHeight > height && (actualHeight = height);
+ result.setAttribute('width', actualWidth);
+ result.setAttribute('height', actualHeight);
+ }
+ placeIframe();
+ $windowElt.on('resize.iframe', placeIframe);
+ function removeIframe() {
+ $backdropElt.off('click.backdrop');
+ $backdropElt[0].removeBackdrop();
+ $windowElt.off('resize.iframe');
+ result.parentNode.removeChild(result);
+ }
+ result.removeIframe = removeIframe;
+ $backdropElt.on('click.backdrop', removeIframe);
+ return result;
+ };
+
// Shows a dialog to force the user to click a button before opening oauth popup
var redirectCallbackConfirm;
var redirectCallbackCancel;
diff --git a/server.js b/server.js
index 87088618..a405c0c9 100644
--- a/server.js
+++ b/server.js
@@ -11,6 +11,7 @@ app.all('*', function(req, res, next) {
res.redirect('https://stackedit.io' + req.url);
}
else {
+ /\.(eot|ttf|woff)$/.test(req.url) && res.header('Access-Control-Allow-Origin', '*');
next();
}
});