Merge branch '004' into develop

This commit is contained in:
Baptiste Grob
2020-07-24 12:18:09 +02:00
243 changed files with 43408 additions and 8500 deletions

View File

@@ -1,297 +0,0 @@
import _ from 'lodash';
import angular from 'angular';
import { Action, SFModelManager, SFItemParams, protocolManager } from 'snjs';
export class ActionsManager {
/* @ngInject */
constructor(
$compile,
$rootScope,
$timeout,
alertManager,
authManager,
httpManager,
modelManager,
syncManager,
) {
this.$compile = $compile;
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.authManager = authManager;
this.httpManager = httpManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
/* Used when decrypting old items with new keys. This array is only kept in memory. */
this.previousPasswords = [];
}
get extensions() {
return this.modelManager.validItemsForContentType('Extension');
}
extensionsInContextOfItem(item) {
return this.extensions.filter((ext) => {
return _.includes(ext.supported_types, item.content_type) ||
ext.actionsWithContextForItem(item).length > 0;
});
}
/**
* Loads an extension in the context of a certain item.
* The server then has the chance to respond with actions that are
* relevant just to this item. The response extension is not saved,
* just displayed as a one-time thing.
*/
async loadExtensionInContextOfItem(extension, item) {
const params = {
content_type: item.content_type,
item_uuid: item.uuid
};
const emptyFunc = () => { };
return this.httpManager.getAbsolute(
extension.url,
params,
emptyFunc,
emptyFunc
).then((response) => {
this.updateExtensionFromRemoteResponse(extension, response);
return extension;
}).catch((response) => {
console.error("Error loading extension", response);
return null;
});
}
updateExtensionFromRemoteResponse(extension, response) {
if (response.description) {
extension.description = response.description;
}
if (response.supported_types) {
extension.supported_types = response.supported_types;
}
if (response.actions) {
extension.actions = response.actions.map((action) => {
return new Action(action);
});
} else {
extension.actions = [];
}
}
async executeAction(action, extension, item) {
action.running = true;
let result;
switch (action.verb) {
case 'get':
result = await this.handleGetAction(action);
break;
case 'render':
result = await this.handleRenderAction(action);
break;
case 'show':
result = await this.handleShowAction(action);
break;
case 'post':
result = await this.handlePostAction(action, item, extension);
break;
default:
break;
}
action.lastExecuted = new Date();
action.running = false;
return result;
}
async decryptResponse(response, keys) {
const responseItem = response.item;
await protocolManager.decryptItem(responseItem, keys);
if (!responseItem.errorDecrypting) {
return {
response: response,
item: responseItem
};
}
if (!response.auth_params) {
/**
* In some cases revisions were missing auth params.
* Instruct the user to email us to get this remedied.
*/
this.alertManager.alert({
text: `We were unable to decrypt this revision using your current keys,
and this revision is missing metadata that would allow us to try different
keys to decrypt it. This can likely be fixed with some manual intervention.
Please email hello@standardnotes.org for assistance.`
});
return {};
}
/* Try previous passwords */
const triedPasswords = [];
for (const passwordCandidate of this.previousPasswords) {
if (triedPasswords.includes(passwordCandidate)) {
continue;
}
triedPasswords.push(passwordCandidate);
const keyResults = await protocolManager.computeEncryptionKeysForUser(
passwordCandidate,
response.auth_params
);
if (!keyResults) {
continue;
}
const nestedResponse = await this.decryptResponse(
response,
keyResults
);
if (nestedResponse.item) {
return nestedResponse;
}
}
return new Promise((resolve, reject) => {
this.presentPasswordModal((password) => {
this.previousPasswords.push(password);
const result = this.decryptResponse(response, keys);
resolve(result);
});
});
}
async handlePostAction(action, item, extension) {
const decrypted = action.access_type === 'decrypted';
const itemParams = await this.outgoingParamsForItem(item, extension, decrypted);
const params = {
items: [itemParams]
};
/* Needed until SNJS detects null function */
const emptyFunc = () => { };
return this.httpManager.postAbsolute(
action.url,
params,
emptyFunc,
emptyFunc
).then((response) => {
action.error = false;
return {response: response};
}).catch((response) => {
action.error = true;
console.error("Action error response:", response);
this.alertManager.alert({
text: "An issue occurred while processing this action. Please try again."
});
return { response: response };
});
}
async handleShowAction(action) {
const win = window.open(action.url, '_blank');
if (win) {
win.focus();
}
return { response: null };
}
async handleGetAction(action) {
/* Needed until SNJS detects null function */
const emptyFunc = () => {};
const onConfirm = async () => {
return this.httpManager.getAbsolute(action.url, {}, emptyFunc, emptyFunc)
.then(async (response) => {
action.error = false;
await this.decryptResponse(response, await this.authManager.keys());
const items = await this.modelManager.mapResponseItemsToLocalModels(
[response.item],
SFModelManager.MappingSourceRemoteActionRetrieved
);
for (const mappedItem of items) {
this.modelManager.setItemDirty(mappedItem, true);
}
this.syncManager.sync();
return {
response: response,
item: response.item
};
}).catch((response) => {
const error = (response && response.error)
|| { message: "An issue occurred while processing this action. Please try again." };
this.alertManager.alert({ text: error.message });
action.error = true;
return { error: error };
});
};
return new Promise((resolve, reject) => {
this.alertManager.confirm({
text: "Are you sure you want to replace the current note contents with this action's results?",
onConfirm: () => {
onConfirm().then(resolve);
}
});
});
}
async handleRenderAction(action) {
/* Needed until SNJS detects null function */
const emptyFunc = () => {};
return this.httpManager.getAbsolute(
action.url,
{},
emptyFunc,
emptyFunc
).then(async (response) => {
action.error = false;
const result = await this.decryptResponse(response, await this.authManager.keys());
const item = this.modelManager.createItem(result.item);
return {
response: result.response,
item: item
};
}).catch((response) => {
const error = (response && response.error)
|| { message: "An issue occurred while processing this action. Please try again." };
this.alertManager.alert({ text: error.message });
action.error = true;
return { error: error };
});
}
async outgoingParamsForItem(item, extension, decrypted = false) {
let keys = await this.authManager.keys();
if (decrypted) {
keys = null;
}
const itemParams = new SFItemParams(
item,
keys,
await this.authManager.getAuthParams()
);
return itemParams.paramsForExtension();
}
presentRevisionPreviewModal(uuid, content) {
const scope = this.$rootScope.$new(true);
scope.uuid = uuid;
scope.content = content;
const el = this.$compile(
`<revision-preview-modal uuid='uuid' content='content'
class='sk-modal'></revision-preview-modal>`
)(scope);
angular.element(document.body).append(el);
}
presentPasswordModal(callback) {
const scope = this.$rootScope.$new(true);
scope.type = "password";
scope.title = "Decryption Assistance";
scope.message = `Unable to decrypt this item with your current keys.
Please enter your account password at the time of this revision.`;
scope.callback = callback;
const el = this.$compile(
`<input-modal type='type' message='message'
title='title' callback='callback'></input-modal>`
)(scope);
angular.element(document.body).append(el);
}
}

View File

@@ -1,71 +0,0 @@
import { SFAlertManager } from 'snjs';
import { SKAlert } from 'sn-stylekit';
export class AlertManager extends SFAlertManager {
/* @ngInject */
constructor($timeout) {
super();
this.$timeout = $timeout;
}
async alert({
title,
text,
closeButtonText = "OK",
onClose} = {}
) {
return new Promise((resolve, reject) => {
const buttons = [
{
text: closeButtonText,
style: "neutral",
action: async () => {
if(onClose) {
this.$timeout(onClose);
}
resolve(true);
}
}
];
const alert = new SKAlert({title, text, buttons});
alert.present();
});
}
async confirm({
title,
text,
confirmButtonText = "Confirm",
cancelButtonText = "Cancel",
onConfirm,
onCancel,
destructive = false
} = {}) {
return new Promise((resolve, reject) => {
const buttons = [
{
text: cancelButtonText,
style: "neutral",
action: async () => {
if(onCancel) {
this.$timeout(onCancel);
}
reject(false);
}
},
{
text: confirmButtonText,
style: destructive ? "danger" : "info",
action: async () => {
if(onConfirm) {
this.$timeout(onConfirm);
}
resolve(true);
}
},
];
const alert = new SKAlert({title, text, buttons});
alert.present();
});
}
}

