This commit is contained in:
Mo Bitar
2017-10-13 10:09:59 -05:00
parent cfb2c6084c
commit 6fd57195cd
63 changed files with 6587 additions and 413 deletions

View File

@@ -70,7 +70,7 @@ module.exports = function(grunt) {
},
app: {
src: [
'app/assets/javascripts/app/services/helpers/*.js',
'app/assets/javascripts/app/services/encryption/*.js',
'app/assets/javascripts/app/*.js',
'app/assets/javascripts/app/frontend/*.js',
'app/assets/javascripts/app/frontend/controllers/*.js',
@@ -142,7 +142,6 @@ module.exports = function(grunt) {
dest: 'vendor/assets/javascripts/compiled.min.js'
}
},
});
grunt.loadNpmTasks('grunt-newer');

View File

@@ -24,7 +24,7 @@ For more information on Standard Notes, see https://standardnotes.org.
Open your browser to http://localhost:3000
In the bottom left, click on "Sign in or Register" or "Account" (if signed in), and make sure you're using the correct server. You can use a production server here as usual (like https://n3.standardnotes.org).
In the bottom left, click on "Sign in or Register" or "Account" (if signed in), and make sure you're using the correct server. You can use a production server here as usual (like https://sync.standardnotes.org).
## License

View File

@@ -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"];
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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){

View File

@@ -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();
}
});

View File

@@ -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);

View File

@@ -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);
}
});

View File

@@ -9,6 +9,7 @@ angular.module('app.frontend')
save: "&",
tags: "=",
allTag: "=",
archiveTag: "=",
updateNoteTag: "&",
removeTag: "&"
},

View File

@@ -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();
}
}
}

View File

@@ -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;

View File

@@ -106,4 +106,8 @@ class Note extends Item {
get content_type() {
return "Note";
}
tagsString() {
return Tag.arrayToDisplayString(this.tags);
}
}

View File

@@ -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(" ");
}
}

View File

@@ -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";
}
}

View File

@@ -3,7 +3,7 @@ class ItemParams {
constructor(item, keys, version) {
this.item = item;
this.keys = keys;
this.version = version;
this.version = version || "002";
}
paramsForExportFile() {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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) {

View 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);
})
};
});

View File

@@ -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);
}
}

View File

