Merge branch 'master' into after-delete-select-first

This commit is contained in:
Mo Bitar
2017-03-01 17:36:57 -06:00
committed by GitHub
66 changed files with 1360 additions and 1278 deletions

View File

@@ -15,87 +15,64 @@ angular.module('app.frontend')
bindToController: true,
link:function(scope, elem, attrs, ctrl) {
/**
* Insert 4 spaces when a tab key is pressed,
* only used when inside of the text editor.
* If the shift key is pressed first, this event is
* not fired.
*/
var handleTab = function (event) {
if (!event.shiftKey && event.which == 9) {
event.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
var spaces = " ";
// Insert 4 spaces
this.value = this.value.substring(0, start)
+ spaces + this.value.substring(end);
// Place cursor 4 spaces away from where
// the tab key was pressed
this.selectionStart = this.selectionEnd = start + 4;
}
}
var handler = function(event) {
if (event.ctrlKey || event.metaKey) {
switch (String.fromCharCode(event.which).toLowerCase()) {
case 's':
event.preventDefault();
$timeout(function(){
ctrl.saveNote(event);
});
break;
case 'e':
event.preventDefault();
$timeout(function(){
ctrl.clickedEditNote();
})
break;
case 'm':
event.preventDefault();
$timeout(function(){
ctrl.toggleMarkdown();
})
break;
case 'o':
event.preventDefault();
$timeout(function(){
ctrl.toggleFullScreen();
})
break;
}
}
};
window.addEventListener('keydown', handler);
var element = document.getElementById("note-text-editor");
element.addEventListener('keydown', handleTab);
scope.$on('$destroy', function(){
window.removeEventListener('keydown', handler);
})
scope.$watch('ctrl.note', function(note, oldNote){
if(note) {
ctrl.setNote(note, oldNote);
} else {
ctrl.note = {};
}
});
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, markdownRenderer, $rootScope, extensionManager, syncManager) {
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager) {
window.addEventListener("message", function(event){
if(event.data.status) {
this.postNoteToExternalEditor();
} else {
var id = event.data.id;
var text = event.data.text;
var data = event.data.data;
if(this.note.uuid === id) {
this.note.text = text;
if(data) {
var changesMade = this.customEditor.setData(id, data);
if(changesMade) {
this.customEditor.setDirty(true);
}
}
this.changesMade();
}
}
}.bind(this), false);
this.setNote = function(note, oldNote) {
this.editorMode = 'edit';
var currentEditor = this.customEditor;
this.customEditor = null;
this.showExtensions = false;
this.showMenu = false;
this.loadTagsString();
var setEditor = function(editor) {
this.customEditor = editor;
this.postNoteToExternalEditor();
}.bind(this)
var editor = this.editorForNote(note);
if(editor) {
if(currentEditor !== editor) {
// switch after timeout, so that note data isnt posted to current editor
$timeout(function(){
setEditor(editor);
}.bind(this));
} else {
// switch immediately
setEditor(editor);
}
} else {
this.customEditor = null;
}
if(note.safeText().length == 0 && note.dummy) {
this.focusTitle(100);
}
@@ -109,19 +86,50 @@ angular.module('app.frontend')
}
}
this.hasAvailableExtensions = function() {
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
this.selectedEditor = function(editor) {
this.showEditorMenu = false;
if(this.customEditor && editor !== this.customEditor) {
this.customEditor.removeItemAsRelationship(this.note);
this.customEditor.setDirty(true);
}
if(editor.default) {
this.customEditor = null;
} else {
this.customEditor = editor;
this.customEditor.addItemAsRelationship(this.note);
this.customEditor.setDirty(true);
}
}.bind(this)
this.editorForNote = function(note) {
var editors = modelManager.itemsForContentType("SN|Editor");
for(var editor of editors) {
if(_.includes(editor.notes, note)) {
return editor;
}
}
return null;
}
this.onPreviewDoubleClick = function() {
this.editorMode = 'edit';
this.focusEditor(100);
this.postNoteToExternalEditor = function() {
var externalEditorElement = document.getElementById("editor-iframe");
if(externalEditorElement) {
externalEditorElement.contentWindow.postMessage({text: this.note.text, data: this.customEditor.dataForKey(this.note.uuid), id: this.note.uuid}, '*');
}
}
this.hasAvailableExtensions = function() {
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
}
this.focusEditor = function(delay) {
setTimeout(function(){
var element = document.getElementById("note-text-editor");
element.focus();
if(element) {
element.focus();
}
}, delay)
}
@@ -135,10 +143,6 @@ angular.module('app.frontend')
this.showMenu = false;
}
this.renderedContent = function() {
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.safeText()));
}
var statusTimeout;
this.saveNote = function($event) {
@@ -198,7 +202,6 @@ angular.module('app.frontend')
}
this.onContentFocus = function() {
this.showSampler = false;
$rootScope.$broadcast("editorFocused");
}
@@ -209,12 +212,7 @@ angular.module('app.frontend')
this.toggleFullScreen = function() {
this.fullscreen = !this.fullscreen;
if(this.fullscreen) {
if(this.editorMode == 'edit') {
// refocus
this.focusEditor(0);
}
} else {
this.focusEditor(0);
}
}
@@ -222,19 +220,6 @@ angular.module('app.frontend')
this.showMenu = false;
}
this.toggleMarkdown = function() {
if(this.editorMode == 'preview') {
this.editorMode = 'edit';
this.focusEditor(0);
} else {
this.editorMode = 'preview';
}
}
this.clickedMenu = function() {
this.showMenu = !this.showMenu;
}
this.deleteNote = function() {
if(confirm("Are you sure you want to delete this note?")) {
this.remove()(this.note);

View File

@@ -1,8 +1,31 @@
angular.module('app.frontend')
.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager, authManager) {
$rootScope.bodyClass = "app-body-class";
.controller('HomeCtrl', function ($scope, $stateParams, $rootScope, $timeout, modelManager, syncManager, authManager) {
function autoSignInFromParams() {
if(!authManager.offline()) {
// check if current account
if(syncManager.serverURL == $stateParams.server && authManager.user.email == $stateParams.email) {
// already signed in, return
return;
} else {
// sign out
syncManager.destroyLocalData(function(){
window.location.reload();
})
}
} else {
authManager.login($stateParams.server, $stateParams.email, $stateParams.pw, function(response){
window.location.reload();
})
}
}
if($stateParams.server && $stateParams.email) {
autoSignInFromParams();
}
syncManager.loadLocalItems(function(items) {
$scope.allTag.didLoad = true;
$scope.$apply();
syncManager.sync(null);
@@ -12,7 +35,9 @@ angular.module('app.frontend')
}, 30000);
});
$scope.allTag = new Tag({all: true});
var allTag = new Tag({all: true});
allTag.needsLoad = true;
$scope.allTag = allTag;
$scope.allTag.title = "All";
$scope.tags = modelManager.tags;
$scope.allTag.notes = modelManager.notes;
@@ -31,6 +56,7 @@ angular.module('app.frontend')
modelManager.createRelationshipBetweenItems(note, tag);
}
note.setDirty(true);
syncManager.sync();
}