View File

@@ -0,0 +1,101 @@
/* eslint-disable prefer-promise-reject-errors */
import { SNAlertService, ButtonType, DismissBlockingDialog } from 'snjs';
import { SKAlert } from 'sn-stylekit';
/** @returns a promise resolving to true if the user confirmed, false if they canceled */
export function confirmDialog({
text,
title,
confirmButtonText = 'Confirm',
cancelButtonText = 'Cancel',
confirmButtonStyle = 'info',
}: {
text: string;
title?: string;
confirmButtonText?: string;
cancelButtonText?: string;
confirmButtonStyle?: 'danger' | 'info';
}) {
return new Promise<boolean>((resolve) => {
const alert = new SKAlert({
title,
text,
buttons: [
{
text: cancelButtonText,
style: 'neutral',
action() {
resolve(false);
},
},
{
text: confirmButtonText,
style: confirmButtonStyle,
action() {
resolve(true);
},
},
],
});
alert.present();
});
}
export function alertDialog({
title,
text,
closeButtonText = 'OK',
}: {
title?: string;
text: string;
closeButtonText?: string;
}) {
return new Promise<void>((resolve) => {
const alert = new SKAlert({
title,
text,
buttons: [
{
text: closeButtonText,
style: 'neutral',
action: resolve,
},
],
});
alert.present();
});
}
export class AlertService implements SNAlertService {
/**
* @deprecated use the standalone `alertDialog` function instead
*/
alert(text: string, title?: string, closeButtonText?: string) {
return alertDialog({ text, title, closeButtonText });
}
confirm(
text: string,
title?: string,
confirmButtonText?: string,
confirmButtonType?: ButtonType,
cancelButtonText?: string
): Promise<boolean> {
return confirmDialog({
text,
title,
confirmButtonText,
cancelButtonText,
confirmButtonStyle:
confirmButtonType === ButtonType.Danger ? 'danger' : 'info',
});
}
blockingDialog(text: string) {
const alert = new SKAlert({ text });
alert.present();
return () => {
alert.dismiss();
};
}
}

View File

@@ -0,0 +1,176 @@
import { WebApplication } from '@/ui_models/application';
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from 'snjs';
function zippableTxtName(name: string, suffix = ""): string {
const sanitizedName = name
.replace(/\//g, '')
.replace(/\\+/g, '')
.replace(/:/g, ' ')
.replace(/\./g, ' ');
const nameEnd = suffix + ".txt";
const maxFileNameLength = 255;
return sanitizedName.slice(0, maxFileNameLength - nameEnd.length) + nameEnd;
}
export class ArchiveManager {
private readonly application: WebApplication
private textFile?: string
constructor(application: WebApplication) {
this.application = application;
}
public async downloadBackup(encrypted: boolean) {
const items = this.application.allItems();
const run = async () => {
// download in Standard Notes format
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
const data = await this.itemsData(items, intent)
if (encrypted) {
this.downloadData(
data!,
`Standard Notes Encrypted Backup - ${this.formattedDate()}.txt`
);
} else {
const data = await this.application.createBackupFile(items, intent);
if (data) {
/** download as zipped plain text files */
this.downloadZippedItems(
items,
/** Add the backup file to the archive */
(zipWriter, zip) => new Promise((resolve) => {
const blob = new Blob([data], { type: 'text/plain' });
const fileName = zippableTxtName(
`Standard Notes Decrypted Backup - ${this.formattedDate()}`
);
zipWriter.add(fileName, new zip.BlobReader(blob), resolve);
})
);
}
}
};
if (
await this.application.privilegesService!
.actionRequiresPrivilege(ProtectedAction.ManageBackups)
) {
this.application.presentPrivilegesModal(
ProtectedAction.ManageBackups,
() => {
run();
});
} else {
run();
}
}
private formattedDate() {
const string = `${new Date()}`;
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
const matches = string.match(/^(.*?) \(/);
if (matches && matches.length >= 2) {
return matches[1];
}
return string;
}
private async itemsData(items: SNItem[], intent: EncryptionIntent) {
const data = await this.application.createBackupFile(items, intent);
if (!data) {
return undefined;
}
const blobData = new Blob([data], { type: 'text/json' });
return blobData;
}
private get zip() {
return (window as any).zip;
}
private async loadZip() {
if (this.zip) {
return;
}
const scriptTag = document.createElement('script');
scriptTag.src = '/assets/zip/zip.js';
scriptTag.async = false;
const headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
return new Promise((resolve) => {
scriptTag.onload = () => {
this.zip.workerScriptsPath = 'assets/zip/';
resolve();
};
});
}
private async downloadZippedItems(
items: SNItem[],
onOpenZip: (zipWriter: any, zip: any) => Promise<void>
) {
await this.loadZip();
this.zip.createWriter(
new this.zip.BlobWriter('application/zip'),
async (zipWriter: any) => {
await onOpenZip(zipWriter, this.zip);
let index = 0;
const nextFile = () => {
const item = items[index];
let name, contents;
if (item.content_type === ContentType.Note) {
const note = item as SNNote;
name = note.title;
contents = note.text;
} else {
name = item.content_type;
contents = JSON.stringify(item.content, null, 2);
}
if (!name) {
name = '';
}
const blob = new Blob([contents], { type: 'text/plain' });
const fileName = item.content_type + '/' +
zippableTxtName(name, `-${item.uuid.split('-')[0]}`);
zipWriter.add(fileName, new this.zip.BlobReader(blob), () => {
index++;
if (index < items.length) {
nextFile();
} else {
zipWriter.close((blob: any) => {
this.downloadData(
blob,
`Standard Notes Backup - ${this.formattedDate()}.zip`
);
zipWriter = null;
});
}
});
};
nextFile();
}, onerror);
}
private hrefForData(data: Blob) {
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (this.textFile) {
window.URL.revokeObjectURL(this.textFile);
}
this.textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return this.textFile;
}
private downloadData(data: Blob, fileName: string) {
const link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = this.hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();
}
}

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,179 +0,0 @@
export class DBManager {
/* @ngInject */
constructor(alertManager) {
this.locked = true;
this.alertManager = alertManager;
}
displayOfflineAlert() {
var message = "There was an issue loading your offline database. This could happen for two reasons:";
message += "\n\n1. You're in a private window in your browser. We can't save your data without access to the local database. Please use a non-private window.";
message += "\n\n2. You have two windows of the app open at the same time. Please close any other app instances and reload the page.";
this.alertManager.alert({text: message});
}
setLocked(locked) {
this.locked = locked;
}
async openDatabase({onUpgradeNeeded} = {}) {
if(this.locked) {
return;
}
const request = window.indexedDB.open("standardnotes", 1);
return new Promise((resolve, reject) => {
request.onerror = (event) => {
if(event.target.errorCode) {
this.alertManager.alert({text: "Offline database issue: " + event.target.errorCode});
} else {
this.displayOfflineAlert();
}
console.error("Offline database issue:", event);
resolve(null);
};
request.onsuccess = (event) => {
const db = event.target.result;
db.onversionchange = function(event) {
db.close();
};
db.onerror = function(errorEvent) {
console.error("Database error: " + errorEvent.target.errorCode);
};
resolve(db);
};
request.onblocked = (event) => {
console.error("Request blocked error:", event.target.errorCode);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.onversionchange = function(event) {
db.close();
};
// Create an objectStore for this database
const objectStore = db.createObjectStore("items", { keyPath: "uuid" });
objectStore.createIndex("uuid", "uuid", { unique: true });
objectStore.transaction.oncomplete = function(event) {
// Ready to store values in the newly created objectStore.
if(db.version === 1 && onUpgradeNeeded) {
onUpgradeNeeded();
}
};
};
});
}
async getAllModels() {
const db = await this.openDatabase();
const objectStore = db.transaction("items").objectStore("items");
const items = [];
return new Promise(async (resolve, reject) => {
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
items.push(cursor.value);
cursor.continue();
} else {
resolve(items);
}
};
});
}
async saveModel(item) {
this.saveModels([item]);
}
async saveModels(items) {
const showGenericError = (error) => {
this.alertManager.alert({text: `Unable to save changes locally due to an unknown system issue. Issue Code: ${error.code} Issue Name: ${error.name}.`});
};
return new Promise(async (resolve, reject) => {
if(items.length === 0) {
resolve();
return;
}
const db = await this.openDatabase();
const transaction = db.transaction("items", "readwrite");
transaction.oncomplete = (event) => {};
transaction.onerror = function(event) {
console.error("Transaction error:", event.target.errorCode);
showGenericError(event.target.error);
};
transaction.onblocked = function(event) {
console.error("Transaction blocked error:", event.target.errorCode);
showGenericError(event.target.error);
};
transaction.onabort = function(event) {
console.error("Offline saving aborted:", event);
const error = event.target.error;
if(error.name == "QuotaExceededError") {
this.alertManager.alert({text: "Unable to save changes locally because your device is out of space. Please free up some disk space and try again, otherwise, your data may end up in an inconsistent state."});
} else {
showGenericError(error);
}
reject(error);
};
const itemObjectStore = transaction.objectStore("items");
const putItem = async (item) => {
return new Promise((resolve, reject) => {
const request = itemObjectStore.put(item);
request.onerror = (event) => {
console.error("DB put error:", event.target.error);
resolve();
};
request.onsuccess = resolve;
});
};
for(const item of items) {
await putItem(item);
}
resolve();
});
}
async deleteModel(item) {
return new Promise(async (resolve, reject) => {
const db = await this.openDatabase();
const request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid);
request.onsuccess = (event) => {
resolve();
};
request.onerror = (event) => {
reject();
};
});
}
async clearAllModels() {
const deleteRequest = window.indexedDB.deleteDatabase("standardnotes");
return new Promise((resolve, reject) => {
deleteRequest.onerror = function(event) {
console.error("Error deleting database.");
resolve();
};
deleteRequest.onsuccess = function(event) {
resolve();
};
deleteRequest.onblocked = function(event) {
console.error("Delete request blocked");
this.alertManager.alert({text: "Your browser is blocking Standard Notes from deleting the local database. Make sure there are no other open windows of this app and try again. If the issue persists, please manually delete app data to sign out."});
resolve();
};
});
}
}

