This commit is contained in:
Mo Bitar
2020-02-05 13:17:20 -06:00
parent 8c898b51be
commit 1ae3790ea3
48 changed files with 99518 additions and 2230 deletions

View File

@@ -1,7 +1,7 @@
import { SFAlertManager } from 'snjs';
import { SNAlertManager } from 'snjs';
import { SKAlert } from 'sn-stylekit';
export class AlertManager extends SFAlertManager {
export class AlertManager extends SNAlertManager {
/* @ngInject */
constructor($timeout) {
super();

View File

@@ -2,8 +2,8 @@ import { PrivilegesManager } from '@/services/privilegesManager';
export class ArchiveManager {
/* @ngInject */
constructor(passcodeManager, authManager, modelManager, privilegesManager) {
this.passcodeManager = passcodeManager;
constructor(lockManager, authManager, modelManager, privilegesManager) {
this.lockManager = lockManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.privilegesManager = privilegesManager;
@@ -22,9 +22,9 @@ export class ArchiveManager {
// download in Standard Notes format
let keys, authParams;
if(encrypted) {
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
if(this.authManager.offline() && this.lockManager.hasPasscode()) {
keys = this.lockManager.keys();
authParams = this.lockManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();
@@ -42,7 +42,7 @@ export class ArchiveManager {
};
if(await this.privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
this.privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
this.godService.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
run();
});
} else {

View File

@@ -1,140 +0,0 @@
import angular from 'angular';
import { StorageManager } from './storageManager';
import { protocolManager, SFAuthManager } from 'snjs';
export class AuthManager extends SFAuthManager {
/* @ngInject */
constructor(
modelManager,
singletonManager,
storageManager,
dbManager,
httpManager,
$rootScope,
$timeout,
$compile
) {
super(storageManager, httpManager, null, $timeout);
this.$rootScope = $rootScope;
this.$compile = $compile;
this.modelManager = modelManager;
this.singletonManager = singletonManager;
this.storageManager = storageManager;
this.dbManager = dbManager;
}
loadInitialData() {
const userData = this.storageManager.getItemSync("user");
if(userData) {
this.user = JSON.parse(userData);
} else {
// legacy, check for uuid
const idData = this.storageManager.getItemSync("uuid");
if(idData) {
this.user = {uuid: idData};
}
}
this.checkForSecurityUpdate();
}
offline() {
return !this.user;
}
isEphemeralSession() {
if(this.ephemeral == null || this.ephemeral == undefined) {
this.ephemeral = JSON.parse(this.storageManager.getItemSync("ephemeral", StorageManager.Fixed));
}
return this.ephemeral;
}
setEphemeral(ephemeral) {
this.ephemeral = ephemeral;
if(ephemeral) {
this.storageManager.setModelStorageMode(StorageManager.Ephemeral);
this.storageManager.setItemsMode(StorageManager.Ephemeral);
} else {
this.storageManager.setModelStorageMode(StorageManager.Fixed);
this.storageManager.setItemsMode(this.storageManager.bestStorageMode());
this.storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
}
}
async getAuthParamsForEmail(url, email, extraParams) {
return super.getAuthParamsForEmail(url, email, extraParams);
}
async login(url, email, password, ephemeral, strictSignin, extraParams) {
return super.login(url, email, password, strictSignin, extraParams).then((response) => {
if(!response.error) {
this.setEphemeral(ephemeral);
this.checkForSecurityUpdate();
}
return response;
});
}
async register(url, email, password, ephemeral) {
return super.register(url, email, password).then((response) => {
if(!response.error) {
this.setEphemeral(ephemeral);
}
return response;
});
}
async changePassword(url, email, current_server_pw, newKeys, newAuthParams) {
return super.changePassword(url, email, current_server_pw, newKeys, newAuthParams).then((response) => {
if(!response.error) {
this.checkForSecurityUpdate();
}
return response;
});
}
async handleAuthResponse(response, email, url, authParams, keys) {
try {
await super.handleAuthResponse(response, email, url, authParams, keys);
this.user = response.user;
this.storageManager.setItem("user", JSON.stringify(response.user));
} catch (e) {
this.dbManager.displayOfflineAlert();
}
}
async verifyAccountPassword(password) {
const authParams = await this.getAuthParams();
const keys = await protocolManager.computeEncryptionKeysForUser(password, authParams);
const success = keys.mk === (await this.keys()).mk;
return success;
}
async checkForSecurityUpdate() {
if(this.offline()) {
return false;
}
const latest = protocolManager.version();
const updateAvailable = await this.protocolVersion() !== latest;
if(updateAvailable !== this.securityUpdateAvailable) {
this.securityUpdateAvailable = updateAvailable;
this.$rootScope.$broadcast("security-update-status-changed");
}
return this.securityUpdateAvailable;
}
presentPasswordWizard(type) {
var scope = this.$rootScope.$new(true);
scope.type = type;
var el = this.$compile( "<password-wizard type='type'></password-wizard>" )(scope);
angular.element(document.body).append(el);
}
signOut() {
super.signout();
this.user = null;
this._authParams = null;
}
}

View File

@@ -1,50 +0,0 @@
import angular from 'angular';
import { SNComponentManager, SFAlertManager } from 'snjs';
import { isDesktopApplication, getPlatformString } from '@/utils';
export class ComponentManager extends SNComponentManager {
/* @ngInject */
constructor(
modelManager,
syncManager,
desktopManager,
nativeExtManager,
$rootScope,
$timeout,
$compile
) {
super({
modelManager,
syncManager,
desktopManager,
nativeExtManager,
alertManager: new SFAlertManager(),
$uiRunner: $rootScope.safeApply,
$timeout: $timeout,
environment: isDesktopApplication() ? "desktop" : "web",
platform: getPlatformString()
});
// this.loggingEnabled = true;
this.$compile = $compile;
this.$rootScope = $rootScope;
}
openModalComponent(component) {
var scope = this.$rootScope.$new(true);
scope.component = component;
var el = this.$compile( "<component-modal component='component' class='sk-modal'></component-modal>" )(scope);
angular.element(document.body).append(el);
}
presentPermissionsDialog(dialog) {
const scope = this.$rootScope.$new(true);
scope.permissionsString = dialog.permissionsString;
scope.component = dialog.component;
scope.callback = dialog.callback;
var el = this.$compile( "<permissions-modal component='component' permissions-string='permissionsString' callback='callback' class='sk-modal'></permissions-modal>" )(scope);
angular.element(document.body).append(el);
}
}

View File

@@ -1,4 +1,4 @@
export class DBManager {
export class DatabaseManager {
/* @ngInject */
constructor(alertManager) {
this.locked = true;

View File

@@ -15,10 +15,10 @@ export class DesktopManager {
modelManager,
syncManager,
authManager,
passcodeManager,
lockManager,
appState
) {
this.passcodeManager = passcodeManager;
this.lockManager = lockManager;
this.modelManager = modelManager;
this.authManager = authManager;
this.syncManager = syncManager;
@@ -202,9 +202,9 @@ export class DesktopManager {
async desktop_requestBackupFile(callback) {
let keys, authParams;
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
if(this.authManager.offline() && this.lockManager.hasPasscode()) {
keys = this.lockManager.keys();
authParams = this.lockManager.passcodeAuthParams();
} else {
keys = await this.authManager.keys();
authParams = await this.authManager.getAuthParams();

View File

@@ -1,44 +1,36 @@
import angular from 'angular';
import { SFPrivilegesManager } from 'snjs';
export class PrivilegesManager extends SFPrivilegesManager {
export class GodService {
/* @ngInject */
constructor(
passcodeManager,
authManager,
syncManager,
singletonManager,
modelManager,
storageManager,
$rootScope,
$compile
) {
super(modelManager, syncManager, singletonManager);
this.$rootScope = $rootScope;
this.$compile = $compile;
}
this.setDelegate({
isOffline: async () => {
return authManager.offline();
},
hasLocalPasscode: async () => {
return passcodeManager.hasPasscode();
},
saveToStorage: async (key, value) => {
return storageManager.setItem(key, value, storageManager.bestStorageMode());
},
getFromStorage: async (key) => {
return storageManager.getItem(key, storageManager.bestStorageMode());
},
verifyAccountPassword: async (password) => {
return authManager.verifyAccountPassword(password);
},
verifyLocalPasscode: async (passcode) => {
return passcodeManager.verifyPasscode(passcode);
},
});
async checkForSecurityUpdate() {
if (this.offline()) {
return false;
}
const latest = protocolManager.version();
const updateAvailable = await this.protocolVersion() !== latest;
if (updateAvailable !== this.securityUpdateAvailable) {
this.securityUpdateAvailable = updateAvailable;
this.$rootScope.$broadcast("security-update-status-changed");
}
return this.securityUpdateAvailable;
}
presentPasswordWizard(type) {
var scope = this.$rootScope.$new(true);
scope.type = type;
var el = this.$compile("<password-wizard type='type'></password-wizard>")(scope);
angular.element(document.body).append(el);
}
async presentPrivilegesModal(action, onSuccess, onCancel) {

View File

@@ -1,13 +0,0 @@
import { SFHttpManager } from 'snjs';
export class HttpManager extends SFHttpManager {
/* @ngInject */
constructor(storageManager, $timeout) {
// calling callbacks in a $timeout allows UI to update
super($timeout);
this.setJWTRequestHandler(async () => {
return storageManager.getItem('jwt');
});
}
}

View File

@@ -2,14 +2,14 @@ export { ActionsManager } from './actionsManager';
export { ArchiveManager } from './archiveManager';
export { AuthManager } from './authManager';
export { ComponentManager } from './componentManager';
export { DBManager } from './dbManager';
export { DatabaseManager } from './databaseManager';
export { DesktopManager } from './desktopManager';
export { HttpManager } from './httpManager';
export { KeyboardManager } from './keyboardManager';
export { MigrationManager } from './migrationManager';
export { ModelManager } from './modelManager';
export { NativeExtManager } from './nativeExtManager';
export { PasscodeManager } from './passcodeManager';
export { LockManager } from './lockManager';
export { PrivilegesManager } from './privilegesManager';
export { SessionHistory } from './sessionHistory';
export { SingletonManager } from './singletonManager';

View File

@@ -0,0 +1,143 @@
import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
import {
APP_STATE_EVENT_WINDOW_DID_BLUR,
APP_STATE_EVENT_WINDOW_DID_FOCUS
} from '../state';
const MILLISECONDS_PER_SECOND = 1000;
const FOCUS_POLL_INTERVAL = 1 * MILLISECONDS_PER_SECOND;
const LOCK_INTERVAL_NONE = 0;
const LOCK_INTERVAL_IMMEDIATE = 1;
const LOCK_INTERVAL_ONE_MINUTE = 60 * MILLISECONDS_PER_SECOND;
const LOCK_INTERVAL_FIVE_MINUTES = 300 * MILLISECONDS_PER_SECOND;
const LOCK_INTERVAL_ONE_HOUR= 3600 * MILLISECONDS_PER_SECOND;
const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey";
export class LockManager {
/* @ngInject */
constructor($rootScope, application, appState) {
this.$rootScope = $rootScope;
this.application = application;
this.appState = appState;
this.observeVisibility();
}
observeVisibility() {
this.appState.addObserver((eventName, data) => {
if(eventName === APP_STATE_EVENT_WINDOW_DID_BLUR) {
this.documentVisibilityChanged(false);
} else if(eventName === APP_STATE_EVENT_WINDOW_DID_FOCUS) {
this.documentVisibilityChanged(true);
}
});
if (!isDesktopApplication()) {
this.beginWebFocusPolling();
}
}
async setAutoLockInterval(interval) {
return this.application.setValue(
STORAGE_KEY_AUTOLOCK_INTERVAL,
JSON.stringify(interval),
);
}
async getAutoLockInterval() {
const interval = await this.application.getValue(
STORAGE_KEY_AUTOLOCK_INTERVAL,
);
if(interval) {
return JSON.parse(interval);
} else {
return LOCK_INTERVAL_NONE;
}
}
/**
* Verify document is in focus every so often as visibilitychange event is
* not triggered on a typical window blur event but rather on tab changes.
*/
beginWebFocusPolling() {
this.pollFocusTimeout = setInterval(() => {
const hasFocus = document.hasFocus();
if(hasFocus && this.lastFocusState === 'hidden') {
this.documentVisibilityChanged(true);
} else if(!hasFocus && this.lastFocusState === 'visible') {
this.documentVisibilityChanged(false);
}
/* Save this to compare against next time around */
this.lastFocusState = hasFocus ? 'visible' : 'hidden';
}, FOCUS_POLL_INTERVAL);
}
getAutoLockIntervalOptions() {
return [
{
value: LOCK_INTERVAL_NONE,
label: "Off"
},
{
value: LOCK_INTERVAL_IMMEDIATE,
label: "Immediately"
},
{
value: LOCK_INTERVAL_ONE_MINUTE,
label: "1m"
},
{
value: LOCK_INTERVAL_FIVE_MINUTES,
label: "5m"
},
{
value: LOCK_INTERVAL_ONE_HOUR,
label: "1h"
}
];
}
documentVisibilityChanged(visible) {
if(visible) {
if(
!this.isLocked() &&
this.lockAfterDate &&
new Date() > this.lockAfterDate
) {
this.application.passcodeLock();
}
this.cancelAutoLockTimer();
} else {
this.beginAutoLockTimer();
}
}
async beginAutoLockTimer() {
var interval = await this.getAutoLockInterval();
if(interval === LOCK_INTERVAL_NONE) {
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 is sufficient. If memory is cleared, then the
* application will lock anyway.
*/
const addToNow = (seconds) => {
const date = new Date();
date.setSeconds(date.getSeconds() + seconds);
return date;
};
this.lockAfterDate = addToNow(interval / MILLISECONDS_PER_SECOND);
this.lockTimeout = setTimeout(() => {
this.cancelAutoLockTimer();
this.application.passcodeLock();
this.lockAfterDate = null;
}, interval);
}
cancelAutoLockTimer() {
clearTimeout(this.lockTimeout);
this.lockAfterDate = null;
}
}

View File

@@ -1,172 +0,0 @@
import { isDesktopApplication } from '@/utils';
import { SFMigrationManager } from 'snjs';
import { ComponentManager } from '@/services/componentManager';
export class MigrationManager extends SFMigrationManager {
/* @ngInject */
constructor(
modelManager,
syncManager,
componentManager,
storageManager,
statusManager,
authManager,
desktopManager
) {
super(modelManager, syncManager, storageManager, authManager);
this.componentManager = componentManager;
this.statusManager = statusManager;
this.desktopManager = desktopManager;
}
registeredMigrations() {
return [
this.editorToComponentMigration(),
this.componentUrlToHostedUrl(),
this.removeTagReferencesFromNotes()
];
}
/*
Migrate SN|Editor to SN|Component. Editors are deprecated as of November 2017. Editors using old APIs must
convert to using the new component API.
*/
editorToComponentMigration() {
return {
name: "editor-to-component",
content_type: "SN|Editor",
handler: async (editors) => {
// Convert editors to components
for(var editor of editors) {
// If there's already a component for this url, then skip this editor
if(editor.url && !this.componentManager.componentForUrl(editor.url)) {
var component = this.modelManager.createItem({
content_type: "SN|Component",
content: {
url: editor.url,
name: editor.name,
area: "editor-editor"
}
});
component.setAppDataItem("data", editor.data);
this.modelManager.addItem(component);
this.modelManager.setItemDirty(component, true);
}
}
for(const editor of editors) {
this.modelManager.setItemToBeDeleted(editor);
}
this.syncManager.sync();
}
};
}
/*
Migrate component.url fields to component.hosted_url. This involves rewriting any note data that relied on the
component.url value to store clientData, such as the CodeEditor, which stores the programming language for the note
in the note's clientData[component.url]. We want to rewrite any matching items to transfer that clientData into
clientData[component.uuid].
April 3, 2019 note: it seems this migration is mis-named. The first part of the description doesn't match what the code is actually doing.
It has nothing to do with url/hosted_url relationship and more to do with just mapping client data from the note's hosted_url to its uuid
Created: July 6, 2018
*/
componentUrlToHostedUrl() {
return {
name: "component-url-to-hosted-url",
content_type: "SN|Component",
handler: async (components) => {
let hasChanges = false;
const notes = this.modelManager.validItemsForContentType("Note");
for(const note of notes) {
for(const component of components) {
const clientData = note.getDomainDataItem(component.hosted_url, ComponentManager.ClientDataDomain);
if(clientData) {
note.setDomainDataItem(component.uuid, clientData, ComponentManager.ClientDataDomain);
note.setDomainDataItem(component.hosted_url, null, ComponentManager.ClientDataDomain);
this.modelManager.setItemDirty(note, true);
hasChanges = true;
}
}
}
if(hasChanges) {
this.syncManager.sync();
}
}
};
}
/*
Migrate notes which have relationships on tags to migrate those relationships to the tags themselves.
That is, notes.content.references should not include any mention of tags.
This will apply to notes created before the schema change. Now, only tags reference notes.
Created: April 3, 2019
*/
removeTagReferencesFromNotes() {
return {
name: "remove-tag-references-from-notes",
content_type: "Note",
handler: async (notes) => {
const needsSync = false;
let status = this.statusManager.addStatusFromString("Optimizing data...");
let dirtyCount = 0;
for(const note of notes) {
if(!note.content) {
continue;
}
const references = note.content.references;
// Remove any tag references, and transfer them to the tag if neccessary.
const newReferences = [];
for(const reference of references) {
if(reference.content_type != "Tag") {
newReferences.push(reference);
continue;
}
// is Tag content_type, we will not be adding this to newReferences
const tag = this.modelManager.findItem(reference.uuid);
if(tag && !tag.hasRelationshipWithItem(note)) {
tag.addItemAsRelationship(note);
this.modelManager.setItemDirty(tag, true);
dirtyCount++;
}
}
if(newReferences.length != references.length) {
note.content.references = newReferences;
this.modelManager.setItemDirty(note, true);
dirtyCount++;
}
}
if(dirtyCount > 0) {
if(isDesktopApplication()) {
this.desktopManager.saveBackup();
}
status = this.statusManager.replaceStatusWithString(status, `${dirtyCount} items optimized.`);
await this.syncManager.sync();
status = this.statusManager.replaceStatusWithString(status, `Optimization complete.`);
setTimeout(() => {
this.statusManager.removeStatus(status);
}, 2000);
} else {
this.statusManager.removeStatus(status);
}
}
};
}
}

View File

@@ -1,170 +0,0 @@
import _ from 'lodash';
import { SFModelManager, SNSmartTag, SFPredicate } from 'snjs';
export class ModelManager extends SFModelManager {
/* @ngInject */
constructor(storageManager, $timeout) {
super($timeout);
this.notes = [];
this.tags = [];
this.components = [];
this.storageManager = storageManager;
this.buildSystemSmartTags();
}
handleSignout() {
super.handleSignout();
this.notes.length = 0;
this.tags.length = 0;
this.components.length = 0;
}
noteCount() {
return this.notes.filter((n) => !n.dummy).length;
}
removeAllItemsFromMemory() {
for(var item of this.items) {
item.deleted = true;
}
this.notifySyncObserversOfModels(this.items);
this.handleSignout();
}
findTag(title) {
return _.find(this.tags, { title: title });
}
findOrCreateTagByTitle(title) {
let tag = this.findTag(title);
if(!tag) {
tag = this.createItem({content_type: "Tag", content: {title: title}});
this.addItem(tag);
this.setItemDirty(tag, true);
}
return tag;
}
addItems(items, globalOnly = false) {
super.addItems(items, globalOnly);
items.forEach((item) => {
// In some cases, you just want to add the item to this.items, and not to the individual arrays
// This applies when you want to keep an item syncable, but not display it via the individual arrays
if(!globalOnly) {
if(item.content_type == "Tag") {
if(!_.find(this.tags, {uuid: item.uuid})) {
this.tags.splice(_.sortedIndexBy(this.tags, item, function(item){
if (item.title) return item.title.toLowerCase();
else return '';
}), 0, item);
}
} else if(item.content_type == "Note") {
if(!_.find(this.notes, {uuid: item.uuid})) {
this.notes.unshift(item);
}
} else if(item.content_type == "SN|Component") {
if(!_.find(this.components, {uuid: item.uuid})) {
this.components.unshift(item);
}
}
}
});
}
resortTag(tag) {
_.pull(this.tags, tag);
this.tags.splice(_.sortedIndexBy(this.tags, tag, function(tag){
if (tag.title) return tag.title.toLowerCase();
else return '';
}), 0, tag);
}
setItemToBeDeleted(item) {
super.setItemToBeDeleted(item);
// remove from relevant array, but don't remove from all items.
// This way, it's removed from the display, but still synced via get dirty items
this.removeItemFromRespectiveArray(item);
}
removeItemLocally(item, callback) {
super.removeItemLocally(item, callback);
this.removeItemFromRespectiveArray(item);
this.storageManager.deleteModel(item).then(callback);
}
removeItemFromRespectiveArray(item) {
if(item.content_type == "Tag") {
_.remove(this.tags, {uuid: item.uuid});
} else if(item.content_type == "Note") {
_.remove(this.notes, {uuid: item.uuid});
} else if(item.content_type == "SN|Component") {
_.remove(this.components, {uuid: item.uuid});
}
}
notesMatchingSmartTag(tag) {
const contentTypePredicate = new SFPredicate("content_type", "=", "Note");
const predicates = [contentTypePredicate, tag.content.predicate];
if(!tag.content.isTrashTag) {
const notTrashedPredicate = new SFPredicate("content.trashed", "=", false);
predicates.push(notTrashedPredicate);
}
const results = this.itemsMatchingPredicates(predicates);
return results;
}
trashSmartTag() {
return this.systemSmartTags.find((tag) => tag.content.isTrashTag);
}
trashedItems() {
return this.notesMatchingSmartTag(this.trashSmartTag());
}
emptyTrash() {
const notes = this.trashedItems();
for(const note of notes) {
this.setItemToBeDeleted(note);
}
}
buildSystemSmartTags() {
this.systemSmartTags = SNSmartTag.systemSmartTags();
}
getSmartTagWithId(id) {
return this.getSmartTags().find((candidate) => candidate.uuid == id);
}
getSmartTags() {
const userTags = this.validItemsForContentType("SN|SmartTag").sort((a, b) => {
return a.content.title < b.content.title ? -1 : 1;
});
return this.systemSmartTags.concat(userTags);
}
/*
Misc
*/
humanReadableDisplayForContentType(contentType) {
return {
"Note" : "note",
"Tag" : "tag",
"SN|SmartTag": "smart tag",
"Extension" : "action-based extension",
"SN|Component" : "component",
"SN|Editor" : "editor",
"SN|Theme" : "theme",
"SF|Extension" : "server extension",
"SF|MFA" : "two-factor authentication setting",
"SN|FileSafe|Credentials": "FileSafe credential",
"SN|FileSafe|FileMetadata": "FileSafe file",
"SN|FileSafe|Integration": "FileSafe integration"
}[contentType];
}
}

View File

@@ -1,285 +0,0 @@
import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
import { StorageManager } from './storageManager';
import { protocolManager } from 'snjs';
const MillisecondsPerSecond = 1000;
export class PasscodeManager {
/* @ngInject */
constructor($rootScope, authManager, storageManager, syncManager) {
this.authManager = authManager;
this.storageManager = storageManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._locked = this._hasPasscode;
this.visibilityObservers = [];
this.passcodeChangeObservers = [];
this.configureAutoLock();
}
addPasscodeChangeObserver(callback) {
this.passcodeChangeObservers.push(callback);
}
lockApplication() {
window.location.reload();
this.cancelAutoLockTimer();
}
isLocked() {
return this._locked;
}
hasPasscode() {
return this._hasPasscode;
}
keys() {
return this._keys;
}
addVisibilityObserver(callback) {
this.visibilityObservers.push(callback);
return callback;
}
removeVisibilityObserver(callback) {
_.pull(this.visibilityObservers, callback);
}
notifiyVisibilityObservers(visible) {
for(const callback of this.visibilityObservers) {
callback(visible);
}
}
async setAutoLockInterval(interval) {
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
}
async getAutoLockInterval() {
const 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();
const keys = await protocolManager.computeEncryptionKeysForUser(passcode, params);
if(keys.pw !== params.hash) {
resolve(false);
} else {
resolve(true);
}
});
}
unlock(passcode, callback) {
var params = this.passcodeAuthParams();
protocolManager.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 = protocolManager.crypto.generateUUIDSync();
protocolManager.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
const keys = results.keys;
const 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() {
PasscodeManager.AutoLockPollFocusInterval = 1 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalNone = 0;
PasscodeManager.AutoLockIntervalImmediate = 1;
PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
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 listener, web only
document.addEventListener('visibilitychange', (e) => {
const visible = document.visibilityState === "visible";
this.documentVisibilityChanged(visible);
});
// verify document is in focus every so often as visibilitychange event is not triggered
// on a typical window blur event but rather on tab changes
this.pollFocusTimeout = setInterval(() => {
const hasFocus = document.hasFocus();
if(hasFocus && this.lastFocusState === "hidden") {
this.documentVisibilityChanged(true);
} else if(!hasFocus && this.lastFocusState === "visible") {
this.documentVisibilityChanged(false);
}
// save this to compare against next time around
this.lastFocusState = hasFocus ? "visible" : "hidden";
}, PasscodeManager.AutoLockPollFocusInterval);
}
}
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();
}
this.notifiyVisibilityObservers(visible);
}
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.
const addToNow = (seconds) => {
const 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;
}
}

View File

@@ -1,44 +0,0 @@
import { NoteHistoryEntry } from '@/models/noteHistoryEntry';
import { SFSessionHistoryManager , SFItemHistory } from 'snjs';
export class SessionHistory extends SFSessionHistoryManager {
/* @ngInject */
constructor(
modelManager,
storageManager,
authManager,
passcodeManager,
$timeout
) {
SFItemHistory.HistoryEntryClassMapping = {
"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 () => {
const offline = authManager.offline();
const auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
const keys = offline ? passcodeManager.keys() : await authManager.keys();
return {
keys: keys,
offline: offline,
auth_params: auth_params
};
};
var contentTypes = ["Note"];
super(
modelManager,
storageManager,
keyRequestHandler,
contentTypes,
$timeout
);
}
}

View File

@@ -1,10 +0,0 @@
import { SFSingletonManager } from 'snjs';
export class SingletonManager extends SFSingletonManager {
// constructor needed for angularjs injection to work
// eslint-disable-next-line no-useless-constructor
/* @ngInject */
constructor(modelManager, syncManager) {
super(modelManager, syncManager);
}
}

View File

@@ -1,241 +0,0 @@
import { protocolManager, SNEncryptedStorage, SFStorageManager , SFItemParams } from 'snjs';
export class MemoryStorage {
constructor() {
this.memory = {};
}
getItem(key) {
return this.memory[key] || null;
}
getItemSync(key) {
return this.getItem(key);
}
get length() {
return Object.keys(this.memory).length;
}
setItem(key, value) {
this.memory[key] = value;
}
removeItem(key) {
delete this.memory[key];
}
clear() {
this.memory = {};
}
keys() {
return Object.keys(this.memory);
}
key(index) {
return Object.keys(this.memory)[index];
}
}
export class StorageManager extends SFStorageManager {
/* @ngInject */
constructor(dbManager, alertManager) {
super();
this.dbManager = dbManager;
this.alertManager = alertManager;
}
initialize(hasPasscode, ephemeral) {
if(hasPasscode) {
// We don't want to save anything in fixed storage except for actual item data (in IndexedDB)
this.storage = this.memoryStorage;
this.itemsStorageMode = StorageManager.FixedEncrypted;
} else if(ephemeral) {
// We don't want to save anything in fixed storage as well as IndexedDB
this.storage = this.memoryStorage;
this.itemsStorageMode = StorageManager.Ephemeral;
} else {
this.storage = localStorage;
this.itemsStorageMode = StorageManager.Fixed;
}
this.modelStorageMode = ephemeral ? StorageManager.Ephemeral : StorageManager.Fixed;
}
get memoryStorage() {
if(!this._memoryStorage) {
this._memoryStorage = new MemoryStorage();
}
return this._memoryStorage;
}
setItemsMode(mode, force) {
var newStorage = this.getVault(mode);
if(newStorage !== this.storage || mode !== this.itemsStorageMode || force) {
// transfer storages
var length = this.storage.length;
for(var i = 0; i < length; i++) {
var key = this.storage.key(i);
newStorage.setItem(key, this.storage.getItem(key));
}
this.itemsStorageMode = mode;
if(newStorage !== this.storage) {
// Only clear if this.storage isn't the same reference as newStorage
this.storage.clear();
}
this.storage = newStorage;
if(mode == StorageManager.FixedEncrypted) {
this.writeEncryptedStorageToDisk();
} else if(mode == StorageManager.Fixed) {
// Remove encrypted storage
this.removeItem("encryptedStorage", StorageManager.Fixed);
}
}
}
getVault(vaultKey) {
if(vaultKey) {
if(vaultKey == StorageManager.Ephemeral || vaultKey == StorageManager.FixedEncrypted) {
return this.memoryStorage;
} else {
return localStorage;
}
} else {
return this.storage;
}
}
async setItem(key, value, vaultKey) {
var storage = this.getVault(vaultKey);
try {
storage.setItem(key, value);
} catch (e) {
console.error("Exception while trying to setItem in StorageManager:", e);
this.alertManager.alert({text: "The application's local storage is out of space. If you have Session History save-to-disk enabled, please disable it, and try again."});
}
if(vaultKey === StorageManager.FixedEncrypted || (!vaultKey && this.itemsStorageMode === StorageManager.FixedEncrypted)) {
return this.writeEncryptedStorageToDisk();
}
}
async getItem(key, vault) {
return this.getItemSync(key, vault);
}
getItemSync(key, vault) {
var storage = this.getVault(vault);
return storage.getItem(key);
}
async removeItem(key, vault) {
var storage = this.getVault(vault);
return storage.removeItem(key);
}
async clear() {
this.memoryStorage.clear();
localStorage.clear();
}
storageAsHash() {
var hash = {};
var length = this.storage.length;
for(var i = 0; i < length; i++) {
var key = this.storage.key(i);
hash[key] = this.storage.getItem(key);
}
return hash;
}
setKeys(keys, authParams) {
this.encryptedStorageKeys = keys;
this.encryptedStorageAuthParams = authParams;
}
async writeEncryptedStorageToDisk() {
var encryptedStorage = new SNEncryptedStorage();
// Copy over totality of current storage
encryptedStorage.content.storage = this.storageAsHash();
// Save new encrypted storage in Fixed storage
var params = new SFItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams);
const syncParams = await params.paramsForSync();
this.setItem("encryptedStorage", JSON.stringify(syncParams), StorageManager.Fixed);
}
async decryptStorage() {
var stored = JSON.parse(this.getItemSync("encryptedStorage", StorageManager.Fixed));
await protocolManager.decryptItem(stored, this.encryptedStorageKeys);
var encryptedStorage = new SNEncryptedStorage(stored);
for(var key of Object.keys(encryptedStorage.content.storage)) {
this.setItem(key, encryptedStorage.storage[key]);
}
}
hasPasscode() {
return this.getItemSync("encryptedStorage", StorageManager.Fixed) !== null;
}
bestStorageMode() {
return this.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed;
}
/*
Model Storage
If using ephemeral storage, we don't need to write it to anything as references will be held already by controllers
and the global modelManager service.
*/
setModelStorageMode(mode) {
if(mode == this.modelStorageMode) {
return;
}
if(mode == StorageManager.Ephemeral) {
// Clear IndexedDB
this.dbManager.clearAllModels(null);
} else {
// Fixed
}
this.modelStorageMode = mode;
}
async getAllModels() {
if(this.modelStorageMode == StorageManager.Fixed) {
return this.dbManager.getAllModels();
}
}
async saveModel(item) {
return this.saveModels([item]);
}
async saveModels(items, onsuccess, onerror) {
if(this.modelStorageMode == StorageManager.Fixed) {
return this.dbManager.saveModels(items);
}
}
async deleteModel(item) {
if(this.modelStorageMode == StorageManager.Fixed) {
return this.dbManager.deleteModel(item);
}
}
async clearAllModels() {
return this.dbManager.clearAllModels();
}
}
StorageManager.FixedEncrypted = "FixedEncrypted"; // encrypted memoryStorage + localStorage persistence
StorageManager.Ephemeral = "Ephemeral"; // memoryStorage
StorageManager.Fixed = "Fixed"; // localStorage

View File

@@ -1,30 +0,0 @@
import angular from 'angular';
import { SFSyncManager } from 'snjs';
export class SyncManager extends SFSyncManager {
/* @ngInject */
constructor(
modelManager,
storageManager,
httpManager,
$timeout,
$interval,
$compile,
$rootScope
) {
super(modelManager, storageManager, httpManager, $timeout, $interval);
this.$rootScope = $rootScope;
this.$compile = $compile;
// this.loggingEnabled = true;
}
presentConflictResolutionModal(items, callback) {
var scope = this.$rootScope.$new(true);
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='sk-modal'></conflict-resolution-modal>" )(scope);
angular.element(document.body).append(el);
}
}

View File

@@ -12,7 +12,7 @@ export class ThemeManager {
componentManager,
desktopManager,
storageManager,
passcodeManager,
lockManager,
appState
) {
this.componentManager = componentManager;
@@ -24,13 +24,6 @@ export class ThemeManager {
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();
});
if (desktopManager.isDesktop) {
appState.addObserver((eventName, data) => {
if (eventName === APP_STATE_EVENT_DESKTOP_EXTS_READY) {