View File

@@ -18,7 +18,16 @@ angular.module('app.frontend')
link:function(scope, elem, attrs, ctrl) {
scope.$watch('ctrl.tag', function(tag, oldTag){
if(tag) {
ctrl.tagDidChange(tag, oldTag);
if(tag.needsLoad) {
scope.$watch('ctrl.tag.didLoad', function(didLoad){
if(didLoad) {
tag.needsLoad = false;
ctrl.tagDidChange(tag, oldTag);
}
});
} else {
ctrl.tagDidChange(tag, oldTag);
}
}
});
}
@@ -26,6 +35,8 @@ angular.module('app.frontend')
})
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) {
this.sortBy = localStorage.getItem("sortBy") || "created_at";
$rootScope.$on("editorFocused", function(){
this.showMenu = false;
}.bind(this))
@@ -34,8 +45,6 @@ angular.module('app.frontend')
this.selectFirstNote(false);
}.bind(this))
var isFirstLoad = true;
this.notesToDisplay = 20;
this.paginate = function() {
this.notesToDisplay += 20
@@ -49,20 +58,16 @@ angular.module('app.frontend')
}
this.noteFilter.text = "";
this.setNotes(tag.notes);
}
tag.notes.forEach(function(note){
this.setNotes = function(notes) {
notes.forEach(function(note){
note.visible = true;
})
this.selectFirstNote(false);
if(isFirstLoad) {
$timeout(function(){
this.createNewNote();
isFirstLoad = false;
}.bind(this))
} else if(tag.notes.length == 0) {
this.createNewNote();
}
var createNew = notes.length == 0;
this.selectFirstNote(createNew);
}
this.selectedTagDelete = function() {
@@ -71,7 +76,7 @@ angular.module('app.frontend')
}
this.selectFirstNote = function(createNew) {
var visibleNotes = this.tag.notes.filter(function(note){
var visibleNotes = this.sortedNotes.filter(function(note){
return note.visible;
});
@@ -114,4 +119,22 @@ angular.module('app.frontend')
}
}.bind(this), 100)
}
this.selectedMenuItem = function() {
this.showMenu = false;
}
this.selectedSortByCreated = function() {
this.setSortBy("created_at");
}
this.selectedSortByUpdated = function() {
this.setSortBy("updated_at");
}
this.setSortBy = function(type) {
this.sortBy = type;
localStorage.setItem("sortBy", type);
}
});