View File

@@ -1,235 +0,0 @@
// An interface used by the Desktop app to interact with SN
import _ from 'lodash';
import { isDesktopApplication } from '@/utils';
import { SFItemParams, SFModelManager } from 'snjs';
const COMPONENT_DATA_KEY_INSTALL_ERROR = 'installError';
const COMPONENT_CONTENT_KEY_PACKAGE_INFO = 'package_info';
const COMPONENT_CONTENT_KEY_LOCAL_URL = 'local_url';
export class DesktopManager {
/* @ngInject */
constructor(
$rootScope,
$timeout,
modelManager,
syncManager,
authManager,
passcodeManager,
appState
) {
this.passcodeManager = passcodeManager;
this.modelManager = modelManager;
this.authManager = authManager;
this.syncManager = syncManager;
this.$rootScope = $rootScope;
this.appState = appState;
this.timeout = $timeout;
this.updateObservers = [];
this.componentActivationObservers = [];
this.isDesktop = isDesktopApplication();
$rootScope.$on("initial-data-loaded", () => {
this.dataLoaded = true;
if(this.dataLoadHandler) {
this.dataLoadHandler();
}
});
$rootScope.$on("major-data-change", () => {
if(this.majorDataChangeHandler) {
this.majorDataChangeHandler();
}
});
}
saveBackup() {
this.majorDataChangeHandler && this.majorDataChangeHandler();
}
getExtServerHost() {
console.assert(
this.extServerHost,
'extServerHost is null'
);
return this.extServerHost;
}
/*
Sending a component in its raw state is really slow for the desktop app
Keys are not passed into ItemParams, so the result is not encrypted
*/
async convertComponentForTransmission(component) {
return new SFItemParams(component).paramsForExportFile(true);
}
// All `components` should be installed
syncComponentsInstallation(components) {
if(!this.isDesktop) {
return;
}
Promise.all(components.map((component) => {
return this.convertComponentForTransmission(component);
})).then((data) => {
this.installationSyncHandler(data);
});
}
async installComponent(component) {
this.installComponentHandler(
await this.convertComponentForTransmission(component)
);
}
registerUpdateObserver(callback) {
const observer = {
callback: callback
};
this.updateObservers.push(observer);
return observer;
}
searchText(text) {
if(!this.isDesktop) {
return;
}
this.lastSearchedText = text;
this.searchHandler && this.searchHandler(text);
}
redoSearch() {
if(this.lastSearchedText) {
this.searchText(this.lastSearchedText);
}
}
deregisterUpdateObserver(observer) {
_.pull(this.updateObservers, observer);
}
// Pass null to cancel search
desktop_setSearchHandler(handler) {
this.searchHandler = handler;
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast("window-gained-focus");
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast("window-lost-focus");
}
desktop_onComponentInstallationComplete(componentData, error) {
const component = this.modelManager.findItem(componentData.uuid);
if(!component) {
return;
}
if(error) {
component.setAppDataItem(
COMPONENT_DATA_KEY_INSTALL_ERROR,
error
);
} else {
const permissableKeys = [
COMPONENT_CONTENT_KEY_PACKAGE_INFO,
COMPONENT_CONTENT_KEY_LOCAL_URL
];
for(const key of permissableKeys) {
component[key] = componentData.content[key];
}
this.modelManager.notifySyncObserversOfModels(
[component],
SFModelManager.MappingSourceDesktopInstalled
);
component.setAppDataItem(
COMPONENT_DATA_KEY_INSTALL_ERROR,
null
);
}
this.modelManager.setItemDirty(component);
this.syncManager.sync();
this.timeout(() => {
for(const observer of this.updateObservers) {
observer.callback(component);
}
});
}
desktop_registerComponentActivationObserver(callback) {
const observer = {id: Math.random, callback: callback};
this.componentActivationObservers.push(observer);
return observer;
}
desktop_deregisterComponentActivationObserver(observer) {
_.pull(this.componentActivationObservers, observer);
}
/* Notify observers that a component has been registered/activated */
async notifyComponentActivation(component) {
const serializedComponent = await this.convertComponentForTransmission(
component
);
this.timeout(() => {
for(const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
}
/* Used to resolve "sn://" */
desktop_setExtServerHost(host) {
this.extServerHost = host;
this.appState.desktopExtensionsReady();
}
desktop_setComponentInstallationSyncHandler(handler) {
this.installationSyncHandler = handler;
}
desktop_setInstallComponentHandler(handler) {
this.installComponentHandler = handler;
}
desktop_setInitialDataLoadHandler(handler) {
this.dataLoadHandler = handler;
if(this.dataLoaded) {
this.dataLoadHandler();
}
}
async desktop_requestBackupFile(callback) {
let keys, authParams;
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();
}
const nullOnEmpty = true;
this.modelManager.getAllItemsJSONData(
keys,
authParams,
nullOnEmpty
).then((data) => {
callback(data);
});
}
desktop_setMajorDataChangeHandler(handler) {
this.majorDataChangeHandler = handler;
}
desktop_didBeginBackup() {
this.appState.beganBackupDownload();
}
desktop_didFinishBackup(success) {
this.appState.endedBackupDownload({
success: success
});
}
}

View File

@@ -0,0 +1,243 @@
import { SNComponent, PurePayload, ComponentMutator, AppDataField } from 'snjs';
/* eslint-disable camelcase */
import { WebApplication } from '@/ui_models/application';
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils';
import { EncryptionIntent, ApplicationService, ApplicationEvent, removeFromArray } from 'snjs';
type UpdateObserverCallback = (component: SNComponent) => void
type ComponentActivationCallback = (payload: PurePayload) => void
type ComponentActivationObserver = {
id: string,
callback: ComponentActivationCallback
}
export class DesktopManager extends ApplicationService {
$rootScope: ng.IRootScopeService
$timeout: ng.ITimeoutService
componentActivationObservers: ComponentActivationObserver[] = []
updateObservers: {
callback: UpdateObserverCallback
}[] = []
isDesktop = isDesktopApplication();
dataLoaded = false
dataLoadHandler?: () => void
majorDataChangeHandler?: () => void
extServerHost?: string
installationSyncHandler?: (payloads: PurePayload[]) => void
installComponentHandler?: (payload: PurePayload) => void
lastSearchedText?: string
searchHandler?: (text?: string) => void
constructor(
$rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService,
application: WebApplication
) {
super(application);
this.$rootScope = $rootScope;
this.$timeout = $timeout;
}
get webApplication() {
return this.application as WebApplication;
}
deinit() {
this.componentActivationObservers.length = 0;
this.updateObservers.length = 0;
super.deinit();
}
async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDataLoaded) {
this.dataLoaded = true;
if (this.dataLoadHandler) {
this.dataLoadHandler();
}
} else if (eventName === ApplicationEvent.MajorDataChange) {
if (this.majorDataChangeHandler) {
this.majorDataChangeHandler();
}
}
}
saveBackup() {
this.majorDataChangeHandler && this.majorDataChangeHandler();
}
getExtServerHost() {
console.assert(
this.extServerHost,
'extServerHost is null'
);
return this.extServerHost;
}
/**
* Sending a component in its raw state is really slow for the desktop app
* Keys are not passed into ItemParams, so the result is not encrypted
*/
async convertComponentForTransmission(component: SNComponent) {
return this.application!.protocolService!.payloadByEncryptingPayload(
component.payloadRepresentation(),
EncryptionIntent.FileDecrypted
);
}
// All `components` should be installed
syncComponentsInstallation(components: SNComponent[]) {
if (!this.isDesktop) {
return;
}
Promise.all(components.map((component) => {
return this.convertComponentForTransmission(component);
})).then((payloads) => {
this.installationSyncHandler!(payloads);
});
}
async installComponent(component: SNComponent) {
this.installComponentHandler!(
await this.convertComponentForTransmission(component)
);
}
registerUpdateObserver(callback: UpdateObserverCallback) {
const observer = {
callback: callback
};
this.updateObservers.push(observer);
return () => {
removeFromArray(this.updateObservers, observer);
};
}
searchText(text?: string) {
if (!this.isDesktop) {
return;
}
this.lastSearchedText = text;
this.searchHandler && this.searchHandler(text);
}
redoSearch() {
if (this.lastSearchedText) {
this.searchText(this.lastSearchedText);
}
}
// Pass null to cancel search
desktop_setSearchHandler(handler: (text?: string) => void) {
this.searchHandler = handler;
}
desktop_windowGainedFocus() {
this.$rootScope.$broadcast('window-gained-focus');
}
desktop_windowLostFocus() {
this.$rootScope.$broadcast('window-lost-focus');
}
async desktop_onComponentInstallationComplete(
componentData: any,
error: any
) {
const component = this.application!.findItem(componentData.uuid);
if (!component) {
return;
}
const updatedComponent = await this.application!.changeAndSaveItem(
component.uuid,
(m) => {
const mutator = m as ComponentMutator;
if (error) {
mutator.setAppDataItem(
AppDataField.ComponentInstallError,
error
);
} else {
mutator.local_url = componentData.content.local_url;
mutator.package_info = componentData.content.package_info;
mutator.setAppDataItem(
AppDataField.ComponentInstallError,
undefined
);
}
})
this.$timeout(() => {
for (const observer of this.updateObservers) {
observer.callback(updatedComponent as SNComponent);
}
});
}
desktop_registerComponentActivationObserver(callback: ComponentActivationCallback) {
const observer = { id: `${Math.random}`, callback: callback };
this.componentActivationObservers.push(observer);
return observer;
}
desktop_deregisterComponentActivationObserver(observer: ComponentActivationObserver) {
removeFromArray(this.componentActivationObservers, observer);
}
/* Notify observers that a component has been registered/activated */
async notifyComponentActivation(component: SNComponent) {
const serializedComponent = await this.convertComponentForTransmission(
component
);
this.$timeout(() => {
for (const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
}
/* Used to resolve 'sn://' */
desktop_setExtServerHost(host: string) {
this.extServerHost = host;
this.webApplication.getAppState().desktopExtensionsReady();
}
desktop_setComponentInstallationSyncHandler(handler: (payloads: PurePayload[]) => void) {
this.installationSyncHandler = handler;
}
desktop_setInstallComponentHandler(handler: (payload: PurePayload) => void) {
this.installComponentHandler = handler;
}
desktop_setInitialDataLoadHandler(handler: () => void) {
this.dataLoadHandler = handler;
if (this.dataLoaded) {
this.dataLoadHandler();
}
}
async desktop_requestBackupFile(callback: (data: any) => void) {
const data = await this.application!.createBackupFile(
undefined,
undefined,
true
);
callback(data);
}
desktop_setMajorDataChangeHandler(handler: () => void) {
this.majorDataChangeHandler = handler;
}
desktop_didBeginBackup() {
this.webApplication.getAppState().beganBackupDownload();
}
desktop_didFinishBackup(success: boolean) {
this.webApplication.getAppState().endedBackupDownload(success);
}
}

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

@@ -1,21 +0,0 @@
export { ActionsManager } from './actionsManager';
export { ArchiveManager } from './archiveManager';
export { AuthManager } from './authManager';
export { ComponentManager } from './componentManager';
export { DBManager } from './dbManager';
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 { PrivilegesManager } from './privilegesManager';
export { SessionHistory } from './sessionHistory';
export { SingletonManager } from './singletonManager';
export { StatusManager } from './statusManager';
export { StorageManager } from './storageManager';
export { SyncManager } from './syncManager';
export { ThemeManager } from './themeManager';
export { AlertManager } from './alertManager';
export { PreferencesManager } from './preferencesManager';

View File

@@ -0,0 +1,9 @@
export { AlertService } from './alertService';
export { ArchiveManager } from './archiveManager';
export { DesktopManager } from './desktopManager';
export { KeyboardManager } from './keyboardManager';
export { LockManager } from './lockManager';
export { NativeExtManager } from './nativeExtManager';
export { PreferencesManager } from './preferencesManager';
export { StatusManager } from './statusManager';
export { ThemeManager } from './themeManager';

View File

@@ -1,115 +0,0 @@
export class KeyboardManager {
constructor() {
this.observers = [];
KeyboardManager.KeyTab = "Tab";
KeyboardManager.KeyBackspace = "Backspace";
KeyboardManager.KeyUp = "ArrowUp";
KeyboardManager.KeyDown = "ArrowDown";
KeyboardManager.KeyModifierShift = "Shift";
KeyboardManager.KeyModifierCtrl = "Control";
// ⌘ key on Mac, ⊞ key on Windows
KeyboardManager.KeyModifierMeta = "Meta";
KeyboardManager.KeyModifierAlt = "Alt";
KeyboardManager.KeyEventDown = "KeyEventDown";
KeyboardManager.KeyEventUp = "KeyEventUp";
KeyboardManager.AllModifiers = [
KeyboardManager.KeyModifierShift,
KeyboardManager.KeyModifierCtrl,
KeyboardManager.KeyModifierMeta,
KeyboardManager.KeyModifierAlt
];
window.addEventListener('keydown', this.handleKeyDown.bind(this));
window.addEventListener('keyup', this.handleKeyUp.bind(this));
}
modifiersForEvent(event) {
const eventModifiers = KeyboardManager.AllModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches = (
((event.ctrlKey || event.key == KeyboardManager.KeyModifierCtrl) && modifier === KeyboardManager.KeyModifierCtrl) ||
((event.metaKey || event.key == KeyboardManager.KeyModifierMeta) && modifier === KeyboardManager.KeyModifierMeta) ||
((event.altKey || event.key == KeyboardManager.KeyModifierAlt) && modifier === KeyboardManager.KeyModifierAlt) ||
((event.shiftKey || event.key == KeyboardManager.KeyModifierShift) && modifier === KeyboardManager.KeyModifierShift)
);
return matches;
});
return eventModifiers;
}
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
const eventModifiers = this.modifiersForEvent(event);
if(eventModifiers.length != modifiers.length) {
return false;
}
for(const modifier of modifiers) {
if(!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if(!key) {
return true;
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() == event.key.toLowerCase();
}
notifyObserver(event, keyEventType) {
for(const observer of this.observers) {
if(observer.element && event.target != observer.element) {
continue;
}
if(observer.elements && !observer.elements.includes(event.target)) {
continue;
}
if(observer.notElement && observer.notElement == event.target) {
continue;
}
if(observer.notElementIds && observer.notElementIds.includes(event.target.id)) {
continue;
}
if(this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
const callback = keyEventType == KeyboardManager.KeyEventDown ? observer.onKeyDown : observer.onKeyUp;
if(callback) {
callback(event);
}
}
}
}
handleKeyDown(event) {
this.notifyObserver(event, KeyboardManager.KeyEventDown);
}
handleKeyUp(event) {
this.notifyObserver(event, KeyboardManager.KeyEventUp);
}
addKeyObserver({key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds}) {
const observer = {key, modifiers, onKeyDown, onKeyUp, element, elements, notElement, notElementIds};
this.observers.push(observer);
return observer;
}
removeKeyObserver(observer) {
this.observers.splice(this.observers.indexOf(observer), 1);
}
}

View File

@@ -0,0 +1,147 @@
import { removeFromArray } from 'snjs';
export enum KeyboardKey {
Tab = "Tab",
Backspace = "Backspace",
Up = "ArrowUp",
Down = "ArrowDown",
};
export enum KeyboardModifier {
Shift = "Shift",
Ctrl = "Control",
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = "Meta",
Alt = "Alt",
};
enum KeyboardKeyEvent {
Down = "KeyEventDown",
Up = "KeyEventUp"
};
type KeyboardObserver = {
key?: KeyboardKey | string
modifiers?: KeyboardModifier[]
onKeyDown?: (event: KeyboardEvent) => void
onKeyUp?: (event: KeyboardEvent) => void
element?: HTMLElement
elements?: HTMLElement[]
notElement?: HTMLElement
notElementIds?: string[]
}
export class KeyboardManager {
private observers: KeyboardObserver[] = []
private handleKeyDown: any
private handleKeyUp: any
constructor() {
this.handleKeyDown = (event: KeyboardEvent) => {
this.notifyObserver(event, KeyboardKeyEvent.Down);
}
this.handleKeyUp = (event: KeyboardEvent) => {
this.notifyObserver(event, KeyboardKeyEvent.Up);
}
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
}
public deinit() {
this.observers.length = 0;
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
this.handleKeyDown = undefined;
this.handleKeyUp = undefined;
}
modifiersForEvent(event: KeyboardEvent) {
const allModifiers = Object.values(KeyboardModifier);
const eventModifiers = allModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches = (
(
(event.ctrlKey || event.key === KeyboardModifier.Ctrl)
&& modifier === KeyboardModifier.Ctrl
) ||
(
(event.metaKey || event.key === KeyboardModifier.Meta)
&& modifier === KeyboardModifier.Meta
) ||
(
(event.altKey || event.key === KeyboardModifier.Alt)
&& modifier === KeyboardModifier.Alt
) ||
(
(event.shiftKey || event.key === KeyboardModifier.Shift)
&& modifier === KeyboardModifier.Shift
)
);
return matches;
});
return eventModifiers;
}
eventMatchesKeyAndModifiers(
event: KeyboardEvent,
key: KeyboardKey | string,
modifiers: KeyboardModifier[] = []
) {
const eventModifiers = this.modifiersForEvent(event);
if (eventModifiers.length !== modifiers.length) {
return false;
}
for (const modifier of modifiers) {
if (!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if (!key) {
return true;
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() === event.key.toLowerCase();
}
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
const target = event.target as HTMLElement;
for (const observer of this.observers) {
if (observer.element && event.target !== observer.element) {
continue;
}
if (observer.elements && !observer.elements.includes(target)) {
continue;
}
if (observer.notElement && observer.notElement === event.target) {
continue;
}
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
continue;
}
if (this.eventMatchesKeyAndModifiers(event, observer.key!, observer.modifiers)) {
const callback = keyEvent === KeyboardKeyEvent.Down
? observer.onKeyDown
: observer.onKeyUp;
if (callback) {
callback(event);
}
}
}
}
addKeyObserver(observer: KeyboardObserver) {
this.observers.push(observer);
return () => {
removeFromArray(this.observers, observer);
};
}
}

View File

@@ -0,0 +1,157 @@
import { WebApplication } from '@/ui_models/application';
import { isDesktopApplication } from '@/utils';
import { AppStateEvent } from '@/ui_models/app_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 {
private application: WebApplication
private unsubState: any
private pollFocusInterval: any
private lastFocusState?: 'hidden' | 'visible'
private lockAfterDate?: Date
private lockTimeout?: any
constructor(application: WebApplication) {
this.application = application;
setImmediate(() => {
this.observeVisibility();
});
}
observeVisibility() {
this.unsubState = this.application.getAppState().addObserver(
async (eventName) => {
if (eventName === AppStateEvent.WindowDidBlur) {
this.documentVisibilityChanged(false);
} else if (eventName === AppStateEvent.WindowDidFocus) {
this.documentVisibilityChanged(true);
}
}
);
if (!isDesktopApplication()) {
this.beginWebFocusPolling();
}
}
deinit() {
this.unsubState();
if (this.pollFocusInterval) {
clearInterval(this.pollFocusInterval);
}
}
async setAutoLockInterval(interval: number) {
return this.application!.setValue(
STORAGE_KEY_AUTOLOCK_INTERVAL,
interval
);
}
async getAutoLockInterval() {
const interval = await this.application!.getValue(
STORAGE_KEY_AUTOLOCK_INTERVAL
);
if (interval) {
return 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.pollFocusInterval = 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"
}
];
}
async documentVisibilityChanged(visible: boolean) {
if (visible) {
const locked = await this.application.isLocked();
if (
!locked &&
this.lockAfterDate &&
new Date() > this.lockAfterDate
) {
this.application.lock();
}
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: number) => {
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.lock();
this.lockAfterDate = undefined;
}, interval);
}
cancelAutoLockTimer() {
clearTimeout(this.lockTimeout);
this.lockAfterDate = undefined;
}
}

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,183 +0,0 @@
/* A class for handling installation of system extensions */
import { isDesktopApplication } from '@/utils';
import { SFPredicate } from 'snjs';
export class NativeExtManager {
/* @ngInject */
constructor(modelManager, syncManager, singletonManager) {
this.modelManager = modelManager;
this.syncManager = syncManager;
this.singletonManager = singletonManager;
this.extManagerId = "org.standardnotes.extensions-manager";
this.batchManagerId = "org.standardnotes.batch-manager";
this.systemExtensions = [];
this.resolveExtensionsManager();
this.resolveBatchManager();
}
isSystemExtension(extension) {
return this.systemExtensions.includes(extension.uuid);
}
resolveExtensionsManager() {
const contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
const packagePredicate = new SFPredicate("package_info.identifier", "=", this.extManagerId);
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
// Resolved Singleton
this.systemExtensions.push(resolvedSingleton.uuid);
var needsSync = false;
if(isDesktopApplication()) {
if(!resolvedSingleton.local_url) {
resolvedSingleton.local_url = window._extensions_manager_location;
needsSync = true;
}
} else {
if(!resolvedSingleton.hosted_url) {
resolvedSingleton.hosted_url = window._extensions_manager_location;
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permission = resolvedSingleton.content.permissions.find((p) => p.name == "stream-items");
if(!permission.content_types.includes("SN|ExtensionRepo")) {
permission.content_types.push("SN|ExtensionRepo");
needsSync = true;
}
if(needsSync) {
this.modelManager.setItemDirty(resolvedSingleton, true);
this.syncManager.sync();
}
}, (valueCallback) => {
// Safe to create. Create and return object.
const url = window._extensions_manager_location;
if(!url) {
console.error("window._extensions_manager_location must be set.");
return;
}
const packageInfo = {
name: "Extensions",
identifier: this.extManagerId
};
var item = {
content_type: "SN|Component",
content: {
name: packageInfo.name,
area: "rooms",
package_info: packageInfo,
permissions: [
{
name: "stream-items",
content_types: [
"SN|Component", "SN|Theme", "SF|Extension",
"Extension", "SF|MFA", "SN|Editor", "SN|ExtensionRepo"
]
}
]
}
};
if(isDesktopApplication()) {
item.content.local_url = window._extensions_manager_location;
} else {
item.content.hosted_url = window._extensions_manager_location;
}
var component = this.modelManager.createItem(item);
this.modelManager.addItem(component);
this.modelManager.setItemDirty(component, true);
this.syncManager.sync();
this.systemExtensions.push(component.uuid);
valueCallback(component);
});
}
resolveBatchManager() {
const contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
const packagePredicate = new SFPredicate("package_info.identifier", "=", this.batchManagerId);
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
// Resolved Singleton
this.systemExtensions.push(resolvedSingleton.uuid);
var needsSync = false;
if(isDesktopApplication()) {
if(!resolvedSingleton.local_url) {
resolvedSingleton.local_url = window._batch_manager_location;
needsSync = true;
}
} else {
if(!resolvedSingleton.hosted_url) {
resolvedSingleton.hosted_url = window._batch_manager_location;
needsSync = true;
}
}
if(needsSync) {
this.modelManager.setItemDirty(resolvedSingleton, true);
this.syncManager.sync();
}
}, (valueCallback) => {
// Safe to create. Create and return object.
const url = window._batch_manager_location;
if(!url) {
console.error("window._batch_manager_location must be set.");
return;
}
const packageInfo = {
name: "Batch Manager",
identifier: this.batchManagerId
};
var item = {
content_type: "SN|Component",
content: {
name: packageInfo.name,
area: "modal",
package_info: packageInfo,
permissions: [
{
name: "stream-items",
content_types: [
"Note", "Tag", "SN|SmartTag",
"SN|Component", "SN|Theme", "SN|UserPreferences",
"SF|Extension", "Extension", "SF|MFA", "SN|Editor",
"SN|FileSafe|Credentials", "SN|FileSafe|FileMetadata", "SN|FileSafe|Integration"
]
}
]
}
};
if(isDesktopApplication()) {
item.content.local_url = window._batch_manager_location;
} else {
item.content.hosted_url = window._batch_manager_location;
}
var component = this.modelManager.createItem(item);
this.modelManager.addItem(component);
this.modelManager.setItemDirty(component, true);
this.syncManager.sync();
this.systemExtensions.push(component.uuid);
valueCallback(component);
});
}
}