@@ -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);
}
/*

View 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();
}
}
});

View 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);

View File

@@ -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();

View File

@@ -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) {

File diff suppressed because one or more lines are too long

View File

@@ -24,14 +24,17 @@ $heading-height: 75px;
#editor-title-bar {
width: 100%;
padding: 20px;
padding-top: 14px;
padding-left: 14px;
padding-bottom: 10px;
padding-right: 10px;
background-color: white;
border-bottom: none;
z-index: 100;
height: auto;
padding-right: 10px;
overflow: visible;
&.fullscreen {

View File

@@ -53,25 +53,6 @@
}
}
.blue-box {
background-color: $blue-color;
color: white;
border-radius: 4px;
padding: 16px 20px;
button {
background-color: white;
color: $blue-color;
border-radius: 3px;
font-weight: bold;
padding: 6px 20px;
width: 100%;
&:hover {
text-decoration: underline;
}
}
}
.dashboard-link {
padding-top: 12px;
font-weight: normal;

View File

@@ -1,7 +1,6 @@
.fake-link {
font-weight: bold;
cursor: pointer;
color: $blue-color;
&:hover {
text-decoration: underline;
@@ -31,7 +30,6 @@ h2 {
a {
font-weight: bold;
cursor: pointer;
color: $blue-color;
&.gray {
color: $dark-gray;
@@ -140,7 +138,6 @@ button.light {
width: 100%;
border: 1px solid rgba(gray, 0.15);
cursor: pointer;
color: $blue-color;
&:hover {
background-color: rgba(gray, 0.10);
@@ -170,7 +167,11 @@ a.disabled {
}
.icon.ion-locked {
margin-left: 5px;
border-left: 1px solid gray;
padding-left: 8px;
}
@@ -211,7 +212,7 @@ a.disabled {
border-right-color: transparent;
border-radius: 50%;
&.blue {
&.tinted {
border: 1px solid $blue-color;
border-right-color: transparent;
}

View File

@@ -0,0 +1,51 @@
#lock-screen {
position: fixed;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 10000;
width: 100vw;
height: 100vh;
background-color: rgba(white, 0.5);
color: black;
font-size: 16px;
display: flex;
align-items: center;
.background {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
}
.content {
// box-shadow: 0 3px 3px rgba(0, 0, 0, 0.175);
border: 1px solid rgba(black, 0.1);
background-color: white;
width: 300px;
// height: 500px;
margin: auto;
padding: 10px 30px;
padding-bottom: 30px;
// position: absolute;
// top: 0; left: 0; bottom: 0; right: 0;
overflow-y: scroll;
p {
margin-bottom: 8px;
margin-top: 0;
}
h3 {
margin-bottom: 0;
}
h4 {
margin-bottom: 6px;
}
}
}

View File

@@ -29,10 +29,33 @@ $blue-color: #086dd6;
}
}
.blue {
.tinted {
color: $blue-color;
}
.tinted-selected {
color: white;
}
.tinted-box {
background-color: $blue-color;
color: white;
border-radius: 4px;
padding: 16px 20px;
button {
background-color: white;
color: $blue-color;
border-radius: 3px;
font-weight: bold;
padding: 6px 20px;
width: 100%;
&:hover {
text-decoration: underline;
}
}
}
html,
body {
font-family: -apple-system, BlinkMacSystemFont,
@@ -67,6 +90,7 @@ body {
}
a {
color: $blue-color;
text-decoration: none;
&:hover {
text-decoration: underline;;
@@ -164,3 +188,7 @@ $section-header-height: 70px;
}
}
}
.icon {
margin-right: 4px;
}

View File

@@ -1,7 +1,7 @@
ul.section-menu-bar {
width: 100%;
padding-top: 0px;
padding-left: 15px;
padding-left: 6px;
padding-right: 21px;
background-color: #f1f1f1;
@@ -63,10 +63,13 @@ ul.section-menu-bar {
> li {
width: 100%;
height: 35px;
height: 40px;
padding-top: 3px;
overflow: hidden;
cursor: pointer;
border-bottom: 1px solid rgba(black, 0.1);
color: $selected-text-color;
float: left;
@@ -103,10 +106,6 @@ ul.section-menu-bar {
overflow-y: scroll;
max-height: calc(85vh - 90px);
&:not(:first-child) {
margin-top: 18px;
}
ul {
margin-top: 0px;
margin-bottom: 0px;
@@ -143,7 +142,7 @@ ul.section-menu-bar {
background-color: $blue-color;
.blue {
.tinted {
color: white;
}

View File

@@ -11,6 +11,8 @@
#notes-title-bar {
color: rgba(black, 0.40);
padding-top: 16px;
padding-left: 14px;
padding-right: 14px;
height: $notes-title-bar-height;
font-weight: normal;
font-size: 18px;
@@ -21,14 +23,15 @@
}
#notes-add-button {
right: 20px;
right: 14px;
}
#tag-menu-bar {
position: relative;
margin: 0 -20px;
width: auto;
margin: 0 -14px;
margin-top: 14px;
// padding-left: 4px;
width: auto;
}
.filter-section {
@@ -101,6 +104,20 @@
margin-top: 4px;
}
.tags-string {
margin-bottom: 4px;
font-size: 13px;
}
.pinned {
.icon {
display: inline-block;
vertical-align: top;
margin-top: 2px;
margin-right: 2px;
}
}
.note-preview {
font-size: 15px;
margin-top: 1px;
@@ -117,6 +134,10 @@
&.selected {
background-color: $blue-color;
color: white;
.pinned {
color: white;
}
}
}

View File

@@ -55,9 +55,6 @@
.status {
color: orange;
&.trusted {
color: $blue-color;
}
}
.buttons {
@@ -74,7 +71,7 @@
text-decoration: underline;
}
&.blue {
&.tinted {
background-color: $blue-color;
color: white;
}

View File

@@ -22,6 +22,10 @@
margin-top: 2px !important;
}
.mt-3 {
margin-top: 3px !important;
}
.mt-5 {
margin-top: 5px !important;
}

View File

@@ -10,3 +10,6 @@ $dark-gray: #2e2e2e;
@import "app/extensions";
@import "app/menus";
@import "app/permissions-modal";
@import "app/lock-screen";
@import "ionicons";

View File

@@ -6,7 +6,7 @@
.mb-10
.step-one{"ng-if" => "!formData.showLogin && !formData.showRegister"}
%h3 Sign in or register to enable sync and encryption.
%h3 Sign in or register to enable sync and end-to-end encryption.
.small-v-space
.button-group.mt-5
@@ -31,10 +31,14 @@
%label.pull-left Sync Server Domain
%input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
.checkbox.mt-10{"ng-if" => "localNotesCount() > 0"}
.checkbox.mt-10
%p
%input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"}
Stay signed in
.checkbox.mt-10{"ng-if" => "notesAndTagsCount() > 0"}
%p
%input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
Merge local notes ({{localNotesCount()}} notes)
Merge local data ({{notesAndTagsCount()}} notes and tags)
%button.ui-button.block.mt-10{"ng-click" => "submitAuthForm()"} {{formData.showLogin ? "Sign In" : "Register"}}
.mt-15{"ng-if" => "formData.showRegister"}
@@ -48,8 +52,8 @@
%div{"ng-if" => "user"}
%h2 {{user.email}}
%p {{server}}
%div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
.spinner.inline.mr-5.blue
%div.bold.mt-10.tinted{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
.spinner.inline.mr-5.tinted
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
@@ -104,20 +108,37 @@
%input.form-control{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
%button.ui-button.block{"ng-click" => "submitSecurityUpdateForm()"} Update
%div.mt-5{"ng-if" => "securityUpdateData.processing"}
%p.blue Processing...
%p.tinted Processing...
.mt-25
%h4 Encryption Status
%p
{{encryptionStatusString()}}
%div.mt-5{"ng-if" => "encryptionEnabled()"}
%i {{encryptionStatusForNotes()}}
.mt-25
%h4 Passcode Lock
%div{"ng-if" => "!hasPasscode() && passcodeOptionAvailable()"}
%p Add an app passcode to lock the app and encrypt on-device key storage.
%a.block.mt-5{"ng-click" => "addPasscodeClicked()", "ng-if" => "!formData.showPasscodeForm"} Add Passcode
.mt-5{"ng-if" => "formData.showPasscodeForm"}
%p.bold Choose a passcode:
%input.form-control.mt-10{:type => 'password', "ng-model" => "formData.passcode", "placeholder" => "Passcode", "autofocus" => "true"}
%input.form-control.mt-10{:type => 'password', "ng-model" => "formData.confirmPasscode", "placeholder" => "Confirm Passcode"}
%button.standard.ui-button.block.tinted.mt-5{"ng-click" => "submitPasscodeForm()"} Set Passcode
%div{"ng-if" => "hasPasscode()"}
%p
Passcode lock is enabled.
%span{"ng-if" => "isDesktopApplication()"} Your passcode will be required on new sessions after app quit.
%a.block.mt-5{"ng-click" => "removePasscodePressed()"} Remove Passcode
%div{"ng-if" => "!passcodeOptionAvailable()"}
%p Passcode lock is only available to permanent sessions. (You chose not to stay signed in.)
.medium-v-space
%h4 Local Encryption
%p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
%div.mt-5
%label Status:
{{encryptionStatusForNotes()}}
.mt-25{"ng-if" => "!importData.loading"}
%h4 Data Archives
.mt-5{"ng-if" => "user"}
@@ -132,25 +153,15 @@
%label.block.mt-5
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
.fake-link Import Data from Archive
.fake-link.tinted Import Data from Archive
%div{"ng-if" => "importData.requestPassword"}
%p Enter the account password associated with the import file.
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password"}
%button.standard.ui-button.block.blue.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import
%input.form-control.mt-5{:type => 'password', "ng-model" => "importData.password", "autofocus" => "true"}
%button.standard.ui-button.block.tinted.mt-5{"ng-click" => "submitImportPassword()"} Decrypt & Import
%p.mt-5{"ng-if" => "user"} Notes are downloaded in the Standard File format, which allows you to re-import back into this app easily. To download as plain text files, choose "Decrypted".
.spinner.mt-10{"ng-if" => "importData.loading"}
.mt-25
%h4 Analytics
%p
Help Standard Notes improve by sending anonymous data on general usage.
%a{"href" => "https://standardnotes.org/privacy", "target" => "_blank"} Learn more.
%div.mt-5
%label Status:
{{analyticsManager.enabled ? "Enabled" : "Disabled"}}
%a{"ng-click" => "analyticsManager.toggleStatus()"} {{analyticsManager.enabled ? "Disable" : "Enable"}}
%a.block.mt-25.red{"ng-click" => "destroyLocalData()"} {{ user ? "Sign out and clear local data" : "Clear all local data" }}

View File

@@ -10,7 +10,7 @@
%ul{"ng-if" => "!extension.hide"}
%li.menu-item{"ng-repeat" => "action in extension.actionsWithContextForItem(item)", "ng-click" => "executeAction(action, extension);",
"ng-class" => "{'faded' : !isActionEnabled(action, extension)}"}
.menu-item-title {{action.label}}
%label.menu-item-title {{action.label}}
.menu-item-subtitle {{action.desc}}
.small.normal{"ng-if" => "!isActionEnabled(action, extension)"}
@@ -19,7 +19,7 @@
%div{"ng-if" => "action.showNestedActions"}
%ul.mt-10
%li.menu-item.white-bg.nested-hover{"ng-repeat" => "subaction in action.subactions", "ng-click" => "executeAction(subaction, extension, action); $event.stopPropagation();", "style" => "margin-top: -1px;"}
.menu-item-title {{subaction.label}}
%label.menu-item-title {{subaction.label}}
.menu-item-subtitle {{subaction.desc}}
%span{"ng-if" => "subaction.running"}
.spinner{"style" => "margin-top: 3px;"}

View File

@@ -4,7 +4,7 @@
%ul
%li.menu-item{"ng-repeat" => "editor in editorManager.systemEditors", "ng-click" => "selectEditor($event, editor)"}
%span.pull-left.mr-10{"ng-if" => "selectedEditor === editor"} ✓
.menu-item-title.pull-left {{editor.name}}
%label.menu-item-title.pull-left {{editor.name}}
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
.header
@@ -13,6 +13,6 @@
%ul
%li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"}
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
.menu-item-title
%label.menu-item-title
{{editor.name}}
%span.inline.blue{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓
%span.inline.tinted{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓

View File

@@ -2,15 +2,15 @@
.panel-body
.container
.float-group.h20
%h1.blue.pull-left Extensions
%h1.tinted.pull-left Extensions
%a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"}
%p Customize your experience with editors, themes, and actions.
.blue-box.mt-10
.tinted-box.mt-10
%h3 Available as part of the Extended subscription.
%p.mt-5 Note history
%p.mt-5 Automated backups
%p.mt-5 All editors, themes, and actions
%p.mt-5 Editors, themes, and actions
%a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"}
%button.mt-10
%h3 Learn More
@@ -24,7 +24,7 @@
%h3 {{theme.name}}
%a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate
%a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate
%div{"ng-if" => "theme.showDetails"}
.mt-3{"ng-if" => "theme.showDetails"}
.link-group
%a.red{"ng-click" => "deleteTheme(theme); $event.stopPropagation();"} Delete
%a{"ng-click" => "theme.showLink = !theme.showLink; $event.stopPropagation();"} Show Link
@@ -43,7 +43,7 @@
%div.mt-5{"ng-if" => "editor.showDetails"}
.link-group
%a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default
%a.blue{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
%a.tinted{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
%a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link
%a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
@@ -86,8 +86,8 @@
%div
.mt-5{"ng-if" => "action.repeat_mode"}
%button.light{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
%button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension); $event.stopPropagation();"} Enable
%button.light.tinted{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension); $event.stopPropagation();"} Disable
%button.light.tinted{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension); $event.stopPropagation();"} Enable
%button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension); $event.stopPropagation();"}
Perform Action
.spinner.mb-5.block{"ng-if" => "action.running"}
@@ -109,7 +109,7 @@
%h3 {{component.name}}
%a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate
%a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate
%div{"ng-if" => "component.showDetails"}
.mt-3{"ng-if" => "component.showDetails"}
.link-group
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
@@ -117,10 +117,24 @@
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
{{component.url}}
%div{"ng-if" => "serverExtensions.length > 0"}
.container.no-bottom.section-margin
%h2 Server Extensions
%ul
%li{"ng-repeat" => "ext in serverExtensions", "ng-click" => "ext.showDetails = !ext.showDetails"}
.container
%strong.red.medium{"ng-if" => "ext.conflict_of"} Conflicted copy
%h3 {{nameForServerExtension(ext)}}
%div.mt-3{"ng-if" => "ext.showDetails"}
.link-group
%a{"ng-click" => "ext.showUrl = !ext.showUrl; $event.stopPropagation();"} Show Link
%a.red{ "ng-click" => "deleteServerExt(ext); $event.stopPropagation();"} Delete
.wrap.mt-5.selectable{"ng-if" => "ext.showUrl"} {{ext.url}}
.container.section-margin
%h2.blue Install
%h2.tinted Install
%p.faded Enter an install link
%form.mt-10.mb-10
%input.form-control{:autofocus => 'autofocus', :name => 'url', :required => true, :autocomplete => "off",
:type => 'url', 'ng-model' => 'formData.installLink', "ng-keyup" => "$event.keyCode == 13 && submitInstallLink();"}
%p.blue{"ng-if" => "formData.successfullyInstalled"} Successfully installed extension.
%p.tinted{"ng-if" => "formData.successfullyInstalled"} Successfully installed extension.

View File

@@ -12,7 +12,7 @@
%p {{permission}}
%h4 Status
%p.status{"ng-class" => "{'trusted' : component.trusted}"} {{component.trusted ? 'Trusted' : 'Untrusted'}}
%p.status{"ng-class" => "{'trusted tinted' : component.trusted}"} {{component.trusted ? 'Trusted' : 'Untrusted'}}
.learn-more
%h4 Details
@@ -22,4 +22,4 @@
.buttons
%button.standard.white{"ng-click" => "deny()"} Deny
%button.standard.blue{"ng-click" => "accept()"} Accept
%button.standard.tinted{"ng-click" => "accept()"} Accept

View File

@@ -16,11 +16,24 @@
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;"} Menu
%ul.dropdown-menu{"ng-if" => "ctrl.showMenu"}
%ul.dropdown-menu.sectioned-menu{"ng-if" => "ctrl.showMenu"}
%li
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleFullScreen()"} Toggle Fullscreen
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.togglePin()"}
%i.icon.ion-ios-flag
{{ctrl.note.pinned ? "Unpin" : "Pin"}}
%li
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"} Delete Note
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleArchiveNote()"}
%i.icon.ion-ios-box
{{ctrl.note.archived ? "Unarcnive" : "Archive"}}
%li
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"}
%i.icon.ion-trash-b
Delete
%li
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleFullScreen()"}
%i.icon.ion-arrow-expand
Toggle Fullscreen
%li{"ng-if" => "ctrl.hasDisabledComponents()"}
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components

View File

@@ -2,7 +2,7 @@
.pull-left
.footer-bar-link{"click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
%account-menu{"ng-if" => "ctrl.showAccountMenu"}
%account-menu{"ng-if" => "ctrl.showAccountMenu", "on-successful-auth" => "ctrl.onAuthSuccess"}
.footer-bar-link{"click-outside" => "ctrl.showExtensionsMenu = false;", "is-open" => "ctrl.showExtensionsMenu"}
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
@@ -15,7 +15,7 @@
.pull-right
.footer-bar-link{"ng-if" => "ctrl.newUpdateAvailable", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
%span.blue.normal New update downloaded. Installs on app restart.
%span.tinted.normal New update downloaded. Installs on app restart.
.footer-bar-link{"style" => "margin-right: 5px;"}
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
@@ -26,3 +26,6 @@
%strong{"ng-if" => "ctrl.offline"} Offline
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh
%span{"ng-if" => "ctrl.hasPasscode()"}
%i.icon.ion-locked{"ng-click" => "ctrl.lockApp()"}

View File

@@ -1,10 +1,11 @@
.main-ui-view
.app
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "will-select" => "tagsWillMakeSelection", "selection-made" => "tagsSelectionMade", "all-tag" => "allTag",
"tags" => "tags", "remove-tag" => "removeTag"}
%lock-screen{"ng-if" => "needsUnlock", "on-success" => "onSuccessfulUnlock"}
.app{"ng-if" => "!needsUnlock"}
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "will-select" => "tagsWillMakeSelection", "selection-made" => "tagsSelectionMade",
"all-tag" => "allTag", "archive-tag" => "archiveTag", "tags" => "tags", "remove-tag" => "removeTag"}
%notes-section{"add-new" => "notesAddNew", "selection-made" => "notesSelectionMade", "tag" => "selectedTag"}
%editor-section{"note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"}
%footer
%footer{"ng-if" => "!needsUnlock"}

View File

@@ -0,0 +1,9 @@
#lock-screen
.content
%h3.center-align Passcode Required
%form.mt-20{"ng-submit" => "submitPasscodeForm()"}
%input.form-control.mt-10{:type => 'password',
"ng-model" => "formData.passcode", "autofocus" => "true",
"placeholder" => "Enter Passcode", "autocomplete" => "new-password"}
%button.standard.ui-button.block.tinted.mt-5{"type" => "submit"} Unlock

View File

@@ -9,7 +9,7 @@
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.noteFilter.text = ''; ctrl.filterTextChanged()"} ✕
%ul.section-menu-bar#tag-menu-bar
%li{"ng-class" => "{'selected' : ctrl.showMenu}"}
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"} Sort
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"} {{ctrl.sortByTitle()}}
%ul.dropdown-menu{"ng-if" => "ctrl.showMenu"}
%li
@@ -27,12 +27,22 @@
.scrollable
.infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | orderBy: ctrl.sortBy:ctrl.sortDescending | limitTo:ctrl.notesToDisplay))",
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy| limitTo:ctrl.notesToDisplay))",
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
%strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting
.pinned.tinted{"ng-if" => "note.pinned", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
%i.icon.ion-ios-flag
%strong.medium Pinned
.tags-string{"ng-if" => "ctrl.tag.all"}
.faded {{note.tagsString()}}
.name{"ng-if" => "note.title"}
{{note.title}}
.note-preview
{{note.text}}
.date {{(note.created_at | appDateTime) || 'Now'}}
.date.faded
%span{"ng-if" => "ctrl.sortBy == 'updated_at'"} Modified {{note.updatedAtString() || 'Now'}}
%span{"ng-if" => "ctrl.sortBy != 'updated_at'"} {{note.createdAtString() || 'Now'}}

View File

@@ -21,3 +21,6 @@
%a.item{"ng-click" => "ctrl.selectedRenameTag($event, tag)", "ng-if" => "!ctrl.editingTag"} Rename
%a.item{"ng-click" => "ctrl.saveTag($event, tag)", "ng-if" => "ctrl.editingTag"} Save
%a.item{"ng-click" => "ctrl.selectedDeleteTag(tag)"} Delete
.tag.faded{"ng-if" => "ctrl.archiveTag", "ng-click" => "ctrl.selectTag(ctrl.archiveTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.archiveTag}"}
.info
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.archiveTag.title"}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html ng-app="app.frontend" ng-controller="BaseCtrl">
<html ng-app="app.frontend">
<head>
<meta charset="utf-8"/>
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>

View File

@@ -53,12 +53,12 @@ module Neeto
font_src: %w('self'),
form_action: %w('self'),
frame_ancestors: %w('none'),
img_src: %w('self' piwik.standardnotes.org data:),
img_src: %w('self' data:),
manifest_src: %w('self'),
media_src: %w('self'),
object_src: %w('self'),
plugin_types: %w(),
script_src: %w('self' 'unsafe-inline' piwik.standardnotes.org),
script_src: %w('self' 'unsafe-inline'),
style_src: %w(* 'unsafe-inline'),
upgrade_insecure_requests: false, # see https://www.w3.org/TR/upgrade-insecure-requests/
}

View File

@@ -20,6 +20,7 @@
"grunt-babel": "^6.0.0",
"grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-cssmin": "^1.0.2",
"grunt-contrib-sass": "^1.0.0",
"grunt-contrib-uglify": "^2.0.0",

BIN
vendor/assets/fonts/ionicons.eot vendored Normal file

Binary file not shown.

2230
vendor/assets/fonts/ionicons.svg vendored Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 326 KiB

BIN
vendor/assets/fonts/ionicons.ttf vendored Normal file

Binary file not shown.

BIN
vendor/assets/fonts/ionicons.woff vendored Normal file

Binary file not shown.