V2
This commit is contained in:
@@ -14,3 +14,25 @@ if(!IEOrEdge && (window.crypto && window.crypto.subtle)) {
|
||||
}
|
||||
|
||||
angular.module('app.frontend', [])
|
||||
|
||||
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, " "));
|
||||
}
|
||||
|
||||
function parametersFromURL(url) {
|
||||
url = url.split("?").slice(-1)[0];
|
||||
var obj = {};
|
||||
url.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
|
||||
obj[decodeURIComponent(key)] = decodeURIComponent(value);
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
function isDesktopApplication() {
|
||||
return window && window.process && window.process.type && window.process.versions["electron"];
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
class BaseCtrl {
|
||||
constructor($rootScope, $scope, syncManager, dbManager, analyticsManager, componentManager) {
|
||||
dbManager.openDatabase(null, function(){
|
||||
// new database, delete syncToken so that items can be refetched entirely from server
|
||||
syncManager.clearSyncToken();
|
||||
syncManager.sync();
|
||||
})
|
||||
|
||||
$scope.onUpdateAvailable = function(version) {
|
||||
$rootScope.$broadcast('new-update-available', version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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, " "));
|
||||
}
|
||||
|
||||
function parametersFromURL(url) {
|
||||
url = url.split("?").slice(-1)[0];
|
||||
var obj = {};
|
||||
url.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
|
||||
obj[decodeURIComponent(key)] = decodeURIComponent(value);
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
angular.module('app.frontend').controller('BaseCtrl', BaseCtrl);
|
||||
@@ -37,7 +37,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager) {
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager, storageManager) {
|
||||
|
||||
this.componentManager = componentManager;
|
||||
this.componentStack = [];
|
||||
@@ -50,6 +50,10 @@ angular.module('app.frontend')
|
||||
this.syncTakingTooLong = true;
|
||||
}.bind(this));
|
||||
|
||||
$rootScope.$on("sync:completed", function(){
|
||||
this.syncTakingTooLong = false;
|
||||
}.bind(this));
|
||||
|
||||
$rootScope.$on("tag-changed", function(){
|
||||
this.loadTagsString();
|
||||
}.bind(this));
|
||||
@@ -291,7 +295,7 @@ angular.module('app.frontend')
|
||||
if(success) {
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
var status = "All changes saved"
|
||||
var status = "All changes saved";
|
||||
if(authManager.offline()) {
|
||||
status += " (offline)";
|
||||
}
|
||||
@@ -368,12 +372,34 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.togglePin = function() {
|
||||
this.note.setAppDataItem("pinned", !this.note.pinned);
|
||||
this.note.setDirty(true);
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.toggleArchiveNote = function() {
|
||||
this.note.setAppDataItem("archived", !this.note.archived);
|
||||
this.note.setDirty(true);
|
||||
this.changesMade();
|
||||
$rootScope.$broadcast("noteArchived");
|
||||
}
|
||||
|
||||
this.clickedEditNote = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Tags
|
||||
*/
|
||||
|
||||
this.loadTagsString = function() {
|
||||
var string = "";
|
||||
@@ -419,16 +445,23 @@ angular.module('app.frontend')
|
||||
this.updateTags()(this.note, tags);
|
||||
}
|
||||
|
||||
/* Components */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Components
|
||||
*/
|
||||
|
||||
let alertKey = "displayed-component-disable-alert";
|
||||
|
||||
this.disableComponent = function(component) {
|
||||
componentManager.disableComponentForItem(component, this.note);
|
||||
componentManager.setEventFlowForComponent(component, false);
|
||||
if(!localStorage.getItem(alertKey)) {
|
||||
if(!storageManager.getItem(alertKey)) {
|
||||
alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane.");
|
||||
localStorage.setItem(alertKey, true);
|
||||
storageManager.setItem(alertKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +488,19 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Editor Customization
|
||||
*/
|
||||
|
||||
this.onSystemEditorLoad = function() {
|
||||
if(this.loadedTabListener) {
|
||||
return;
|
||||
|
||||
@@ -22,7 +22,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, syncManager) {
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, syncManager, storageManager, passcodeManager) {
|
||||
|
||||
this.user = authManager.user;
|
||||
|
||||
@@ -31,7 +31,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
this.updateOfflineStatus();
|
||||
|
||||
if(this.offline) {
|
||||
if(this.offline && !passcodeManager.hasPasscode()) {
|
||||
this.showAccountMenu = true;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
this.findErrors();
|
||||
|
||||
this.onAuthSuccess = function() {
|
||||
this.showAccountMenu = false;
|
||||
}.bind(this)
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.serverData = {};
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
@@ -61,6 +65,14 @@ angular.module('app.frontend')
|
||||
this.showAccountMenu = false;
|
||||
}
|
||||
|
||||
this.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
this.lockApp = function() {
|
||||
$rootScope.lockApplication();
|
||||
}
|
||||
|
||||
this.refreshData = function() {
|
||||
this.isRefreshing = true;
|
||||
syncManager.sync(function(response){
|
||||
|
||||
@@ -1,56 +1,86 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, syncManager, authManager, themeManager) {
|
||||
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
|
||||
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager) {
|
||||
|
||||
function urlParam(key) {
|
||||
return $location.search()[key];
|
||||
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
|
||||
|
||||
$scope.onUpdateAvailable = function(version) {
|
||||
$rootScope.$broadcast('new-update-available', version);
|
||||
}
|
||||
|
||||
function autoSignInFromParams() {
|
||||
var server = urlParam("server");
|
||||
var email = urlParam("email");
|
||||
var pw = urlParam("pw");
|
||||
|
||||
if(!authManager.offline()) {
|
||||
// check if current account
|
||||
if(syncManager.serverURL === server && authManager.user.email === email) {
|
||||
// already signed in, return
|
||||
return;
|
||||
} else {
|
||||
// sign out
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
} else {
|
||||
authManager.login(server, email, pw, function(response){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
$rootScope.lockApplication = function() {
|
||||
// Render first to show lock screen immediately, then refresh
|
||||
$scope.needsUnlock = true;
|
||||
// Reloading wipes current objects from memory
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if(urlParam("server")) {
|
||||
autoSignInFromParams();
|
||||
function load() {
|
||||
// pass keys to storageManager to decrypt storage
|
||||
storageManager.setKeys(passcodeManager.keys());
|
||||
|
||||
openDatabase();
|
||||
// Retrieve local data and begin sycing timer
|
||||
initiateSync();
|
||||
// Configure "All" psuedo-tag
|
||||
loadAllTag();
|
||||
// Configure "Archived" psuedo-tag
|
||||
loadArchivedTag();
|
||||
}
|
||||
|
||||
syncManager.loadLocalItems(function(items) {
|
||||
$scope.allTag.didLoad = true;
|
||||
themeManager.activateInitialTheme();
|
||||
$scope.$apply();
|
||||
if(passcodeManager.isLocked()) {
|
||||
$scope.needsUnlock = true;
|
||||
} else {
|
||||
load();
|
||||
}
|
||||
|
||||
$scope.onSuccessfulUnlock = function() {
|
||||
$timeout(() => {
|
||||
$scope.needsUnlock = false;
|
||||
load();
|
||||
})
|
||||
}
|
||||
|
||||
function openDatabase() {
|
||||
dbManager.setLocked(false);
|
||||
dbManager.openDatabase(null, function() {
|
||||
// new database, delete syncToken so that items can be refetched entirely from server
|
||||
syncManager.clearSyncToken();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
function initiateSync() {
|
||||
authManager.loadInitialData();
|
||||
syncManager.loadLocalItems(function(items) {
|
||||
$scope.allTag.didLoad = true;
|
||||
themeManager.activateInitialTheme();
|
||||
$scope.$apply();
|
||||
|
||||
|
||||
syncManager.sync(null);
|
||||
// refresh every 30s
|
||||
setInterval(function () {
|
||||
syncManager.sync(null);
|
||||
}, 30000);
|
||||
});
|
||||
// refresh every 30s
|
||||
setInterval(function () {
|
||||
syncManager.sync(null);
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
function loadAllTag() {
|
||||
var allTag = new Tag({all: true, title: "All"});
|
||||
allTag.needsLoad = true;
|
||||
$scope.allTag = allTag;
|
||||
$scope.tags = modelManager.tags;
|
||||
$scope.allTag.notes = modelManager.notes;
|
||||
}
|
||||
|
||||
function loadArchivedTag() {
|
||||
var archiveTag = new Tag({archiveTag: true, title: "Archived"});
|
||||
$scope.archiveTag = archiveTag;
|
||||
$scope.archiveTag.notes = modelManager.notes;
|
||||
}
|
||||
|
||||
/*
|
||||
Editor Callbacks
|
||||
@@ -116,6 +146,7 @@ angular.module('app.frontend')
|
||||
tag.setDirty(true);
|
||||
syncManager.sync(callback);
|
||||
$rootScope.$broadcast("tag-changed");
|
||||
modelManager.resortTag(tag);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -140,7 +171,7 @@ angular.module('app.frontend')
|
||||
$scope.notesAddNew = function(note) {
|
||||
modelManager.addItem(note);
|
||||
|
||||
if(!$scope.selectedTag.all) {
|
||||
if(!$scope.selectedTag.all && !$scope.selectedTag.archiveTag) {
|
||||
modelManager.createRelationshipBetweenItems($scope.selectedTag, note);
|
||||
}
|
||||
}
|
||||
@@ -210,4 +241,39 @@ angular.module('app.frontend')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Handle Auto Sign In From URL
|
||||
|
||||
function urlParam(key) {
|
||||
return $location.search()[key];
|
||||
}
|
||||
|
||||
function autoSignInFromParams() {
|
||||
var server = urlParam("server");
|
||||
var email = urlParam("email");
|
||||
var pw = urlParam("pw");
|
||||
|
||||
if(!authManager.offline()) {
|
||||
// check if current account
|
||||
if(syncManager.serverURL === server && authManager.user.email === email) {
|
||||
// already signed in, return
|
||||
return;
|
||||
} else {
|
||||
// sign out
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
} else {
|
||||
authManager.login(server, email, pw, false, function(response){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(urlParam("server")) {
|
||||
autoSignInFromParams();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class LockScreen {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/lock-screen.html";
|
||||
this.scope = {
|
||||
onSuccess: "&",
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, passcodeManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.submitPasscodeForm = function() {
|
||||
passcodeManager.unlock($scope.formData.passcode, (success) => {
|
||||
if(!success) {
|
||||
alert("Invalid passcode. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.onSuccess()();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('lockScreen', () => new LockScreen);
|
||||
@@ -31,9 +31,9 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) {
|
||||
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager) {
|
||||
|
||||
this.sortBy = localStorage.getItem("sortBy") || "created_at";
|
||||
this.sortBy = storageManager.getItem("sortBy") || "created_at";
|
||||
this.sortDescending = this.sortBy != "title";
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
@@ -44,11 +44,26 @@ angular.module('app.frontend')
|
||||
this.selectFirstNote(false);
|
||||
}.bind(this))
|
||||
|
||||
$rootScope.$on("noteArchived", function() {
|
||||
this.selectFirstNote(false);
|
||||
}.bind(this))
|
||||
|
||||
this.notesToDisplay = 20;
|
||||
this.paginate = function() {
|
||||
this.notesToDisplay += 20
|
||||
}
|
||||
|
||||
this.sortByTitle = function() {
|
||||
var base = "Sort |";
|
||||
if(this.sortBy == "created_at") {
|
||||
return base + " Date added";
|
||||
} else if(this.sortBy == "updated_at") {
|
||||
return base + " Date modifed";
|
||||
} else if(this.sortBy == "title") {
|
||||
return base + " Title";
|
||||
}
|
||||
}
|
||||
|
||||
this.tagDidChange = function(tag, oldTag) {
|
||||
this.showMenu = false;
|
||||
|
||||
@@ -98,6 +113,14 @@ angular.module('app.frontend')
|
||||
this.noteFilter = {text : ''};
|
||||
|
||||
this.filterNotes = function(note) {
|
||||
if(this.tag.archiveTag) {
|
||||
return note.archived;
|
||||
}
|
||||
|
||||
if(note.archived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var filterText = this.noteFilter.text.toLowerCase();
|
||||
if(filterText.length == 0) {
|
||||
note.visible = true;
|
||||
@@ -139,7 +162,7 @@ angular.module('app.frontend')
|
||||
|
||||
this.setSortBy = function(type) {
|
||||
this.sortBy = type;
|
||||
localStorage.setItem("sortBy", type);
|
||||
storageManager.setItem("sortBy", type);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ angular.module('app.frontend')
|
||||
save: "&",
|
||||
tags: "=",
|
||||
allTag: "=",
|
||||
archiveTag: "=",
|
||||
updateNoteTag: "&",
|
||||
removeTag: "&"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
let AppDomain = "org.standardnotes.sn";
|
||||
var dateFormatter;
|
||||
|
||||
class Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
|
||||
constructor(json_obj = {}) {
|
||||
this.appData = {};
|
||||
this.updateFromJSON(json_obj);
|
||||
|
||||
this.observers = [];
|
||||
|
||||
if(!this.uuid) {
|
||||
@@ -30,7 +32,7 @@ class Item {
|
||||
try {
|
||||
return JSON.parse(this.content);
|
||||
} catch (e) {
|
||||
console.log("Error parsing json", e);
|
||||
console.log("Error parsing json", e, this);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -81,7 +83,10 @@ class Item {
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObj) {
|
||||
|
||||
this.appData = contentObj.appData;
|
||||
if(!this.appData) {
|
||||
this.appData = {};
|
||||
}
|
||||
}
|
||||
|
||||
createContentJSONFromProperties() {
|
||||
@@ -93,7 +98,10 @@ class Item {
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
return {references: this.referenceParams()}
|
||||
return {
|
||||
references: this.referenceParams(),
|
||||
appData: this.appData
|
||||
}
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
@@ -137,4 +145,71 @@ class Item {
|
||||
doNotEncrypt() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
App Data
|
||||
*/
|
||||
|
||||
setAppDataItem(key, value) {
|
||||
var data = this.appData[AppDomain];
|
||||
if(!data) {
|
||||
data = {}
|
||||
}
|
||||
data[key] = value;
|
||||
this.appData[AppDomain] = data;
|
||||
}
|
||||
|
||||
getAppDataItem(key) {
|
||||
var data = this.appData[AppDomain];
|
||||
if(data) {
|
||||
return data[key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get pinned() {
|
||||
return this.getAppDataItem("pinned");
|
||||
}
|
||||
|
||||
get archived() {
|
||||
return this.getAppDataItem("archived");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Dates
|
||||
*/
|
||||
|
||||
createdAtString() {
|
||||
return this.dateToLocalizedString(this.created_at);
|
||||
}
|
||||
|
||||
updatedAtString() {
|
||||
return this.dateToLocalizedString(this.updated_at);
|
||||
}
|
||||
|
||||
dateToLocalizedString(date) {
|
||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||
if (!dateFormatter) {
|
||||
var locale = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;
|
||||
dateFormatter = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
weekday: 'long',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
return dateFormatter.format(date);
|
||||
} else {
|
||||
// IE < 11, Safari <= 9.0.
|
||||
// In English, this generates the string most similar to
|
||||
// the toLocaleDateString() result above.
|
||||
return date.toDateString() + ' ' + date.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,15 @@ class SyncAdapter extends Item {
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
// There was a bug with the way Base64 content was parsed in previous releases related to this item.
|
||||
// The bug would not parse the JSON behind the base64 string and thus saved data in an invalid format.
|
||||
// This is the line: https://github.com/standardnotes/web/commit/1ad0bf73d8e995b7588854f1b1e4e4a02303a42f#diff-15753bac364782a3a5876032bcdbf99aR76
|
||||
// We'll remedy this for affected users by trying to parse the content string
|
||||
if(typeof this.content !== 'object') {
|
||||
try {
|
||||
this.content = JSON.parse(this.content);
|
||||
} catch (e) {}
|
||||
}
|
||||
var params = this.content || {};
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
|
||||
@@ -106,4 +106,8 @@ class Note extends Item {
|
||||
get content_type() {
|
||||
return "Note";
|
||||
}
|
||||
|
||||
tagsString() {
|
||||
return Tag.arrayToDisplayString(this.tags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +86,14 @@ class Tag extends Item {
|
||||
allReferencedObjects() {
|
||||
return this.notes;
|
||||
}
|
||||
|
||||
static arrayToDisplayString(tags, includeComma) {
|
||||
return tags.map(function(tag, i){
|
||||
var text = "#" + tag.title;
|
||||
if(i != tags.length - 1) {
|
||||
text += includeComma ? ", " : " ";
|
||||
}
|
||||
return text;
|
||||
}).join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class EncryptedStorage extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObject) {
|
||||
super.mapContentToLocalProperties(contentObject)
|
||||
this.storage = contentObject.storage;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
storage: this.storage,
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|EncryptedStorage";
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ class ItemParams {
|
||||
constructor(item, keys, version) {
|
||||
this.item = item;
|
||||
this.keys = keys;
|
||||
this.version = version;
|
||||
this.version = version || "002";
|
||||
}
|
||||
|
||||
paramsForExportFile() {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
angular.module('app.frontend')
|
||||
.config(function ($locationProvider) {
|
||||
|
||||
var runningInElectron = window && window.process && window.process.type && window.process.versions["electron"];
|
||||
if(!runningInElectron) {
|
||||
if(!isDesktopApplication()) {
|
||||
if (window.history && window.history.pushState) {
|
||||
$locationProvider.html5Mode({
|
||||
enabled: true,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
class AnalyticsManager {
|
||||
|
||||
constructor(authManager) {
|
||||
this.authManager = authManager;
|
||||
|
||||
var status = localStorage.getItem("analyticsEnabled");
|
||||
if(status === null) {
|
||||
this.enabled = false;
|
||||
} else {
|
||||
this.enabled = JSON.parse(status);
|
||||
}
|
||||
|
||||
if(this.enabled === true) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
setStatus(enabled) {
|
||||
this.enabled = enabled;
|
||||
localStorage.setItem("analyticsEnabled", JSON.stringify(enabled));
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
toggleStatus() {
|
||||
this.setStatus(!this.enabled);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// load analytics
|
||||
window._paq = window._paq || [];
|
||||
|
||||
(function() {
|
||||
var u="https://piwik.standardnotes.org/";
|
||||
window._paq.push(['setTrackerUrl', u+'piwik.php']);
|
||||
window._paq.push(['setSiteId', '2']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.id="piwik", g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
|
||||
var analyticsId = this.authManager.getUserAnalyticsId();
|
||||
if(analyticsId) {
|
||||
window._paq.push(['setUserId', analyticsId]);
|
||||
}
|
||||
window._paq.push(['trackPageView', "AppInterface"]);
|
||||
window._paq.push(['enableLinkTracking']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('analyticsManager', AnalyticsManager);
|
||||
@@ -7,49 +7,62 @@ angular.module('app.frontend')
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager);
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager);
|
||||
}
|
||||
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager) {
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
|
||||
var userData = localStorage.getItem("user");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
var idData = localStorage.getItem("uuid");
|
||||
if(idData) {
|
||||
this.user = {uuid: idData};
|
||||
this.loadInitialData = function() {
|
||||
var userData = storageManager.getItem("user");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
var idData = storageManager.getItem("uuid");
|
||||
if(idData) {
|
||||
this.user = {uuid: idData};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.getUserAnalyticsId = function() {
|
||||
if(!this.user || !this.user.uuid) {
|
||||
return null;
|
||||
}
|
||||
// anonymize user id irreversably
|
||||
return Neeto.crypto.hmac256(this.user.uuid, Neeto.crypto.sha256(localStorage.getItem("pw")));
|
||||
}
|
||||
|
||||
this.offline = function() {
|
||||
return !this.user;
|
||||
}
|
||||
|
||||
this.isEphemeralSession = function() {
|
||||
if(this.ephemeral == null || this.ephemeral == undefined) {
|
||||
this.ephemeral = JSON.parse(storageManager.getItem("ephemeral", StorageManager.Fixed));
|
||||
}
|
||||
return this.ephemeral;
|
||||
}
|
||||
|
||||
this.setEphemeral = function(ephemeral) {
|
||||
this.ephemeral = ephemeral;
|
||||
if(ephemeral) {
|
||||
storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Ephemeral);
|
||||
} else {
|
||||
storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
|
||||
this.getAuthParams = function() {
|
||||
if(!this._authParams) {
|
||||
this._authParams = JSON.parse(localStorage.getItem("auth_params"));
|
||||
this._authParams = JSON.parse(storageManager.getItem("auth_params"));
|
||||
}
|
||||
return this._authParams;
|
||||
}
|
||||
|
||||
this.keys = function() {
|
||||
var mk = localStorage.getItem("mk");
|
||||
if(!mk) {
|
||||
return null;
|
||||
if(!this._keys) {
|
||||
var mk = storageManager.getItem("mk");
|
||||
if(!mk) {
|
||||
return null;
|
||||
}
|
||||
this._keys = {mk: mk, ak: storageManager.getItem("ak")};
|
||||
}
|
||||
var keys = {mk: mk, ak: localStorage.getItem("ak")};
|
||||
return keys;
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
this.protocolVersion = function() {
|
||||
@@ -99,7 +112,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.login = function(url, email, password, callback) {
|
||||
this.login = function(url, email, password, ephemeral, callback) {
|
||||
this.getAuthParamsForEmail(url, email, function(authParams){
|
||||
|
||||
if(!authParams || !authParams.pw_cost) {
|
||||
@@ -134,7 +147,11 @@ angular.module('app.frontend')
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = {password: keys.pw, email: email};
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.setEphemeral(ephemeral);
|
||||
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
|
||||
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
console.error("Error logging in", response);
|
||||
@@ -148,11 +165,16 @@ angular.module('app.frontend')
|
||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||
try {
|
||||
if(url) {
|
||||
localStorage.setItem("server", url);
|
||||
storageManager.setItem("server", url);
|
||||
}
|
||||
localStorage.setItem("user", JSON.stringify(response.user));
|
||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
||||
localStorage.setItem("jwt", response.token);
|
||||
|
||||
this.user = response.user;
|
||||
storageManager.setItem("user", JSON.stringify(response.user));
|
||||
|
||||
this._authParams = authParams;
|
||||
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||
|
||||
storageManager.setItem("jwt", response.token);
|
||||
this.saveKeys(keys);
|
||||
} catch(e) {
|
||||
dbManager.displayOfflineAlert();
|
||||
@@ -160,18 +182,24 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.saveKeys = function(keys) {
|
||||
localStorage.setItem("pw", keys.pw);
|
||||
localStorage.setItem("mk", keys.mk);
|
||||
localStorage.setItem("ak", keys.ak);
|
||||
this._keys = keys;
|
||||
storageManager.setItem("pw", keys.pw);
|
||||
storageManager.setItem("mk", keys.mk);
|
||||
storageManager.setItem("ak", keys.ak);
|
||||
}
|
||||
|
||||
this.register = function(url, email, password, callback) {
|
||||
this.register = function(url, email, password, ephemeral, callback) {
|
||||
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
||||
var requestUrl = url + "/auth";
|
||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.setEphemeral(ephemeral);
|
||||
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
|
||||
storageManager.setModelStorageMode(ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed);
|
||||
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
console.error("Registration error", response);
|
||||
@@ -182,7 +210,7 @@ angular.module('app.frontend')
|
||||
|
||||
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 requestUrl = storageManager.getItem("server") + "/auth/change_pw";
|
||||
var params = _.merge({new_password: keys.pw}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
@@ -200,10 +228,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.updateAuthParams = function(authParams, callback) {
|
||||
var requestUrl = localStorage.getItem("server") + "/auth/update";
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/update";
|
||||
var params = authParams;
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
localStorage.setItem("auth_params", JSON.stringify(authParams));
|
||||
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
@@ -246,6 +274,8 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.signOut = function() {
|
||||
this._keys = null;
|
||||
this.user = null;
|
||||
this._authParams = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
class DBManager {
|
||||
|
||||
constructor() {
|
||||
this.locked = true;
|
||||
}
|
||||
|
||||
displayOfflineAlert() {
|
||||
var message = "There was an issue loading your offline database. This could happen for two reasons:";
|
||||
message += "\n\n1. You're in a private window in your browser. We can't save your data without access to the local database. Please use a non-private window.";
|
||||
@@ -7,7 +11,15 @@ class DBManager {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
setLocked(locked) {
|
||||
this.locked = locked;
|
||||
}
|
||||
|
||||
openDatabase(callback, onUgradeNeeded) {
|
||||
if(this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = window.indexedDB.open("standardnotes", 1);
|
||||
|
||||
request.onerror = function(event) {
|
||||
@@ -57,7 +69,7 @@ class DBManager {
|
||||
};
|
||||
}
|
||||
|
||||
getAllItems(callback) {
|
||||
getAllModels(callback) {
|
||||
this.openDatabase((db) => {
|
||||
var objectStore = db.transaction("items").objectStore("items");
|
||||
var items = [];
|
||||
@@ -74,11 +86,11 @@ class DBManager {
|
||||
}, null)
|
||||
}
|
||||
|
||||
saveItem(item) {
|
||||
this.saveItems([item]);
|
||||
saveModel(item) {
|
||||
this.saveModels([item]);
|
||||
}
|
||||
|
||||
saveItems(items, callback) {
|
||||
saveModels(items, callback) {
|
||||
|
||||
if(items.length == 0) {
|
||||
if(callback) {
|
||||
@@ -115,7 +127,7 @@ class DBManager {
|
||||
}, null)
|
||||
}
|
||||
|
||||
deleteItem(item, callback) {
|
||||
deleteModel(item, callback) {
|
||||
this.openDatabase((db) => {
|
||||
var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
|
||||
request.onsuccess = function(event) {
|
||||
@@ -126,26 +138,17 @@ class DBManager {
|
||||
}, null)
|
||||
}
|
||||
|
||||
getItemByUUID(uuid, callback) {
|
||||
this.openDatabase((db) => {
|
||||
var request = db.transaction("items", "readonly").objectStore("items").get(uuid);
|
||||
request.onsuccess = function(event) {
|
||||
callback(event.result);
|
||||
};
|
||||
}, null);
|
||||
}
|
||||
|
||||
clearAllItems(callback) {
|
||||
clearAllModels(callback) {
|
||||
var deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
|
||||
|
||||
deleteRequest.onerror = function(event) {
|
||||
console.log("Error deleting database.");
|
||||
callback();
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
deleteRequest.onsuccess = function(event) {
|
||||
console.log("Database deleted successfully");
|
||||
callback();
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
deleteRequest.onblocked = function(event) {
|
||||
|
||||
@@ -3,18 +3,19 @@ class AccountMenu {
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu.html";
|
||||
this.scope = {};
|
||||
this.scope = {
|
||||
"onSuccessfulAuth" : "&"
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, authManager, modelManager, syncManager, dbManager, analyticsManager, $timeout) {
|
||||
controller($scope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL};
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
$scope.analyticsManager = analyticsManager;
|
||||
|
||||
$scope.encryptionKey = function() {
|
||||
return authManager.keys().mk;
|
||||
@@ -29,7 +30,7 @@ class AccountMenu {
|
||||
}
|
||||
|
||||
$scope.dashboardURL = function() {
|
||||
return `${$scope.server}/dashboard/?server=${$scope.server}&id=${encodeURIComponent($scope.user.email)}&pw=${$scope.serverPassword()}`;
|
||||
return `${$scope.server}/dashboard/#server=${$scope.server}&id=${encodeURIComponent($scope.user.email)}&pw=${$scope.serverPassword()}`;
|
||||
}
|
||||
|
||||
$scope.newPasswordData = {};
|
||||
@@ -95,7 +96,7 @@ class AccountMenu {
|
||||
$scope.login = function() {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
@@ -120,7 +121,7 @@ class AccountMenu {
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral ,function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
@@ -132,10 +133,6 @@ 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?")) {
|
||||
@@ -146,18 +143,20 @@ class AccountMenu {
|
||||
|
||||
$scope.onAuthSuccess = function() {
|
||||
var block = function() {
|
||||
window.location.reload();
|
||||
$timeout(function(){
|
||||
$scope.onSuccessfulAuth()();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
if($scope.formData.mergeLocal) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
block();
|
||||
})
|
||||
}, true)
|
||||
} else {
|
||||
dbManager.clearAllItems(function(){
|
||||
$timeout(function(){
|
||||
block();
|
||||
})
|
||||
modelManager.resetLocalMemory();
|
||||
storageManager.clearAllModels(function(){
|
||||
block();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -206,26 +205,25 @@ class AccountMenu {
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
try {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
$scope.encryptionStatusForNotes = function() {
|
||||
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
||||
return items.length + "/" + items.length + " notes and tags encrypted";
|
||||
}
|
||||
|
||||
$scope.importJSONData = function(data, password, callback) {
|
||||
var onDataReady = function() {
|
||||
var items = modelManager.mapResponseItemsToLocalModels(data.items);
|
||||
@@ -424,6 +422,103 @@ class AccountMenu {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Encryption Status
|
||||
*/
|
||||
|
||||
$scope.notesAndTagsCount = function() {
|
||||
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
||||
return items.length;
|
||||
}
|
||||
|
||||
$scope.encryptionStatusForNotes = function() {
|
||||
var length = $scope.notesAndTagsCount();
|
||||
return length + "/" + length + " notes and tags encrypted";
|
||||
}
|
||||
|
||||
$scope.encryptionEnabled = function() {
|
||||
return passcodeManager.hasPasscode() || !authManager.offline();
|
||||
}
|
||||
|
||||
$scope.encryptionSource = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "Account keys";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Local Passcode";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.encryptionStatusString = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "End-to-end encryption is enabled. Your data is encrypted before being synced to your private account.";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Encryption is enabled. Your data is encrypted using your passcode before being stored on disk.";
|
||||
} else {
|
||||
return "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Passcode Lock
|
||||
*/
|
||||
|
||||
$scope.passcodeOptionAvailable = function() {
|
||||
// If you're signed in with an ephemeral session, passcode lock is unavailable
|
||||
return authManager.offline() || !authManager.isEphemeralSession();
|
||||
}
|
||||
|
||||
$scope.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
$scope.addPasscodeClicked = function() {
|
||||
$scope.formData.showPasscodeForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasscodeForm = function() {
|
||||
var passcode = $scope.formData.passcode;
|
||||
if(passcode !== $scope.formData.confirmPasscode) {
|
||||
alert("The two passcodes you entered do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
passcodeManager.setPasscode(passcode, () => {
|
||||
$timeout(function(){
|
||||
$scope.formData.showPasscodeForm = false;
|
||||
var offline = authManager.offline();
|
||||
|
||||
var message = "You've succesfully set an app passcode.";
|
||||
if(offline) { message += " Your items will now be encrypted using this passcode."; }
|
||||
alert(message);
|
||||
|
||||
if(offline) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.removePasscodePressed = function() {
|
||||
var signedIn = !authManager.offline();
|
||||
var message = "Are you sure you want to remove your local passcode?";
|
||||
if(!signedIn) {
|
||||
message += " This will remove encryption from your local data.";
|
||||
}
|
||||
if(confirm(message)) {
|
||||
passcodeManager.clearPasscode();
|
||||
if(authManager.offline()) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isDesktopApplication = function() {
|
||||
return isDesktopApplication();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ class GlobalExtensionsMenu {
|
||||
$scope.editorManager = editorManager;
|
||||
$scope.componentManager = componentManager;
|
||||
|
||||
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
|
||||
|
||||
$scope.selectedAction = function(action, extension) {
|
||||
extensionManager.executeAction(action, extension, null, function(response){
|
||||
if(response && response.error) {
|
||||
@@ -53,6 +55,38 @@ class GlobalExtensionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
// Server extensions
|
||||
|
||||
$scope.deleteServerExt = function(ext) {
|
||||
if(confirm("Are you sure you want to delete and disable this extension?")) {
|
||||
_.remove($scope.serverExtensions, {uuid: ext.uuid});
|
||||
modelManager.setItemToBeDeleted(ext);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.nameForServerExtension = function(ext) {
|
||||
var url = ext.url;
|
||||
if(!url) {
|
||||
return "Invalid Extension";
|
||||
}
|
||||
if(url.includes("gdrive")) {
|
||||
return "Google Drive Sync";
|
||||
} else if(url.includes("file_attacher")) {
|
||||
return "File Attacher";
|
||||
} else if(url.includes("onedrive")) {
|
||||
return "OneDrive Sync";
|
||||
} else if(url.includes("backup.email_archive")) {
|
||||
return "Daily Email Backups";
|
||||
} else if(url.includes("dropbox")) {
|
||||
return "Dropbox Sync";
|
||||
} else if(url.includes("revisions")) {
|
||||
return "Revision History";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Editors
|
||||
|
||||
@@ -127,6 +161,7 @@ class GlobalExtensionsMenu {
|
||||
|
||||
modelManager.addItem(ext);
|
||||
syncManager.sync();
|
||||
$scope.serverExtensions.push(ext);
|
||||
completion();
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class SNCrypto {
|
||||
var pw_cost = this.defaultPasswordGenerationCost();
|
||||
var pw_nonce = this.generateRandomKey(512);
|
||||
var pw_salt = this.sha256([email, pw_nonce].join(":"));
|
||||
this.generateSymmetricKeyPair({email: email, password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
|
||||
this.generateSymmetricKeyPair({password: password, pw_salt: pw_salt, pw_cost: pw_cost}, function(keys){
|
||||
callback({pw: keys[0], mk: keys[1], ak: keys[2]}, {pw_salt: pw_salt, pw_cost: pw_cost, version: "002"});
|
||||
}.bind(this));
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class EncryptionHelper {
|
||||
return fullCiphertext;
|
||||
}
|
||||
|
||||
static encryptItem(item, keys, version) {
|
||||
static encryptItem(item, keys, version = "002") {
|
||||
var params = {};
|
||||
// encrypt item key
|
||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||
@@ -73,7 +73,10 @@ class EncryptionHelper {
|
||||
// is encrypted, continue to below
|
||||
} else {
|
||||
// is base64 encoded
|
||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
||||
try {
|
||||
item.content = JSON.parse(Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)));
|
||||
} catch (e) {}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
class ExtensionManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager) {
|
||||
constructor(httpManager, modelManager, authManager, syncManager, storageManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
||||
this.enabledRepeatActionUrls = JSON.parse(storageManager.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.decryptedExtensions = JSON.parse(storageManager.getItem("decryptedExtensions")) || [];
|
||||
this.syncManager = syncManager;
|
||||
this.storageManager = storageManager;
|
||||
|
||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
||||
for (var ext of items) {
|
||||
@@ -49,7 +50,7 @@ class ExtensionManager {
|
||||
this.decryptedExtensions.push(extension.url);
|
||||
}
|
||||
|
||||
localStorage.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions))
|
||||
this.storageManager.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions))
|
||||
|
||||
extension.encrypted = this.extensionUsesEncryptedData(extension);
|
||||
}
|
||||
@@ -240,7 +241,7 @@ class ExtensionManager {
|
||||
|
||||
disableRepeatAction(action, extension) {
|
||||
_.pull(this.enabledRepeatActionUrls, action.url);
|
||||
localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
this.modelManager.removeItemChangeObserver(action.url);
|
||||
|
||||
console.assert(this.isRepeatActionEnabled(action) == false);
|
||||
@@ -249,7 +250,7 @@ class ExtensionManager {
|
||||
enableRepeatAction(action, extension) {
|
||||
if(!_.find(this.enabledRepeatActionUrls, action.url)) {
|
||||
this.enabledRepeatActionUrls.push(action.url);
|
||||
localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
}
|
||||
|
||||
if(action.repeat_mode) {
|
||||
|
||||
42
app/assets/javascripts/app/services/filters/sortBy.js
Normal file
42
app/assets/javascripts/app/services/filters/sortBy.js
Normal file
@@ -0,0 +1,42 @@
|
||||
angular.module('app.frontend')
|
||||
.filter('sortBy', function ($filter) {
|
||||
return function(items, sortBy) {
|
||||
let sortValueFn = (a, b, pinCheck = false) => {
|
||||
if(!pinCheck) {
|
||||
if(a.pinned && b.pinned) {
|
||||
return sortValueFn(a, b, true);
|
||||
}
|
||||
if(a.pinned) { return -1; }
|
||||
if(b.pinned) { return 1; }
|
||||
}
|
||||
|
||||
var aValue = a[sortBy] || "";
|
||||
var bValue = b[sortBy] || "";
|
||||
|
||||
let vector = 1;
|
||||
if(sortBy == "title") {
|
||||
aValue = aValue.toLowerCase();
|
||||
bValue = bValue.toLowerCase();
|
||||
|
||||
if(aValue.length == 0 && bValue.length == 0) {
|
||||
return 0;
|
||||
} else if(aValue.length == 0 && bValue.length != 0) {
|
||||
return 1;
|
||||
} else if(aValue.length != 0 && bValue.length == 0) {
|
||||
return -1;
|
||||
} else {
|
||||
vector = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if(aValue > bValue) { return -1 * vector;}
|
||||
else if(aValue < bValue) { return 1 * vector;}
|
||||
return 0;
|
||||
}
|
||||
|
||||
items = items || [];
|
||||
return items.sort(function(a, b){
|
||||
return sortValueFn(a, b);
|
||||
})
|
||||
};
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
class HttpManager {
|
||||
|
||||
constructor($timeout) {
|
||||
constructor($timeout, storageManager) {
|
||||
// calling callbacks in a $timeout allows angular UI to update
|
||||
this.$timeout = $timeout;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
setAuthHeadersForRequest(request) {
|
||||
var token = localStorage.getItem("jwt");
|
||||
var token = this.storageManager.getItem("jwt");
|
||||
if(token) {
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem("jwt"));
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
class ModelManager {
|
||||
|
||||
constructor(dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
constructor(storageManager) {
|
||||
this.storageManager = storageManager;
|
||||
this.notes = [];
|
||||
this.tags = [];
|
||||
this.itemSyncObservers = [];
|
||||
this.itemChangeObservers = [];
|
||||
this.items = [];
|
||||
this._extensions = [];
|
||||
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component"];
|
||||
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component", "SF|Extension"];
|
||||
}
|
||||
|
||||
resetLocalMemory() {
|
||||
this.notes.length = 0;
|
||||
this.tags.length = 0;
|
||||
this.items.length = 0;
|
||||
this._extensions.length = 0;
|
||||
}
|
||||
|
||||
get allItems() {
|
||||
@@ -26,9 +33,14 @@ class ModelManager {
|
||||
alternateUUIDForItem(item, callback) {
|
||||
// we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup)
|
||||
var newItem = this.createItem(item);
|
||||
|
||||
newItem.uuid = Neeto.crypto.generateUUID();
|
||||
|
||||
// Update uuids of relationships
|
||||
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
||||
|
||||
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
||||
|
||||
this.removeItemLocally(item, function(){
|
||||
this.addItem(newItem);
|
||||
newItem.setDirty(true);
|
||||
@@ -53,6 +65,12 @@ class ModelManager {
|
||||
})
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
}
|
||||
|
||||
findItem(itemId) {
|
||||
return _.find(this.items, {uuid: itemId});
|
||||
}
|
||||
@@ -151,6 +169,8 @@ class ModelManager {
|
||||
item = new Theme(json_obj);
|
||||
} else if(json_obj.content_type == "SN|Component") {
|
||||
item = new Component(json_obj);
|
||||
} else if(json_obj.content_type == "SF|Extension") {
|
||||
item = new SyncAdapter(json_obj);
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -189,14 +209,16 @@ class ModelManager {
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
addItem(item) {
|
||||
this.addItems([item]);
|
||||
resortTag(tag) {
|
||||
_.pull(this.tags, tag);
|
||||
this.tags.splice(_.sortedIndexBy(this.tags, tag, function(tag){
|
||||
if (tag.title) return tag.title.toLowerCase();
|
||||
else return ''
|
||||
}), 0, tag);
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
addItem(item) {
|
||||
this.addItems([item]);
|
||||
}
|
||||
|
||||
resolveReferencesForItem(item) {
|
||||
@@ -288,7 +310,7 @@ class ModelManager {
|
||||
_.pull(this._extensions, item);
|
||||
}
|
||||
|
||||
this.dbManager.deleteItem(item, callback);
|
||||
this.storageManager.deleteModel(item, callback);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
77
app/assets/javascripts/app/services/passcodeManager.js
Normal file
77
app/assets/javascripts/app/services/passcodeManager.js
Normal file
@@ -0,0 +1,77 @@
|
||||
angular.module('app.frontend')
|
||||
.provider('passcodeManager', function () {
|
||||
|
||||
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||
return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager);
|
||||
}
|
||||
|
||||
function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||
|
||||
this._hasPasscode = storageManager.getItem("offlineParams", StorageManager.Fixed) != null;
|
||||
this._locked = this._hasPasscode;
|
||||
|
||||
this.isLocked = function() {
|
||||
return this._locked;
|
||||
}
|
||||
|
||||
this.hasPasscode = function() {
|
||||
return this._hasPasscode;
|
||||
}
|
||||
|
||||
this.keys = function() {
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
this.unlock = function(passcode, callback) {
|
||||
var params = JSON.parse(storageManager.getItem("offlineParams", StorageManager.Fixed));
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
|
||||
if(keys.pw !== params.hash) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._keys = keys;
|
||||
this.decryptLocalStorage(keys);
|
||||
this._locked = false;
|
||||
callback(true);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.setPasscode = function(passcode, callback) {
|
||||
var cost = Neeto.crypto.defaultPasswordGenerationCost();
|
||||
var salt = Neeto.crypto.generateRandomKey(512);
|
||||
var defaultParams = {pw_cost: cost, pw_salt: salt};
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
|
||||
defaultParams.hash = keys.pw;
|
||||
this._keys = keys;
|
||||
this._hasPasscode = true;
|
||||
|
||||
// Encrypting will initially clear localStorage
|
||||
this.encryptLocalStorage(keys);
|
||||
|
||||
// After it's cleared, it's safe to write to it
|
||||
storageManager.setItem("offlineParams", JSON.stringify(defaultParams), StorageManager.Fixed);
|
||||
callback(true);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.clearPasscode = function() {
|
||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
|
||||
storageManager.removeItem("offlineParams", StorageManager.Fixed);
|
||||
this._keys = null;
|
||||
this._hasPasscode = false;
|
||||
}
|
||||
|
||||
this.encryptLocalStorage = function(keys) {
|
||||
storageManager.setKeys(keys);
|
||||
// Switch to Ephemeral storage, wiping Fixed storage
|
||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted);
|
||||
}
|
||||
|
||||
this.decryptLocalStorage = function(keys) {
|
||||
storageManager.setKeys(keys);
|
||||
storageManager.decryptStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
221
app/assets/javascripts/app/services/storageManager.js
Normal file
221
app/assets/javascripts/app/services/storageManager.js
Normal file
@@ -0,0 +1,221 @@
|
||||
class MemoryStorage {
|
||||
constructor() {
|
||||
this.memory = {};
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.memory[key] || null;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.memory).length;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.memory[key] = value;
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.memory[key];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.memory = {};
|
||||
}
|
||||
|
||||
keys() {
|
||||
return Object.keys(this.memory);
|
||||
}
|
||||
|
||||
key(index) {
|
||||
return Object.keys(this.memory)[index];
|
||||
}
|
||||
}
|
||||
|
||||
class StorageManager {
|
||||
|
||||
constructor(dbManager) {
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
initialize(hasPasscode, ephemeral) {
|
||||
if(hasPasscode) {
|
||||
// We don't want to save anything in fixed storage except for actual item data (in IndexedDB)
|
||||
this.storage = this.memoryStorage;
|
||||
} else if(ephemeral) {
|
||||
// We don't want to save anything in fixed storage as well as IndexedDB
|
||||
this.storage = this.memoryStorage;
|
||||
} else {
|
||||
this.storage = localStorage;
|
||||
}
|
||||
|
||||
this.modelStorageMode = ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed;
|
||||
}
|
||||
|
||||
get memoryStorage() {
|
||||
if(!this._memoryStorage) {
|
||||
this._memoryStorage = new MemoryStorage();
|
||||
}
|
||||
return this._memoryStorage;
|
||||
}
|
||||
|
||||
setItemsMode(mode) {
|
||||
var newStorage = this.getVault(mode);
|
||||
if(newStorage !== this.storage) {
|
||||
// transfer storages
|
||||
var length = this.storage.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
var key = this.storage.key(i);
|
||||
newStorage.setItem(key, this.storage.getItem(key));
|
||||
}
|
||||
|
||||
this.storage.clear();
|
||||
this.storage = newStorage;
|
||||
|
||||
if(mode == StorageManager.FixedEncrypted) {
|
||||
this.writeEncryptedStorageToDisk();
|
||||
} else if(mode == StorageManager.Fixed) {
|
||||
// Remove encrypted storage
|
||||
this.removeItem("encryptedStorage", StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVault(vaultKey) {
|
||||
if(vaultKey) {
|
||||
return this.storageForVault(vaultKey);
|
||||
} else {
|
||||
return this.storage;
|
||||
}
|
||||
}
|
||||
|
||||
storageForVault(vault) {
|
||||
if(vault == StorageManager.Ephemeral || vault == StorageManager.FixedEncrypted) {
|
||||
return this.memoryStorage;
|
||||
} else {
|
||||
return localStorage;
|
||||
}
|
||||
}
|
||||
|
||||
setItem(key, value, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
storage.setItem(key, value);
|
||||
|
||||
if(vault === StorageManager.FixedEncrypted) {
|
||||
this.writeEncryptedStorageToDisk();
|
||||
}
|
||||
}
|
||||
|
||||
getItem(key, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
return storage.getItem(key);
|
||||
}
|
||||
|
||||
removeItem(key, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
storage.removeItem(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.memoryStorage.clear();
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
storageAsHash() {
|
||||
var hash = {};
|
||||
var length = this.storage.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
var key = this.storage.key(i);
|
||||
hash[key] = this.storage.getItem(key)
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
setKeys(keys) {
|
||||
this.encryptedStorageKeys = keys;
|
||||
}
|
||||
|
||||
writeEncryptedStorageToDisk() {
|
||||
var encryptedStorage = new EncryptedStorage();
|
||||
// Copy over totality of current storage
|
||||
encryptedStorage.storage = this.storageAsHash();
|
||||
// Save new encrypted storage in Fixed storage
|
||||
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys);
|
||||
this.setItem("encryptedStorage", JSON.stringify(params.paramsForSync()), StorageManager.Fixed);
|
||||
}
|
||||
|
||||
decryptStorage() {
|
||||
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
|
||||
EncryptionHelper.decryptItem(stored, this.encryptedStorageKeys);
|
||||
var encryptedStorage = new EncryptedStorage(stored);
|
||||
|
||||
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||
this.setItem(key, encryptedStorage.storage[key]);
|
||||
}
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this.getItem("encryptedStorage", StorageManager.Fixed) !== null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Model Storage
|
||||
|
||||
If using ephemeral storage, we don't need to write it to anything as references will be held already by controllers
|
||||
and the global modelManager service.
|
||||
*/
|
||||
|
||||
setModelStorageMode(mode) {
|
||||
if(mode == this.modelStorageMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode == StorageManager.Ephemeral) {
|
||||
// Clear IndexedDB
|
||||
this.dbManager.clearAllModels(null);
|
||||
} else {
|
||||
// Fixed
|
||||
}
|
||||
|
||||
this.modelStorageMode = mode;
|
||||
}
|
||||
|
||||
getAllModels(callback) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.getAllModels(callback);
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
|
||||
saveModel(item) {
|
||||
this.saveModels([item]);
|
||||
}
|
||||
|
||||
saveModels(items, callback) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.saveModels(items, callback);
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
|
||||
deleteModel(item, callback) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.deleteModel(item, callback);
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
|
||||
clearAllModels(callback) {
|
||||
this.dbManager.clearAllModels(callback);
|
||||
}
|
||||
}
|
||||
|
||||
StorageManager.FixedEncrypted = "FixedEncrypted"; // encrypted memoryStorage + localStorage persistence
|
||||
StorageManager.Ephemeral = "Ephemeral"; // memoryStorage
|
||||
StorageManager.Fixed = "Fixed"; // localStorage
|
||||
|
||||
angular.module('app.frontend').service('storageManager', StorageManager);
|
||||
@@ -1,6 +1,6 @@
|
||||
class SyncManager {
|
||||
|
||||
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout) {
|
||||
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout, storageManager, passcodeManager) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
@@ -8,25 +8,33 @@ class SyncManager {
|
||||
this.dbManager = dbManager;
|
||||
this.$interval = $interval;
|
||||
this.$timeout = $timeout;
|
||||
this.storageManager = storageManager;
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.syncStatus = {};
|
||||
}
|
||||
|
||||
get serverURL() {
|
||||
return localStorage.getItem("server") || window._default_sf_server;
|
||||
return this.storageManager.getItem("server") || window._default_sf_server;
|
||||
}
|
||||
|
||||
get masterKey() {
|
||||
return localStorage.getItem("mk");
|
||||
return this.storageManager.getItem("mk");
|
||||
}
|
||||
|
||||
get serverPassword() {
|
||||
return localStorage.getItem("pw");
|
||||
return this.storageManager.getItem("pw");
|
||||
}
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
var version = this.authManager.protocolVersion();
|
||||
if(items.length == 0) {
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
// Use null to use the latest protocol version if offline
|
||||
var version = this.authManager.offline() ? null : this.authManager.protocolVersion();
|
||||
var keys = this.authManager.offline() ? this.passcodeManager.keys() : this.authManager.keys();
|
||||
var params = items.map(function(item) {
|
||||
var itemParams = new ItemParams(item, null, version);
|
||||
var itemParams = new ItemParams(item, keys, version);
|
||||
itemParams = itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
@@ -34,12 +42,12 @@ class SyncManager {
|
||||
return itemParams;
|
||||
}.bind(this));
|
||||
|
||||
this.dbManager.saveItems(params, callback);
|
||||
this.storageManager.saveModels(params, callback);
|
||||
}
|
||||
|
||||
loadLocalItems(callback) {
|
||||
var params = this.dbManager.getAllItems(function(items){
|
||||
var items = this.handleItemsResponse(items, null, null);
|
||||
var params = this.storageManager.getAllModels(function(items){
|
||||
var items = this.handleItemsResponse(items, null);
|
||||
Item.sortItemsByDate(items);
|
||||
callback(items);
|
||||
}.bind(this))
|
||||
@@ -61,12 +69,40 @@ class SyncManager {
|
||||
|
||||
}
|
||||
|
||||
markAllItemsDirtyAndSaveOffline(callback) {
|
||||
var items = this.modelManager.allItems;
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
/*
|
||||
In the case of signing in and merging local data, we alternative UUIDs
|
||||
to avoid overwriting data a user may retrieve that has the same UUID.
|
||||
Alternating here forces us to to create duplicates of the items instead.
|
||||
*/
|
||||
markAllItemsDirtyAndSaveOffline(callback, alternateUUIDs) {
|
||||
var originalItems = this.modelManager.allItems;
|
||||
|
||||
var block = (items) => {
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.writeItemsToLocalStorage(items, false, callback);
|
||||
}
|
||||
|
||||
if(alternateUUIDs) {
|
||||
var index = 0;
|
||||
|
||||
let alternateNextItem = () => {
|
||||
if(index >= originalItems.length) {
|
||||
// We don't use originalItems as altnerating UUID will have deleted them.
|
||||
block(this.modelManager.allItems);
|
||||
return;
|
||||
}
|
||||
|
||||
var item = originalItems[index];
|
||||
this.modelManager.alternateUUIDForItem(item, alternateNextItem);
|
||||
++index;
|
||||
}
|
||||
|
||||
alternateNextItem();
|
||||
} else {
|
||||
block(originalItems);
|
||||
}
|
||||
this.writeItemsToLocalStorage(items, false, callback);
|
||||
}
|
||||
|
||||
get syncURL() {
|
||||
@@ -75,12 +111,12 @@ class SyncManager {
|
||||
|
||||
set syncToken(token) {
|
||||
this._syncToken = token;
|
||||
localStorage.setItem("syncToken", token);
|
||||
this.storageManager.setItem("syncToken", token);
|
||||
}
|
||||
|
||||
get syncToken() {
|
||||
if(!this._syncToken) {
|
||||
this._syncToken = localStorage.getItem("syncToken");
|
||||
this._syncToken = this.storageManager.getItem("syncToken");
|
||||
}
|
||||
return this._syncToken;
|
||||
}
|
||||
@@ -88,15 +124,15 @@ class SyncManager {
|
||||
set cursorToken(token) {
|
||||
this._cursorToken = token;
|
||||
if(token) {
|
||||
localStorage.setItem("cursorToken", token);
|
||||
this.storageManager.setItem("cursorToken", token);
|
||||
} else {
|
||||
localStorage.removeItem("cursorToken");
|
||||
this.storageManager.removeItem("cursorToken");
|
||||
}
|
||||
}
|
||||
|
||||
get cursorToken() {
|
||||
if(!this._cursorToken) {
|
||||
this._cursorToken = localStorage.getItem("cursorToken");
|
||||
this._cursorToken = this.storageManager.getItem("cursorToken");
|
||||
}
|
||||
return this._cursorToken;
|
||||
}
|
||||
@@ -129,7 +165,7 @@ class SyncManager {
|
||||
this.syncStatus.checker = this.$interval(function(){
|
||||
// check to see if the ongoing sync is taking too long, alert the user
|
||||
var secondsPassed = (new Date() - this.syncStatus.syncStart) / 1000;
|
||||
var warningThreshold = 5; // seconds
|
||||
var warningThreshold = 5.0; // seconds
|
||||
if(secondsPassed > warningThreshold) {
|
||||
this.$rootScope.$broadcast("sync:taking-too-long");
|
||||
this.stopCheckingIfSyncIsTakingTooLong();
|
||||
@@ -159,7 +195,6 @@ class SyncManager {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||
if(this.authManager.offline()) {
|
||||
@@ -288,7 +323,8 @@ class SyncManager {
|
||||
}
|
||||
|
||||
handleItemsResponse(responseItems, omitFields) {
|
||||
EncryptionHelper.decryptMultipleItems(responseItems, this.authManager.keys());
|
||||
var keys = this.authManager.keys() || this.passcodeManager.keys();
|
||||
EncryptionHelper.decryptMultipleItems(responseItems, keys);
|
||||
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
||||
return items;
|
||||
}
|
||||
@@ -302,45 +338,64 @@ class SyncManager {
|
||||
|
||||
var i = 0;
|
||||
var handleNext = function() {
|
||||
if (i < unsaved.length) {
|
||||
var mapping = unsaved[i];
|
||||
var itemResponse = mapping.item;
|
||||
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
if(!item) {
|
||||
// could be deleted
|
||||
return;
|
||||
}
|
||||
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 from the old account into a new account
|
||||
this.modelManager.alternateUUIDForItem(item, handleNext);
|
||||
} else if(error.tag === "sync_conflict") {
|
||||
// create a new item with the same contents of this item if the contents differ
|
||||
itemResponse.uuid = null; // we want a new uuid for the new item
|
||||
var dup = this.modelManager.createItem(itemResponse);
|
||||
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
|
||||
this.modelManager.addItem(dup);
|
||||
dup.conflict_of = item.uuid;
|
||||
dup.setDirty(true);
|
||||
}
|
||||
}
|
||||
++i;
|
||||
} else {
|
||||
if(i >= unsaved.length) {
|
||||
// Handled all items
|
||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||
return;
|
||||
}
|
||||
|
||||
var handled = false;
|
||||
var mapping = unsaved[i];
|
||||
var itemResponse = mapping.item;
|
||||
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
|
||||
if(!item) {
|
||||
// Could be deleted
|
||||
return;
|
||||
}
|
||||
|
||||
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 from the old account into a new account
|
||||
handled = true;
|
||||
this.modelManager.alternateUUIDForItem(item, handleNext);
|
||||
}
|
||||
|
||||
else if(error.tag === "sync_conflict") {
|
||||
// Create a new item with the same contents of this item if the contents differ
|
||||
|
||||
// We want a new uuid for the new item. Note that this won't neccessarily adjust references.
|
||||
itemResponse.uuid = null;
|
||||
|
||||
var dup = this.modelManager.createItem(itemResponse);
|
||||
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
|
||||
this.modelManager.addItem(dup);
|
||||
dup.conflict_of = item.uuid;
|
||||
dup.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
if(!handled) {
|
||||
handleNext();
|
||||
}
|
||||
|
||||
}.bind(this);
|
||||
|
||||
handleNext();
|
||||
}
|
||||
|
||||
clearSyncToken() {
|
||||
localStorage.removeItem("syncToken");
|
||||
this.storageManager.removeItem("syncToken");
|
||||
}
|
||||
|
||||
destroyLocalData(callback) {
|
||||
localStorage.clear();
|
||||
this.dbManager.clearAllItems(function(){
|
||||
this.storageManager.clear();
|
||||
this.storageManager.clearAllModels(function(){
|
||||
if(callback) {
|
||||
this.$timeout(function(){
|
||||
callback();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
class ThemeManager {
|
||||
|
||||
constructor(modelManager, syncManager, $rootScope) {
|
||||
constructor(modelManager, syncManager, $rootScope, storageManager) {
|
||||
this.syncManager = syncManager;
|
||||
this.modelManager = modelManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
get themes() {
|
||||
@@ -16,7 +17,7 @@ class ThemeManager {
|
||||
*/
|
||||
|
||||
get activeTheme() {
|
||||
var activeThemeId = localStorage.getItem("activeTheme");
|
||||
var activeThemeId = this.storageManager.getItem("activeTheme");
|
||||
if(!activeThemeId) {
|
||||
return null;
|
||||
}
|
||||
@@ -53,14 +54,14 @@ class ThemeManager {
|
||||
link.media = "screen,print";
|
||||
link.id = theme.uuid;
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
localStorage.setItem("activeTheme", theme.uuid);
|
||||
this.storageManager.setItem("activeTheme", theme.uuid);
|
||||
|
||||
this.currentTheme = theme;
|
||||
this.$rootScope.$broadcast("theme-changed");
|
||||
}
|
||||
|
||||
deactivateTheme(theme) {
|
||||
localStorage.removeItem("activeTheme");
|
||||
this.storageManager.removeItem("activeTheme");
|
||||
var element = document.getElementById(theme.uuid);
|
||||
if(element) {
|
||||
element.disabled = true;
|
||||
@@ -72,7 +73,7 @@ class ThemeManager {
|
||||
}
|
||||
|
||||
isThemeActive(theme) {
|
||||
return localStorage.getItem("activeTheme") === theme.uuid;
|
||||
return this.storageManager.getItem("activeTheme") === theme.uuid;
|
||||
}
|
||||
|
||||
fileNameFromPath(filePath) {
|
||||
|
||||
Reference in New Issue
Block a user