View File

@@ -0,0 +1,206 @@
import { isDesktopApplication } from '@/utils';
import {
SNPredicate,
ContentType,
SNComponent,
ApplicationService,
ComponentAction,
FillItemContent,
ComponentMutator,
Copy,
dictToArray
} from 'snjs';
import { PayloadContent } from '@node_modules/snjs/dist/@types/protocol/payloads/generator';
import { ComponentPermission } from '@node_modules/snjs/dist/@types/models/app/component';
/** A class for handling installation of system extensions */
export class NativeExtManager extends ApplicationService {
extManagerId = 'org.standardnotes.extensions-manager';
batchManagerId = 'org.standardnotes.batch-manager';
/** @override */
async onAppLaunch() {
super.onAppLaunch();
this.reload();
}
get extManagerPred() {
const extManagerId = 'org.standardnotes.extensions-manager';
return SNPredicate.CompoundPredicate([
new SNPredicate('content_type', '=', ContentType.Component),
new SNPredicate('package_info.identifier', '=', extManagerId)
]);
}
get batchManagerPred() {
const batchMgrId = 'org.standardnotes.batch-manager';
return SNPredicate.CompoundPredicate([
new SNPredicate('content_type', '=', ContentType.Component),
new SNPredicate('package_info.identifier', '=', batchMgrId)
]);
}
get extMgrUrl() {
return (window as any)._extensions_manager_location;
}
get batchMgrUrl() {
return (window as any)._batch_manager_location;
}
reload() {
this.application!.singletonManager!.registerPredicate(this.extManagerPred);
this.application!.singletonManager!.registerPredicate(this.batchManagerPred);
this.resolveExtensionsManager();
this.resolveBatchManager();
}
async resolveExtensionsManager() {
const extensionsManager = (await this.application!.singletonManager!.findOrCreateSingleton(
this.extManagerPred,
ContentType.Component,
this.extensionsManagerTemplateContent()
)) as SNComponent;
let needsSync = false;
if (isDesktopApplication()) {
if (!extensionsManager.local_url) {
await this.application!.changeItem(extensionsManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.local_url = this.extMgrUrl;
});
needsSync = true;
}
} else {
if (!extensionsManager.hosted_url) {
await this.application!.changeItem(extensionsManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.hosted_url = this.extMgrUrl;
});
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permissions = Copy(extensionsManager!.permissions) as ComponentPermission[];
const permission = permissions.find((p) => {
return p.name === ComponentAction.StreamItems
});
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
permission.content_types!.push(ContentType.ExtensionRepo);
await this.application!.changeItem(extensionsManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.permissions = permissions;
});
needsSync = true;
}
if (needsSync) {
this.application!.saveItem(extensionsManager.uuid);
}
}
extensionsManagerTemplateContent() {
const url = this.extMgrUrl;
if (!url) {
throw Error('this.extMgrUrl must be set.');
}
const packageInfo = {
name: 'Extensions',
identifier: this.extManagerId
};
const content = FillItemContent({
name: packageInfo.name,
area: 'rooms',
package_info: packageInfo,
permissions: [
{
name: ComponentAction.StreamItems,
content_types: [
ContentType.Component,
ContentType.Theme,
ContentType.ServerExtension,
ContentType.ActionsExtension,
ContentType.Mfa,
ContentType.Editor,
ContentType.ExtensionRepo
]
}
]
}) as PayloadContent;
if (isDesktopApplication()) {
content.local_url = this.extMgrUrl;
} else {
content.hosted_url = this.extMgrUrl;
}
return content;
}
async resolveBatchManager() {
const batchManager = (await this.application!.singletonManager!.findOrCreateSingleton(
this.batchManagerPred,
ContentType.Component,
this.batchManagerTemplateContent()
)) as SNComponent;
let needsSync = false;
if (isDesktopApplication()) {
if (!batchManager.local_url) {
await this.application!.changeItem(batchManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.local_url = this.batchMgrUrl;
});
needsSync = true;
}
} else {
if (!batchManager.hosted_url) {
await this.application!.changeItem(batchManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.hosted_url = this.batchMgrUrl;
});
needsSync = true;
}
}
// Handle addition of SN|ExtensionRepo permission
const permissions = Copy(batchManager!.permissions) as ComponentPermission[];
const permission = permissions.find((p) => {
return p.name === ComponentAction.StreamItems
});
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
permission.content_types!.push(ContentType.ExtensionRepo);
await this.application!.changeItem(batchManager.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.permissions = permissions;
});
needsSync = true;
}
if (needsSync) {
this.application!.saveItem(batchManager.uuid);
}
}
batchManagerTemplateContent() {
const url = this.batchMgrUrl;
if (!url) {
throw Error('window._batch_manager_location must be set.');
}
const packageInfo = {
name: 'Batch Manager',
identifier: this.batchManagerId
};
const allContentType = dictToArray(ContentType);
const content = FillItemContent({
name: packageInfo.name,
area: 'modal',
package_info: packageInfo,
permissions: [
{
name: ComponentAction.StreamItems,
content_types: allContentType
}
]
});
if (isDesktopApplication()) {
content.local_url = this.batchMgrUrl;
} else {
content.hosted_url = this.batchMgrUrl;
}
return content;
}
}

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,86 +0,0 @@
import { SFPredicate, SFItem } from 'snjs';
export const PREF_TAGS_PANEL_WIDTH = 'tagsPanelWidth';
export const PREF_NOTES_PANEL_WIDTH = 'notesPanelWidth';
export const PREF_EDITOR_WIDTH = 'editorWidth';
export const PREF_EDITOR_LEFT = 'editorLeft';
export const PREF_EDITOR_MONOSPACE_ENABLED = 'monospaceFont';
export const PREF_EDITOR_SPELLCHECK = 'spellcheck';
export const PREF_EDITOR_RESIZERS_ENABLED = 'marginResizersEnabled';
export const PREF_SORT_NOTES_BY = 'sortBy';
export const PREF_SORT_NOTES_REVERSE = 'sortReverse';
export const PREF_NOTES_SHOW_ARCHIVED = 'showArchived';
export const PREF_NOTES_HIDE_PINNED = 'hidePinned';
export const PREF_NOTES_HIDE_NOTE_PREVIEW = 'hideNotePreview';
export const PREF_NOTES_HIDE_DATE = 'hideDate';
export const PREF_NOTES_HIDE_TAGS = 'hideTags';
export class PreferencesManager {
/* @ngInject */
constructor(
modelManager,
singletonManager,
appState,
syncManager
) {
this.singletonManager = singletonManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.appState = appState;
this.modelManager.addItemSyncObserver(
'user-prefs',
'SN|UserPreferences',
(allItems, validItems, deletedItems, source, sourceKey) => {
this.preferencesDidChange();
}
);
}
load() {
const prefsContentType = 'SN|UserPreferences';
const contentTypePredicate = new SFPredicate(
'content_type',
'=',
prefsContentType
);
this.singletonManager.registerSingleton(
[contentTypePredicate],
(resolvedSingleton) => {
this.userPreferences = resolvedSingleton;
},
(valueCallback) => {
// Safe to create. Create and return object.
const prefs = new SFItem({content_type: prefsContentType});
this.modelManager.addItem(prefs);
this.modelManager.setItemDirty(prefs);
this.syncManager.sync();
valueCallback(prefs);
}
);
}
preferencesDidChange() {
this.appState.setUserPreferences(this.userPreferences);
}
syncUserPreferences() {
if(this.userPreferences) {
this.modelManager.setItemDirty(this.userPreferences);
this.syncManager.sync();
}
}
getValue(key, defaultValue) {
if(!this.userPreferences) { return defaultValue; }
const value = this.userPreferences.getAppDataItem(key);
return (value !== undefined && value != null) ? value : defaultValue;
}
setUserPrefValue(key, value, sync) {
this.userPreferences.setAppDataItem(key, value);
if(sync) {
this.syncUserPreferences();
}
}
}

