This commit is contained in:
Mo Bitar
2020-02-05 17:00:11 -06:00
parent 1ae3790ea3
commit 13dd41b9ab
10 changed files with 479 additions and 4963 deletions

View File

@@ -7,10 +7,12 @@ class ActionsMenuCtrl extends PureCtrl {
$scope,
$timeout,
actionsManager,
godService
) {
super($timeout);
this.$timeout = $timeout;
this.actionsManager = actionsManager;
this.godService = godService;
}
$onInit() {
@@ -64,7 +66,7 @@ class ActionsMenuCtrl extends PureCtrl {
switch (action.verb) {
case 'render': {
const item = result.item;
this.actionsManager.presentRevisionPreviewModal(
this.godService.presentRevisionPreviewModal(
item.uuid,
item.content
);

View File

@@ -1,4 +1,8 @@
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
import {
PAYLOAD_SOURCE_REMOTE_ACTION_RETRIEVED,
CONTENT_TYPE_NOTE,
CONTENT_TYPE_COMPONENT
} from 'snjs';
import template from '%/directives/revision-preview-modal.pug';
class RevisionPreviewModalCtrl {
@@ -6,44 +10,40 @@ class RevisionPreviewModalCtrl {
constructor(
$element,
$scope,
$timeout,
syncManager,
$timeout
) {
this.$element = $element;
this.$scope = $scope;
this.$timeout = $timeout;
this.syncManager = syncManager;
this.createNote();
this.configureEditor();
this.configure();
$scope.$on('$destroy', () => {
if (this.identifier) {
this.application.componentManager.deregisterHandler(this.identifier);
}
});
}
createNote() {
this.note = new SFItem({
content: this.content,
content_type: "Note"
async configure() {
this.note = await this.application.createItem({
contentType: CONTENT_TYPE_NOTE,
content: this.content
});
}
configureEditor() {
/**
* Set UUID so editoForNote can find proper editor, but then generate new uuid
* for note as not to save changes to original, if editor makes changes.
*/
this.note.uuid = this.uuid;
const editorForNote = this.application.componentManager.editorForNote(this.note);
this.note.uuid = protocolManager.crypto.generateUUIDSync();
this.note.uuid = await this.application.generateUuid();
if (editorForNote) {
/**
* Create temporary copy, as a lot of componentManager is uuid based, so might
* interfere with active editor. Be sure to copy only the content, as the top level
* editor object has non-copyable properties like .window, which cannot be transfered
*/
const editorCopy = new SNComponent({
const editorCopy = await this.application.createItem({
contentType: CONTENT_TYPE_COMPONENT,
content: editorForNote.content
});
editorCopy.readonly = true;
@@ -85,13 +85,12 @@ class RevisionPreviewModalCtrl {
const uuid = this.uuid;
item = this.application.findItem({uuid: uuid});
item.content = Object.assign({}, this.content);
this.modelManager.mapResponseItemsToLocalModels(
[item],
SFModelManager.MappingSourceRemoteActionRetrieved
);
await this.application.mergeItem({
item: item,
source: PAYLOAD_SOURCE_REMOTE_ACTION_RETRIEVED
});
}
this.modelManager.setItemDirty(item);
this.syncManager.sync();
this.application.saveItem({item});
this.dismiss();
};

View File

@@ -4,16 +4,14 @@ class SessionHistoryMenuCtrl {
/* @ngInject */
constructor(
$timeout,
actionsManager,
alertManager,
sessionHistory,
godService,
application,
) {
this.$timeout = $timeout;
this.alertManager = alertManager;
this.actionsManager = actionsManager;
this.sessionHistory = sessionHistory;
this.diskEnabled = this.sessionHistory.diskEnabled;
this.autoOptimize = this.sessionHistory.autoOptimize;
this.godService = godService;
this.application = application;
this.diskEnabled = this.application.historyManager.isDiskEnabled();
this.autoOptimize = this.application.historyManager.isAutoOptimizeEnabled();
}
$onInit() {
@@ -21,7 +19,7 @@ class SessionHistoryMenuCtrl {
}
reloadHistory() {
const history = this.sessionHistory.historyForItem(this.item);
const history = this.application.historyManager.historyForItem(this.item);
this.entries = history.entries.slice(0).sort((a, b) => {
return a.item.updated_at < b.item.updated_at ? 1 : -1;
});
@@ -29,7 +27,7 @@ class SessionHistoryMenuCtrl {
}
openRevision(revision) {
this.actionsManager.presentRevisionPreviewModal(
this.godService.presentRevisionPreviewModal(
revision.item.uuid,
revision.item.content
);
@@ -47,11 +45,11 @@ class SessionHistoryMenuCtrl {
}
clearItemHistory() {
this.alertManager.confirm({
this.application.alertManager.confirm({
text: "Are you sure you want to delete the local session history for this note?",
destructive: true,
onConfirm: () => {
this.sessionHistory.clearHistoryForItem(this.item).then(() => {
this.application.historyManager.clearHistoryForItem(this.item).then(() => {
this.$timeout(() => {
this.reloadHistory();
});
@@ -61,11 +59,11 @@ class SessionHistoryMenuCtrl {
}
clearAllHistory() {
this.alertManager.confirm({
this.application.alertManager.confirm({
text: "Are you sure you want to delete the local session history for all notes?",
destructive: true,
onConfirm: () => {
this.sessionHistory.clearAllHistory().then(() => {
this.application.historyManager.clearAllHistory().then(() => {
this.$timeout(() => {
this.reloadHistory();
});
@@ -76,14 +74,14 @@ class SessionHistoryMenuCtrl {
toggleDiskSaving() {
const run = () => {
this.sessionHistory.toggleDiskSaving().then(() => {
this.application.historyManager.toggleDiskSaving().then(() => {
this.$timeout(() => {
this.diskEnabled = this.sessionHistory.diskEnabled;
this.diskEnabled = this.application.historyManager.diskEnabled;
});
});
};
if (!this.sessionHistory.diskEnabled) {
this.alertManager.confirm({
if (!this.application.historyManager.diskEnabled) {
this.application.alertManager.confirm({
text: `Are you sure you want to save history to disk? This will decrease general
performance, especially as you type. You are advised to disable this feature
if you experience any lagging.`,
@@ -96,9 +94,9 @@ class SessionHistoryMenuCtrl {
}
toggleAutoOptimize() {
this.sessionHistory.toggleAutoOptimize().then(() => {
this.application.historyManager.toggleAutoOptimize().then(() => {
this.$timeout(() => {
this.autoOptimize = this.sessionHistory.autoOptimize;
this.autoOptimize = this.application.historyManager.autoOptimize;
});
});
}

View File

@@ -5,11 +5,11 @@ class SyncResolutionMenuCtrl {
constructor(
$timeout,
archiveManager,
syncManager,
application
) {
this.$timeout = $timeout;
this.archiveManager = archiveManager;
this.syncManager = syncManager;
this.application = application;
this.status = {};
}
@@ -24,11 +24,11 @@ class SyncResolutionMenuCtrl {
async performSyncResolution() {
this.status.resolving = true;
await this.syncManager.resolveOutOfSync();
await this.application.resolveOutOfSync();
this.$timeout(() => {
this.status.resolving = false;
this.status.attemptedResolution = true;
if (this.syncManager.isOutOfSync()) {
if (this.application.getSyncStatus().isOutOfSync()) {
this.status.fail = true;
} else {
this.status.success = true;

View File

@@ -1,20 +0,0 @@
import { SFItemHistoryEntry } from 'snjs';
export class NoteHistoryEntry extends SFItemHistoryEntry {
previewTitle() {
return this.item.updated_at.toLocaleString();
}
previewSubTitle() {
if(!this.hasPreviousEntry) {
return `${this.textCharDiffLength} characters loaded`;
} else if(this.textCharDiffLength < 0) {
return `${this.textCharDiffLength * -1} characters removed`;
} else if(this.textCharDiffLength > 0) {
return `${this.textCharDiffLength} characters added`;
} else {
return "Title or metadata changed";
}
}
}

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

@@ -5,10 +5,12 @@ export class GodService {
/* @ngInject */
constructor(
$rootScope,
$compile
$compile,
application
) {
this.$rootScope = $rootScope;
this.$compile = $compile;
this.application = application;
}
async checkForSecurityUpdate() {
@@ -16,7 +18,7 @@ export class GodService {
return false;
}
const latest = protocolManager.version();
const latest = await this.application.getUserVersion();
const updateAvailable = await this.protocolVersion() !== latest;
if (updateAvailable !== this.securityUpdateAvailable) {
this.securityUpdateAvailable = updateAvailable;
@@ -70,4 +72,29 @@ export class GodService {
authenticationInProgress() {
return this.currentAuthenticationElement != null;
}
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);
}
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);
}
}

View File

@@ -29,6 +29,12 @@ export class WebDeviceInterface extends DeviceInterface {
localStorage.clear();
}
openUrl(url) {
const win = window.open(url, '_blank');
if (win) {
win.focus();
}
}
/** @database */

4995
dist/javascripts/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long