View File

@@ -37,6 +37,7 @@ class Item {
updateFromJSON(json) {
_.merge(this, json);
if(this.created_at) {
this.created_at = new Date(this.created_at);
this.updated_at = new Date(this.updated_at);
@@ -112,6 +113,10 @@ class Item {
this.setDirty(true);
}
locallyClearAllReferences() {
}
mergeMetadataFromItem(item) {
_.merge(this, _.omit(item, ["content"]));
}

View File

@@ -0,0 +1,89 @@
class Editor extends Item {
constructor(json_obj) {
super(json_obj);
if(!this.notes) {
this.notes = [];
}
if(!this.data) {
this.data = {};
}
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.url = contentObject.url;
this.name = contentObject.name;
this.data = contentObject.data || {};
}
structureParams() {
var params = {
url: this.url,
name: this.name,
data: this.data
};
_.merge(params, super.structureParams());
return params;
}
referenceParams() {
var references = _.map(this.notes, function(note){
return {uuid: note.uuid, content_type: note.content_type};
})
return references;
}
addItemAsRelationship(item) {
if(item.content_type == "Note") {
if(!_.find(this.notes, item)) {
this.notes.push(item);
}
}
super.addItemAsRelationship(item);
}
removeItemAsRelationship(item) {
if(item.content_type == "Note") {
_.pull(this.notes, item);
}
super.removeItemAsRelationship(item);
}
removeAllRelationships() {
super.removeAllRelationships();
this.notes = [];
}
locallyClearAllReferences() {
super.locallyClearAllReferences();
this.notes = [];
}
allReferencedObjects() {
return this.notes;
}
toJSON() {
return {uuid: this.uuid}
}
get content_type() {
return "SN|Editor";
}
setData(key, value) {
var dataHasChanged = JSON.stringify(this.data[key]) !== JSON.stringify(value);
if(dataHasChanged) {
this.data[key] = value;
return true;
}
return false;
}
dataForKey(key) {
return this.data[key] || {};
}
}

View File

@@ -79,6 +79,7 @@ class Extension extends Item {
super.mapContentToLocalProperties(contentObject)
this.name = contentObject.name;
this.url = contentObject.url;
this.supported_types = contentObject.supported_types;
this.actions = contentObject.actions.map(function(action){
return new Action(action);
})
@@ -99,7 +100,8 @@ class Extension extends Item {
var params = {
name: this.name,
url: this.url,
actions: this.actions
actions: this.actions,
supported_types: this.supported_types
};
_.merge(params, super.structureParams());

View File

@@ -56,6 +56,14 @@ class Note extends Item {
this.tags = [];
}
locallyClearAllReferences() {
super.locallyClearAllReferences();
this.tags.forEach(function(tag){
_.pull(tag.notes, this);
}.bind(this))
this.tags = [];
}
isBeingRemovedLocally() {
this.tags.forEach(function(tag){
_.pull(tag.notes, this);

View File

@@ -55,6 +55,15 @@ class Tag extends Item {
this.notes = [];
}
locallyClearAllReferences() {
super.locallyClearAllReferences();
this.notes.forEach(function(note){
_.pull(note.tags, this);
}.bind(this))
this.notes = [];
}
isBeingRemovedLocally() {
this.notes.forEach(function(note){
_.pull(note.tags, this);

View File

@@ -7,7 +7,7 @@ angular.module('app.frontend')
})
.state('home', {
url: '/',
url: '/?server&email&pw',
parent: 'base',
views: {
'content@' : {

View File

@@ -73,13 +73,12 @@ angular.module('app.frontend')
}
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
var mk = keys.mk;
var requestUrl = url + "/auth/sign_in";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = {password: keys.pw, email: email};
_.merge(request, params);
request.post().then(function(response){
this.handleAuthResponse(response, email, url, authParams, mk, keys.pw);
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
callback(response);
}.bind(this))
.catch(function(response){
@@ -91,7 +90,9 @@ angular.module('app.frontend')
}
this.handleAuthResponse = function(response, email, url, authParams, mk, pw) {
localStorage.setItem("server", url);
if(url) {
localStorage.setItem("server", url);
}
localStorage.setItem("user", JSON.stringify(response.plain().user));
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
localStorage.setItem("mk", mk);
@@ -101,13 +102,12 @@ angular.module('app.frontend')
this.register = function(url, email, password, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
var mk = keys.mk;
var requestUrl = url + "/auth";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = _.merge({password: keys.pw, email: email}, authParams);
_.merge(request, params);
request.post().then(function(response){
this.handleAuthResponse(response, email, url, authParams, mk, keys.pw);
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
callback(response);
}.bind(this))
.catch(function(response){
@@ -117,55 +117,26 @@ angular.module('app.frontend')
}.bind(this));
}
// this.changePassword = function(current_password, new_password) {
// this.getAuthParamsForEmail(email, function(authParams){
// if(!authParams) {
// callback(null);
// return;
// }
// Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: current_password, email: user.email}, authParams), function(currentKeys) {
// Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: new_password, email: user.email}, authParams), function(newKeys){
// var data = {};
// data.current_password = currentKeys.pw;
// data.password = newKeys.pw;
// data.password_confirmation = newKeys.pw;
//
// var user = this.user;
//
// this._performPasswordChange(currentKeys, newKeys, function(response){
// if(response && !response.error) {
// // this.showNewPasswordForm = false;
// // reencrypt data with new mk
// this.reencryptAllItemsAndSave(user, newKeys.mk, currentKeys.mk, function(success){
// if(success) {
// this.setMk(newKeys.mk);
// alert("Your password has been changed and your data re-encrypted.");
// } else {
// // rollback password
// this._performPasswordChange(newKeys, currentKeys, function(response){
// alert("There was an error changing your password. Your password has been rolled back.");
// window.location.reload();
// })
// }
// }.bind(this));
// } else {
// // this.showNewPasswordForm = false;
// alert("There was an error changing your password. Please try again.");
// }
// }.bind(this))
// }.bind(this));
// }.bind(this));
// }.bind(this));
// }
this.changePassword = function(email, new_password, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
var requestUrl = localStorage.getItem("server") + "/auth/change_pw";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = _.merge({new_password: keys.pw}, authParams);
_.merge(request, params);
this._performPasswordChange = function(url, email, current_keys, new_keys, callback) {
var requestUrl = url + "/auth";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = {password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email};
_.merge(request, params);
request.patch().then(function(response){
callback(response);
})
request.post().then(function(response){
this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw);
callback(response.plain());
}.bind(this))
.catch(function(response){
var error = response.data;
if(!error) {
error = {message: "Something went wrong while changing your password. Your password was not changed. Please try again."}
}
console.log("Change pw error", response);
callback({error: error});
})
}.bind(this));
}
this.staticifyObject = function(object) {

View File

@@ -102,7 +102,6 @@ class DBManager {
this.openDatabase((db) => {
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
request.onsuccess = function(event) {
console.log("Successfully deleted item", item.uuid);
if(callback) {
callback(true);
}

View File

@@ -0,0 +1,27 @@
angular
.module('app.frontend')
.directive('clickOutside', ['$document', function($document) {
return {
restrict: 'A',
replace: false,
link : function($scope, $element, attrs) {
var didApplyClickOutside = false;
$element.bind('click', function(e) {
didApplyClickOutside = false;
if (attrs.isOpen) {
e.stopPropagation();
}
});
$document.bind('click', function() {
if(!didApplyClickOutside) {
$scope.$apply(attrs.clickOutside);
didApplyClickOutside = true;
}
})
}
}
}]);

View File

@@ -20,11 +20,15 @@ angular
});
function showSpinner() {
if(scope.hidePromise) {
$timeout.cancel(scope.hidePromise);
scope.hidePromise = null;
}
showElement(true);
}
function hideSpinner() {
$timeout(showElement.bind(this, false), getDelay());
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
}
function showElement(show) {

View File

@@ -6,19 +6,15 @@ class AccountMenu {
this.scope = {};
}
controller($scope, authManager, modelManager, syncManager, $timeout) {
controller($scope, authManager, modelManager, syncManager, dbManager, $timeout) {
'ngInject';
$scope.formData = {url: syncManager.serverURL};
$scope.formData = {mergeLocal: true, url: syncManager.serverURL};
$scope.user = authManager.user;
$scope.server = syncManager.serverURL;
$scope.syncStatus = syncManager.syncStatus;
$scope.changePasswordPressed = function() {
$scope.showNewPasswordForm = !$scope.showNewPasswordForm;
}
$scope.encryptionKey = function() {
return syncManager.masterKey;
}
@@ -31,25 +27,61 @@ class AccountMenu {
return `${$scope.server}/dashboard/?server=${$scope.server}&id=${$scope.user.email}&pw=${$scope.serverPassword()}`;
}
$scope.newPasswordData = {};
$scope.showPasswordChangeForm = function() {
$scope.newPasswordData.showForm = true;
}
$scope.submitPasswordChange = function() {
$scope.passwordChangeData.status = "Generating New Keys...";
$timeout(function(){
if(data.password != data.password_confirmation) {
alert("Your new password does not match its confirmation.");
return;
}
if($scope.newPasswordData.newPassword != $scope.newPasswordData.newPasswordConfirmation) {
alert("Your new password does not match its confirmation.");
$scope.newPasswordData.status = null;
return;
}
authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
var email = $scope.user.email;
if(!email) {
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
$scope.newPasswordData.status = null;
return;
}
$scope.newPasswordData.status = "Generating New Keys...";
$scope.newPasswordData.showForm = false;
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
syncManager.sync(function(response){
authManager.changePassword(email, $scope.newPasswordData.newPassword, function(response){
if(response.error) {
alert("There was an error changing your password. Please try again.");
$scope.newPasswordData.status = null;
return;
}
// re-encrypt all items
$scope.newPasswordData.status = "Re-encrypting all items with your new key...";
modelManager.setAllItemsDirty();
syncManager.sync(function(response){
if(response.error) {
alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.")
return;
}
$scope.newPasswordData.status = "Successfully changed password and re-encrypted all items.";
$timeout(function(){
alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.")
$scope.newPasswordData = {};
}, 1000)
});
})
})
}
$scope.loginSubmitPressed = function() {
$scope.formData.status = "Generating Login Keys...";
console.log("logging in with url", $scope.formData.url);
$timeout(function(){
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
if(!response || response.error) {
@@ -66,6 +98,11 @@ class AccountMenu {
}
$scope.submitRegistrationForm = function() {
var confirmation = prompt("Please confirm your password. Note that because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.")
if(confirmation !== $scope.formData.user_password) {
alert("The two passwords you entered do not match. Please try again.");
return;
}
$scope.formData.status = "Generating Account Keys...";
$timeout(function(){
@@ -81,10 +118,32 @@ class AccountMenu {
})
}
$scope.localNotesCount = function() {
return modelManager.filteredNotes.length;
}
$scope.mergeLocalChanged = function() {
if(!$scope.formData.mergeLocal) {
if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) {
$scope.formData.mergeLocal = true;
}
}
}
$scope.onAuthSuccess = function() {
syncManager.markAllItemsDirtyAndSaveOffline(function(){
var block = function() {
window.location.reload();
})
}
if($scope.formData.mergeLocal) {
syncManager.markAllItemsDirtyAndSaveOffline(function(){
block();
})
} else {
dbManager.clearAllItems(function(){
block();
})
}
}
$scope.destroyLocalData = function() {
@@ -116,6 +175,8 @@ class AccountMenu {
$scope.importData = null;
if(!response) {
alert("There was an error importing your data. Please try again.");
} else {
alert("Your data was successfully imported.")
}
})
})
@@ -149,12 +210,11 @@ class AccountMenu {
}
$scope.importJSONData = function(data, password, callback) {
console.log("Importing data", data);
var onDataReady = function() {
var items = modelManager.mapResponseItemsToLocalModels(data.items);
items.forEach(function(item){
item.setDirty(true);
item.deleted = false;
item.markAllReferencesDirty();
})

View File

@@ -11,6 +11,8 @@ class ContextualExtensionsMenu {
controller($scope, modelManager, extensionManager) {
'ngInject';
$scope.renderData = {};
$scope.extensions = _.map(extensionManager.extensionsInContextOfItem($scope.item), function(ext){
return _.cloneDeep(ext);
});
@@ -27,12 +29,30 @@ class ContextualExtensionsMenu {
}
$scope.executeAction = function(action, extension) {
if(action.verb == "nested") {
action.showNestedActions = !action.showNestedActions;
return;
}
action.running = true;
extensionManager.executeAction(action, extension, $scope.item, function(response){
action.running = false;
$scope.handleActionResponse(action, response);
})
}
$scope.handleActionResponse = function(action, response) {
switch (action.verb) {
case "render": {
var item = response.item;
if(item.content_type == "Note") {
$scope.renderData.title = item.title;
$scope.renderData.text = item.text;
$scope.renderData.showRenderModal = true;
}
}
}
}
$scope.accessTypeForExtension = function(extension) {
return extensionManager.extensionUsesEncryptedData(extension) ? "encrypted" : "decrypted";
}

View File

@@ -0,0 +1,73 @@
class EditorMenu {
constructor() {
this.restrict = "E";
this.templateUrl = "frontend/directives/editor-menu.html";
this.scope = {
callback: "&",
selectedEditor: "="
};
}
controller($scope, modelManager, extensionManager, syncManager) {
'ngInject';
$scope.formData = {};
let editorContentType = "SN|Editor";
let defaultEditor = {
default: true,
name: "Plain"
}
$scope.sysEditors = [defaultEditor];
$scope.editors = modelManager.itemsForContentType(editorContentType);
$scope.editorForUrl = function(url) {
return $scope.editors.filter(function(editor){return editor.url == url})[0];
}
$scope.selectEditor = function(editor) {
$scope.callback()(editor);
}
$scope.deleteEditor = function(editor) {
if(confirm("Are you sure you want to delete this editor?")) {
modelManager.setItemToBeDeleted(editor);
syncManager.sync();
_.pull($scope.editors, editor);
}
}
$scope.submitNewEditorRequest = function() {
var editor = createEditor($scope.formData.url);
modelManager.addItem(editor);
editor.setDirty(true);
syncManager.sync();
$scope.editors.push(editor);
$scope.formData = {};
}
function createEditor(url) {
var name = getParameterByName("name", url);
return modelManager.createItem({
content_type: editorContentType,
url: url,
name: name
})
}
function getParameterByName(name, url) {
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
}
}
angular.module('app.frontend').directive('editorMenu', () => new EditorMenu);

View File

@@ -21,7 +21,14 @@ class GlobalExtensionsMenu {
if($scope.newExtensionData.url) {
extensionManager.addExtension($scope.newExtensionData.url, function(response){
if(!response) {
alert("Unable to register this extension. Make sure the link is valid and try again.");
if($scope.newExtensionData.url.indexOf("type=sf") != -1) {
alert("Unable to register this extension. You are attempting to register a Standard File extension in Standard Notes. You should instead open your Standard File Dashboard and register this extension there.");
} else if($scope.newExtensionData.url.indexOf("name=") != -1) {
// user is mistakenly trying to register editor extension, most likely
alert("Unable to register this extension. It looks like you may be trying to install an editor extension. To do that, click 'Editor' under the current note's title.");
} else {
alert("Unable to register this extension. Make sure the link is valid and try again.");
}
} else {
$scope.newExtensionData.url = "";
$scope.showNewExtensionForm = false;

View File

@@ -28,7 +28,7 @@ class ExtensionManager {
extensionsInContextOfItem(item) {
return this.extensions.filter(function(ext){
return ext.actionsWithContextForItem(item).length > 0;
return _.includes(ext.supported_types, item.content_type) || ext.actionsWithContextForItem(item).length > 0;
})
}
@@ -152,9 +152,29 @@ class ExtensionManager {
case "get": {
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
action.error = false;
var items = response.items;
this.modelManager.mapResponseItemsToLocalModels(items);
customCallback(items);
var items = response.items || [response.item];
EncryptionHelper.decryptMultipleItems(items, localStorage.getItem("mk"));
items = this.modelManager.mapResponseItemsToLocalModels(items);
for(var item of items) {
item.setDirty(true);
}
this.syncManager.sync(null);
customCallback({items: items});
}.bind(this))
.catch(function(response){
action.error = true;
customCallback(null);
})
break;
}
case "render": {
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
action.error = false;
EncryptionHelper.decryptItem(response.item, localStorage.getItem("mk"));
var item = this.modelManager.createItem(response.item);
customCallback({item: item});
}.bind(this))
.catch(function(response){
action.error = true;
@@ -281,7 +301,11 @@ class ExtensionManager {
}
outgoingParamsForItem(item, extension) {
var itemParams = new ItemParams(item, this.syncManager.masterKey);
var ek = this.syncManager.masterKey;
if(!this.extensionUsesEncryptedData(extension)) {
ek = null;
}
var itemParams = new ItemParams(item, ek);
return itemParams.paramsForExtension();
}

View File

@@ -1,4 +1,3 @@
// Start from filter
angular.module('app.frontend').filter('startFrom', function() {
return function(input, start) {
return input.slice(start);

View File

@@ -0,0 +1,5 @@
angular.module('app.frontend').filter('trusted', ['$sce', function ($sce) {
return function(url) {
return $sce.trustAsResourceUrl(url);
};
}]);

View File

@@ -60,11 +60,13 @@ class SNCrypto {
}
base64(text) {
return CryptoJS.enc.Utf8.parse(text).toString(CryptoJS.enc.Base64)
// return CryptoJS.enc.Utf8.parse(text).toString(CryptoJS.enc.Base64)
return window.btoa(text);
}
base64Decode(base64String) {
return CryptoJS.enc.Base64.parse(base64String).toString(CryptoJS.enc.Utf8)
// return CryptoJS.enc.Base64.parse(base64String).toString(CryptoJS.enc.Utf8)
return window.atob(base64String);
}
sha256(text) {

View File

@@ -1,14 +1,8 @@
class EncryptionHelper {
static encryptItem(item, key) {
var item_key = null;
if(item.enc_item_key) {
// we reuse the key, but this is optional
item_key = Neeto.crypto.decryptText(item.enc_item_key, key);
} else {
item_key = Neeto.crypto.generateRandomEncryptionKey();
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
}
var item_key = Neeto.crypto.generateRandomEncryptionKey();
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
var ek = Neeto.crypto.firstHalfOfKey(item_key);
var ak = Neeto.crypto.secondHalfOfKey(item_key);

View File

@@ -1,21 +0,0 @@
angular.module('app.frontend')
.service('markdownRenderer', function ($sce) {
marked.setOptions({
breaks: true,
sanitize: true
});
this.renderedContentForText = function(text) {
if(!text || text.length == 0) {
return "";
}
return marked(text);
}
this.renderHtml = function(html_code) {
return $sce.trustAsHtml(html_code);
};
});

View File

@@ -8,6 +8,7 @@ class ModelManager {
this.itemChangeObservers = [];
this.items = [];
this._extensions = [];
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor"];
}
get allItems() {
@@ -58,15 +59,17 @@ class ModelManager {
}
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
var models = [];
var models = [], processedObjects = [];
// first loop should add and process items
for (var json_obj of items) {
json_obj = _.omit(json_obj, omitFields || [])
var item = this.findItem(json_obj["uuid"]);
if(json_obj["deleted"] == true) {
if(item) {
this.removeItemLocally(item)
}
continue;
if(json_obj["deleted"] == true || !_.includes(this.acceptableContentTypes, json_obj["content_type"])) {
if(item) {
this.removeItemLocally(item)
}
continue;
}
_.omit(json_obj, omitFields);
@@ -79,11 +82,16 @@ class ModelManager {
this.addItem(item);
if(json_obj.content) {
this.resolveReferencesForItem(item);
}
models.push(item);
processedObjects.push(json_obj);
}
// second loop should process references
for (var index in processedObjects) {
var json_obj = processedObjects[index];
if(json_obj.content) {
this.resolveReferencesForItem(models[index]);
}
}
this.notifySyncObserversOfModels(models);
@@ -120,6 +128,8 @@ class ModelManager {
item = new Tag(json_obj);
} else if(json_obj.content_type == "Extension") {
item = new Extension(json_obj);
} else if(json_obj.content_type == "SN|Editor") {
item = new Editor(json_obj);
} else {
item = new Item(json_obj);
}
@@ -144,9 +154,7 @@ class ModelManager {
}
} else if(item.content_type == "Note") {
if(!_.find(this.notes, {uuid: item.uuid})) {
this.notes.splice(_.sortedLastIndexBy(this.notes, item, function(item){
return -item.created_at;
}), 0, item);
this.notes.unshift(item);
}
} else if(item.content_type == "Extension") {
if(!_.find(this._extensions, {uuid: item.uuid})) {
@@ -167,11 +175,13 @@ class ModelManager {
}
resolveReferencesForItem(item) {
item.locallyClearAllReferences();
var contentObject = item.contentObject;
if(!contentObject.references) {
return;
}
for(var reference of contentObject.references) {
var referencedItem = this.findItem(reference.uuid);
if(referencedItem) {
@@ -225,6 +235,17 @@ class ModelManager {
item.removeAllRelationships();
}
/* Used when changing encryption key */
setAllItemsDirty() {
var relevantItems = this.allItems.filter(function(item){
return _.includes(this.acceptableContentTypes, item.content_type);
}.bind(this));
for(var item of relevantItems) {
item.setDirty(true);
}
}
removeItemLocally(item, callback) {
_.pull(this.items, item);

View File

@@ -98,10 +98,37 @@ class SyncManager {
return this._cursorToken;
}
get queuedCallbacks() {
if(!this._queuedCallbacks) {
this._queuedCallbacks = [];
}
return this._queuedCallbacks;
}
clearQueuedCallbacks() {
this._queuedCallbacks = [];
}
callQueuedCallbacksAndCurrent(currentCallback, response) {
var allCallbacks = this.queuedCallbacks;
if(currentCallback) {
allCallbacks.push(currentCallback);
}
if(allCallbacks.length) {
for(var eachCallback of allCallbacks) {
eachCallback(response);
}
this.clearQueuedCallbacks();
}
}
sync(callback, options = {}) {
if(this.syncStatus.syncOpInProgress) {
this.repeatOnCompletion = true;
if(callback) {
this.queuedCallbacks.push(callback);
}
console.log("Sync op in progress; returning.");
return;
}
@@ -116,18 +143,17 @@ class SyncManager {
return;
}
var isContinuationSync = this.needsMoreSync;
var isContinuationSync = this.syncStatus.needsMoreSync;
this.repeatOnCompletion = false;
this.syncStatus.syncOpInProgress = true;
let submitLimit = 100;
var subItems = allDirtyItems.slice(0, submitLimit);
if(subItems.length < allDirtyItems.length) {
// more items left to be synced, repeat
this.needsMoreSync = true;
this.syncStatus.needsMoreSync = true;
} else {
this.needsMoreSync = false;
this.syncStatus.needsMoreSync = false;
}
if(!isContinuationSync) {
@@ -153,12 +179,14 @@ class SyncManager {
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
// merge only metadata for saved items
// we write saved items to disk now because it clears their dirty status then saves
// if we saved items before completion, we had have to save them as dirty and save them again on success as clean
var omitFields = ["content", "auth_hash"];
var saved = this.handleItemsResponse(response.saved_items, omitFields);
this.handleUnsavedItemsResponse(response.unsaved)
this.writeItemsToLocalStorage(saved, false, null);
this.writeItemsToLocalStorage(retrieved, false, null);
@@ -169,14 +197,17 @@ class SyncManager {
this.syncToken = response.sync_token;
this.cursorToken = response.cursor_token;
if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) {
if(this.cursorToken || this.syncStatus.needsMoreSync) {
setTimeout(function () {
this.sync(callback, options);
}.bind(this), 10); // wait 10ms to allow UI to update
} else if(this.repeatOnCompletion) {
this.repeatOnCompletion = false;
setTimeout(function () {
this.sync(callback, options);
}.bind(this), 10); // wait 10ms to allow UI to update
} else {
if(callback) {
callback(response);
}
this.callQueuedCallbacksAndCurrent(callback, response);
}
}.bind(this))
@@ -190,9 +221,7 @@ class SyncManager {
this.$rootScope.$broadcast("sync:error", error);
if(callback) {
callback({error: "Sync error"});
}
this.callQueuedCallbacksAndCurrent(callback, {error: "Sync error"});
}.bind(this))
}
@@ -211,7 +240,7 @@ class SyncManager {
var item = this.modelManager.findItem(itemResponse.uuid);
var error = mapping.error;
if(error.tag == "uuid_conflict") {
// uuid conflicts can occur if a user attempts to import an old data archive with uuids form the old account into a new account
// uuid conflicts can occur if a user attempts to import an old data archive with uuids from the old account into a new account
this.modelManager.alternateUUIDForItem(item, handleNext);
}
++i;