View File

@@ -0,0 +1,83 @@
import { WebApplication } from '@/ui_models/application';
import {
SNPredicate,
ContentType,
ApplicationService,
SNUserPrefs,
WebPrefKey,
UserPrefsMutator,
FillItemContent
} from 'snjs';
export class PreferencesManager extends ApplicationService {
private userPreferences!: SNUserPrefs
private loadingPrefs = false;
/** @override */
async onAppLaunch() {
super.onAppLaunch();
this.reloadSingleton();
this.streamPreferences();
}
get webApplication() {
return this.application as WebApplication;
}
streamPreferences() {
this.application!.streamItems(
ContentType.UserPrefs,
() => {
this.reloadSingleton();
}
);
}
private async reloadSingleton() {
if(this.loadingPrefs) {
return;
}
this.loadingPrefs = true;
const contentType = ContentType.UserPrefs;
const predicate = new SNPredicate('content_type', '=', contentType);
const previousRef = this.userPreferences;
this.userPreferences = (await this.application!.singletonManager!.findOrCreateSingleton(
predicate,
contentType,
FillItemContent({})
)) as SNUserPrefs;
this.loadingPrefs = false;
const didChange = !previousRef || (
this.userPreferences.lastSyncBegan?.getTime() !== previousRef?.lastSyncBegan?.getTime()
)
if (didChange) {
this.webApplication.getAppState().setUserPreferences(this.userPreferences);
}
}
syncUserPreferences() {
if (this.userPreferences) {
this.application!.saveItem(this.userPreferences.uuid);
}
}
getValue(key: WebPrefKey, defaultValue?: any) {
if (!this.userPreferences) { return defaultValue; }
const value = this.userPreferences.getPref(key);
return (value !== undefined && value !== null) ? value : defaultValue;
}
async setUserPrefValue(key: WebPrefKey, value: any, sync = false) {
await this.application!.changeItem(
this.userPreferences.uuid,
(m) => {
const mutator = m as UserPrefsMutator;
mutator.setWebPref(key, value);
}
)
if (sync) {
this.syncUserPreferences();
}
}
}

