Merge pull request #265 from standardnotes/sk-styles

3.0
This commit is contained in:
Mo Bitar
2018-12-21 11:33:27 -06:00
committed by GitHub
77 changed files with 4172 additions and 2660 deletions

View File

@@ -22,10 +22,12 @@ angular.module('app')
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) {
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager,
syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory, privilegesManager) {
this.spellcheck = true;
this.componentManager = componentManager;
this.componentStack = [];
$rootScope.$on("sync:taking-too-long", function(){
this.syncTakingTooLong = true;
@@ -87,11 +89,14 @@ angular.module('app')
}
});
// Observe editor changes to see if the current note should update its editor
modelManager.addItemSyncObserver("editor-component-observer", "SN|Component", (allItems, validItems, deletedItems, source) => {
if(!this.note) { return; }
// Reload componentStack in case new ones were added or removed
this.reloadComponentStackArray();
// Observe editor changes to see if the current note should update its editor
var editors = allItems.filter(function(item) {
return item.isEditor();
});
@@ -157,24 +162,7 @@ angular.module('app')
}
this.editorForNote = function(note) {
let editors = componentManager.componentsForArea("editor-editor");
for(var editor of editors) {
if(editor.isExplicitlyEnabledForItem(note)) {
return editor;
}
}
// No editor found for note. Use default editor, if note does not prefer system editor
if(!note.getAppDataItem("prefersPlainEditor")) {
return editors.filter((e) => {return e.isDefaultEditor()})[0];
}
}
this.onEditorMenuClick = function() {
// App bar menu item click
this.showEditorMenu = !this.showEditorMenu;
this.showMenu = false;
this.showExtensions = false;
return componentManager.editorForNote(note);
}
this.closeAllMenus = function() {
@@ -183,6 +171,17 @@ angular.module('app')
this.showExtensions = false;
}
this.toggleMenu = function(menu) {
this[menu] = !this[menu];
let allMenus = ['showMenu', 'showEditorMenu', 'showExtensions', 'showSessionHistory'];
for(var candidate of allMenus) {
if(candidate != menu) {
this[candidate] = false;
}
}
}
this.editorMenuOnSelect = function(component) {
if(!component || component.area == "editor-editor") {
// if plain editor or other editor
@@ -329,7 +328,7 @@ angular.module('app')
}
this.showSavingStatus = function() {
this.noteStatus = $sce.trustAsHtml("Saving...");
this.noteStatus = {message: "Saving..."};
}
this.showAllChangesSavedStatus = function() {
@@ -340,7 +339,7 @@ angular.module('app')
if(authManager.offline()) {
status += " (offline)";
}
this.noteStatus = $sce.trustAsHtml(status);
this.noteStatus = {message: status};
}
this.showErrorStatus = function(error) {
@@ -352,7 +351,7 @@ angular.module('app')
}
this.saveError = true;
this.syncTakingTooLong = false;
this.noteStatus = $sce.trustAsHtml(`<span class='error bold'>${error.message}</span><br>${error.desc}`)
this.noteStatus = error;
}
this.contentChanged = function() {
@@ -382,16 +381,28 @@ angular.module('app')
}
}
this.deleteNote = function() {
if(this.note.locked) {
alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
return;
this.deleteNote = async function() {
let run = () => {
$timeout(() => {
if(this.note.locked) {
alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
return;
}
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
if(confirm(`Are you sure you want to delete ${title}?`)) {
this.remove()(this.note);
this.showMenu = false;
}
});
}
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
if(confirm(`Are you sure you want to delete ${title}?`)) {
this.remove()(this.note);
this.showMenu = false;
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => {
run();
});
} else {
run();
}
}
@@ -405,6 +416,18 @@ angular.module('app')
this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true});
}
this.toggleProtectNote = function() {
this.note.content.protected = !this.note.content.protected;
this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true});
// Show privilegesManager if Protection is not yet set up
privilegesManager.actionHasPrivilegesConfigured(PrivilegesManager.ActionViewProtectedNotes).then((configured) => {
if(!configured) {
privilegesManager.presentPrivilegesManagementModal();
}
})
}
this.toggleNotePreview = function() {
this.note.content.hidePreview = !this.note.content.hidePreview;
this.changesMade({dontUpdateClientModified: true, dontUpdatePreviews: true});
@@ -499,7 +522,7 @@ angular.module('app')
this.loadPreferences = function() {
this.monospaceFont = authManager.getUserPrefValue("monospaceFont", "monospace");
this.spellcheck = authManager.getUserPrefValue("spellcheck", true);
this.marginResizersEnabled = authManager.getUserPrefValue("marginResizersEnabled", false);
this.marginResizersEnabled = authManager.getUserPrefValue("marginResizersEnabled", true);
if(!document.getElementById("editor-content")) {
// Elements have not yet loaded due to ng-if around wrapper
@@ -601,7 +624,9 @@ angular.module('app')
this.reloadComponentContext();
}
}, contextRequestHandler: (component) => {
return this.note;
if(component == this.selectedEditor || this.componentStack.includes(component)) {
return this.note;
}
}, focusHandler: (component, focused) => {
if(component.isEditor() && focused) {
this.closeAllMenus();
@@ -662,9 +687,18 @@ angular.module('app')
}
}});
this.reloadComponentStackArray = function() {
this.componentStack = componentManager.componentsForArea("editor-stack").sort((a, b) => {
// Careful here. For some reason (probably because re-assigning array everytime quickly destroys componentView elements, causing deallocs),
// sorting by updated_at (or any other property that may always be changing)
// causes weird problems with ext communication when changing notes or activating/deactivating in quick succession
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
}
this.reloadComponentContext = function() {
// componentStack is used by the template to ng-repeat
this.componentStack = componentManager.componentsForArea("editor-stack");
this.reloadComponentStackArray();
/*
In the past, we were doing this looping code even if the note wasn't currently defined.
The problem is if an editor stack item loaded first, requested to stream items, and the note was undefined,
@@ -679,7 +713,7 @@ angular.module('app')
if(this.note) {
for(var component of this.componentStack) {
if(component.active) {
component.hidden = component.isExplicitlyDisabledForItem(this.note);
component.hidden = !component.isExplicitlyEnabledForItem(this.note);
}
}
}
@@ -690,13 +724,15 @@ angular.module('app')
}
this.toggleStackComponentForCurrentItem = function(component) {
if(component.hidden) {
// If it's hidden, we want to show it
// If it's not active, then hidden won't be set, and we mean to activate and show it.
if(component.hidden || !component.active) {
// Unhide, associate with current item
component.hidden = false;
this.associateComponentWithCurrentNote(component);
if(!component.active) {
componentManager.activateComponent(component);
}
this.associateComponentWithCurrentNote(component);
componentManager.contextItemDidChangeInArea("editor-stack");
} else {
// not hidden, hide

View File

@@ -23,7 +23,8 @@ angular.module('app')
}
})
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) {
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager,
privilegesManager) {
authManager.checkForSecurityUpdate().then((available) => {
this.securityUpdateAvailable = available;
@@ -137,11 +138,70 @@ angular.module('app')
this.componentManager = componentManager;
this.rooms = [];
this.themesWithIcons = [];
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
this.rooms = modelManager.components.filter((candidate) => {return candidate.area == "rooms" && !candidate.deleted});
});
modelManager.addItemSyncObserver("footer-bar-themes", "SN|Theme", (allItems, validItems, deletedItems, source) => {
let themes = modelManager.validItemsForContentType("SN|Theme").filter((candidate) => {
return !candidate.deleted && candidate.content.package_info.dock_icon;
}).sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
let differ = themes.length != this.themesWithIcons.length;
this.themesWithIcons = themes;
if(differ) {
this.reloadDockShortcuts();
}
});
this.reloadDockShortcuts = function() {
let shortcuts = [];
for(var theme of this.themesWithIcons) {
var icon = theme.content.package_info.dock_icon;
if(!icon) {
continue;
}
shortcuts.push({
component: theme,
icon: icon
})
}
this.dockShortcuts = shortcuts.sort((a, b) => {
// circles first, then images
var aType = a.icon.type;
var bType = b.icon.type;
if(aType == bType) {
return 0;
} else if(aType == "circle" && bType == "svg") {
return -1;
} else if(bType == "circle" && aType == "svg") {
return 1;
}
});
}
this.initSvgForShortcut = function(shortcut) {
var id = "dock-svg-" + shortcut.component.uuid;
var element = document.getElementById(id);
var parser = new DOMParser();
var svg = shortcut.component.content.package_info.dock_icon.source;
var doc = parser.parseFromString(svg, "image/svg+xml");
element.appendChild(doc.documentElement);
}
this.selectShortcut = function(shortcut) {
componentManager.toggleComponent(shortcut.component);
}
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
// RIP: There used to be code here that checked if component.active was true, and if so, displayed the component.
// However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer,
@@ -172,7 +232,31 @@ angular.module('app')
}
}
this.selectRoom = function(room) {
room.showRoom = !room.showRoom;
this.selectRoom = async function(room) {
let run = () => {
$timeout(() => {
room.showRoom = !room.showRoom;
})
}
if(!room.showRoom) {
// About to show, check if has privileges
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => {
run();
});
} else {
run();
}
} else {
run();
}
}
this.clickOutsideAccountMenu = function() {
if(privilegesManager.authenticationInProgress()) {
return;
}
this.showAccountMenu = false;
}
});

View File

@@ -1,6 +1,7 @@
angular.module('app')
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) {
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager,
privilegesManager) {
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
@@ -10,6 +11,17 @@ angular.module('app')
$rootScope.$broadcast('new-update-available', version);
}
$rootScope.$on("panel-resized", (event, info) => {
if(info.panel == "notes") { this.notesCollapsed = info.collapsed; }
if(info.panel == "tags") { this.tagsCollapsed = info.collapsed; }
let appClass = "";
if(this.notesCollapsed) { appClass += "collapsed-notes"; }
if(this.tagsCollapsed) { appClass += " collapsed-tags"; }
$scope.appClass = appClass;
})
/* Used to avoid circular dependencies where syncManager cannot be imported but rootScope can */
$rootScope.sync = function(source) {
syncManager.sync();
@@ -83,14 +95,14 @@ angular.module('app')
syncManager.loadLocalItems().then(() => {
$timeout(() => {
$scope.allTag.didLoad = true;
$rootScope.$broadcast("initial-data-loaded");
$rootScope.$broadcast("initial-data-loaded"); // This needs to be processed first before sync is called so that singletonManager observers function properly.
syncManager.sync();
// refresh every 30s
setInterval(function () {
syncManager.sync();
}, 30000);
})
syncManager.sync();
// refresh every 30s
setInterval(function () {
syncManager.sync();
}, 30000);
});
authManager.addEventHandler((event) => {
@@ -196,7 +208,7 @@ angular.module('app')
modelManager.setItemToBeDeleted(tag);
syncManager.sync().then(() => {
// force scope tags to update on sub directives
$scope.safeApply();
$rootScope.safeApply();
});
}
}
@@ -218,7 +230,7 @@ angular.module('app')
Shared Callbacks
*/
$scope.safeApply = function(fn) {
$rootScope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest')
this.$eval(fn);
@@ -250,7 +262,7 @@ angular.module('app')
// when deleting items while ofline, we need to explictly tell angular to refresh UI
setTimeout(function () {
$rootScope.notifyDelete();
$scope.safeApply();
$rootScope.safeApply();
}, 50);
} else {
$timeout(() => {

View File

@@ -41,7 +41,6 @@ class LockScreen {
})
}
}
}
angular.module('app').directive('lockScreen', () => new LockScreen);

View File

@@ -31,7 +31,8 @@ angular.module('app')
}
}
})
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager, desktopManager) {
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager,
storageManager, desktopManager, privilegesManager) {
this.panelController = {};
this.searchSubmitted = false;
@@ -44,6 +45,7 @@ angular.module('app')
let prevSortValue = this.sortBy;
this.sortBy = authManager.getUserPrefValue("sortBy", "created_at");
this.sortReverse = authManager.getUserPrefValue("sortReverse", false);
if(this.sortBy == "updated_at") {
// use client_updated_at instead
@@ -55,7 +57,6 @@ angular.module('app')
this.selectFirstNote();
})
}
this.sortDescending = this.sortBy != "title";
this.showArchived = authManager.getUserPrefValue("showArchived", false);
this.hidePinned = authManager.getUserPrefValue("hidePinned", false);
@@ -66,14 +67,18 @@ angular.module('app')
let width = authManager.getUserPrefValue("notesPanelWidth");
if(width) {
this.panelController.setWidth(width);
if(this.panelController.isCollapsed()) {
$rootScope.$broadcast("panel-resized", {panel: "notes", collapsed: this.panelController.isCollapsed()})
}
}
}
this.loadPreferences();
this.onPanelResize = function(newWidth) {
this.onPanelResize = function(newWidth, lastLeft, isAtMaxWidth, isCollapsed) {
authManager.setUserPrefValue("notesPanelWidth", newWidth);
authManager.syncUserPreferences();
$rootScope.$broadcast("panel-resized", {panel: "notes", collapsed: isCollapsed})
}
angular.element(document).ready(() => {
@@ -203,19 +208,31 @@ angular.module('app')
}
}
this.selectNote = function(note, viaClick = false) {
this.selectNote = async function(note, viaClick = false) {
if(!note) {
this.createNewNote();
return;
}
this.selectedNote = note;
note.conflict_of = null; // clear conflict
this.selectionMade()(note);
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
let run = () => {
$timeout(() => {
this.selectedNote = note;
note.conflict_of = null; // clear conflict
this.selectionMade()(note);
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
if(viaClick && this.isFiltering()) {
desktopManager.searchText(this.noteFilter.text);
if(viaClick && this.isFiltering()) {
desktopManager.searchText(this.noteFilter.text);
}
})
}
if(note.content.protected && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewProtectedNotes)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewProtectedNotes, () => {
run();
});
} else {
run();
}
}
@@ -298,17 +315,21 @@ angular.module('app')
this.selectedSortByCreated = function() {
this.setSortBy("created_at");
this.sortDescending = true;
}
this.selectedSortByUpdated = function() {
this.setSortBy("client_updated_at");
this.sortDescending = true;
}
this.selectedSortByTitle = function() {
this.setSortBy("title");
this.sortDescending = false;
}
this.toggleReverseSort = function() {
this.selectedMenuItem();
this.sortReverse = !this.sortReverse;
authManager.setUserPrefValue("sortReverse", this.sortReverse);
authManager.syncUserPreferences();
}
this.setSortBy = function(type) {

View File

@@ -47,13 +47,17 @@ angular.module('app')
let width = authManager.getUserPrefValue("tagsPanelWidth");
if(width) {
this.panelController.setWidth(width);
if(this.panelController.isCollapsed()) {
$rootScope.$broadcast("panel-resized", {panel: "tags", collapsed: this.panelController.isCollapsed()})
}
}
}
this.loadPreferences();
this.onPanelResize = function(newWidth) {
this.onPanelResize = function(newWidth, lastLeft, isAtMaxWidth, isCollapsed) {
authManager.setUserPrefValue("tagsPanelWidth", newWidth, true);
$rootScope.$broadcast("panel-resized", {panel: "tags", collapsed: isCollapsed})
}
this.componentManager = componentManager;

View File

@@ -0,0 +1,15 @@
angular
.module('app')
.directive( 'elemReady', function( $parse ) {
return {
restrict: 'A',
link: function( $scope, elem, attrs ) {
elem.ready(function(){
$scope.$apply(function(){
var func = $parse(attrs.elemReady);
func($scope);
})
})
}
}
})

View File

@@ -2,9 +2,6 @@ angular.module('app').directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
// elem.css('overflow-x', 'hidden');
// elem.css('height', 'inherit');
var offset = parseInt(attrs.threshold) || 0;
var e = elem[0]

View File

@@ -0,0 +1,15 @@
angular
.module('app')
.directive('snEnter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.snEnter, {'event': event});
});
event.preventDefault();
}
});
};
});

