@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -41,7 +41,6 @@ class LockScreen {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('lockScreen', () => new LockScreen);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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]
|
||||
|
||||
|
||||
15
app/assets/javascripts/app/directives/functional/snEnter.js
Normal file
15
app/assets/javascripts/app/directives/functional/snEnter.js
Normal 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();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class MenuRow {
|
||||
this.scope = {
|
||||
action: "&",
|
||||
circle: "=",
|
||||
circleAlign: "=",
|
||||
label: "=",
|
||||
subtitle: "=",
|
||||
hasButton: "=",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
app/assets/javascripts/app/models/privileges.js
Normal file
34
app/assets/javascripts/app/models/privileges.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager {
|
||||
super($timeout);
|
||||
|
||||
this.setJWTRequestHandler(async () => {
|
||||
return storageManager.getItem("jwt");;
|
||||
return storageManager.getItem("jwt");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
309
app/assets/javascripts/app/services/privilegesManager.js
Normal file
309
app/assets/javascripts/app/services/privilegesManager.js
Normal 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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user