View File

@@ -1,81 +0,0 @@
import angular from 'angular';
import { SFPrivilegesManager } from 'snjs';
export class PrivilegesManager extends SFPrivilegesManager {
/* @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 presentPrivilegesModal(action, onSuccess, onCancel) {
if (this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
const customSuccess = async () => {
onSuccess && await onSuccess();
this.currentAuthenticationElement = null;
};
const customCancel = async () => {
onCancel && await onCancel();
this.currentAuthenticationElement = null;
};
const scope = this.$rootScope.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
const 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;
}
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;
}
}

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,46 +1,43 @@
import _ from 'lodash';
import { removeFromArray } from 'snjs';
import { FooterStatus } from '@/types';
type StatusCallback = (string: string) => void
export class StatusManager {
constructor() {
this.statuses = [];
this.observers = [];
}
statusFromString(string) {
private statuses: FooterStatus[] = []
private observers: StatusCallback[] = []
statusFromString(string: string) {
return {string: string};
}
replaceStatusWithString(status, string) {
replaceStatusWithString(status: FooterStatus, string: string) {
this.removeStatus(status);
return this.addStatusFromString(string);
}
addStatusFromString(string) {
addStatusFromString(string: string) {
return this.addStatus(this.statusFromString(string));
}
addStatus(status) {
if(typeof status !== "object") {
console.error("Attempting to set non-object status", status);
return;
}
addStatus(status: FooterStatus) {
this.statuses.push(status);
this.notifyObservers();
return status;
}
removeStatus(status) {
_.pull(this.statuses, status);
removeStatus(status: FooterStatus) {
removeFromArray(this.statuses, status);
this.notifyObservers();
return null;
return undefined;
}
getStatusString() {
let result = "";
let result = '';
this.statuses.forEach((status, index) => {
if(index > 0) {
result += " ";
result += ' ';
}
result += status.string;
});
@@ -54,11 +51,10 @@ export class StatusManager {
}
}
addStatusObserver(callback) {
addStatusObserver(callback: StatusCallback) {
this.observers.push(callback);
}
removeStatusObserver(callback) {
_.pull(this.statuses, callback);
return () => {
removeFromArray(this.observers, callback);
}
}
}

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

@@ -1,150 +0,0 @@
import _ from 'lodash';
import angular from 'angular';
import { SNTheme, SFItemParams } from 'snjs';
import { StorageManager } from './storageManager';
import {
APP_STATE_EVENT_DESKTOP_EXTS_READY
} from '@/state';
export class ThemeManager {
/* @ngInject */
constructor(
componentManager,
desktopManager,
storageManager,
passcodeManager,
appState
) {
this.componentManager = componentManager;
this.storageManager = storageManager;
this.desktopManager = desktopManager;
this.activeThemes = [];
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();
});
if (desktopManager.isDesktop) {
appState.addObserver((eventName, data) => {
if (eventName === APP_STATE_EVENT_DESKTOP_EXTS_READY) {
this.activateCachedThemes();
}
});
} else {
this.activateCachedThemes();
}
}
activateCachedThemes() {
const cachedThemes = this.getCachedThemes();
const writeToCache = false;
for (const theme of cachedThemes) {
this.activateTheme(theme, writeToCache);
}
}
registerObservers() {
this.desktopManager.registerUpdateObserver((component) => {
// Reload theme if active
if (component.active && component.isTheme()) {
this.deactivateTheme(component);
setTimeout(() => {
this.activateTheme(component);
}, 10);
}
});
this.componentManager.registerHandler({
identifier: "themeManager",
areas: ["themes"],
activationHandler: (component) => {
if (component.active) {
this.activateTheme(component);
} else {
this.deactivateTheme(component);
}
}
});
}
hasActiveTheme() {
return this.componentManager.getActiveThemes().length > 0;
}
deactivateAllThemes() {
var activeThemes = this.componentManager.getActiveThemes();
for (var theme of activeThemes) {
if (theme) {
this.componentManager.deactivateComponent(theme);
}
}
this.decacheThemes();
}
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;
link.type = "text/css";
link.rel = "stylesheet";
link.media = "screen,print";
link.id = theme.uuid;
document.getElementsByTagName("head")[0].appendChild(link);
if (writeToCache) {
this.cacheThemes();
}
}
deactivateTheme(theme) {
var element = document.getElementById(theme.uuid);
if (element) {
element.disabled = true;
element.parentNode.removeChild(element);
}
_.remove(this.activeThemes, { uuid: theme.uuid });
this.cacheThemes();
}
async cacheThemes() {
const mapped = await Promise.all(this.activeThemes.map(async (theme) => {
const transformer = new SFItemParams(theme);
const params = await transformer.paramsForLocalStorage();
return params;
}));
const data = JSON.stringify(mapped);
return this.storageManager.setItem(ThemeManager.CachedThemesKey, data, StorageManager.Fixed);
}
async decacheThemes() {
return this.storageManager.removeItem(ThemeManager.CachedThemesKey, StorageManager.Fixed);
}
getCachedThemes() {
const cachedThemes = this.storageManager.getItemSync(ThemeManager.CachedThemesKey, StorageManager.Fixed);
if (cachedThemes) {
const parsed = JSON.parse(cachedThemes);
return parsed.map((theme) => {
return new SNTheme(theme);
});
} else {
return [];
}
}
}