View File

@@ -10,10 +10,11 @@ class AccountMenu {
}
controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager,
$timeout, $compile, archiveManager) {
$timeout, $compile, archiveManager, privilegesManager) {
'ngInject';
$scope.formData = {mergeLocal: true, ephemeral: false};
$scope.user = authManager.user;
syncManager.getServerURL().then((url) => {
@@ -38,7 +39,6 @@ class AccountMenu {
}
$scope.canAddPasscode = !authManager.isEphemeralSession();
$scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() {
@@ -167,10 +167,27 @@ class AccountMenu {
$scope.openPasswordWizard = function(type) {
// Close the account menu
$scope.close();
authManager.presentPasswordWizard(type);
}
$scope.openPrivilegesModal = async function() {
$scope.close();
let run = () => {
$timeout(() => {
privilegesManager.presentPrivilegesManagementModal();
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePrivileges)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePrivileges, () => {
run();
});
} else {
run();
}
}
// Allows indexeddb unencrypted logs to be deleted
// clearAllModels will remove data from backing store, but not from working memory
// See: https://github.com/standardnotes/desktop/issues/131
@@ -229,36 +246,49 @@ class AccountMenu {
})
}
$scope.importFileSelected = function(files) {
$scope.importData = {};
$scope.importFileSelected = async function(files) {
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
try {
var data = JSON.parse(e.target.result);
$timeout(function(){
if(data.auth_params) {
// request password
$scope.importData.requestPassword = true;
$scope.importData.data = data;
let run = () => {
$timeout(() => {
$scope.importData = {};
$timeout(() => {
var element = document.getElementById("import-password-request");
if(element) {
element.scrollIntoView(false);
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
try {
var data = JSON.parse(e.target.result);
$timeout(function(){
if(data.auth_params) {
// request password
$scope.importData.requestPassword = true;
$scope.importData.data = data;
$timeout(() => {
var element = document.getElementById("import-password-request");
if(element) {
element.scrollIntoView(false);
}
})
} else {
$scope.performImport(data, null);
}
})
} else {
$scope.performImport(data, null);
} catch (e) {
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
}
})
} catch (e) {
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
}
}
reader.readAsText(file);
})
}
reader.readAsText(file);
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
run();
});
} else {
run();
}
}
$scope.importJSONData = function(data, password, callback) {
@@ -316,7 +346,7 @@ class AccountMenu {
Export
*/
$scope.downloadDataArchive = function() {
$scope.downloadDataArchive = async function() {
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
}
@@ -362,6 +392,35 @@ class AccountMenu {
Passcode Lock
*/
$scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions();
$scope.reloadAutoLockInterval = function() {
passcodeManager.getAutoLockInterval().then((interval) => {
$timeout(() => {
$scope.selectedAutoLockInterval = interval;
})
})
}
$scope.reloadAutoLockInterval();
$scope.selectAutoLockInterval = async function(interval) {
let run = async () => {
await passcodeManager.setAutoLockInterval(interval);
$timeout(() => {
$scope.reloadAutoLockInterval();
});
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
}
}
$scope.hasPasscode = function() {
return passcodeManager.hasPasscode();
}
@@ -377,10 +436,12 @@ class AccountMenu {
return;
}
let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode : passcodeManager.setPasscode;
let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode.bind(passcodeManager) : passcodeManager.setPasscode.bind(passcodeManager);
fn(passcode, () => {
$timeout(function(){
$timeout(() => {
$scope.formData.passcode = null;
$scope.formData.confirmPasscode = null;
$scope.formData.showPasscodeForm = false;
var offline = authManager.offline();
@@ -393,27 +454,50 @@ class AccountMenu {
})
}
$scope.changePasscodePressed = function() {
$scope.formData.changingPasscode = true;
$scope.addPasscodeClicked();
$scope.formData.changingPasscode = false;
$scope.changePasscodePressed = async function() {
let run = () => {
$timeout(() => {
$scope.formData.changingPasscode = true;
$scope.addPasscodeClicked();
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
}
}
$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();
$scope.removePasscodePressed = async function() {
let run = () => {
$timeout(() => {
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();
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
// we don't want to write unencrypted data to disk.
// $rootScope.$broadcast("major-data-change");
}
if(authManager.offline()) {
syncManager.markAllItemsDirtyAndSaveOffline();
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
// we don't want to write unencrypted data to disk.
// $rootScope.$broadcast("major-data-change");
}
}
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
}
}

View File

@@ -30,6 +30,16 @@ class ComponentView {
controller($scope, $rootScope, $timeout, componentManager, desktopManager, themeManager) {
'ngInject';
$scope.onVisibilityChange = function() {
if(document.visibilityState == "hidden") {
return;
}
if($scope.issueLoading) {
$scope.reloadComponent();
}
}
$scope.themeHandlerIdentifier = "component-view-" + Math.random();
componentManager.registerHandler({identifier: $scope.themeHandlerIdentifier, areas: ["themes"], activationHandler: (component) => {
$scope.reloadThemeStatus();
@@ -45,34 +55,58 @@ class ComponentView {
return;
}
// activationHandlers may be called multiple times, design below to be idempotent
if(component.active) {
$scope.loading = true;
let iframe = componentManager.iframeForComponent(component);
if(iframe) {
// begin loading error handler. If onload isn't called in x seconds, display an error
if($scope.loadTimeout) { $timeout.cancel($scope.loadTimeout);}
$scope.loadTimeout = $timeout(() => {
if($scope.loading) {
$scope.issueLoading = true;
}
}, 3500);
iframe.onload = function(event) {
// console.log("iframe loaded for component", component.name, "cancelling load timeout", $scope.loadTimeout);
$timeout.cancel($scope.loadTimeout);
$scope.loading = false;
$scope.issueLoading = false;
componentManager.registerComponentWindow(component, iframe.contentWindow);
}.bind(this);
}
}
},
$timeout(() => {
$scope.handleActivation();
})
},
actionHandler: (component, action, data) => {
if(action == "set-size") {
componentManager.handleSetSizeEvent(component, data);
}
}}
);
}
});
$scope.handleActivation = function() {
// activationHandlers may be called multiple times, design below to be idempotent
let component = $scope.component;
if(!component.active) {
return;
}
$scope.loading = true;
let iframe = componentManager.iframeForComponent(component);
if(iframe) {
// begin loading error handler. If onload isn't called in x seconds, display an error
if($scope.loadTimeout) { $timeout.cancel($scope.loadTimeout);}
$scope.loadTimeout = $timeout(() => {
if($scope.loading) {
$scope.loading = false;
$scope.issueLoading = true;
if(!$scope.didAttemptReload) {
$scope.didAttemptReload = true;
$scope.reloadComponent();
} else {
// We'll attempt to reload when the tab gains focus
document.addEventListener("visibilitychange", $scope.onVisibilityChange);
}
}
}, 3500);
iframe.onload = (event) => {
$timeout.cancel($scope.loadTimeout);
componentManager.registerComponentWindow(component, iframe.contentWindow);
// Add small timeout to, as $scope.loading controls loading overlay,
// which is used to avoid flicker when enabling extensions while having an enabled theme
// we don't use ng-show because it causes problems with rendering iframes after timeout, for some reason.
$timeout(() => {
$scope.loading = false;
$scope.issueLoading = false;
}, 7)
};
}
}
/*
@@ -106,7 +140,11 @@ class ComponentView {
$scope.reloadComponent = function() {
console.log("Reloading component", $scope.component);
componentManager.reloadComponent($scope.component);
// force iFrame to deinit, allows new one to be created
$scope.componentValid = false;
componentManager.reloadComponent($scope.component).then(() => {
$scope.reloadStatus();
});
}
$scope.reloadStatus = function(doManualReload = true) {
@@ -114,7 +152,7 @@ class ComponentView {
$scope.reloading = true;
let previouslyValid = $scope.componentValid;
var expired, offlineRestricted, urlError;
var offlineRestricted, urlError;
offlineRestricted = component.offlineOnly && !isDesktopApplication();
@@ -123,13 +161,23 @@ class ComponentView {
||
(isDesktopApplication() && (!component.local_url && !component.hasValidHostedUrl()))
expired = component.valid_until && component.valid_until <= new Date();
$scope.expired = component.valid_until && component.valid_until <= new Date();
$scope.componentValid = !offlineRestricted && !urlError && !expired;
// Here we choose our own readonly state based on custom logic. However, if a parent
// wants to implement their own readonly logic, they can lock it.
if(!component.lockReadonly) {
component.readonly = $scope.expired;
}
$scope.componentValid = !offlineRestricted && !urlError;
if(!$scope.componentValid) {
// required to disable overlay
$scope.loading = false;
}
if(offlineRestricted) $scope.error = 'offline-restricted';
else if(urlError) $scope.error = 'url-missing';
else if(expired) $scope.error = 'expired';
else $scope.error = null;
if($scope.componentValid !== previouslyValid) {
@@ -139,7 +187,7 @@ class ComponentView {
}
}
if(expired && doManualReload) {
if($scope.expired && doManualReload) {
// Try reloading, handled by footer, which will open Extensions window momentarily to pull in latest data
// Upon completion, this method, reloadStatus, will be called, upon where doManualReload will be false to prevent recursion.
$rootScope.$broadcast("reload-ext-data");
@@ -189,10 +237,10 @@ class ComponentView {
}
desktopManager.deregisterUpdateObserver($scope.updateObserver);
document.removeEventListener("visibilitychange", $scope.onVisibilityChange);
}
$scope.$on("$destroy", function() {
// console.log("Deregistering handler", $scope.identifier, $scope.component.name);
$scope.destroy();
});
}

View File

@@ -19,10 +19,6 @@ class EditorMenu {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
$scope.stack = componentManager.componentsForArea("editor-stack").sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
$scope.isDesktop = isDesktopApplication();
$scope.defaultEditor = $scope.editors.filter((e) => {return e.isDefaultEditor()})[0];
@@ -77,17 +73,10 @@ class EditorMenu {
if(component == $scope.selectedEditor) {
return true;
} else if(component.area == "editor-stack") {
return $scope.stackComponentEnabled(component);
} else {
return false;
}
}
$scope.stackComponentEnabled = function(component) {
return component.active && !component.isExplicitlyDisabledForItem($scope.currentItem);
}
}
}

View File

@@ -7,6 +7,7 @@ class MenuRow {
this.scope = {
action: "&",
circle: "=",
circleAlign: "=",
label: "=",
subtitle: "=",
hasButton: "=",

View File

@@ -32,6 +32,10 @@ class PanelResizer {
scope.control.flash = function() {
scope.flash();
}
scope.control.isCollapsed = function() {
return scope.isCollapsed();
}
}
controller($scope, $element, modelManager, actionsManager, $timeout, $compile) {
@@ -52,16 +56,19 @@ class PanelResizer {
// Handle Double Click Event
var widthBeforeLastDblClick = 0;
resizerColumn.ondblclick = () => {
var collapsed = $scope.isCollapsed();
$timeout(() => {
if(collapsed) {
var preClickCollapseState = $scope.isCollapsed();
if(preClickCollapseState) {
$scope.setWidth(widthBeforeLastDblClick || $scope.defaultWidth);
} else {
widthBeforeLastDblClick = lastWidth;
$scope.setWidth(minWidth);
}
$scope.finishSettingWidth();
$scope.onResizeFinish()(lastWidth, lastLeft, $scope.isAtMaxWidth());
var newCollapseState = !preClickCollapseState;
$scope.onResizeFinish()(lastWidth, lastLeft, $scope.isAtMaxWidth(), newCollapseState);
})
}
@@ -274,7 +281,7 @@ class PanelResizer {
let isMaxWidth = $scope.isAtMaxWidth();
if($scope.onResizeFinish) {
$scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth);
$scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth, $scope.isCollapsed());
}
$scope.finishSettingWidth();

View File

@@ -102,6 +102,9 @@ class PasswordWizard {
if(preprocessor) {
preprocessor(() => {
next();
}, () => {
// on fail
$scope.isContinuing = false;
})
} else {
next();
@@ -114,7 +117,7 @@ class PasswordWizard {
$scope.preprocessorForStep = function(step) {
if(step == PasswordStep) {
return (callback) => {
return (onSuccess, onFail) => {
$scope.showSpinner = true;
$scope.continueTitle = "Generating Keys...";
$timeout(() => {
@@ -122,7 +125,9 @@ class PasswordWizard {
$scope.showSpinner = false;
$scope.continueTitle = DefaultContinueTitle;
if(success) {
callback();
onSuccess();
} else {
onFail && onFail();
}
});
})

View File

@@ -0,0 +1,92 @@
class PrivilegesAuthModal {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/privileges-auth-modal.html";
this.scope = {
action: "=",
onSuccess: "=",
onCancel: "=",
};
}
link($scope, el, attrs) {
$scope.dismiss = function() {
el.remove();
}
}
controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
'ngInject';
$scope.authenticationParameters = {};
$scope.sessionLengthOptions = privilegesManager.getSessionLengthOptions();
privilegesManager.getSelectedSessionLength().then((length) => {
$timeout(() => {
$scope.selectedSessionLength = length;
})
})
$scope.selectSessionLength = function(length) {
$scope.selectedSessionLength = length;
}
privilegesManager.netCredentialsForAction($scope.action).then((credentials) => {
$timeout(() => {
$scope.requiredCredentials = credentials.sort();
});
});
$scope.promptForCredential = function(credential) {
return privilegesManager.displayInfoForCredential(credential).prompt;
}
$scope.cancel = function() {
$scope.dismiss();
$scope.onCancel && $scope.onCancel();
}
$scope.isCredentialInFailureState = function(credential) {
if(!$scope.failedCredentials) {
return false;
}
return $scope.failedCredentials.find((candidate) => {
return candidate == credential;
}) != null;
}
$scope.validate = function() {
var failed = [];
for(var cred of $scope.requiredCredentials) {
var value = $scope.authenticationParameters[cred];
if(!value || value.length == 0) {
failed.push(cred);
}
}
$scope.failedCredentials = failed;
return failed.length == 0;
}
$scope.submit = function() {
if(!$scope.validate()) {
return;
}
privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => {
$timeout(() => {
if(result.success) {
privilegesManager.setSessionLength($scope.selectedSessionLength);
$scope.onSuccess();
$scope.dismiss();
} else {
$scope.failedCredentials = result.failedCredentials;
}
})
})
}
}
}
angular.module('app').directive('privilegesAuthModal', () => new PrivilegesAuthModal);

View File

@@ -0,0 +1,88 @@
class PrivilegesManagementModal {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/privileges-management-modal.html";
this.scope = {
};
}
link($scope, el, attrs) {
$scope.dismiss = function() {
el.remove();
}
}
controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
'ngInject';
$scope.dummy = {};
$scope.hasPasscode = passcodeManager.hasPasscode();
$scope.hasAccount = !authManager.offline();
$scope.displayInfoForCredential = function(credential) {
let info = privilegesManager.displayInfoForCredential(credential);
if(credential == PrivilegesManager.CredentialLocalPasscode) {
info["availability"] = $scope.hasPasscode;
} else if(credential == PrivilegesManager.CredentialAccountPassword) {
info["availability"] = $scope.hasAccount;
} else {
info["availability"] = true;
}
return info;
}
$scope.displayInfoForAction = function(action) {
return privilegesManager.displayInfoForAction(action).label;
}
$scope.isCredentialRequiredForAction = function(action, credential) {
if(!$scope.privileges) {
return false;
}
return $scope.privileges.isCredentialRequiredForAction(action, credential);
}
$scope.clearSession = function() {
privilegesManager.clearSession().then(() => {
$scope.reloadPrivileges();
})
}
$scope.reloadPrivileges = async function() {
$scope.availableActions = privilegesManager.getAvailableActions();
$scope.availableCredentials = privilegesManager.getAvailableCredentials();
let sessionEndDate = await privilegesManager.getSessionExpirey();
$scope.sessionExpirey = sessionEndDate.toLocaleString();
$scope.sessionExpired = new Date() >= sessionEndDate;
$scope.credentialDisplayInfo = {};
for(let cred of $scope.availableCredentials) {
$scope.credentialDisplayInfo[cred] = $scope.displayInfoForCredential(cred);
}
privilegesManager.getPrivileges().then((privs) => {
$timeout(() => {
$scope.privileges = privs;
})
})
}
$scope.checkboxValueChanged = function(action, credential) {
$scope.privileges.toggleCredentialForAction(action, credential);
privilegesManager.savePrivileges();
}
$scope.reloadPrivileges();
$scope.cancel = function() {
$scope.dismiss();
$scope.onCancel && $scope.onCancel();
}
}
}
angular.module('app').directive('privilegesManagementModal', () => new PrivilegesManagementModal);

View File

@@ -10,15 +10,56 @@ class RevisionPreviewModal {
}
link($scope, el, attrs) {
$scope.dismiss = function() {
el.remove();
}
$scope.el = el;
}
controller($scope, modelManager, syncManager) {
controller($scope, modelManager, syncManager, componentManager, $timeout) {
'ngInject';
$scope.dismiss = function() {
$scope.el.remove();
$scope.$destroy();
}
$scope.$on("$destroy", function() {
if($scope.identifier) {
componentManager.deregisterHandler($scope.identifier);
}
});
$scope.note = new SFItem({content: $scope.content, content_type: "Note"});
// Set UUID to editoForNote can find proper editor,
// but then generate new uuid for note as not to save changes to original, if editor makes changes.
$scope.note.uuid = $scope.uuid;
let editorForNote = componentManager.editorForNote($scope.note);
$scope.note.uuid = SFJS.crypto.generateUUIDSync();
if(editorForNote) {
// Create temporary copy, as a lot of componentManager is uuid based,
// so might interfere with active editor. Be sure to copy only the content, as the
// top level editor object has non-copyable properties like .window, which cannot be transfered
let editorCopy = new SNComponent({content: editorForNote.content});
editorCopy.readonly = true;
editorCopy.lockReadonly = true;
$scope.identifier = editorCopy.uuid;
componentManager.registerHandler({identifier: $scope.identifier, areas: ["editor-editor"],
contextRequestHandler: (component) => {
if(component == $scope.editor) {
return $scope.note;
}
},
componentForSessionKeyHandler: (key) => {
if(key == $scope.editor.sessionKey) {
return $scope.editor;
}
}
});
$scope.editor = editorCopy;
}
$scope.restore = function(asCopy) {
if(!asCopy && !confirm("Are you sure you want to replace the current note's contents with what you see in this preview?")) {
return;

View File

@@ -1,6 +1,6 @@
angular.module('app')
.filter('sortBy', function ($filter) {
return function(items, sortBy) {
return function(items, sortBy, reverse) {
let sortValueFn = (a, b, pinCheck = false) => {
if(!pinCheck) {
if(a.pinned && b.pinned) {
@@ -14,6 +14,11 @@ angular.module('app')
var bValue = b[sortBy] || "";
let vector = 1;
if(reverse) {
vector *= -1;
}
if(sortBy == "title") {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
@@ -21,11 +26,11 @@ angular.module('app')
if(aValue.length == 0 && bValue.length == 0) {
return 0;
} else if(aValue.length == 0 && bValue.length != 0) {
return 1;
return 1 * vector;
} else if(aValue.length != 0 && bValue.length == 0) {
return -1;
return -1 * vector;
} else {
vector = -1;
vector *= -1;
}
}

View File

@@ -0,0 +1,34 @@
class SNPrivileges extends SFItem {
setCredentialsForAction(action, credentials) {
this.content.desktopPrivileges[action] = credentials;
}
getCredentialsForAction(action) {
return this.content.desktopPrivileges[action] || [];
}
toggleCredentialForAction(action, credential) {
if(this.isCredentialRequiredForAction(action, credential)) {
this.removeCredentialForAction(action, credential);
} else {
this.addCredentialForAction(action, credential);
}
}
removeCredentialForAction(action, credential) {
_.pull(this.content.desktopPrivileges[action], credential);
}
addCredentialForAction(action, credential) {
var credentials = this.getCredentialsForAction(action);
credentials.push(credential);
this.setCredentialsForAction(action, credentials);
}
isCredentialRequiredForAction(action, credential) {
var credentialsRequired = this.getCredentialsForAction(action);
return credentialsRequired.includes(credential);
}
}

View File

@@ -202,7 +202,7 @@ class ActionsManager {
var scope = this.$rootScope.$new(true);
scope.uuid = uuid;
scope.content = content;
var el = this.$compile( "<revision-preview-modal uuid='uuid' content='content' class='modal'></revision-preview-modal>" )(scope);
var el = this.$compile( "<revision-preview-modal uuid='uuid' content='content' class='sk-modal'></revision-preview-modal>" )(scope);
angular.element(document.body).append(el);
}

View File

@@ -1,9 +1,10 @@
class ArchiveManager {
constructor(passcodeManager, authManager, modelManager) {
constructor(passcodeManager, authManager, modelManager, privilegesManager) {
this.passcodeManager = passcodeManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.privilegesManager = privilegesManager;
}
/*
@@ -15,26 +16,36 @@ class ArchiveManager {
}
async downloadBackupOfItems(items, encrypted) {
// download in Standard File format
var keys, authParams;
if(encrypted) {
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
let run = async () => {
// download in Standard File format
var keys, authParams;
if(encrypted) {
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
}
}
}
this.__itemsData(items, keys, authParams).then((data) => {
let modifier = encrypted ? "Encrypted" : "Decrypted";
this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
this.__itemsData(items, keys, authParams).then((data) => {
let modifier = encrypted ? "Encrypted" : "Decrypted";
this.__downloadData(data, `Standard Notes ${modifier} Backup - ${this.__formattedDate()}.txt`);
// download as zipped plain text files
if(!keys) {
this.__downloadZippedItems(items);
}
})
// download as zipped plain text files
if(!keys) {
this.__downloadZippedItems(items);
}
})
}
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
run();
});
} else {
run();
}
}
/*

View File

@@ -44,7 +44,7 @@ class AuthManager extends SFAuthManager {
this.storageManager.setItemsMode(StorageManager.Ephemeral);
} else {
this.storageManager.setModelStorageMode(StorageManager.Fixed);
this.storageManager.setItemsMode(this.storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
this.storageManager.setItemsMode(this.storageManager.bestStorageMode());
this.storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
}
}
@@ -92,6 +92,13 @@ class AuthManager extends SFAuthManager {
}
}
async verifyAccountPassword(password) {
let authParams = await this.getAuthParams();
let keys = await SFJS.crypto.computeEncryptionKeysForUser(password, authParams);
let success = keys.mk === (await this.keys()).mk;
return success;
}
async checkForSecurityUpdate() {
if(this.offline()) {
return false;

View File

@@ -33,7 +33,7 @@ class ComponentManager {
desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
if(component.active && component.isTheme()) {
this.postActiveThemeToAllComponents();
this.postActiveThemesToAllComponents();
}
})
@@ -147,14 +147,15 @@ class ComponentManager {
});
}
postActiveThemeToAllComponents() {
postActiveThemesToAllComponents() {
for(var component of this.components) {
// Skip over components that are themes themselves,
// or components that are not active, or components that don't have a window
if(component.isTheme() || !component.active || !component.window) {
continue;
}
this.postActiveThemeToComponent(component);
this.postActiveThemesToComponent(component);
}
}
@@ -162,14 +163,16 @@ class ComponentManager {
return this.componentsForArea("themes").filter((theme) => {return theme.active});
}
postActiveThemeToComponent(component) {
var themes = this.getActiveThemes();
var urls = themes.map((theme) => {
urlsForActiveThemes() {
let themes = this.getActiveThemes();
return themes.map((theme) => {
return this.urlForComponent(theme);
})
var data = {
themes: urls
}
}
postActiveThemesToComponent(component) {
let urls = this.urlsForActiveThemes();
let data = { themes: urls }
this.sendMessageToComponent(component, {action: "themes", data: data})
}
@@ -291,7 +294,18 @@ class ComponentManager {
}
componentForSessionKey(key) {
return _.find(this.components, {sessionKey: key});
let component = _.find(this.components, {sessionKey: key});
if(!component) {
for(let handler of this.handlers) {
if(handler.componentForSessionKeyHandler) {
component = handler.componentForSessionKeyHandler(key);
if(component) {
break;
}
}
}
}
return component;
}
handleMessage(component, message) {
@@ -302,6 +316,24 @@ class ComponentManager {
return;
}
// Actions that won't succeeed with readonly mode
let readwriteActions = [
"save-items",
"associate-item",
"deassociate-item",
"create-item",
"create-items",
"delete-items",
"set-component-data"
];
if(component.readonly && readwriteActions.includes(message.action)) {
// A component can be marked readonly if changes should not be saved.
// Particullary used for revision preview windows where the notes should not be savable.
alert(`The extension ${component.name} is trying to save, but it is in a locked state and cannot accept changes.`);
return;
}
/**
Possible Messages:
set-size
@@ -343,11 +375,13 @@ class ComponentManager {
this.handleInstallLocalComponentMessage(component, message);
} else if(message.action === "present-conflict-resolution") {
this.handlePresentConflictResolutionMessage(component, message);
} else if(message.action === "duplicate-item") {
this.handleDuplicateItemMessage(component, message);
}
// Notify observers
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
if(handler.actionHandler && (handler.areas.includes(component.area) || handler.areas.includes("*"))) {
this.timeout(function(){
handler.actionHandler(component, message.action, message.data);
})
@@ -539,6 +573,24 @@ class ComponentManager {
});
}
handleDuplicateItemMessage(component, message) {
var itemParams = message.data.item;
var item = this.modelManager.findItem(itemParams.uuid);
var requiredPermissions = [
{
name: "stream-items",
content_types: [item.content_type]
}
];
this.runWithPermissions(component, requiredPermissions, () => {
var duplicate = this.modelManager.duplicateItem(item);
this.syncManager.sync();
this.replyToMessage(component, message, {item: this.jsonForItem(duplicate, component)});
});
}
handleCreateItemsMessage(component, message) {
var responseItems = message.data.item ? [message.data.item] : message.data.items;
let uniqueContentTypes = _.uniq(responseItems.map((item) => {return item.content_type}));
@@ -629,22 +681,35 @@ class ComponentManager {
}
handleToggleComponentMessage(sourceComponent, targetComponent, message) {
if(targetComponent.area == "modal") {
this.openModalComponent(targetComponent);
this.toggleComponent(targetComponent);
}
toggleComponent(component) {
if(component.area == "modal") {
this.openModalComponent(component);
} else {
if(targetComponent.active) {
this.deactivateComponent(targetComponent);
if(component.active) {
this.deactivateComponent(component);
} else {
if(targetComponent.content_type == "SN|Theme" && !targetComponent.isLayerable()) {
if(component.content_type == "SN|Theme") {
// Deactive currently active theme if new theme is not layerable
var activeThemes = this.getActiveThemes();
for(var theme of activeThemes) {
if(theme && !theme.isLayerable()) {
this.deactivateComponent(theme);
}
// Activate current before deactivating others, so as not to flicker
this.activateComponent(component);
if(!component.isLayerable()) {
setTimeout(() => {
for(var theme of activeThemes) {
if(theme && !theme.isLayerable()) {
this.deactivateComponent(theme);
}
}
}, 10);
}
} else {
this.activateComponent(component);
}
this.activateComponent(targetComponent);
}
}
}
@@ -765,14 +830,14 @@ class ComponentManager {
var permissions = dialog.permissions;
var component = dialog.component;
var callback = dialog.callback;
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='modal'></permissions-modal>" )(dialog);
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='sk-modal'></permissions-modal>" )(dialog);
angular.element(document.body).append(el);
}
openModalComponent(component) {
var scope = this.$rootScope.$new(true);
scope.component = component;
var el = this.$compile( "<component-modal component='component' class='modal'></component-modal>" )(scope);
var el = this.$compile( "<component-modal component='component' class='sk-modal'></component-modal>" )(scope);
angular.element(document.body).append(el);
}
@@ -782,6 +847,10 @@ class ComponentManager {
deregisterHandler(identifier) {
var handler = _.find(this.handlers, {identifier: identifier});
if(!handler) {
console.log("Attempting to deregister non-existing handler");
return;
}
this.handlers.splice(this.handlers.indexOf(handler), 1);
}
@@ -805,34 +874,39 @@ class ComponentManager {
data: {
uuid: component.uuid,
environment: isDesktopApplication() ? "desktop" : "web",
platform: getPlatformString()
platform: getPlatformString(),
activeThemeUrls: this.urlsForActiveThemes()
}
});
this.postActiveThemeToComponent(component);
this.postActiveThemesToComponent(component);
this.desktopManager.notifyComponentActivation(component);
}
/* Performs func in timeout, but syncronously, if used `await waitTimeout` */
async waitTimeout(func) {
return new Promise((resolve, reject) => {
this.timeout(() => {
func();
resolve();
});
})
}
// No longer used. See comment in activateComponent.
// async waitTimeout(func) {
// return new Promise((resolve, reject) => {
// this.timeout(() => {
// func();
// resolve();
// });
// })
// }
async activateComponent(component, dontSync = false) {
var didChange = component.active != true;
component.active = true;
for(var handler of this.handlers) {
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
// We want to run the handler in a $timeout so the UI updates, but we also don't want it to run asyncronously
// so that the steps below this one are run before the handler. So we run in a waitTimeout.
await this.waitTimeout(() => {
// Update 12/18: We were using this.waitTimeout previously, however, that caused the iframe.onload callback to never be called
// for some reason for iframes on desktop inside the revision-preview-modal. So we'll use safeApply instead. I'm not quite sure
// where the original "so the UI updates" comment applies to, but we'll have to keep an eye out to see if this causes problems somewhere else.
this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -848,7 +922,7 @@ class ComponentManager {
}
if(component.area == "themes") {
this.postActiveThemeToAllComponents();
this.postActiveThemesToAllComponents();
}
}
@@ -859,7 +933,8 @@ class ComponentManager {
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
await this.waitTimeout(() => {
// See comment in activateComponent regarding safeApply and awaitTimeout
this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -881,7 +956,7 @@ class ComponentManager {
})
if(component.area == "themes") {
this.postActiveThemeToAllComponents();
this.postActiveThemesToAllComponents();
}
}
@@ -893,7 +968,8 @@ class ComponentManager {
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
await this.waitTimeout(() => {
// See comment in activateComponent regarding safeApply and awaitTimeout
this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -908,7 +984,7 @@ class ComponentManager {
})
if(component.area == "themes") {
this.postActiveThemeToAllComponents();
this.postActiveThemesToAllComponents();
}
//
@@ -919,7 +995,8 @@ class ComponentManager {
component.active = true;
for(var handler of this.handlers) {
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
await this.waitTimeout(() => {
// See comment in activateComponent regarding safeApply and awaitTimeout
this.$rootScope.safeApply(() => {
handler.activationHandler && handler.activationHandler(component);
})
}
@@ -930,7 +1007,7 @@ class ComponentManager {
}
if(component.area == "themes") {
this.postActiveThemeToAllComponents();
this.postActiveThemesToAllComponents();
}
})
}
@@ -981,10 +1058,7 @@ class ComponentManager {
if(!iframe) {
return;
}
var width = data.width;
var height = data.height;
iframe.width = width;
iframe.height = height;
setSize(iframe, data);
// On Firefox, resizing a component iframe does not seem to have an effect with editor-stack extensions.
@@ -1007,6 +1081,20 @@ class ComponentManager {
}
}
editorForNote(note) {
let editors = this.componentsForArea("editor-editor");
for(var editor of editors) {
if(editor.isExplicitlyEnabledForItem(note)) {
return editor;
}
}
// No editor found for note. Use default editor, if note does not prefer system editor
if(!note.getAppDataItem("prefersPlainEditor")) {
return editors.filter((e) => {return e.isDefaultEditor()})[0];
}
}
}

View File

@@ -79,6 +79,14 @@ class DesktopManager {
this.searchHandler = handler;
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast("window-gained-focus");
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast("window-lost-focus");
}
desktop_onComponentInstallationComplete(componentData, error) {
console.log("Web|Component Installation/Update Complete", componentData, error);

View File

@@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager {
super($timeout);
this.setJWTRequestHandler(async () => {
return storageManager.getItem("jwt");;
return storageManager.getItem("jwt");
})
}
}

View File

@@ -7,7 +7,8 @@ SFModelManager.ContentTypeClassMapping = {
"SN|Theme" : SNTheme,
"SN|Component" : SNComponent,
"SF|Extension" : SNServerExtension,
"SF|MFA" : SNMfa
"SF|MFA" : SNMfa,
"SN|Privileges" : SNPrivileges
};
SFItem.AppDomain = "org.standardnotes.sn";

View File

@@ -1,100 +1,247 @@
angular.module('app')
.provider('passcodeManager', function () {
const MillisecondsPerSecond = 1000;
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager);
}
class PasscodeManager {
function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
constructor($rootScope, authManager, storageManager, syncManager) {
this.authManager = authManager;
this.storageManager = storageManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
this._hasPasscode = storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._locked = this._hasPasscode;
this.isLocked = function() {
return this._locked;
}
this.passcodeChangeObservers = [];
this.hasPasscode = function() {
return this._hasPasscode;
}
this.configureAutoLock();
}
this.keys = function() {
return this._keys;
}
addPasscodeChangeObserver(callback) {
this.passcodeChangeObservers.push(callback);
}
this.passcodeAuthParams = function() {
var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed));
if(authParams && !authParams.version) {
var keys = this.keys();
if(keys && keys.ak) {
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
authParams.version = "002";
} else {
authParams.version = "001";
}
}
return authParams;
}
lockApplication() {
window.location.reload();
this.cancelAutoLockTimer();
}
this.unlock = function(passcode, callback) {
var params = this.passcodeAuthParams();
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
if(keys.pw !== params.hash) {
callback(false);
return;
}
isLocked() {
return this._locked;
}
this._keys = keys;
this._authParams = params;
this.decryptLocalStorage(keys, params).then(() => {
this._locked = false;
callback(true);
})
});
}
hasPasscode() {
return this._hasPasscode;
}
this.setPasscode = (passcode, callback) => {
var uuid = SFJS.crypto.generateUUIDSync();
keys() {
return this._keys;
}
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
let keys = results.keys;
let authParams = results.authParams;
async setAutoLockInterval(interval) {
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
}
authParams.hash = keys.pw;
this._keys = keys;
this._hasPasscode = true;
this._authParams = authParams;
// Encrypting will initially clear localStorage
this.encryptLocalStorage(keys, authParams);
// After it's cleared, it's safe to write to it
storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
callback(true);
});
}
this.changePasscode = (newPasscode, callback) => {
this.setPasscode(newPasscode, callback);
}
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, authParams) {
storageManager.setKeys(keys, authParams);
// Switch to Ephemeral storage, wiping Fixed storage
// Last argument is `force`, which we set to true because in the case of changing passcode
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
}
this.decryptLocalStorage = async function(keys, authParams) {
storageManager.setKeys(keys, authParams);
return storageManager.decryptStorage();
async getAutoLockInterval() {
let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted);
if(interval) {
return JSON.parse(interval);
} else {
return PasscodeManager.AutoLockIntervalNone;
}
}
});
passcodeAuthParams() {
var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed));
if(authParams && !authParams.version) {
var keys = this.keys();
if(keys && keys.ak) {
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams.
authParams.version = "002";
} else {
authParams.version = "001";
}
}
return authParams;
}
async verifyPasscode(passcode) {
return new Promise(async (resolve, reject) => {
var params = this.passcodeAuthParams();
let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params);
if(keys.pw !== params.hash) {
resolve(false);
} else {
resolve(true);
}
})
}
unlock(passcode, callback) {
var params = this.passcodeAuthParams();
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
if(keys.pw !== params.hash) {
callback(false);
return;
}
this._keys = keys;
this._authParams = params;
this.decryptLocalStorage(keys, params).then(() => {
this._locked = false;
callback(true);
})
});
}
setPasscode(passcode, callback) {
var uuid = SFJS.crypto.generateUUIDSync();
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
let keys = results.keys;
let authParams = results.authParams;
authParams.hash = keys.pw;
this._keys = keys;
this._hasPasscode = true;
this._authParams = authParams;
// Encrypting will initially clear localStorage
this.encryptLocalStorage(keys, authParams);
// After it's cleared, it's safe to write to it
this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
callback(true);
this.notifyObserversOfPasscodeChange();
});
}
changePasscode(newPasscode, callback) {
this.setPasscode(newPasscode, callback);
}
clearPasscode() {
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
this.storageManager.removeItem("offlineParams", StorageManager.Fixed);
this._keys = null;
this._hasPasscode = false;
this.notifyObserversOfPasscodeChange();
}
notifyObserversOfPasscodeChange() {
for(var observer of this.passcodeChangeObservers) {
observer();
}
}
encryptLocalStorage(keys, authParams) {
this.storageManager.setKeys(keys, authParams);
// Switch to Ephemeral storage, wiping Fixed storage
// Last argument is `force`, which we set to true because in the case of changing passcode
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
}
async decryptLocalStorage(keys, authParams) {
this.storageManager.setKeys(keys, authParams);
return this.storageManager.decryptStorage();
}
configureAutoLock() {
if(isDesktopApplication()) {
// desktop only
this.$rootScope.$on("window-lost-focus", () => {
this.documentVisibilityChanged(false);
})
this.$rootScope.$on("window-gained-focus", () => {
this.documentVisibilityChanged(true);
})
} else {
// tab visibility listender, web only
document.addEventListener('visibilitychange', (e) => {
let visible = document.visibilityState == "visible";
this.documentVisibilityChanged(visible);
});
}
PasscodeManager.AutoLockIntervalNone = 0;
PasscodeManager.AutoLockIntervalImmediate = 1;
PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
}
getAutoLockIntervalOptions() {
return [
{
value: PasscodeManager.AutoLockIntervalNone,
label: "Off"
},
{
value: PasscodeManager.AutoLockIntervalImmediate,
label: "Immediately"
},
{
value: PasscodeManager.AutoLockIntervalOneMinute,
label: "1m"
},
{
value: PasscodeManager.AutoLockIntervalFiveMinutes,
label: "5m"
},
{
value: PasscodeManager.AutoLockIntervalOneHour,
label: "1h"
}
]
}
documentVisibilityChanged(visible) {
if(visible) {
// check to see if lockAfterDate is not null, and if the application isn't locked.
// if that's the case, it needs to be locked immediately.
if(this.lockAfterDate && new Date() > this.lockAfterDate && !this.isLocked()) {
this.lockApplication();
} else {
if(!this.isLocked()) {
this.syncManager.sync();
}
}
this.cancelAutoLockTimer();
} else {
this.beginAutoLockTimer();
}
}
async beginAutoLockTimer() {
var interval = await this.getAutoLockInterval();
if(interval == PasscodeManager.AutoLockIntervalNone) {
return;
}
// Use a timeout if possible, but if the computer is put to sleep, timeouts won't work.
// Need to set a date as backup. this.lockAfterDate does not need to be persisted, as
// living in memory seems sufficient. If memory is cleared, then the application will lock anyway.
let addToNow = (seconds) => {
let date = new Date();
date.setSeconds(date.getSeconds() + seconds);
return date;
}
this.lockAfterDate = addToNow(interval / MillisecondsPerSecond);
this.lockTimeout = setTimeout(() => {
this.lockApplication();
// We don't need to look at this anymore since we've succeeded with timeout lock
this.lockAfterDate = null;
}, interval);
}
cancelAutoLockTimer() {
clearTimeout(this.lockTimeout);
this.lockAfterDate = null;
}
}
angular.module('app').service('passcodeManager', PasscodeManager);

View File

@@ -0,0 +1,309 @@
class PrivilegesManager {
constructor(passcodeManager, authManager, singletonManager, modelManager, storageManager, $rootScope, $compile) {
this.passcodeManager = passcodeManager;
this.authManager = authManager;
this.singletonManager = singletonManager;
this.modelManager = modelManager;
this.storageManager = storageManager;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.loadPrivileges();
PrivilegesManager.CredentialAccountPassword = "CredentialAccountPassword";
PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode";
PrivilegesManager.ActionManageExtensions = "ActionManageExtensions";
PrivilegesManager.ActionManageBackups = "ActionManageBackups";
PrivilegesManager.ActionViewProtectedNotes = "ActionViewProtectedNotes";
PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges";
PrivilegesManager.ActionManagePasscode = "ActionManagePasscode";
PrivilegesManager.ActionDeleteNote = "ActionDeleteNote";
PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey";
PrivilegesManager.SessionLengthKey = "SessionLengthKey";
PrivilegesManager.SessionLengthNone = 0;
PrivilegesManager.SessionLengthFiveMinutes = 300;
PrivilegesManager.SessionLengthOneHour = 3600;
PrivilegesManager.SessionLengthOneWeek = 604800;
this.availableActions = [
PrivilegesManager.ActionViewProtectedNotes,
PrivilegesManager.ActionDeleteNote,
PrivilegesManager.ActionManagePasscode,
PrivilegesManager.ActionManageBackups,
PrivilegesManager.ActionManageExtensions,
PrivilegesManager.ActionManagePrivileges,
]
this.availableCredentials = [
PrivilegesManager.CredentialAccountPassword,
PrivilegesManager.CredentialLocalPasscode
];
this.sessionLengths = [
PrivilegesManager.SessionLengthNone,
PrivilegesManager.SessionLengthFiveMinutes,
PrivilegesManager.SessionLengthOneHour,
PrivilegesManager.SessionLengthOneWeek,
PrivilegesManager.SessionLengthIndefinite
]
}
getAvailableActions() {
return this.availableActions;
}
getAvailableCredentials() {
return this.availableCredentials;
}
presentPrivilegesModal(action, onSuccess, onCancel) {
if(this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
let customSuccess = () => {
onSuccess && onSuccess();
this.currentAuthenticationElement = null;
}
let customCancel = () => {
onCancel && onCancel();
this.currentAuthenticationElement = null;
}
var scope = this.$rootScope.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
var el = this.$compile( "<privileges-auth-modal action='action' on-success='onSuccess' on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>" )(scope);
angular.element(document.body).append(el);
this.currentAuthenticationElement = el;
}
async netCredentialsForAction(action) {
let credentials = (await this.getPrivileges()).getCredentialsForAction(action);
let netCredentials = [];
for(var cred of credentials) {
if(cred == PrivilegesManager.CredentialAccountPassword) {
if(!this.authManager.offline()) {
netCredentials.push(cred);
}
} else if(cred == PrivilegesManager.CredentialLocalPasscode) {
if(this.passcodeManager.hasPasscode()) {
netCredentials.push(cred);
}
}
}
return netCredentials;
}
presentPrivilegesManagementModal() {
var scope = this.$rootScope.$new(true);
var el = this.$compile( "<privileges-management-modal class='sk-modal'></privileges-management-modal>")(scope);
angular.element(document.body).append(el);
}
authenticationInProgress() {
return this.currentAuthenticationElement != null;
}
async loadPrivileges() {
return new Promise((resolve, reject) => {
let prefsContentType = "SN|Privileges";
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
this.privileges = resolvedSingleton;
if(!this.privileges.content.desktopPrivileges) {
this.privileges.content.desktopPrivileges = {};
}
resolve(resolvedSingleton);
}, (valueCallback) => {
// Safe to create. Create and return object.
var privs = new SNPrivileges({content_type: prefsContentType});
this.modelManager.addItem(privs);
privs.setDirty(true);
this.$rootScope.sync();
valueCallback(privs);
resolve(privs);
});
});
}
async getPrivileges() {
if(this.privileges) {
return this.privileges;
} else {
return this.loadPrivileges();
}
}
displayInfoForCredential(credential) {
let metadata = {}
metadata[PrivilegesManager.CredentialAccountPassword] = {
label: "Account Password",
prompt: "Please enter your account password."
}
metadata[PrivilegesManager.CredentialLocalPasscode] = {
label: "Local Passcode",
prompt: "Please enter your local passcode."
}
return metadata[credential];
}
displayInfoForAction(action) {
let metadata = {};
metadata[PrivilegesManager.ActionManageExtensions] = {
label: "Manage Extensions"
};
metadata[PrivilegesManager.ActionManageBackups] = {
label: "Download/Import Backups"
};
metadata[PrivilegesManager.ActionViewProtectedNotes] = {
label: "View Protected Notes"
};
metadata[PrivilegesManager.ActionManagePrivileges] = {
label: "Manage Privileges"
};
metadata[PrivilegesManager.ActionManagePasscode] = {
label: "Manage Passcode"
}
metadata[PrivilegesManager.ActionDeleteNote] = {
label: "Delete Notes"
}
return metadata[action];
}
getSessionLengthOptions() {
return [
{
value: PrivilegesManager.SessionLengthNone,
label: "Don't Remember"
},
{
value: PrivilegesManager.SessionLengthFiveMinutes,
label: "5 Minutes"
},
{
value: PrivilegesManager.SessionLengthOneHour,
label: "1 Hour"
},
{
value: PrivilegesManager.SessionLengthOneWeek,
label: "1 Week"
}
]
}
async setSessionLength(length) {
let addToNow = (seconds) => {
let date = new Date();
date.setSeconds(date.getSeconds() + seconds);
return date;
}
let expiresAt = addToNow(length);
return Promise.all([
this.storageManager.setItem(PrivilegesManager.SessionExpiresAtKey, JSON.stringify(expiresAt), this.storageManager.bestStorageMode()),
this.storageManager.setItem(PrivilegesManager.SessionLengthKey, JSON.stringify(length), this.storageManager.bestStorageMode()),
])
}
async clearSession() {
return this.setSessionLength(PrivilegesManager.SessionLengthNone);
}
async getSelectedSessionLength() {
let length = await this.storageManager.getItem(PrivilegesManager.SessionLengthKey, this.storageManager.bestStorageMode());
if(length) {
return JSON.parse(length);
} else {
return PrivilegesManager.SessionLengthNone;
}
}
async getSessionExpirey() {
let expiresAt = await this.storageManager.getItem(PrivilegesManager.SessionExpiresAtKey, this.storageManager.bestStorageMode());
if(expiresAt) {
return new Date(JSON.parse(expiresAt));
} else {
return new Date();
}
}
async actionHasPrivilegesConfigured(action) {
return (await this.netCredentialsForAction(action)).length > 0;
}
async actionRequiresPrivilege(action) {
let expiresAt = await this.getSessionExpirey();
if(expiresAt > new Date()) {
return false;
}
return (await this.netCredentialsForAction(action)).length > 0;
}
async savePrivileges() {
let privs = await this.getPrivileges();
privs.setDirty(true);
this.$rootScope.sync();
}
async authenticateAction(action, credentialAuthMapping) {
var requiredCredentials = (await this.netCredentialsForAction(action));
var successfulCredentials = [], failedCredentials = [];
for(let requiredCredential of requiredCredentials) {
var passesAuth = await this._verifyAuthenticationParameters(requiredCredential, credentialAuthMapping[requiredCredential]);
if(passesAuth) {
successfulCredentials.push(requiredCredential);
} else {
failedCredentials.push(requiredCredential);
}
}
return {
success: failedCredentials.length == 0,
successfulCredentials: successfulCredentials,
failedCredentials: failedCredentials
}
}
async _verifyAuthenticationParameters(credential, value) {
let verifyAccountPassword = async (password) => {
return this.authManager.verifyAccountPassword(password);
}
let verifyLocalPasscode = async (passcode) => {
return this.passcodeManager.verifyPasscode(passcode);
}
if(credential == PrivilegesManager.CredentialAccountPassword) {
return verifyAccountPassword(value);
} else if(credential == PrivilegesManager.CredentialLocalPasscode) {
return verifyLocalPasscode(value);
}
}
}
angular.module('app').service('privilegesManager', PrivilegesManager);

View File

@@ -6,10 +6,17 @@ class SessionHistory extends SFSessionHistoryManager {
"Note" : NoteHistoryEntry
}
// Session History can be encrypted with passcode keys. If it changes, we need to resave session
// history with the new keys.
passcodeManager.addPasscodeChangeObserver(() => {
this.saveToDisk();
})
var keyRequestHandler = async () => {
let offline = authManager.offline();
let auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
let keys = offline ? passcodeManager.keys() : await authManager.keys();
return {
keys: keys,
offline: offline,

View File

@@ -24,9 +24,29 @@ class SingletonManager {
$rootScope.$on("initial-data-loaded", (event, data) => {
this.resolveSingletons(modelManager.allItems, null, true);
this.initialDataLoaded = true;
})
/*
If an item alternates its uuid on registration, singletonHandlers might need to update
their local reference to the object, since the object reference will change on uuid alternation
*/
modelManager.addModelUuidChangeObserver("singleton-manager", (oldModel, newModel) => {
for(var handler of this.singletonHandlers) {
if(handler.singleton && SFPredicate.ItemSatisfiesPredicates(newModel, handler.predicates)) {
// Reference is now invalid, calling resolveSingleton should update it
handler.singleton = null;
this.resolveSingletons([newModel]);
}
}
})
$rootScope.$on("sync:completed", (event, data) => {
// Wait for initial data load before handling any sync. If we don't want for initial data load,
// then the singleton resolver won't have the proper items to work with to determine whether to resolve or create.
if(!this.initialDataLoaded) {
return;
}
// The reason we also need to consider savedItems in consolidating singletons is in case of sync conflicts,
// a new item can be created, but is never processed through "retrievedItems" since it is only created locally then saved.
@@ -108,7 +128,6 @@ class SingletonManager {
var singleton = allExtantItemsMatchingPredicate[0];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
}
}
} else {

View File

@@ -174,6 +174,10 @@ class StorageManager extends SFStorageManager {
return this.getItemSync("encryptedStorage", StorageManager.Fixed) !== null;
}
bestStorageMode() {
return this.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed;
}
/*
Model Storage

View File

@@ -11,7 +11,7 @@ class SyncManager extends SFSyncManager {
scope.item1 = items[0];
scope.item2 = items[1];
scope.callback = callback;
var el = this.$compile( "<conflict-resolution-modal item1='item1' item2='item2' callback='callback' class='modal'></conflict-resolution-modal>" )(scope);
var el = this.$compile( "<conflict-resolution-modal item1='item1' item2='item2' callback='callback' class='sk-modal'></conflict-resolution-modal>" )(scope);
angular.element(document.body).append(el);
}

View File

@@ -1,9 +1,39 @@
class ThemeManager {
constructor(componentManager, desktopManager) {
constructor(componentManager, desktopManager, storageManager, passcodeManager) {
this.componentManager = componentManager;
this.storageManager = storageManager;
this.desktopManager = desktopManager;
this.activeThemes = [];
desktopManager.registerUpdateObserver((component) => {
ThemeManager.CachedThemesKey = "cachedThemes";
this.registerObservers();
// When a passcode is added, all local storage will be encrypted (it doesn't know what was
// originally saved as Fixed or FixedEncrypted). We want to rewrite cached themes here to Fixed
// so that it's readable without authentication.
passcodeManager.addPasscodeChangeObserver(() => {
this.cacheThemes();
})
// The desktop application won't have its applicationDataPath until the angular document is ready,
// so it wont be able to resolve local theme urls until thats ready
angular.element(document).ready(() => {
this.activateCachedThemes();
});
}
activateCachedThemes() {
let cachedThemes = this.getCachedThemes();
let writeToCache = false;
for(var theme of cachedThemes) {
this.activateTheme(theme, writeToCache);
}
}
registerObservers() {
this.desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
if(component.active && component.isTheme()) {
this.deactivateTheme(component);
@@ -13,7 +43,7 @@ class ThemeManager {
}
})
componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
this.componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
if(component.active) {
this.activateTheme(component);
} else {
@@ -33,9 +63,17 @@ class ThemeManager {
this.componentManager.deactivateComponent(theme);
}
}
this.decacheThemes();
}
activateTheme(theme) {
activateTheme(theme, writeToCache = true) {
if(_.find(this.activeThemes, {uuid: theme.uuid})) {
return;
}
this.activeThemes.push(theme);
var url = this.componentManager.urlForComponent(theme);
var link = document.createElement("link");
link.href = url;
@@ -44,6 +82,10 @@ class ThemeManager {
link.media = "screen,print";
link.id = theme.uuid;
document.getElementsByTagName("head")[0].appendChild(link);
if(writeToCache) {
this.cacheThemes();
}
}
deactivateTheme(theme) {
@@ -52,6 +94,36 @@ class ThemeManager {
element.disabled = true;
element.parentNode.removeChild(element);
}
_.remove(this.activeThemes, {uuid: theme.uuid});
this.cacheThemes();
}
async cacheThemes() {
let mapped = await Promise.all(this.activeThemes.map(async (theme) => {
let transformer = new SFItemParams(theme);
let params = await transformer.paramsForLocalStorage();
return params;
}));
let data = JSON.stringify(mapped);
return this.storageManager.setItem(ThemeManager.CachedThemesKey, data, StorageManager.Fixed);
}
async decacheThemes() {
return this.storageManager.removeItem(ThemeManager.CachedThemesKey, StorageManager.Fixed);
}
getCachedThemes() {
let cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
if(cachedThemes) {
let parsed = JSON.parse(cachedThemes);
return parsed.map((theme) => {
return new SNTheme(theme);
});
} else {
return [];
}
}
}