View File

@@ -0,0 +1,179 @@
import { WebApplication } from '@/ui_models/application';
import _ from 'lodash';
import {
StorageValueModes,
EncryptionIntent,
ApplicationService,
SNTheme,
ComponentArea,
removeFromArray,
ApplicationEvent
} from 'snjs';
import { AppStateEvent } from '@/ui_models/app_state';
const CACHED_THEMES_KEY = 'cachedThemes';
export class ThemeManager extends ApplicationService {
private activeThemes: string[] = []
private unsubState?: () => void
private unregisterDesktop!: () => void
private unregisterComponent!: () => void
/** @override */
async onAppLaunch() {
super.onAppLaunch();
this.unsubState = this.webApplication.getAppState().addObserver(
async (eventName) => {
if (eventName === AppStateEvent.DesktopExtsReady) {
this.activateCachedThemes();
}
}
);
}
onAppEvent(event: ApplicationEvent) {
super.onAppEvent(event);
if (event === ApplicationEvent.SignedOut) {
this.deactivateAllThemes();
}
}
get webApplication() {
return this.application as WebApplication;
}
deinit() {
this.unsubState?.();
(this.unsubState as any) = undefined;
this.activeThemes.length = 0;
this.unregisterDesktop();
this.unregisterComponent();
(this.unregisterDesktop as any) = undefined;
(this.unregisterComponent as any) = undefined;
super.deinit();
}
/** @override */
async onAppStart() {
super.onAppStart();
this.registerObservers();
if (!this.webApplication.getDesktopService().isDesktop) {
this.activateCachedThemes();
}
}
private async activateCachedThemes() {
const cachedThemes = await this.getCachedThemes();
const writeToCache = false;
for (const theme of cachedThemes) {
this.activateTheme(theme, writeToCache);
}
}
private registerObservers() {
this.unregisterDesktop = this.webApplication.getDesktopService()
.registerUpdateObserver((component) => {
if (component.active && component.isTheme()) {
this.deactivateTheme(component.uuid);
setTimeout(() => {
this.activateTheme(component as SNTheme);
}, 10);
}
});
this.unregisterComponent = this.application!.componentManager!.registerHandler({
identifier: 'themeManager',
areas: [ComponentArea.Themes],
activationHandler: (uuid, component) => {
if (component?.active) {
this.activateTheme(component as SNTheme);
} else {
this.deactivateTheme(uuid);
}
}
});
}
private deactivateAllThemes() {
for (const uuid of this.activeThemes) {
this.deactivateTheme(uuid, false);
}
this.activeThemes = [];
this.decacheThemes();
}
private activateTheme(theme: SNTheme, writeToCache = true) {
if (this.activeThemes.find((uuid) => uuid === theme.uuid)) {
return;
}
this.activeThemes.push(theme.uuid);
const url = this.application!.componentManager!.urlForComponent(theme)!;
const link = document.createElement('link');
link.href = url;
link.type = 'text/css';
link.rel = 'stylesheet';
link.media = 'screen,print';
link.id = theme.uuid;
document.getElementsByTagName('head')[0].appendChild(link);
if (writeToCache) {
this.cacheThemes();
}
}
private deactivateTheme(uuid: string, recache = true) {
const element = document.getElementById(uuid) as HTMLLinkElement;
if (element) {
element.disabled = true;
element.parentNode!.removeChild(element);
}
removeFromArray(this.activeThemes, uuid);
if (recache) {
this.cacheThemes();
}
}
private async cacheThemes() {
const themes = this.application!.getAll(this.activeThemes) as SNTheme[];
const mapped = await Promise.all(themes.map(async (theme) => {
const payload = theme.payloadRepresentation();
const processedPayload = await this.application!.protocolService!.payloadByEncryptingPayload(
payload,
EncryptionIntent.LocalStorageDecrypted
);
return processedPayload;
}));
return this.application!.setValue(
CACHED_THEMES_KEY,
mapped,
StorageValueModes.Nonwrapped
);
}
private async decacheThemes() {
if (this.application) {
return this.application.removeValue(
CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped
);
}
}
private async getCachedThemes() {
const cachedThemes = await this.application!.getValue(
CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped
);
if (cachedThemes) {
const themes = [];
for (const cachedTheme of cachedThemes) {
const payload = this.application!.createPayloadFromObject(cachedTheme);
const theme = this.application!.createItemFromPayload(payload) as SNTheme;
themes.push(theme);
}
return themes;
} else {
return [];
}
}
}