Merge 2.1
This commit is contained in:
159
app/assets/javascripts/app/services/actionsManager.js
Normal file
159
app/assets/javascripts/app/services/actionsManager.js
Normal file
@@ -0,0 +1,159 @@
|
||||
class ActionsManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.extensions;
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter(function(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.
|
||||
*/
|
||||
loadExtensionInContextOfItem(extension, item, callback) {
|
||||
this.httpManager.getAbsolute(extension.url, {content_type: item.content_type, item_uuid: item.uuid}, function(response){
|
||||
this.updateExtensionFromRemoteResponse(extension, response);
|
||||
callback && callback(extension);
|
||||
}.bind(this), function(response){
|
||||
console.log("Error loading extension", response);
|
||||
if(callback) {
|
||||
callback(null);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
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(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
} else {
|
||||
extension.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
executeAction(action, extension, item, callback) {
|
||||
|
||||
var customCallback = function(response) {
|
||||
action.running = false;
|
||||
callback(response);
|
||||
}
|
||||
|
||||
action.running = true;
|
||||
|
||||
let decrypted = action.access_type == "decrypted";
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
var items = response.items || [response.item];
|
||||
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
customCallback({items: items});
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
EncryptionHelper.decryptItem(response.item, this.authManager.keys());
|
||||
var item = this.modelManager.createItem(response.item, true /* Dont notify observers */);
|
||||
customCallback({item: item});
|
||||
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "show": {
|
||||
var win = window.open(action.url, '_blank');
|
||||
win.focus();
|
||||
customCallback();
|
||||
break;
|
||||
}
|
||||
|
||||
case "post": {
|
||||
var params = {};
|
||||
|
||||
if(action.all) {
|
||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
||||
params.items = items.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension, decrypted);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
params.items = [this.outgoingParamsForItem(item, extension, decrypted)];
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
customCallback(response);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
var keys = this.authManager.keys();
|
||||
if(decrypted) {
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, keys, this.authManager.protocolVersion());
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
performPost(action, extension, params, callback) {
|
||||
this.httpManager.postAbsolute(action.url, params, function(response){
|
||||
action.error = false;
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
console.log("Action error response:", response);
|
||||
if(callback) {
|
||||
callback({error: "Request error"});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('actionsManager', ActionsManager);
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.provider('authManager', function () {
|
||||
|
||||
function domainName() {
|
||||
@@ -7,11 +7,11 @@ angular.module('app.frontend')
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager);
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager);
|
||||
}
|
||||
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
|
||||
|
||||
this.loadInitialData = function() {
|
||||
var userData = storageManager.getItem("user");
|
||||
@@ -43,11 +43,10 @@ angular.module('app.frontend')
|
||||
this.ephemeral = ephemeral;
|
||||
if(ephemeral) {
|
||||
storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Ephemeral);
|
||||
storageManager.setItemsMode(StorageManager.Ephemeral);
|
||||
} else {
|
||||
storageManager.setModelStorageMode(StorageManager.Fixed);
|
||||
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
|
||||
|
||||
storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
@@ -95,9 +94,9 @@ angular.module('app.frontend')
|
||||
return supportedVersions.includes(version);
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(url, email, callback) {
|
||||
this.getAuthParamsForEmail = function(url, email, extraParams, callback) {
|
||||
var requestUrl = url + "/auth/params";
|
||||
httpManager.getAbsolute(requestUrl, {email: email}, function(response){
|
||||
httpManager.getAbsolute(requestUrl, _.merge({email: email}, extraParams), function(response){
|
||||
callback(response);
|
||||
}, function(response){
|
||||
console.error("Error getting auth params", response);
|
||||
@@ -120,8 +119,8 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.login = function(url, email, password, ephemeral, callback) {
|
||||
this.getAuthParamsForEmail(url, email, function(authParams){
|
||||
this.login = function(url, email, password, ephemeral, extraParams, callback) {
|
||||
this.getAuthParamsForEmail(url, email, extraParams, function(authParams){
|
||||
|
||||
if(authParams.error) {
|
||||
callback(authParams);
|
||||
@@ -134,31 +133,30 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
if(!this.isProtocolVersionSupported(authParams.version)) {
|
||||
alert("The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security-update for more information.");
|
||||
callback({didDisplayAlert: true});
|
||||
let message = "The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security-update for more information.";
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||
var string = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||
let message = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||
"If we attempted to generate your login keys here, it would take hours. " +
|
||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to login."
|
||||
alert(string)
|
||||
callback({didDisplayAlert: true});
|
||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in."
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
var minimum = this.costMinimumForVersion(authParams.version);
|
||||
if(authParams.pw_cost < minimum) {
|
||||
alert("Unable to login due to insecure password parameters. Please visit standardnotes.org/help/password-upgrade for more information.");
|
||||
callback({didDisplayAlert: true});
|
||||
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/password-upgrade for more information.";
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
|
||||
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = {password: keys.pw, email: email};
|
||||
var params = _.merge({password: keys.pw, email: email}, extraParams);
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
this.setEphemeral(ephemeral);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
@@ -291,5 +289,45 @@ angular.module('app.frontend')
|
||||
this._authParams = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* User Preferences */
|
||||
|
||||
let prefsContentType = "SN|UserPreferences";
|
||||
|
||||
singletonManager.registerSingleton({content_type: prefsContentType}, (resolvedSingleton) => {
|
||||
this.userPreferences = resolvedSingleton;
|
||||
this.userPreferencesDidChange();
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
var prefs = new Item({content_type: prefsContentType});
|
||||
modelManager.addItem(prefs);
|
||||
prefs.setDirty(true);
|
||||
$rootScope.sync("authManager singletonCreate");
|
||||
valueCallback(prefs);
|
||||
});
|
||||
|
||||
this.userPreferencesDidChange = function() {
|
||||
$rootScope.$broadcast("user-preferences-changed");
|
||||
}
|
||||
|
||||
this.syncUserPreferences = function() {
|
||||
this.userPreferences.setDirty(true);
|
||||
$rootScope.sync("syncUserPreferences");
|
||||
}
|
||||
|
||||
this.getUserPrefValue = function(key, defaultValue) {
|
||||
if(!this.userPreferences) { return defaultValue; }
|
||||
var value = this.userPreferences.getAppDataItem(key);
|
||||
return (value !== undefined && value != null) ? value : defaultValue;
|
||||
}
|
||||
|
||||
this.setUserPrefValue = function(key, value, sync) {
|
||||
if(!this.userPreferences) { console.log("Prefs are null, not setting value", key); return; }
|
||||
this.userPreferences.setAppDataItem(key, value);
|
||||
if(sync) {
|
||||
this.syncUserPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,27 +3,45 @@ let ClientDataDomain = "org.standardnotes.sn.components";
|
||||
|
||||
class ComponentManager {
|
||||
|
||||
constructor($rootScope, modelManager, syncManager, themeManager, $timeout, $compile) {
|
||||
constructor($rootScope, modelManager, syncManager, desktopManager, nativeExtManager, $timeout, $compile) {
|
||||
this.$compile = $compile;
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.themeManager = themeManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.nativeExtManager = nativeExtManager;
|
||||
this.timeout = $timeout;
|
||||
this.streamObservers = [];
|
||||
this.contextStreamObservers = [];
|
||||
this.activeComponents = [];
|
||||
|
||||
const detectFocusChange = (event) => {
|
||||
for(var component of this.activeComponents) {
|
||||
if(document.activeElement == this.iframeForComponent(component)) {
|
||||
this.timeout(() => {
|
||||
this.focusChangedForComponent(component);
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener ? window.addEventListener('focus', detectFocusChange, true) : window.attachEvent('onfocusout', detectFocusChange);
|
||||
window.addEventListener ? window.addEventListener('blur', detectFocusChange, true) : window.attachEvent('onblur', detectFocusChange);
|
||||
|
||||
desktopManager.registerUpdateObserver((component) => {
|
||||
// Reload theme if active
|
||||
if(component.active && component.isTheme()) {
|
||||
this.postActiveThemeToAllComponents();
|
||||
}
|
||||
})
|
||||
|
||||
// this.loggingEnabled = true;
|
||||
|
||||
this.permissionDialogs = [];
|
||||
|
||||
this.handlers = [];
|
||||
|
||||
$rootScope.$on("theme-changed", function(){
|
||||
this.postThemeToComponents();
|
||||
}.bind(this))
|
||||
|
||||
window.addEventListener("message", function(event){
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Web app: received message", event);
|
||||
@@ -31,7 +49,7 @@ class ComponentManager {
|
||||
this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data);
|
||||
}.bind(this), false);
|
||||
|
||||
this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems, source) {
|
||||
this.modelManager.addItemSyncObserver("component-manager", "*", (allItems, validItems, deletedItems, source) => {
|
||||
|
||||
/* If the source of these new or updated items is from a Component itself saving items, we don't need to notify
|
||||
components again of the same item. Regarding notifying other components than the issuing component, other mapping sources
|
||||
@@ -41,7 +59,18 @@ class ComponentManager {
|
||||
return;
|
||||
}
|
||||
|
||||
var syncedComponents = allItems.filter(function(item){return item.content_type === "SN|Component" });
|
||||
var syncedComponents = allItems.filter(function(item) {
|
||||
return item.content_type === "SN|Component" || item.content_type == "SN|Theme"
|
||||
});
|
||||
|
||||
/* We only want to sync if the item source is Retrieved, not MappingSourceRemoteSaved to avoid
|
||||
recursion caused by the component being modified and saved after it is updated.
|
||||
*/
|
||||
if(syncedComponents.length > 0 && source != ModelManager.MappingSourceRemoteSaved) {
|
||||
// Ensure any component in our data is installed by the system
|
||||
this.desktopManager.syncComponentsInstallation(syncedComponents);
|
||||
}
|
||||
|
||||
for(var component of syncedComponents) {
|
||||
var activeComponent = _.find(this.activeComponents, {uuid: component.uuid});
|
||||
if(component.active && !component.deleted && !activeComponent) {
|
||||
@@ -56,6 +85,10 @@ class ComponentManager {
|
||||
return observer.contentTypes.indexOf(item.content_type) !== -1;
|
||||
})
|
||||
|
||||
if(relevantItems.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
@@ -63,9 +96,9 @@ class ComponentManager {
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(observer.component, requiredPermissions, observer.originalMessage.permissions, function(){
|
||||
this.runWithPermissions(observer.component, requiredPermissions, () => {
|
||||
this.sendItemsInReply(observer.component, relevantItems, observer.originalMessage);
|
||||
}.bind(this))
|
||||
})
|
||||
}
|
||||
|
||||
var requiredContextPermissions = [
|
||||
@@ -75,36 +108,45 @@ class ComponentManager {
|
||||
];
|
||||
|
||||
for(let observer of this.contextStreamObservers) {
|
||||
this.runWithPermissions(observer.component, requiredContextPermissions, observer.originalMessage.permissions, function(){
|
||||
for(let handler of this.handlers) {
|
||||
if(!handler.areas.includes(observer.component.area)) {
|
||||
continue;
|
||||
}
|
||||
for(let handler of this.handlers) {
|
||||
if(!handler.areas.includes(observer.component.area) && !handler.areas.includes("*")) {
|
||||
continue;
|
||||
}
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(observer.component);
|
||||
if(itemInContext) {
|
||||
var matchingItem = _.find(allItems, {uuid: itemInContext.uuid});
|
||||
if(matchingItem) {
|
||||
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source);
|
||||
this.runWithPermissions(observer.component, requiredContextPermissions, () => {
|
||||
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
});
|
||||
}
|
||||
|
||||
postThemeToComponents() {
|
||||
postActiveThemeToAllComponents() {
|
||||
for(var component of this.components) {
|
||||
if(!component.active || !component.window) {
|
||||
// Skip over components that are themes themselves,
|
||||
// or components that are not active, or components that don't have a window
|
||||
if(component.isTheme() || !component.active || !component.window) {
|
||||
continue;
|
||||
}
|
||||
this.postThemeToComponent(component);
|
||||
this.postActiveThemeToComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
postThemeToComponent(component) {
|
||||
getActiveTheme() {
|
||||
return this.componentsForArea("themes").find((theme) => {return theme.active});
|
||||
}
|
||||
|
||||
postActiveThemeToComponent(component) {
|
||||
var activeTheme = this.getActiveTheme();
|
||||
var data = {
|
||||
themes: [this.themeManager.currentTheme ? this.themeManager.currentTheme.url : null]
|
||||
themes: [activeTheme ? this.urlForComponent(activeTheme) : null]
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, {action: "themes", data: data})
|
||||
@@ -112,7 +154,7 @@ class ComponentManager {
|
||||
|
||||
contextItemDidChangeInArea(area) {
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(area) === false) {
|
||||
if(handler.areas.includes(area) === false && !handler.areas.includes("*")) {
|
||||
continue;
|
||||
}
|
||||
var observers = this.contextStreamObservers.filter(function(observer){
|
||||
@@ -120,8 +162,10 @@ class ComponentManager {
|
||||
})
|
||||
|
||||
for(let observer of observers) {
|
||||
var itemInContext = handler.contextRequestHandler(observer.component);
|
||||
this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage);
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(observer.component);
|
||||
this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +173,8 @@ class ComponentManager {
|
||||
jsonForItem(item, component, source) {
|
||||
var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted};
|
||||
params.content = item.createContentJSONFromProperties();
|
||||
params.clientData = item.getDomainDataItem(component.url, ClientDataDomain) || {};
|
||||
/* Legacy is using component.url key, so if it's present, use it, otherwise use uuid */
|
||||
params.clientData = item.getDomainDataItem(component.url || component.uuid, ClientDataDomain) || {};
|
||||
|
||||
/* This means the this function is being triggered through a remote Saving response, which should not update
|
||||
actual local content values. The reason is, Save responses may be delayed, and a user may have changed some values
|
||||
@@ -139,7 +184,7 @@ class ComponentManager {
|
||||
if(source && source == ModelManager.MappingSourceRemoteSaved) {
|
||||
params.isMetadataUpdate = true;
|
||||
}
|
||||
this.removePrivatePropertiesFromResponseItems([params]);
|
||||
this.removePrivatePropertiesFromResponseItems([params], component);
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -160,8 +205,33 @@ class ComponentManager {
|
||||
this.replyToMessage(component, originalMessage, response);
|
||||
}
|
||||
|
||||
replyToMessage(component, originalMessage, replyData) {
|
||||
var reply = {
|
||||
action: "reply",
|
||||
original: originalMessage,
|
||||
data: replyData
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, reply);
|
||||
}
|
||||
|
||||
sendMessageToComponent(component, message) {
|
||||
let permissibleActionsWhileHidden = ["component-registered", "themes"];
|
||||
if(component.hidden && !permissibleActionsWhileHidden.includes(message.action)) {
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Component disabled for current item, not sending any messages.", component.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Web|sendMessageToComponent", component, message);
|
||||
}
|
||||
component.window.postMessage(message, "*");
|
||||
}
|
||||
|
||||
get components() {
|
||||
return this.modelManager.itemsForContentType("SN|Component");
|
||||
return this.modelManager.allItemsMatchingTypes(["SN|Component", "SN|Theme"]);
|
||||
}
|
||||
|
||||
componentsForArea(area) {
|
||||
@@ -170,9 +240,17 @@ class ComponentManager {
|
||||
})
|
||||
}
|
||||
|
||||
urlForComponent(component) {
|
||||
if(component.offlineOnly || (isDesktopApplication() && component.local_url)) {
|
||||
return component.local_url && component.local_url.replace("sn://", this.desktopManager.getApplicationDataPath() + "/");
|
||||
} else {
|
||||
return component.hosted_url || component.url;
|
||||
}
|
||||
}
|
||||
|
||||
componentForUrl(url) {
|
||||
return this.components.filter(function(component){
|
||||
return component.url === url;
|
||||
return component.url === url || component.hosted_url === url;
|
||||
})[0];
|
||||
}
|
||||
|
||||
@@ -191,91 +269,46 @@ class ComponentManager {
|
||||
|
||||
/**
|
||||
Possible Messages:
|
||||
set-size
|
||||
stream-items
|
||||
stream-context-item
|
||||
save-items
|
||||
select-item
|
||||
associate-item
|
||||
deassociate-item
|
||||
clear-selection
|
||||
create-item
|
||||
delete-items
|
||||
set-component-data
|
||||
save-context-client-data
|
||||
get-context-client-data
|
||||
set-size
|
||||
stream-items
|
||||
stream-context-item
|
||||
save-items
|
||||
select-item
|
||||
associate-item
|
||||
deassociate-item
|
||||
clear-selection
|
||||
create-item
|
||||
delete-items
|
||||
set-component-data
|
||||
install-local-component
|
||||
toggle-activate-component
|
||||
request-permissions
|
||||
*/
|
||||
|
||||
if(message.action === "stream-items") {
|
||||
this.handleStreamItemsMessage(component, message);
|
||||
}
|
||||
|
||||
else if(message.action === "stream-context-item") {
|
||||
} else if(message.action === "stream-context-item") {
|
||||
this.handleStreamContextItemMessage(component, message);
|
||||
} else if(message.action === "set-component-data") {
|
||||
this.handleSetComponentDataMessage(component, message);
|
||||
} else if(message.action === "delete-items") {
|
||||
this.handleDeleteItemsMessage(component, message);
|
||||
} else if(message.action === "create-item") {
|
||||
this.handleCreateItemMessage(component, message);
|
||||
} else if(message.action === "save-items") {
|
||||
this.handleSaveItemsMessage(component, message);
|
||||
} else if(message.action === "toggle-activate-component") {
|
||||
let componentToToggle = this.modelManager.findItem(message.data.uuid);
|
||||
this.handleToggleComponentMessage(component, componentToToggle, message);
|
||||
} else if(message.action === "request-permissions") {
|
||||
this.handleRequestPermissionsMessage(component, message);
|
||||
} else if(message.action === "install-local-component") {
|
||||
this.handleInstallLocalComponentMessage(component, message);
|
||||
}
|
||||
|
||||
else if(message.action === "set-component-data") {
|
||||
component.componentData = message.data.componentData;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
else if(message.action === "delete-items") {
|
||||
var items = message.data.items;
|
||||
var noun = items.length == 1 ? "item" : "items";
|
||||
if(confirm(`Are you sure you want to delete ${items.length} ${noun}?`)) {
|
||||
for(var item of items) {
|
||||
var model = this.modelManager.findItem(item.uuid);
|
||||
this.modelManager.setItemToBeDeleted(model);
|
||||
}
|
||||
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
else if(message.action === "create-item") {
|
||||
var responseItem = message.data.item;
|
||||
this.removePrivatePropertiesFromResponseItems([responseItem]);
|
||||
var item = this.modelManager.createItem(responseItem);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
this.modelManager.addItem(item);
|
||||
this.modelManager.resolveReferencesForItem(item);
|
||||
item.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
this.replyToMessage(component, message, {item: this.jsonForItem(item, component)})
|
||||
}
|
||||
|
||||
else if(message.action === "save-items") {
|
||||
var responseItems = message.data.items;
|
||||
|
||||
this.removePrivatePropertiesFromResponseItems(responseItems);
|
||||
|
||||
/*
|
||||
We map the items here because modelManager is what updates the UI. If you were to instead get the items directly,
|
||||
this would update them server side via sync, but would never make its way back to the UI.
|
||||
*/
|
||||
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved);
|
||||
|
||||
for(var item of localItems) {
|
||||
var responseItem = _.find(responseItems, {uuid: item.uuid});
|
||||
_.merge(item.content, responseItem.content);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync((response) => {
|
||||
// Allow handlers to be notified when a save begins and ends, to update the UI
|
||||
var saveMessage = Object.assign({}, message);
|
||||
saveMessage.action = response && response.error ? "save-error" : "save-success";
|
||||
this.handleMessage(component, saveMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Notify observers
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area)) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
this.timeout(function(){
|
||||
handler.actionHandler(component, message.action, message.data);
|
||||
})
|
||||
@@ -283,9 +316,18 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
removePrivatePropertiesFromResponseItems(responseItems) {
|
||||
removePrivatePropertiesFromResponseItems(responseItems, component, options = {}) {
|
||||
if(component) {
|
||||
// System extensions can bypass this step
|
||||
if(this.nativeExtManager.isSystemExtension(component)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Don't allow component to overwrite these properties.
|
||||
let privateProperties = ["appData"];
|
||||
var privateProperties = ["appData", "autoupdateDisabled", "permissions", "active"];
|
||||
if(options) {
|
||||
if(options.includeUrls) { privateProperties = privateProperties.concat(["url", "hosted_url", "local_url"])}
|
||||
}
|
||||
for(var responseItem of responseItems) {
|
||||
|
||||
// Do not pass in actual items here, otherwise that would be destructive.
|
||||
@@ -293,7 +335,7 @@ class ComponentManager {
|
||||
console.assert(typeof responseItem.setDirty !== 'function');
|
||||
|
||||
for(var prop of privateProperties) {
|
||||
delete responseItem[prop];
|
||||
delete responseItem.content[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,25 +348,24 @@ class ComponentManager {
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, message.permissions, function(){
|
||||
if(!_.find(this.streamObservers, {identifier: component.url})) {
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
if(!_.find(this.streamObservers, {identifier: component.uuid})) {
|
||||
// for pushing laster as changes come in
|
||||
this.streamObservers.push({
|
||||
identifier: component.url,
|
||||
identifier: component.uuid,
|
||||
component: component,
|
||||
originalMessage: message,
|
||||
contentTypes: message.data.content_types
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// push immediately now
|
||||
var items = [];
|
||||
for(var contentType of message.data.content_types) {
|
||||
items = items.concat(this.modelManager.itemsForContentType(contentType));
|
||||
}
|
||||
this.sendItemsInReply(component, items, message);
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
handleStreamContextItemMessage(component, message) {
|
||||
@@ -335,55 +376,223 @@ class ComponentManager {
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, message.permissions, function(){
|
||||
if(!_.find(this.contextStreamObservers, {identifier: component.url})) {
|
||||
this.runWithPermissions(component, requiredPermissions, function(){
|
||||
if(!_.find(this.contextStreamObservers, {identifier: component.uuid})) {
|
||||
// for pushing laster as changes come in
|
||||
this.contextStreamObservers.push({
|
||||
identifier: component.url,
|
||||
identifier: component.uuid,
|
||||
component: component,
|
||||
originalMessage: message
|
||||
})
|
||||
}
|
||||
|
||||
// push immediately now
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) === false) {
|
||||
continue;
|
||||
for(let handler of this.handlersForArea(component.area)) {
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
this.sendContextItemInReply(component, itemInContext, message);
|
||||
}
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
this.sendContextItemInReply(component, itemInContext, message);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
runWithPermissions(component, requiredPermissions, requestedPermissions, runFunction) {
|
||||
|
||||
var acquiredPermissions = component.permissions;
|
||||
|
||||
var requestedMatchesRequired = true;
|
||||
|
||||
for(var required of requiredPermissions) {
|
||||
var matching = _.find(requestedPermissions, required);
|
||||
if(!matching) {
|
||||
requestedMatchesRequired = false;
|
||||
break;
|
||||
isItemWithinComponentContextJurisdiction(item, component) {
|
||||
for(let handler of this.handlersForArea(component.area)) {
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
if(itemInContext && itemInContext.uuid == item.uuid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!requestedMatchesRequired) {
|
||||
// Error with Component permissions request
|
||||
console.error("You are requesting permissions", requestedPermissions, "when you need to be requesting", requiredPermissions, ". Component:", component);
|
||||
handlersForArea(area) {
|
||||
return this.handlers.filter((candidate) => {return candidate.areas.includes(area)});
|
||||
}
|
||||
|
||||
handleSaveItemsMessage(component, message) {
|
||||
var responseItems = message.data.items;
|
||||
var requiredPermissions;
|
||||
|
||||
// Check if you're just trying to save the context item, which requires only stream-context-item permissions
|
||||
if(responseItems.length == 1 && this.isItemWithinComponentContextJurisdiction(responseItems[0], component)) {
|
||||
requiredPermissions = [
|
||||
{
|
||||
name: "stream-context-item"
|
||||
}
|
||||
];
|
||||
} else {
|
||||
var requiredContentTypes = _.uniq(responseItems.map((i) => {return i.content_type})).sort();
|
||||
requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: requiredContentTypes
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
|
||||
this.removePrivatePropertiesFromResponseItems(responseItems, component, {includeUrls: true});
|
||||
|
||||
/*
|
||||
We map the items here because modelManager is what updates the UI. If you were to instead get the items directly,
|
||||
this would update them server side via sync, but would never make its way back to the UI.
|
||||
*/
|
||||
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved);
|
||||
|
||||
for(var item of localItems) {
|
||||
var responseItem = _.find(responseItems, {uuid: item.uuid});
|
||||
_.merge(item.content, responseItem.content);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url || component.uuid, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
item.setDirty(true);
|
||||
}
|
||||
|
||||
this.syncManager.sync((response) => {
|
||||
// Allow handlers to be notified when a save begins and ends, to update the UI
|
||||
var saveMessage = Object.assign({}, message);
|
||||
saveMessage.action = response && response.error ? "save-error" : "save-success";
|
||||
this.replyToMessage(component, message, {error: response.error})
|
||||
this.handleMessage(component, saveMessage);
|
||||
}, null, "handleSaveItemsMessage");
|
||||
});
|
||||
}
|
||||
|
||||
handleCreateItemMessage(component, message) {
|
||||
var requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: [message.data.item.content_type]
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
var responseItem = message.data.item;
|
||||
this.removePrivatePropertiesFromResponseItems([responseItem], component);
|
||||
var item = this.modelManager.createItem(responseItem);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url || component.uuid, responseItem.clientData, ClientDataDomain);
|
||||
}
|
||||
this.modelManager.addItem(item);
|
||||
this.modelManager.resolveReferencesForItem(item);
|
||||
item.setDirty(true);
|
||||
this.syncManager.sync("handleCreateItemMessage");
|
||||
this.replyToMessage(component, message, {item: this.jsonForItem(item, component)})
|
||||
});
|
||||
}
|
||||
|
||||
handleDeleteItemsMessage(component, message) {
|
||||
var requiredContentTypes = _.uniq(message.data.items.map((i) => {return i.content_type})).sort();
|
||||
var requiredPermissions = [
|
||||
{
|
||||
name: "stream-items",
|
||||
content_types: requiredContentTypes
|
||||
}
|
||||
];
|
||||
|
||||
this.runWithPermissions(component, requiredPermissions, () => {
|
||||
var itemsData = message.data.items;
|
||||
var noun = itemsData.length == 1 ? "item" : "items";
|
||||
if(confirm(`Are you sure you want to delete ${itemsData.length} ${noun}?`)) {
|
||||
// Filter for any components and deactivate before deleting
|
||||
for(var itemData of itemsData) {
|
||||
var model = this.modelManager.findItem(itemData.uuid);
|
||||
if(["SN|Component", "SN|Theme"].includes(model.content_type)) {
|
||||
this.deactivateComponent(model, true);
|
||||
}
|
||||
this.modelManager.setItemToBeDeleted(model);
|
||||
}
|
||||
|
||||
this.syncManager.sync("handleDeleteItemsMessage");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleRequestPermissionsMessage(component, message) {
|
||||
this.runWithPermissions(component, message.data.permissions, () => {
|
||||
this.replyToMessage(component, message, {approved: true});
|
||||
});
|
||||
}
|
||||
|
||||
handleSetComponentDataMessage(component, message) {
|
||||
// A component setting its own data does not require special permissions
|
||||
this.runWithPermissions(component, [], () => {
|
||||
component.componentData = message.data.componentData;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("handleSetComponentDataMessage");
|
||||
});
|
||||
}
|
||||
|
||||
handleToggleComponentMessage(sourceComponent, targetComponent, message) {
|
||||
if(targetComponent.area == "modal") {
|
||||
this.openModalComponent(targetComponent);
|
||||
} else {
|
||||
if(targetComponent.active) {
|
||||
this.deactivateComponent(targetComponent);
|
||||
} else {
|
||||
if(targetComponent.content_type == "SN|Theme") {
|
||||
// Deactive currently active theme
|
||||
var activeTheme = this.getActiveTheme();
|
||||
if(activeTheme) {
|
||||
this.deactivateComponent(activeTheme);
|
||||
}
|
||||
}
|
||||
this.activateComponent(targetComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInstallLocalComponentMessage(sourceComponent, message) {
|
||||
// Only extensions manager has this permission
|
||||
if(!this.nativeExtManager.isSystemExtension(sourceComponent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetComponent = this.modelManager.findItem(message.data.uuid);
|
||||
this.desktopManager.installComponent(targetComponent);
|
||||
}
|
||||
|
||||
runWithPermissions(component, requiredPermissions, runFunction) {
|
||||
|
||||
if(!component.permissions) {
|
||||
component.permissions = [];
|
||||
}
|
||||
|
||||
var acquiredMatchesRequested = angular.toJson(component.permissions.sort()) === angular.toJson(requestedPermissions.sort());
|
||||
var acquiredPermissions = component.permissions;
|
||||
var acquiredMatchesRequired = true;
|
||||
|
||||
if(!acquiredMatchesRequested) {
|
||||
this.promptForPermissions(component, requestedPermissions, function(approved){
|
||||
for(var required of requiredPermissions) {
|
||||
var matching = acquiredPermissions.find((candidate) => {
|
||||
var matchesContentTypes = true;
|
||||
if(candidate.content_types && required.content_types) {
|
||||
matchesContentTypes = JSON.stringify(candidate.content_types.sort()) == JSON.stringify(required.content_types.sort());
|
||||
}
|
||||
return candidate.name == required.name && matchesContentTypes;
|
||||
});
|
||||
|
||||
if(!matching) {
|
||||
/* Required permissions can be 1 content type, and requestedPermisisons may send an array of content types.
|
||||
In the case of an array, we can just check to make sure that requiredPermissions content type is found in the array
|
||||
*/
|
||||
matching = acquiredPermissions.find((candidate) => {
|
||||
return Array.isArray(candidate.content_types)
|
||||
&& Array.isArray(required.content_types)
|
||||
&& candidate.content_types.containsPrimitiveSubset(required.content_types);
|
||||
});
|
||||
|
||||
if(!matching) {
|
||||
acquiredMatchesRequired = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!acquiredMatchesRequired) {
|
||||
this.promptForPermissions(component, requiredPermissions, function(approved){
|
||||
if(approved) {
|
||||
runFunction();
|
||||
}
|
||||
@@ -393,107 +602,86 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
promptForPermissions(component, requestedPermissions, callback) {
|
||||
// since these calls are asyncronous, multiple dialogs may be requested at the same time. We only want to present one and trigger all callbacks based on one modal result
|
||||
var existingDialog = _.find(this.permissionDialogs, {component: component});
|
||||
|
||||
component.trusted = component.url.startsWith("https://standardnotes.org") || component.url.startsWith("https://extensions.standardnotes.org");
|
||||
promptForPermissions(component, permissions, callback) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.component = component;
|
||||
scope.permissions = requestedPermissions;
|
||||
scope.permissions = permissions;
|
||||
scope.actionBlock = callback;
|
||||
|
||||
scope.callback = function(approved) {
|
||||
if(approved) {
|
||||
component.permissions = requestedPermissions;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
for(var existing of this.permissionDialogs) {
|
||||
if(existing.component === component && existing.actionBlock) {
|
||||
existing.actionBlock(approved);
|
||||
for(var permission of permissions) {
|
||||
if(!component.permissions.includes(permission)) {
|
||||
component.permissions.push(permission);
|
||||
}
|
||||
}
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("promptForPermissions");
|
||||
}
|
||||
|
||||
this.permissionDialogs = this.permissionDialogs.filter(function(dialog){
|
||||
return dialog.component !== component;
|
||||
this.permissionDialogs = this.permissionDialogs.filter((pendingDialog) => {
|
||||
// Remove self
|
||||
if(pendingDialog == scope) {
|
||||
pendingDialog.actionBlock && pendingDialog.actionBlock(approved);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(pendingDialog.component == component) {
|
||||
// remove pending dialogs that are encapsulated by already approved permissions, and run its function
|
||||
if(pendingDialog.permissions == permissions || permissions.containsObjectSubset(pendingDialog.permissions)) {
|
||||
// If approved, run the action block. Otherwise, if canceled, cancel any pending ones as well, since the user was
|
||||
// explicit in their intentions
|
||||
if(approved) {
|
||||
pendingDialog.actionBlock && pendingDialog.actionBlock(approved);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
if(this.permissionDialogs.length > 0) {
|
||||
this.presentDialog(this.permissionDialogs[0]);
|
||||
}
|
||||
|
||||
}.bind(this);
|
||||
|
||||
// since these calls are asyncronous, multiple dialogs may be requested at the same time. We only want to present one and trigger all callbacks based on one modal result
|
||||
var existingDialog = _.find(this.permissionDialogs, {component: component});
|
||||
|
||||
this.permissionDialogs.push(scope);
|
||||
|
||||
if(!existingDialog) {
|
||||
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='permissions-modal'></permissions-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
this.presentDialog(scope);
|
||||
} else {
|
||||
console.log("Existing dialog, not presenting.");
|
||||
}
|
||||
}
|
||||
|
||||
replyToMessage(component, originalMessage, replyData) {
|
||||
var reply = {
|
||||
action: "reply",
|
||||
original: originalMessage,
|
||||
data: replyData
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, reply);
|
||||
presentDialog(dialog) {
|
||||
var permissions = dialog.permissions;
|
||||
var component = dialog.component;
|
||||
var callback = dialog.callback;
|
||||
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='modal'></permissions-modal>" )(dialog);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
sendMessageToComponent(component, message) {
|
||||
if(component.ignoreEvents && message.action !== "component-registered") {
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Component disabled for current item, not sending any messages.", component.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(this.loggingEnabled) {
|
||||
console.log("Web|sendMessageToComponent", component, message);
|
||||
}
|
||||
component.window.postMessage(message, "*");
|
||||
}
|
||||
|
||||
installComponent(url) {
|
||||
var name = getParameterByName("name", url);
|
||||
var area = getParameterByName("area", url);
|
||||
var component = this.modelManager.createItem({
|
||||
content_type: "SN|Component",
|
||||
url: url,
|
||||
name: name,
|
||||
area: area
|
||||
})
|
||||
|
||||
this.modelManager.addItem(component);
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
activateComponent(component) {
|
||||
var didChange = component.active != true;
|
||||
|
||||
component.active = true;
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area)) {
|
||||
handler.activationHandler(component);
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
if(!this.activeComponents.includes(component)) {
|
||||
this.activeComponents.push(component);
|
||||
}
|
||||
openModalComponent(component) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.component = component;
|
||||
var el = this.$compile( "<component-modal component='component' class='modal'></component-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
registerHandler(handler) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
deregisterHandler(identifier) {
|
||||
var handler = _.find(this.handlers, {identifier: identifier});
|
||||
this.handlers.splice(this.handlers.indexOf(handler), 1);
|
||||
}
|
||||
|
||||
// Called by other views when the iframe is ready
|
||||
registerComponentWindow(component, componentWindow) {
|
||||
if(component.window === componentWindow) {
|
||||
@@ -507,24 +695,56 @@ class ComponentManager {
|
||||
}
|
||||
component.window = componentWindow;
|
||||
component.sessionKey = Neeto.crypto.generateUUID();
|
||||
this.sendMessageToComponent(component, {action: "component-registered", sessionKey: component.sessionKey, componentData: component.componentData});
|
||||
this.postThemeToComponent(component);
|
||||
this.sendMessageToComponent(component, {
|
||||
action: "component-registered",
|
||||
sessionKey: component.sessionKey,
|
||||
componentData: component.componentData,
|
||||
data: {
|
||||
uuid: component.uuid,
|
||||
environment: isDesktopApplication() ? "desktop" : "web"
|
||||
}
|
||||
});
|
||||
this.postActiveThemeToComponent(component);
|
||||
}
|
||||
|
||||
deactivateComponent(component) {
|
||||
activateComponent(component, dontSync = false) {
|
||||
var didChange = component.active != true;
|
||||
|
||||
component.active = true;
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
handler.activationHandler(component);
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange && !dontSync) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("activateComponent");
|
||||
}
|
||||
|
||||
if(!this.activeComponents.includes(component)) {
|
||||
this.activeComponents.push(component);
|
||||
}
|
||||
|
||||
if(component.area == "themes") {
|
||||
this.postActiveThemeToAllComponents();
|
||||
}
|
||||
}
|
||||
|
||||
deactivateComponent(component, dontSync = false) {
|
||||
var didChange = component.active != false;
|
||||
component.active = false;
|
||||
component.sessionKey = null;
|
||||
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area)) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
handler.activationHandler(component);
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange) {
|
||||
if(didChange && !dontSync) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
this.syncManager.sync("deactivateComponent");
|
||||
}
|
||||
|
||||
_.pull(this.activeComponents, component);
|
||||
@@ -536,57 +756,23 @@ class ComponentManager {
|
||||
this.contextStreamObservers = this.contextStreamObservers.filter(function(o){
|
||||
return o.component !== component;
|
||||
})
|
||||
|
||||
if(component.area == "themes") {
|
||||
this.postActiveThemeToAllComponents();
|
||||
}
|
||||
}
|
||||
|
||||
deleteComponent(component) {
|
||||
this.modelManager.setItemToBeDeleted(component);
|
||||
this.syncManager.sync();
|
||||
this.syncManager.sync("deleteComponent");
|
||||
}
|
||||
|
||||
isComponentActive(component) {
|
||||
return component.active;
|
||||
}
|
||||
|
||||
disassociateComponentWithItem(component, item) {
|
||||
_.pull(component.associatedItemIds, item.uuid);
|
||||
|
||||
if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.disassociatedItemIds.push(item.uuid);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
associateComponentWithItem(component, item) {
|
||||
_.pull(component.disassociatedItemIds, item.uuid);
|
||||
|
||||
if(component.associatedItemIds.includes(item.uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.associatedItemIds.push(item.uuid);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
enableComponentsForItem(components, item) {
|
||||
for(var component of components) {
|
||||
_.pull(component.disassociatedItemIds, item.uuid);
|
||||
component.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
setEventFlowForComponent(component, on) {
|
||||
component.ignoreEvents = !on;
|
||||
}
|
||||
|
||||
iframeForComponent(component) {
|
||||
for(var frame of document.getElementsByTagName("iframe")) {
|
||||
for(var frame of Array.from(document.getElementsByTagName("iframe"))) {
|
||||
var componentId = frame.dataset.componentId;
|
||||
if(componentId === component.uuid) {
|
||||
return frame;
|
||||
@@ -594,7 +780,40 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
focusChangedForComponent(component) {
|
||||
let focused = document.activeElement == this.iframeForComponent(component);
|
||||
for(var handler of this.handlers) {
|
||||
// Notify all handlers, and not just ones that match this component type
|
||||
handler.focusHandler && handler.focusHandler(component, focused);
|
||||
}
|
||||
}
|
||||
|
||||
handleSetSizeEvent(component, data) {
|
||||
var setSize = function(element, size) {
|
||||
var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`;
|
||||
var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`;
|
||||
element.setAttribute("style", `width:${widthString}; height:${heightString}; `);
|
||||
}
|
||||
|
||||
if(data.type === "content") {
|
||||
var iframe = this.iframeForComponent(component);
|
||||
var width = data.width;
|
||||
var height = data.height;
|
||||
iframe.width = width;
|
||||
iframe.height = height;
|
||||
|
||||
setSize(iframe, data);
|
||||
} else {
|
||||
var container = document.getElementById("component-" + component.uuid);
|
||||
if(container) {
|
||||
// in the case of Modals, sometimes they may be "active" because they were so in another session,
|
||||
// but no longer actually visible. So check to make sure the container exists
|
||||
setSize(container, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('componentManager', ComponentManager);
|
||||
angular.module('app').service('componentManager', ComponentManager);
|
||||
|
||||
@@ -158,4 +158,4 @@ class DBManager {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('dbManager', DBManager);
|
||||
angular.module('app').service('dbManager', DBManager);
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
class DesktopManager {
|
||||
|
||||
constructor($rootScope, modelManager, authManager, passcodeManager) {
|
||||
constructor($rootScope, $timeout, modelManager, syncManager, authManager, passcodeManager) {
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.timeout = $timeout;
|
||||
this.updateObservers = [];
|
||||
|
||||
this.isDesktop = isDesktopApplication();
|
||||
|
||||
$rootScope.$on("initial-data-loaded", () => {
|
||||
this.dataLoaded = true;
|
||||
@@ -22,6 +27,84 @@ class DesktopManager {
|
||||
})
|
||||
}
|
||||
|
||||
getApplicationDataPath() {
|
||||
console.assert(this.applicationDataPath, "applicationDataPath is null");
|
||||
return this.applicationDataPath;
|
||||
}
|
||||
|
||||
/* Sending a component in its raw state is really slow for the desktop app */
|
||||
convertComponentForTransmission(component) {
|
||||
return new ItemParams(component).paramsForExportFile(true);
|
||||
}
|
||||
|
||||
// All `components` should be installed
|
||||
syncComponentsInstallation(components) {
|
||||
if(!this.isDesktop) return;
|
||||
|
||||
var data = components.map((component) => {
|
||||
return this.convertComponentForTransmission(component);
|
||||
})
|
||||
this.installationSyncHandler(data);
|
||||
}
|
||||
|
||||
installComponent(component) {
|
||||
this.installComponentHandler(this.convertComponentForTransmission(component));
|
||||
}
|
||||
|
||||
registerUpdateObserver(callback) {
|
||||
var observer = {id: Math.random, callback: callback};
|
||||
this.updateObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
deregisterUpdateObserver(observer) {
|
||||
_.pull(this.updateObservers, observer);
|
||||
}
|
||||
|
||||
desktop_onComponentInstallationComplete(componentData, error) {
|
||||
console.log("Web|Component Installation/Update Complete", componentData, error);
|
||||
|
||||
// Desktop is only allowed to change these keys:
|
||||
let permissableKeys = ["package_info", "local_url"];
|
||||
var component = this.modelManager.findItem(componentData.uuid);
|
||||
|
||||
if(!component) {
|
||||
console.error("desktop_onComponentInstallationComplete component is null for uuid", componentData.uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
if(error) {
|
||||
component.setAppDataItem("installError", error);
|
||||
} else {
|
||||
for(var key of permissableKeys) {
|
||||
component[key] = componentData.content[key];
|
||||
}
|
||||
this.modelManager.notifySyncObserversOfModels([component], ModelManager.MappingSourceDesktopInstalled);
|
||||
component.setAppDataItem("installError", null);
|
||||
}
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("onComponentInstallationComplete");
|
||||
|
||||
this.timeout(() => {
|
||||
for(var observer of this.updateObservers) {
|
||||
observer.callback(component);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Used to resolve "sn://" */
|
||||
desktop_setApplicationDataPath(path) {
|
||||
this.applicationDataPath = path;
|
||||
}
|
||||
|
||||
desktop_setComponentInstallationSyncHandler(handler) {
|
||||
this.installationSyncHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInstallComponentHandler(handler) {
|
||||
this.installComponentHandler = handler;
|
||||
}
|
||||
|
||||
desktop_setInitialDataLoadHandler(handler) {
|
||||
this.dataLoadHandler = handler;
|
||||
if(this.dataLoaded) {
|
||||
@@ -56,4 +139,4 @@ class DesktopManager {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('desktopManager', DesktopManager);
|
||||
angular.module('app').service('desktopManager', DesktopManager);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('mbAutofocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: "="
|
||||
},
|
||||
link : function($scope, $element) {
|
||||
$timeout(function() {
|
||||
if($scope.shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -1,27 +0,0 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('clickOutside', ['$document', function($document) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: false,
|
||||
link : function($scope, $element, attrs) {
|
||||
|
||||
var didApplyClickOutside = false;
|
||||
|
||||
$element.bind('click', function(e) {
|
||||
didApplyClickOutside = false;
|
||||
if (attrs.isOpen) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
$document.bind('click', function() {
|
||||
if(!didApplyClickOutside) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -1,46 +0,0 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('delayHide', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
show: '=',
|
||||
delay: '@'
|
||||
},
|
||||
link: function(scope, elem, attrs) {
|
||||
var showTimer;
|
||||
|
||||
showElement(false);
|
||||
|
||||
//This is where all the magic happens!
|
||||
// Whenever the scope variable updates we simply
|
||||
// show if it evaluates to 'true' and hide if 'false'
|
||||
scope.$watch('show', function(newVal){
|
||||
newVal ? showSpinner() : hideSpinner();
|
||||
});
|
||||
|
||||
function showSpinner() {
|
||||
if(scope.hidePromise) {
|
||||
$timeout.cancel(scope.hidePromise);
|
||||
scope.hidePromise = null;
|
||||
}
|
||||
showElement(true);
|
||||
}
|
||||
|
||||
function hideSpinner() {
|
||||
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
|
||||
}
|
||||
|
||||
function showElement(show) {
|
||||
show ? elem.css({display:''}) : elem.css({display:'none'});
|
||||
}
|
||||
|
||||
function getDelay() {
|
||||
var delay = parseInt(scope.delay);
|
||||
|
||||
return angular.isNumber(delay) ? delay : 200;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('fileChange', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
handler: '&'
|
||||
},
|
||||
link: function (scope, element) {
|
||||
element.on('change', function (event) {
|
||||
scope.$apply(function(){
|
||||
scope.handler({files: event.target.files});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
angular.module('app.frontend').directive('infiniteScroll', [
|
||||
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
|
||||
return {
|
||||
link: function(scope, elem, attrs) {
|
||||
// elem.css('overflow-x', 'hidden');
|
||||
// elem.css('height', 'inherit');
|
||||
|
||||
var offset = parseInt(attrs.threshold) || 0;
|
||||
var e = elem[0]
|
||||
|
||||
elem.on('scroll', function(){
|
||||
if(scope.$eval(attrs.canLoad) && e.scrollTop + e.offsetHeight >= e.scrollHeight - offset) {
|
||||
scope.$apply(attrs.infiniteScroll);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
@@ -1,20 +0,0 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('lowercase', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, modelCtrl) {
|
||||
var lowercase = function(inputValue) {
|
||||
if (inputValue == undefined) inputValue = '';
|
||||
var lowercased = inputValue.toLowerCase();
|
||||
if (lowercased !== inputValue) {
|
||||
modelCtrl.$setViewValue(lowercased);
|
||||
modelCtrl.$render();
|
||||
}
|
||||
return lowercased;
|
||||
}
|
||||
modelCtrl.$parsers.push(lowercase);
|
||||
lowercase(scope[attrs.ngModel]);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
angular
|
||||
.module('app.frontend')
|
||||
.directive('selectOnClick', ['$window', function ($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.on('focus', function () {
|
||||
if (!$window.getSelection().toString()) {
|
||||
// Required for mobile Safari
|
||||
this.setSelectionRange(0, this.value.length)
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,580 +0,0 @@
|
||||
class AccountMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu.html";
|
||||
this.scope = {
|
||||
"onSuccessfulAuth" : "&"
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
|
||||
$scope.encryptedBackupsAvailable = function() {
|
||||
return authManager.user || passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
|
||||
$scope.encryptionKey = function() {
|
||||
return authManager.keys().mk;
|
||||
}
|
||||
|
||||
$scope.authKey = function() {
|
||||
return authManager.keys().ak;
|
||||
}
|
||||
|
||||
$scope.serverPassword = function() {
|
||||
return syncManager.serverPassword;
|
||||
}
|
||||
|
||||
$scope.dashboardURL = function() {
|
||||
return `${$scope.server}/dashboard/#server=${$scope.server}&id=${encodeURIComponent($scope.user.email)}&pw=${$scope.serverPassword()}`;
|
||||
}
|
||||
|
||||
$scope.newPasswordData = {};
|
||||
|
||||
$scope.showPasswordChangeForm = function() {
|
||||
$scope.newPasswordData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
|
||||
if($scope.newPasswordData.newPassword != $scope.newPasswordData.newPasswordConfirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var email = $scope.user.email;
|
||||
if(!email) {
|
||||
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.newPasswordData.status = "Generating New Keys...";
|
||||
$scope.newPasswordData.showForm = false;
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync(function(response){
|
||||
authManager.changePassword(email, $scope.newPasswordData.newPassword, function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// re-encrypt all items
|
||||
$scope.newPasswordData.status = "Re-encrypting all items with your new key...";
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
$scope.newPasswordData.status = "Successfully changed password and re-encrypted all items.";
|
||||
$timeout(function(){
|
||||
alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.submitAuthForm = function() {
|
||||
if($scope.formData.showLogin) {
|
||||
$scope.login();
|
||||
} else {
|
||||
$scope.register();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.login = function() {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
if(!response || (response && !response.didDisplayAlert)) {
|
||||
alert(error.message);
|
||||
}
|
||||
} else {
|
||||
$scope.onAuthSuccess();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.register = function() {
|
||||
let confirmation = $scope.formData.password_conf;
|
||||
if(confirmation !== $scope.formData.user_password) {
|
||||
alert("The two passwords you entered do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.formData.confirmPassword = false;
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral ,function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
alert(error.message);
|
||||
} else {
|
||||
$scope.onAuthSuccess();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.mergeLocalChanged = function() {
|
||||
if(!$scope.formData.mergeLocal) {
|
||||
if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) {
|
||||
$scope.formData.mergeLocal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.onAuthSuccess = function() {
|
||||
var block = function() {
|
||||
$timeout(function(){
|
||||
$scope.onSuccessfulAuth()();
|
||||
syncManager.refreshErroredItems();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
if($scope.formData.mergeLocal) {
|
||||
// Allows desktop to make backup file
|
||||
$rootScope.$broadcast("major-data-change");
|
||||
$scope.clearDatabaseAndRewriteAllItems(true, block);
|
||||
}
|
||||
|
||||
else {
|
||||
modelManager.resetLocalMemory();
|
||||
storageManager.clearAllModels(function(){
|
||||
block();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Allows indexeddb unencrypted logs to be deleted
|
||||
// clearAllModels will remove data from backing store, but not from working memory
|
||||
// See: https://github.com/standardnotes/desktop/issues/131
|
||||
$scope.clearDatabaseAndRewriteAllItems = function(alternateUuids, callback) {
|
||||
storageManager.clearAllModels(function(){
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
callback && callback();
|
||||
}, alternateUuids)
|
||||
});
|
||||
}
|
||||
|
||||
$scope.destroyLocalData = function() {
|
||||
if(!confirm("Are you sure you want to end your session? This will delete all local items and extensions.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.signOut();
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
/* Import/Export */
|
||||
|
||||
$scope.archiveFormData = {encrypted: $scope.encryptedBackupsAvailable() ? true : false};
|
||||
$scope.user = authManager.user;
|
||||
|
||||
$scope.submitImportPassword = function() {
|
||||
$scope.performImport($scope.importData.data, $scope.importData.password);
|
||||
}
|
||||
|
||||
$scope.performImport = function(data, password) {
|
||||
$scope.importData.loading = true;
|
||||
// allow loading indicator to come up with timeout
|
||||
$timeout(function(){
|
||||
$scope.importJSONData(data, password, function(response, errorCount){
|
||||
$timeout(function(){
|
||||
$scope.importData.loading = false;
|
||||
$scope.importData = null;
|
||||
|
||||
// Update UI before showing alert
|
||||
setTimeout(function () {
|
||||
if(!response) {
|
||||
alert("There was an error importing your data. Please try again.");
|
||||
} else {
|
||||
if(errorCount > 0) {
|
||||
var message = `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
|
||||
alert(message);
|
||||
} else {
|
||||
alert("Your data was successfully imported.")
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.importFileSelected = function(files) {
|
||||
$scope.importData = {};
|
||||
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
$scope.importJSONData = function(data, password, callback) {
|
||||
var onDataReady = function(errorCount) {
|
||||
var items = modelManager.mapResponseItemsToLocalModels(data.items, ModelManager.MappingSourceFileImport);
|
||||
items.forEach(function(item){
|
||||
item.setDirty(true);
|
||||
item.deleted = false;
|
||||
item.markAllReferencesDirty();
|
||||
|
||||
// We don't want to activate any components during import process in case of exceptions
|
||||
// breaking up the import proccess
|
||||
if(item.content_type == "SN|Component") {
|
||||
item.active = false;
|
||||
}
|
||||
})
|
||||
|
||||
syncManager.sync((response) => {
|
||||
callback(response, errorCount);
|
||||
}, {additionalFields: ["created_at", "updated_at"]});
|
||||
}.bind(this)
|
||||
|
||||
if(data.auth_params) {
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
|
||||
try {
|
||||
EncryptionHelper.decryptMultipleItems(data.items, keys, false); /* throws = false as we don't want to interrupt all decryption if just one fails */
|
||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
||||
data.items.forEach(function(item){
|
||||
item.enc_item_key = null;
|
||||
item.auth_hash = null;
|
||||
});
|
||||
|
||||
var errorCount = 0;
|
||||
// Don't import items that didn't decrypt properly
|
||||
data.items = data.items.filter(function(item){
|
||||
if(item.errorDecrypting) {
|
||||
errorCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
onDataReady(errorCount);
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Error decrypting", e);
|
||||
alert("There was an error decrypting your items. Make sure the password you entered is correct and try again.");
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
}.bind(this));
|
||||
} else {
|
||||
onDataReady();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Export
|
||||
*/
|
||||
|
||||
function loadZip(callback) {
|
||||
if(window.zip) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = "/assets/zip/zip.js";
|
||||
scriptTag.async = false;
|
||||
var headTag = document.getElementsByTagName('head')[0];
|
||||
headTag.appendChild(scriptTag);
|
||||
scriptTag.onload = function() {
|
||||
zip.workerScriptsPath = "assets/zip/";
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadZippedNotes(notes) {
|
||||
loadZip(function(){
|
||||
|
||||
zip.createWriter(new zip.BlobWriter("application/zip"), function(zipWriter) {
|
||||
|
||||
var index = 0;
|
||||
function nextFile() {
|
||||
var note = notes[index];
|
||||
var blob = new Blob([note.text], {type: 'text/plain'});
|
||||
zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), function() {
|
||||
index++;
|
||||
if(index < notes.length) {
|
||||
nextFile();
|
||||
} else {
|
||||
zipWriter.close(function(blob) {
|
||||
downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`)
|
||||
zipWriter = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nextFile();
|
||||
}, onerror);
|
||||
})
|
||||
}
|
||||
|
||||
var textFile = null;
|
||||
|
||||
function hrefForData(data) {
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (textFile !== null) {
|
||||
window.URL.revokeObjectURL(textFile);
|
||||
}
|
||||
|
||||
textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return textFile;
|
||||
}
|
||||
|
||||
function downloadData(data, fileName) {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', fileName);
|
||||
link.href = hrefForData(data);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
$scope.downloadDataArchive = function() {
|
||||
// download in Standard File format
|
||||
var keys, authParams, protocolVersion;
|
||||
if($scope.archiveFormData.encrypted) {
|
||||
if(authManager.offline() && passcodeManager.hasPasscode()) {
|
||||
keys = passcodeManager.keys();
|
||||
authParams = passcodeManager.passcodeAuthParams();
|
||||
protocolVersion = authParams.version;
|
||||
} else {
|
||||
keys = authManager.keys();
|
||||
authParams = authManager.getAuthParams();
|
||||
protocolVersion = authManager.protocolVersion();
|
||||
}
|
||||
}
|
||||
var data = $scope.itemsData(keys, authParams, protocolVersion);
|
||||
downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
if(!keys) {
|
||||
var notes = modelManager.allItemsMatchingTypes(["Note"]);
|
||||
downloadZippedNotes(notes);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.itemsData = function(keys, authParams, protocolVersion) {
|
||||
let data = modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||
let blobData = new Blob([data], {type: 'text/json'});
|
||||
return blobData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Advanced
|
||||
|
||||
$scope.reencryptPressed = function() {
|
||||
if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
|
||||
$timeout(function(){
|
||||
alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 002 Update
|
||||
|
||||
$scope.securityUpdateAvailable = function() {
|
||||
var keys = authManager.keys()
|
||||
return keys && !keys.ak;
|
||||
}
|
||||
|
||||
$scope.clickedSecurityUpdate = function() {
|
||||
if(!$scope.securityUpdateData) {
|
||||
$scope.securityUpdateData = {};
|
||||
}
|
||||
$scope.securityUpdateData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitSecurityUpdateForm = function() {
|
||||
$scope.securityUpdateData.processing = true;
|
||||
var authParams = authManager.getAuthParams();
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){
|
||||
if(keys.mk !== authManager.keys().mk) {
|
||||
alert("Invalid password. Please try again.");
|
||||
$timeout(function(){
|
||||
$scope.securityUpdateData.processing = false;
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.saveKeys(keys);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Encryption Status
|
||||
*/
|
||||
|
||||
$scope.notesAndTagsCount = function() {
|
||||
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
||||
return items.length;
|
||||
}
|
||||
|
||||
$scope.encryptionStatusForNotes = function() {
|
||||
var length = $scope.notesAndTagsCount();
|
||||
return length + "/" + length + " notes and tags encrypted";
|
||||
}
|
||||
|
||||
$scope.encryptionEnabled = function() {
|
||||
return passcodeManager.hasPasscode() || !authManager.offline();
|
||||
}
|
||||
|
||||
$scope.encryptionSource = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "Account keys";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Local Passcode";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.encryptionStatusString = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "End-to-end encryption is enabled. Your data is encrypted before being synced to your private account.";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Encryption is enabled. Your data is encrypted using your passcode before being stored on disk.";
|
||||
} else {
|
||||
return "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Passcode Lock
|
||||
*/
|
||||
|
||||
$scope.passcodeOptionAvailable = function() {
|
||||
// If you're signed in with an ephemeral session, passcode lock is unavailable
|
||||
return authManager.offline() || !authManager.isEphemeralSession();
|
||||
}
|
||||
|
||||
$scope.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
$scope.addPasscodeClicked = function() {
|
||||
$scope.formData.showPasscodeForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasscodeForm = function() {
|
||||
var passcode = $scope.formData.passcode;
|
||||
if(passcode !== $scope.formData.confirmPasscode) {
|
||||
alert("The two passcodes you entered do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
passcodeManager.setPasscode(passcode, () => {
|
||||
$timeout(function(){
|
||||
$scope.formData.showPasscodeForm = false;
|
||||
var offline = authManager.offline();
|
||||
|
||||
// Allow UI to update before showing alert
|
||||
setTimeout(function () {
|
||||
var message = "You've succesfully set an app passcode.";
|
||||
if(offline) { message += " Your items will now be encrypted using this passcode."; }
|
||||
alert(message);
|
||||
}, 10);
|
||||
|
||||
if(offline) {
|
||||
// Allows desktop to make backup file
|
||||
$rootScope.$broadcast("major-data-change");
|
||||
$scope.clearDatabaseAndRewriteAllItems(false);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.removePasscodePressed = function() {
|
||||
var signedIn = !authManager.offline();
|
||||
var message = "Are you sure you want to remove your local passcode?";
|
||||
if(!signedIn) {
|
||||
message += " This will remove encryption from your local data.";
|
||||
}
|
||||
if(confirm(message)) {
|
||||
passcodeManager.clearPasscode();
|
||||
|
||||
if(authManager.offline()) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
|
||||
// we don't want to write unencrypted data to disk.
|
||||
// $rootScope.$broadcast("major-data-change");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isDesktopApplication = function() {
|
||||
return isDesktopApplication();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountMenu', () => new AccountMenu);
|
||||
@@ -1,86 +0,0 @@
|
||||
class ContextualExtensionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/contextual-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, extensionManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.renderData = {};
|
||||
|
||||
$scope.extensions = _.map(extensionManager.extensionsInContextOfItem($scope.item), function(ext){
|
||||
// why are we cloning deep? commenting out because we want original reference so that extension.hide is saved between menu opens
|
||||
// return _.cloneDeep(ext);
|
||||
return ext;
|
||||
});
|
||||
|
||||
for(let ext of $scope.extensions) {
|
||||
ext.loading = true;
|
||||
extensionManager.loadExtensionInContextOfItem(ext, $scope.item, function(scopedExtension) {
|
||||
ext.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
$scope.executeAction = function(action, extension, parentAction) {
|
||||
if(!$scope.isActionEnabled(action, extension)) {
|
||||
alert("This action requires " + action.access_type + " access to this note. You can change this setting in the Extensions menu on the bottom of the app.");
|
||||
return;
|
||||
}
|
||||
if(action.verb == "nested") {
|
||||
action.showNestedActions = !action.showNestedActions;
|
||||
return;
|
||||
}
|
||||
action.running = true;
|
||||
extensionManager.executeAction(action, extension, $scope.item, function(response){
|
||||
action.running = false;
|
||||
$scope.handleActionResponse(action, response);
|
||||
|
||||
// reload extension actions
|
||||
extensionManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
|
||||
// keep nested state
|
||||
if(parentAction) {
|
||||
var matchingAction = _.find(ext.actions, {label: parentAction.label});
|
||||
matchingAction.showNestedActions = true;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.handleActionResponse = function(action, response) {
|
||||
switch (action.verb) {
|
||||
case "render": {
|
||||
var item = response.item;
|
||||
if(item.content_type == "Note") {
|
||||
$scope.renderData.title = item.title;
|
||||
$scope.renderData.text = item.text;
|
||||
$scope.renderData.showRenderModal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isActionEnabled = function(action, extension) {
|
||||
if(action.access_type) {
|
||||
var extEncryptedAccess = extension.encrypted;
|
||||
if(action.access_type == "decrypted" && extEncryptedAccess) {
|
||||
return false;
|
||||
} else if(action.access_type == "encrypted" && !extEncryptedAccess) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.accessTypeForExtension = function(extension) {
|
||||
return extension.encrypted ? "encrypted" : "decrypted";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('contextualExtensionsMenu', () => new ContextualExtensionsMenu);
|
||||
@@ -1,30 +0,0 @@
|
||||
class EditorMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/editor-menu.html";
|
||||
this.scope = {
|
||||
callback: "&",
|
||||
selectedEditor: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.editors = componentManager.componentsForArea("editor-editor");
|
||||
|
||||
$scope.selectEditor = function($event, editor) {
|
||||
if(editor) {
|
||||
editor.conflict_of = null; // clear conflict if applicable
|
||||
}
|
||||
$scope.callback()(editor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('editorMenu', () => new EditorMenu);
|
||||
@@ -1,225 +0,0 @@
|
||||
class GlobalExtensionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/global-extensions-menu.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, extensionManager, syncManager, modelManager, themeManager, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.extensionManager = extensionManager;
|
||||
$scope.themeManager = themeManager;
|
||||
$scope.componentManager = componentManager;
|
||||
|
||||
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
|
||||
|
||||
$scope.selectedAction = function(action, extension) {
|
||||
extensionManager.executeAction(action, extension, null, function(response){
|
||||
if(response && response.error) {
|
||||
action.error = true;
|
||||
alert("There was an error performing this action. Please try again.");
|
||||
} else {
|
||||
action.error = false;
|
||||
syncManager.sync(null);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.changeExtensionEncryptionFormat = function(encrypted, extension) {
|
||||
extension.encrypted = encrypted;
|
||||
extension.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.deleteActionExtension = function(extension) {
|
||||
if(confirm("Are you sure you want to delete this extension?")) {
|
||||
extensionManager.deleteExtension(extension);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.reloadExtensionsPressed = function() {
|
||||
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
|
||||
extensionManager.refreshExtensionsFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.deleteTheme = function(theme) {
|
||||
if(confirm("Are you sure you want to delete this theme?")) {
|
||||
themeManager.deactivateTheme(theme);
|
||||
modelManager.setItemToBeDeleted(theme);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.renameExtension = function(extension) {
|
||||
extension.tempName = extension.name;
|
||||
extension.rename = true;
|
||||
}
|
||||
|
||||
$scope.submitExtensionRename = function(extension) {
|
||||
extension.name = extension.tempName;
|
||||
extension.tempName = null;
|
||||
extension.setDirty(true);
|
||||
extension.rename = false;
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.clickedExtension = function(extension) {
|
||||
if(extension.rename) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($scope.currentlyExpandedExtension && $scope.currentlyExpandedExtension !== extension) {
|
||||
$scope.currentlyExpandedExtension.showDetails = false;
|
||||
$scope.currentlyExpandedExtension.rename = false;
|
||||
}
|
||||
|
||||
extension.showDetails = !extension.showDetails;
|
||||
|
||||
if(extension.showDetails) {
|
||||
$scope.currentlyExpandedExtension = extension;
|
||||
}
|
||||
}
|
||||
|
||||
// Server extensions
|
||||
|
||||
$scope.deleteServerExt = function(ext) {
|
||||
if(confirm("Are you sure you want to delete and disable this extension?")) {
|
||||
_.remove($scope.serverExtensions, {uuid: ext.uuid});
|
||||
modelManager.setItemToBeDeleted(ext);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.nameForServerExtension = function(ext) {
|
||||
var url = ext.url;
|
||||
if(!url) {
|
||||
return "Invalid Extension";
|
||||
}
|
||||
if(url.includes("gdrive")) {
|
||||
return "Google Drive Sync";
|
||||
} else if(url.includes("file_attacher")) {
|
||||
return "File Attacher";
|
||||
} else if(url.includes("onedrive")) {
|
||||
return "OneDrive Sync";
|
||||
} else if(url.includes("backup.email_archive")) {
|
||||
return "Daily Email Backups";
|
||||
} else if(url.includes("dropbox")) {
|
||||
return "Dropbox Sync";
|
||||
} else if(url.includes("revisions")) {
|
||||
return "Revision History";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Components
|
||||
|
||||
$scope.revokePermissions = function(component) {
|
||||
component.permissions = [];
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.deleteComponent = function(component) {
|
||||
if(confirm("Are you sure you want to delete this component?")) {
|
||||
componentManager.deleteComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.makeEditorDefault = function(component) {
|
||||
var currentDefault = componentManager.componentsForArea("editor-editor").filter((e) => {return e.isDefaultEditor()})[0];
|
||||
if(currentDefault) {
|
||||
currentDefault.setAppDataItem("defaultEditor", false);
|
||||
currentDefault.setDirty(true);
|
||||
}
|
||||
component.setAppDataItem("defaultEditor", true);
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.removeEditorDefault = function(component) {
|
||||
component.setAppDataItem("defaultEditor", false);
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
// Installation
|
||||
|
||||
$scope.submitInstallLink = function() {
|
||||
|
||||
var fullLink = $scope.formData.installLink;
|
||||
if(!fullLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
var completion = function() {
|
||||
$scope.formData.installLink = "";
|
||||
$scope.formData.successfullyInstalled = true;
|
||||
}
|
||||
|
||||
var links = fullLink.split(",");
|
||||
for(var link of links) {
|
||||
var type = getParameterByName("type", link);
|
||||
|
||||
if(type == "sf") {
|
||||
$scope.handleSyncAdapterLink(link, completion);
|
||||
} else if(type == "editor") {
|
||||
$scope.handleEditorLink(link, completion);
|
||||
} else if(link.indexOf(".css") != -1 || type == "theme") {
|
||||
$scope.handleThemeLink(link, completion);
|
||||
} else if(type == "component") {
|
||||
$scope.handleComponentLink(link, completion);
|
||||
}
|
||||
|
||||
else {
|
||||
$scope.handleActionLink(link, completion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.handleSyncAdapterLink = function(link, completion) {
|
||||
var params = parametersFromURL(link);
|
||||
params["url"] = link;
|
||||
var ext = new SyncAdapter({content: params});
|
||||
ext.setDirty(true);
|
||||
|
||||
modelManager.addItem(ext);
|
||||
syncManager.sync();
|
||||
$scope.serverExtensions.push(ext);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleThemeLink = function(link, completion) {
|
||||
themeManager.submitTheme(link);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleComponentLink = function(link, completion) {
|
||||
componentManager.installComponent(link);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleActionLink = function(link, completion) {
|
||||
if(link) {
|
||||
extensionManager.addExtension(link, function(response){
|
||||
if(!response) {
|
||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('globalExtensionsMenu', () => new GlobalExtensionsMenu);
|
||||
@@ -1,70 +0,0 @@
|
||||
class PermissionsModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/permissions-modal.html";
|
||||
this.scope = {
|
||||
show: "=",
|
||||
component: "=",
|
||||
permissions: "=",
|
||||
callback: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
|
||||
$scope.dismiss = function() {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
$scope.accept = function() {
|
||||
$scope.callback(true);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
$scope.deny = function() {
|
||||
$scope.callback(false);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
$scope.formattedPermissions = $scope.permissions.map(function(permission){
|
||||
if(permission.name === "stream-items") {
|
||||
var title = "Access to ";
|
||||
var types = permission.content_types.map(function(type){
|
||||
return (type + "s").toLowerCase();
|
||||
})
|
||||
var typesString = "";
|
||||
var separator = ", ";
|
||||
|
||||
for(var i = 0;i < types.length;i++) {
|
||||
var type = types[i];
|
||||
if(i == 0) {
|
||||
// first element
|
||||
typesString = typesString + type;
|
||||
} else if(i == types.length - 1) {
|
||||
// last element
|
||||
if(types.length > 2) {
|
||||
typesString += separator + "and " + typesString;
|
||||
} else if(types.length == 2) {
|
||||
typesString = typesString + " and " + type;
|
||||
}
|
||||
} else {
|
||||
typesString += separator + type;
|
||||
}
|
||||
}
|
||||
|
||||
return title + typesString;
|
||||
} else if(permission.name === "stream-context-item") {
|
||||
var mapping = {
|
||||
"editor-stack" : "working note",
|
||||
"note-tags" : "working note",
|
||||
"editor-editor": "working note"
|
||||
}
|
||||
return "Access to " + mapping[$scope.component.area];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('permissionsModal', () => new PermissionsModal);
|
||||
@@ -1,337 +0,0 @@
|
||||
class ExtensionManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager, storageManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.enabledRepeatActionUrls = JSON.parse(storageManager.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.syncManager = syncManager;
|
||||
this.storageManager = storageManager;
|
||||
|
||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(allItems, validItems, deletedItems){
|
||||
for (var ext of validItems) {
|
||||
for (var action of ext.actions) {
|
||||
if(_.includes(this.enabledRepeatActionUrls, action.url)) {
|
||||
this.enableRepeatAction(action, ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.extensions;
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter(function(ext){
|
||||
return _.includes(ext.supported_types, item.content_type) || ext.actionsWithContextForItem(item).length > 0;
|
||||
})
|
||||
}
|
||||
|
||||
actionWithURL(url) {
|
||||
for (var extension of this.extensions) {
|
||||
return _.find(extension.actions, {url: url})
|
||||
}
|
||||
}
|
||||
|
||||
addExtension(url, callback) {
|
||||
this.retrieveExtensionFromServer(url, callback);
|
||||
}
|
||||
|
||||
deleteExtension(extension) {
|
||||
for(var action of extension.actions) {
|
||||
if(action.repeat_mode) {
|
||||
if(this.isRepeatActionEnabled(action)) {
|
||||
this.disableRepeatAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.modelManager.setItemToBeDeleted(extension);
|
||||
this.syncManager.sync(null);
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
loadExtensionInContextOfItem(extension, item, callback) {
|
||||
|
||||
this.httpManager.getAbsolute(extension.url, {content_type: item.content_type, item_uuid: item.uuid}, function(response){
|
||||
this.updateExtensionFromRemoteResponse(extension, response);
|
||||
callback && callback(extension);
|
||||
}.bind(this), function(response){
|
||||
console.log("Error loading extension", response);
|
||||
if(callback) {
|
||||
callback(null);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
/*
|
||||
Registers new extension and saves it to user's account
|
||||
*/
|
||||
retrieveExtensionFromServer(url, callback) {
|
||||
this.httpManager.getAbsolute(url, {}, function(response){
|
||||
if(typeof response !== 'object') {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
var ext = this.handleExtensionLoadExternalResponseItem(url, response);
|
||||
if(callback) {
|
||||
callback(ext);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
console.error("Error registering extension", response);
|
||||
callback(null);
|
||||
})
|
||||
}
|
||||
|
||||
handleExtensionLoadExternalResponseItem(url, externalResponseItem) {
|
||||
// Don't allow remote response to set these flags
|
||||
delete externalResponseItem.encrypted;
|
||||
delete externalResponseItem.uuid;
|
||||
|
||||
var extension = _.find(this.extensions, {url: url});
|
||||
if(extension) {
|
||||
this.updateExtensionFromRemoteResponse(extension, externalResponseItem);
|
||||
} else {
|
||||
extension = new Extension(externalResponseItem);
|
||||
extension.url = url;
|
||||
extension.setDirty(true);
|
||||
this.modelManager.addItem(extension);
|
||||
this.syncManager.sync(null);
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
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(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
} else {
|
||||
extension.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
refreshExtensionsFromServer() {
|
||||
for (var url of this.enabledRepeatActionUrls) {
|
||||
var action = this.actionWithURL(url);
|
||||
if(action) {
|
||||
this.disableRepeatAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
for(var ext of this.extensions) {
|
||||
this.retrieveExtensionFromServer(ext.url, function(extension){
|
||||
extension.setDirty(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
executeAction(action, extension, item, callback) {
|
||||
|
||||
if(extension.encrypted && this.authManager.offline()) {
|
||||
alert("To send data encrypted, you must have an encryption key, and must therefore be signed in.");
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var customCallback = function(response) {
|
||||
action.running = false;
|
||||
callback(response);
|
||||
}
|
||||
|
||||
action.running = true;
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
var items = response.items || [response.item];
|
||||
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
customCallback({items: items});
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
EncryptionHelper.decryptItem(response.item, this.authManager.keys());
|
||||
var item = this.modelManager.createItem(response.item);
|
||||
customCallback({item: item});
|
||||
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "show": {
|
||||
var win = window.open(action.url, '_blank');
|
||||
win.focus();
|
||||
customCallback();
|
||||
break;
|
||||
}
|
||||
|
||||
case "post": {
|
||||
var params = {};
|
||||
|
||||
if(action.all) {
|
||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
||||
params.items = items.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
params.items = [this.outgoingParamsForItem(item, extension)];
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
customCallback(response);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
}
|
||||
|
||||
isRepeatActionEnabled(action) {
|
||||
return _.includes(this.enabledRepeatActionUrls, action.url);
|
||||
}
|
||||
|
||||
disableRepeatAction(action, extension) {
|
||||
_.pull(this.enabledRepeatActionUrls, action.url);
|
||||
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
this.modelManager.removeItemChangeObserver(action.url);
|
||||
|
||||
console.assert(this.isRepeatActionEnabled(action) == false);
|
||||
}
|
||||
|
||||
enableRepeatAction(action, extension) {
|
||||
if(!_.find(this.enabledRepeatActionUrls, action.url)) {
|
||||
this.enabledRepeatActionUrls.push(action.url);
|
||||
this.storageManager.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
}
|
||||
|
||||
if(action.repeat_mode) {
|
||||
|
||||
if(action.repeat_mode == "watch") {
|
||||
this.modelManager.addItemChangeObserver(action.url, action.content_types, function(changedItems){
|
||||
this.triggerWatchAction(action, extension, changedItems);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
if(action.repeat_mode == "loop") {
|
||||
// todo
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
queueAction(action, extension, delay, changedItems) {
|
||||
this.actionQueue = this.actionQueue || [];
|
||||
if(_.find(this.actionQueue, {url: action.url})) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionQueue.push(action);
|
||||
|
||||
setTimeout(function () {
|
||||
this.triggerWatchAction(action, extension, changedItems);
|
||||
_.pull(this.actionQueue, action);
|
||||
}.bind(this), delay * 1000);
|
||||
}
|
||||
|
||||
triggerWatchAction(action, extension, changedItems) {
|
||||
if(action.repeat_timeout > 0) {
|
||||
var lastExecuted = action.lastExecuted;
|
||||
var diffInSeconds = (new Date() - lastExecuted)/1000;
|
||||
if(diffInSeconds < action.repeat_timeout) {
|
||||
var delay = action.repeat_timeout - diffInSeconds;
|
||||
this.queueAction(action, extension, delay, changedItems);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
|
||||
if(action.verb == "post") {
|
||||
var params = {};
|
||||
params.items = changedItems.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
action.running = true;
|
||||
this.performPost(action, extension, params, function(){
|
||||
action.running = false;
|
||||
});
|
||||
} else {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension) {
|
||||
var keys = this.authManager.keys();
|
||||
if(!extension.encrypted) {
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, keys, this.authManager.protocolVersion());
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
performPost(action, extension, params, callback) {
|
||||
|
||||
if(extension.encrypted) {
|
||||
params.auth_params = this.authManager.getAuthParams();
|
||||
}
|
||||
|
||||
this.httpManager.postAbsolute(action.url, params, function(response){
|
||||
action.error = false;
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
action.error = true;
|
||||
console.log("Action error response:", response);
|
||||
if(callback) {
|
||||
callback({error: "Request error"});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('extensionManager', ExtensionManager);
|
||||
@@ -1,11 +0,0 @@
|
||||
angular.module('app.frontend')
|
||||
.filter('appDate', function ($filter) {
|
||||
return function (input) {
|
||||
return input ? $filter('date')(new Date(input), 'MM/dd/yyyy', 'UTC') : '';
|
||||
};
|
||||
})
|
||||
.filter('appDateTime', function ($filter) {
|
||||
return function (input) {
|
||||
return input ? $filter('date')(new Date(input), 'MM/dd/yyyy h:mm a') : '';
|
||||
};
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
angular.module('app.frontend')
|
||||
.filter('sortBy', function ($filter) {
|
||||
return function(items, sortBy) {
|
||||
let sortValueFn = (a, b, pinCheck = false) => {
|
||||
if(!pinCheck) {
|
||||
if(a.pinned && b.pinned) {
|
||||
return sortValueFn(a, b, true);
|
||||
}
|
||||
if(a.pinned) { return -1; }
|
||||
if(b.pinned) { return 1; }
|
||||
}
|
||||
|
||||
var aValue = a[sortBy] || "";
|
||||
var bValue = b[sortBy] || "";
|
||||
|
||||
let vector = 1;
|
||||
if(sortBy == "title") {
|
||||
aValue = aValue.toLowerCase();
|
||||
bValue = bValue.toLowerCase();
|
||||
|
||||
if(aValue.length == 0 && bValue.length == 0) {
|
||||
return 0;
|
||||
} else if(aValue.length == 0 && bValue.length != 0) {
|
||||
return 1;
|
||||
} else if(aValue.length != 0 && bValue.length == 0) {
|
||||
return -1;
|
||||
} else {
|
||||
vector = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if(aValue > bValue) { return -1 * vector;}
|
||||
else if(aValue < bValue) { return 1 * vector;}
|
||||
return 0;
|
||||
}
|
||||
|
||||
items = items || [];
|
||||
return items.sort(function(a, b){
|
||||
return sortValueFn(a, b);
|
||||
})
|
||||
};
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
angular.module('app.frontend').filter('startFrom', function() {
|
||||
return function(input, start) {
|
||||
return input.slice(start);
|
||||
};
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
angular.module('app.frontend').filter('trusted', ['$sce', function ($sce) {
|
||||
return function(url) {
|
||||
return $sce.trustAsResourceUrl(url);
|
||||
};
|
||||
}]);
|
||||
@@ -77,4 +77,4 @@ class HttpManager {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('httpManager', HttpManager);
|
||||
angular.module('app').service('httpManager', HttpManager);
|
||||
|
||||
@@ -50,7 +50,7 @@ class MigrationManager {
|
||||
this.modelManager.setItemToBeDeleted(editor);
|
||||
}
|
||||
|
||||
this.syncManager.sync();
|
||||
this.syncManager.sync("addEditorToComponentMigrator");
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -58,4 +58,4 @@ class MigrationManager {
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('migrationManager', MigrationManager);
|
||||
angular.module('app').service('migrationManager', MigrationManager);
|
||||
|
||||
@@ -3,8 +3,10 @@ class ModelManager {
|
||||
constructor(storageManager) {
|
||||
ModelManager.MappingSourceRemoteRetrieved = "MappingSourceRemoteRetrieved";
|
||||
ModelManager.MappingSourceRemoteSaved = "MappingSourceRemoteSaved";
|
||||
ModelManager.MappingSourceLocalSaved = "MappingSourceLocalSaved";
|
||||
ModelManager.MappingSourceLocalRetrieved = "MappingSourceLocalRetrieved";
|
||||
ModelManager.MappingSourceComponentRetrieved = "MappingSourceComponentRetrieved";
|
||||
ModelManager.MappingSourceDesktopInstalled = "MappingSourceDesktopInstalled"; // When a component is installed by the desktop and some of its values change
|
||||
ModelManager.MappingSourceRemoteActionRetrieved = "MappingSourceRemoteActionRetrieved"; /* aciton-based Extensions like note history */
|
||||
ModelManager.MappingSourceFileImport = "MappingSourceFileImport";
|
||||
|
||||
@@ -18,7 +20,7 @@ class ModelManager {
|
||||
this._extensions = [];
|
||||
this.acceptableContentTypes = [
|
||||
"Note", "Tag", "Extension", "SN|Editor", "SN|Theme",
|
||||
"SN|Component", "SF|Extension", "SN|UserPreferences"
|
||||
"SN|Component", "SF|Extension", "SN|UserPreferences", "SF|MFA"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -52,6 +54,8 @@ class ModelManager {
|
||||
|
||||
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
||||
|
||||
console.log(item.uuid, "-->", newItem.uuid);
|
||||
|
||||
var block = () => {
|
||||
this.addItem(newItem);
|
||||
newItem.setDirty(true);
|
||||
@@ -60,9 +64,10 @@ class ModelManager {
|
||||
}
|
||||
|
||||
if(removeOriginal) {
|
||||
this.removeItemLocally(item, function(){
|
||||
block();
|
||||
});
|
||||
// Set to deleted, then run through mapping function so that observers can be notified
|
||||
item.deleted = true;
|
||||
this.mapResponseItemsToLocalModels([item], ModelManager.MappingSourceLocalSaved);
|
||||
block();
|
||||
} else {
|
||||
block();
|
||||
}
|
||||
@@ -79,13 +84,13 @@ class ModelManager {
|
||||
}
|
||||
|
||||
allItemsMatchingTypes(contentTypes) {
|
||||
return this.items.filter(function(item){
|
||||
return this.allItems.filter(function(item){
|
||||
return (_.includes(contentTypes, item.content_type) || _.includes(contentTypes, "*")) && !item.dummy;
|
||||
})
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return this.allItems.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
}
|
||||
@@ -103,6 +108,10 @@ class ModelManager {
|
||||
return tag;
|
||||
}
|
||||
|
||||
didSyncModelsOffline(items) {
|
||||
this.notifySyncObserversOfModels(items, ModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModels(items, source) {
|
||||
return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source);
|
||||
}
|
||||
@@ -139,7 +148,8 @@ class ModelManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
var unknownContentType = !_.includes(this.acceptableContentTypes, json_obj["content_type"]);
|
||||
let contentType = json_obj["content_type"] || (item && item.content_type);
|
||||
var unknownContentType = !_.includes(this.acceptableContentTypes, contentType);
|
||||
if(json_obj.deleted == true || unknownContentType) {
|
||||
if(item && !unknownContentType) {
|
||||
modelsToNotifyObserversOf.push(item);
|
||||
@@ -149,7 +159,7 @@ class ModelManager {
|
||||
}
|
||||
|
||||
if(!item) {
|
||||
item = this.createItem(json_obj);
|
||||
item = this.createItem(json_obj, true);
|
||||
}
|
||||
|
||||
this.addItem(item);
|
||||
@@ -172,6 +182,7 @@ class ModelManager {
|
||||
return models;
|
||||
}
|
||||
|
||||
/* Note that this function is public, and can also be called manually (desktopManager uses it) */
|
||||
notifySyncObserversOfModels(models, source) {
|
||||
for(var observer of this.itemSyncObservers) {
|
||||
var allRelevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"});
|
||||
@@ -202,7 +213,7 @@ class ModelManager {
|
||||
}
|
||||
}
|
||||
|
||||
createItem(json_obj) {
|
||||
createItem(json_obj, dontNotifyObservers) {
|
||||
var item;
|
||||
if(json_obj.content_type == "Note") {
|
||||
item = new Note(json_obj);
|
||||
@@ -217,13 +228,24 @@ class ModelManager {
|
||||
} else if(json_obj.content_type == "SN|Component") {
|
||||
item = new Component(json_obj);
|
||||
} else if(json_obj.content_type == "SF|Extension") {
|
||||
item = new SyncAdapter(json_obj);
|
||||
item = new ServerExtension(json_obj);
|
||||
} else if(json_obj.content_type == "SF|MFA") {
|
||||
item = new Mfa(json_obj);
|
||||
}
|
||||
|
||||
else {
|
||||
item = new Item(json_obj);
|
||||
}
|
||||
|
||||
// Some observers would be interested to know when an an item is locally created
|
||||
// If we don't send this out, these observers would have to wait until MappingSourceRemoteSaved
|
||||
// to hear about it, but sometimes, RemoveSaved is explicitly ignored by the observer to avoid
|
||||
// recursive callbacks. See componentManager's syncObserver callback.
|
||||
// dontNotifyObservers is currently only set true by modelManagers mapResponseItemsToLocalModels
|
||||
if(!dontNotifyObservers) {
|
||||
this.notifySyncObserversOfModels([item], ModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
item.addObserver(this, function(changedItem){
|
||||
this.notifyItemChangeObserversOfModels([changedItem]);
|
||||
}.bind(this));
|
||||
@@ -232,7 +254,7 @@ class ModelManager {
|
||||
}
|
||||
|
||||
createDuplicateItem(itemResponse, sourceItem) {
|
||||
var dup = this.createItem(itemResponse);
|
||||
var dup = this.createItem(itemResponse, true);
|
||||
this.resolveReferencesForItem(dup);
|
||||
return dup;
|
||||
}
|
||||
@@ -405,6 +427,25 @@ class ModelManager {
|
||||
|
||||
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Misc
|
||||
*/
|
||||
|
||||
humanReadableDisplayForContentType(contentType) {
|
||||
return {
|
||||
"Note" : "note",
|
||||
"Tag" : "tag",
|
||||
"Extension" : "action-based extension",
|
||||
"SN|Component" : "component",
|
||||
"SN|Editor" : "editor",
|
||||
"SN|Theme" : "theme",
|
||||
"SF|Extension" : "server extension",
|
||||
"SF|MFA" : "two-factor authentication setting"
|
||||
}[contentType];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('modelManager', ModelManager);
|
||||
angular.module('app').service('modelManager', ModelManager);
|
||||
|
||||
92
app/assets/javascripts/app/services/nativeExtManager.js
Normal file
92
app/assets/javascripts/app/services/nativeExtManager.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/* A class for handling installation of system extensions */
|
||||
|
||||
class NativeExtManager {
|
||||
|
||||
constructor(modelManager, syncManager, singletonManager) {
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.singletonManager = singletonManager;
|
||||
|
||||
this.extensionsIdentifier = "org.standardnotes.extensions-manager";
|
||||
this.systemExtensions = [];
|
||||
|
||||
this.resolveExtensionsManager();
|
||||
}
|
||||
|
||||
isSystemExtension(extension) {
|
||||
return this.systemExtensions.includes(extension.uuid);
|
||||
}
|
||||
|
||||
resolveExtensionsManager() {
|
||||
|
||||
this.singletonManager.registerSingleton({content_type: "SN|Component", package_info: {identifier: this.extensionsIdentifier}}, (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;
|
||||
}
|
||||
}
|
||||
|
||||
if(needsSync) {
|
||||
resolvedSingleton.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager");
|
||||
}
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
let url = window._extensions_manager_location;
|
||||
console.log("Installing Extensions Manager from URL", url);
|
||||
if(!url) {
|
||||
console.error("window._extensions_manager_location must be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
let packageInfo = {
|
||||
name: "Extensions",
|
||||
identifier: this.extensionsIdentifier
|
||||
}
|
||||
|
||||
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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager createNew");
|
||||
|
||||
this.systemExtensions.push(component.uuid);
|
||||
|
||||
valueCallback(component);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('nativeExtManager', NativeExtManager);
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('app.frontend')
|
||||
angular.module('app')
|
||||
.provider('passcodeManager', function () {
|
||||
|
||||
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||
@@ -41,7 +41,7 @@ angular.module('app.frontend')
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.setPasscode = function(passcode, callback) {
|
||||
this.setPasscode = (passcode, callback) => {
|
||||
var cost = Neeto.crypto.defaultPasswordGenerationCost();
|
||||
var salt = Neeto.crypto.generateRandomKey(512);
|
||||
var defaultParams = {pw_cost: cost, pw_salt: salt, version: "002"};
|
||||
@@ -60,6 +60,10 @@ angular.module('app.frontend')
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.changePasscode = (newPasscode, callback) => {
|
||||
this.setPasscode(newPasscode, callback);
|
||||
}
|
||||
|
||||
this.clearPasscode = function() {
|
||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
|
||||
storageManager.removeItem("offlineParams", StorageManager.Fixed);
|
||||
@@ -70,7 +74,8 @@ angular.module('app.frontend')
|
||||
this.encryptLocalStorage = function(keys) {
|
||||
storageManager.setKeys(keys);
|
||||
// Switch to Ephemeral storage, wiping Fixed storage
|
||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted);
|
||||
// Last argument is `force`, which we set to true because in the case of changing passcode
|
||||
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
|
||||
}
|
||||
|
||||
this.decryptLocalStorage = function(keys) {
|
||||
|
||||
174
app/assets/javascripts/app/services/singletonManager.js
Normal file
174
app/assets/javascripts/app/services/singletonManager.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
The SingletonManager allows controllers to register an item as a singleton, which means only one instance of that model
|
||||
should exist, both on the server and on the client. When the SingletonManager detects multiple items matching the singleton predicate,
|
||||
the oldest ones will be deleted, leaving the newest ones.
|
||||
|
||||
We will treat the model most recently arrived from the server as the most recent one. The reason for this is, if you're offline,
|
||||
a singleton can be created, as in the case of UserPreferneces. Then when you sign in, you'll retrieve your actual user preferences.
|
||||
In that case, even though the offline singleton has a more recent updated_at, the server retreived value is the one we care more about.
|
||||
*/
|
||||
|
||||
class SingletonManager {
|
||||
|
||||
constructor($rootScope, modelManager) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.singletonHandlers = [];
|
||||
|
||||
$rootScope.$on("initial-data-loaded", (event, data) => {
|
||||
this.resolveSingletons(modelManager.allItems, null, true);
|
||||
})
|
||||
|
||||
$rootScope.$on("sync:completed", (event, data) => {
|
||||
// The reason we also need to consider savedItems in consolidating singletons is in case of sync conflicts,
|
||||
// a new item can be created, but is never processed through "retrievedItems" since it is only created locally then saved.
|
||||
|
||||
// HOWEVER, by considering savedItems, we are now ruining everything, especially during sign in. A singleton can be created
|
||||
// offline, and upon sign in, will sync all items to the server, and by combining retrievedItems & savedItems, and only choosing
|
||||
// the latest, you are now resolving to the most recent one, which is in the savedItems list and not retrieved items, defeating
|
||||
// the whole purpose of this thing.
|
||||
|
||||
// Updated solution: resolveSingletons will now evaluate both of these arrays separately.
|
||||
this.resolveSingletons(data.retrievedItems, data.savedItems);
|
||||
})
|
||||
}
|
||||
|
||||
registerSingleton(predicate, resolveCallback, createBlock) {
|
||||
/*
|
||||
predicate: a key/value pair that specifies properties that should match in order for an item to be considered a predicate
|
||||
resolveCallback: called when one or more items are deleted and a new item becomes the reigning singleton
|
||||
createBlock: called when a sync is complete and no items are found. The createBlock should create the item and return it.
|
||||
*/
|
||||
this.singletonHandlers.push({
|
||||
predicate: predicate,
|
||||
resolutionCallback: resolveCallback,
|
||||
createBlock: createBlock
|
||||
});
|
||||
}
|
||||
|
||||
resolveSingletons(retrievedItems, savedItems, initialLoad) {
|
||||
retrievedItems = retrievedItems || [];
|
||||
savedItems = savedItems || [];
|
||||
|
||||
for(let singletonHandler of this.singletonHandlers) {
|
||||
var predicate = singletonHandler.predicate;
|
||||
let retrievedSingletonItems = this.filterItemsWithPredicate(retrievedItems, predicate);
|
||||
|
||||
// We only want to consider saved items count to see if it's more than 0, and do nothing else with it.
|
||||
// This way we know there was some action and things need to be resolved. The saved items will come up
|
||||
// in filterItemsWithPredicate(this.modelManager.allItems) and be deleted anyway
|
||||
let savedSingletonItemsCount = this.filterItemsWithPredicate(savedItems, predicate).length;
|
||||
|
||||
if(retrievedSingletonItems.length > 0 || savedSingletonItemsCount > 0) {
|
||||
/*
|
||||
Check local inventory and make sure only 1 similar item exists. If more than 1, delete oldest
|
||||
Note that this local inventory will also contain whatever is in retrievedItems.
|
||||
However, as stated in the header comment, retrievedItems take precendence over existing items,
|
||||
even if they have a lower updated_at value
|
||||
*/
|
||||
var allExtantItemsMatchingPredicate = this.filterItemsWithPredicate(this.modelManager.allItems, predicate);
|
||||
|
||||
/*
|
||||
If there are more than 1 matches, delete everything not in `retrievedSingletonItems`,
|
||||
then delete all but the latest in `retrievedSingletonItems`
|
||||
*/
|
||||
if(allExtantItemsMatchingPredicate.length >= 2) {
|
||||
|
||||
// Items that will be deleted
|
||||
var toDelete = [];
|
||||
// The item that will be chosen to be kept
|
||||
var winningItem, sorted;
|
||||
|
||||
if(retrievedSingletonItems.length > 0) {
|
||||
for(let extantItem of allExtantItemsMatchingPredicate) {
|
||||
if(!retrievedSingletonItems.includes(extantItem)) {
|
||||
// Delete it
|
||||
toDelete.push(extantItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort incoming singleton items by most recently updated first, then delete all the rest
|
||||
sorted = retrievedSingletonItems.sort((a, b) => {
|
||||
return a.updated_at < b.updated_at;
|
||||
})
|
||||
|
||||
} else {
|
||||
// We're in here because of savedItems
|
||||
// This can be the case if retrievedSingletonItems/retrievedItems length is 0, but savedSingletonItemsCount is non zero.
|
||||
// In this case, we want to sort by date and delete all but the most recent one
|
||||
sorted = allExtantItemsMatchingPredicate.sort((a, b) => {
|
||||
return a.updated_at < b.updated_at;
|
||||
});
|
||||
}
|
||||
|
||||
winningItem = sorted[0];
|
||||
|
||||
// Delete everything but the first one
|
||||
toDelete = toDelete.concat(sorted.slice(1, sorted.length));
|
||||
|
||||
for(var d of toDelete) {
|
||||
this.modelManager.setItemToBeDeleted(d);
|
||||
}
|
||||
|
||||
this.$rootScope.sync("resolveSingletons");
|
||||
|
||||
// Send remaining item to callback
|
||||
singletonHandler.singleton = winningItem;
|
||||
singletonHandler.resolutionCallback(winningItem);
|
||||
|
||||
} else if(allExtantItemsMatchingPredicate.length == 1) {
|
||||
if(!singletonHandler.singleton) {
|
||||
// Not yet notified interested parties of object
|
||||
var singleton = allExtantItemsMatchingPredicate[0];
|
||||
singletonHandler.singleton = singleton;
|
||||
singletonHandler.resolutionCallback(singleton);
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Retrieved items does not include any items of interest. If we don't have a singleton registered to this handler,
|
||||
// we need to create one. Only do this on actual sync completetions and not on initial data load. Because we want
|
||||
// to get the latest from the server before making the decision to create a new item
|
||||
if(!singletonHandler.singleton && !initialLoad && !singletonHandler.pendingCreateBlockCallback) {
|
||||
singletonHandler.pendingCreateBlockCallback = true;
|
||||
singletonHandler.createBlock((created) => {
|
||||
singletonHandler.singleton = created;
|
||||
singletonHandler.pendingCreateBlockCallback = false;
|
||||
singletonHandler.resolutionCallback(created);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterItemsWithPredicate(items, predicate) {
|
||||
return items.filter((candidate) => {
|
||||
return this.itemSatisfiesPredicate(candidate, predicate);
|
||||
})
|
||||
}
|
||||
|
||||
itemSatisfiesPredicate(candidate, predicate) {
|
||||
for(var key in predicate) {
|
||||
var predicateValue = predicate[key];
|
||||
var candidateValue = candidate[key];
|
||||
if(typeof predicateValue == 'object') {
|
||||
// Check nested properties
|
||||
if(!candidateValue) {
|
||||
// predicateValue is 'object' but candidateValue is null
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this.itemSatisfiesPredicate(candidateValue, predicateValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if(candidateValue != predicateValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('singletonManager', SingletonManager);
|
||||
@@ -62,9 +62,9 @@ class StorageManager {
|
||||
return this._memoryStorage;
|
||||
}
|
||||
|
||||
setItemsMode(mode) {
|
||||
setItemsMode(mode, force) {
|
||||
var newStorage = this.getVault(mode);
|
||||
if(newStorage !== this.storage) {
|
||||
if(newStorage !== this.storage || mode !== this.itemsStorageMode || force) {
|
||||
// transfer storages
|
||||
var length = this.storage.length;
|
||||
for(var i = 0; i < length; i++) {
|
||||
@@ -161,7 +161,6 @@ class StorageManager {
|
||||
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||
this.setItem(key, encryptedStorage.storage[key]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
@@ -228,4 +227,4 @@ StorageManager.FixedEncrypted = "FixedEncrypted"; // encrypted memoryStorage + l
|
||||
StorageManager.Ephemeral = "Ephemeral"; // memoryStorage
|
||||
StorageManager.Fixed = "Fixed"; // localStorage
|
||||
|
||||
angular.module('app.frontend').service('storageManager', StorageManager);
|
||||
angular.module('app').service('storageManager', StorageManager);
|
||||
|
||||
@@ -21,10 +21,6 @@ class SyncManager {
|
||||
return this.storageManager.getItem("mk");
|
||||
}
|
||||
|
||||
get serverPassword() {
|
||||
return this.storageManager.getItem("pw");
|
||||
}
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
if(items.length == 0) {
|
||||
callback && callback();
|
||||
@@ -62,6 +58,11 @@ class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.$rootScope.$broadcast("sync:completed", {});
|
||||
|
||||
// Required in order for modelManager to notify sync observers
|
||||
this.modelManager.didSyncModelsOffline(items);
|
||||
|
||||
if(callback) {
|
||||
callback({success: true});
|
||||
}
|
||||
@@ -92,7 +93,7 @@ class SyncManager {
|
||||
|
||||
let alternateNextItem = () => {
|
||||
if(index >= originalItems.length) {
|
||||
// We don't use originalItems as altnerating UUID will have deleted them.
|
||||
// We don't use originalItems as alternating UUID will have deleted them.
|
||||
block();
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +189,17 @@ class SyncManager {
|
||||
this.$interval.cancel(this.syncStatus.checker);
|
||||
}
|
||||
|
||||
sync(callback, options = {}) {
|
||||
sync(callback, options = {}, source) {
|
||||
|
||||
if(!options) options = {};
|
||||
|
||||
if(typeof callback == 'string') {
|
||||
// is source string, used to avoid filling parameters on call
|
||||
source = callback;
|
||||
callback = null;
|
||||
}
|
||||
|
||||
// console.log("Syncing from", source);
|
||||
|
||||
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||
|
||||
@@ -241,6 +252,11 @@ class SyncManager {
|
||||
this.allRetreivedItems = [];
|
||||
}
|
||||
|
||||
// We also want to do this for savedItems
|
||||
if(!this.allSavedItems) {
|
||||
this.allSavedItems = [];
|
||||
}
|
||||
|
||||
var version = this.authManager.protocolVersion();
|
||||
var keys = this.authManager.keys();
|
||||
|
||||
@@ -265,7 +281,17 @@ class SyncManager {
|
||||
|
||||
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
||||
|
||||
// Filter retrieved_items to remove any items that may be in saved_items for this complete sync operation
|
||||
// When signing in, and a user requires many round trips to complete entire retrieval of data, an item may be saved
|
||||
// on the first trip, then on subsequent trips using cursor_token, this same item may be returned, since it's date is
|
||||
// greater than cursor_token. We keep track of all saved items in whole sync operation with this.allSavedItems
|
||||
// We need this because singletonManager looks at retrievedItems as higher precendence than savedItems, but if it comes in both
|
||||
// then that's problematic.
|
||||
let allSavedUUIDs = this.allSavedItems.map((item) => {return item.uuid});
|
||||
response.retrieved_items = response.retrieved_items.filter((candidate) => {return !allSavedUUIDs.includes(candidate.uuid)});
|
||||
|
||||
// Map retrieved items to local data
|
||||
// Note that deleted items will not be returned
|
||||
var retrieved
|
||||
= this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
||||
|
||||
@@ -281,6 +307,9 @@ class SyncManager {
|
||||
var saved =
|
||||
this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
||||
|
||||
// Append items to master list of saved items for this ongoing sync operation
|
||||
this.allSavedItems = this.allSavedItems.concat(saved);
|
||||
|
||||
// Create copies of items or alternate their uuids if neccessary
|
||||
var unsaved = response.unsaved;
|
||||
this.handleUnsavedItemsResponse(unsaved)
|
||||
@@ -298,12 +327,12 @@ class SyncManager {
|
||||
|
||||
if(this.cursorToken || this.syncStatus.needsMoreSync) {
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options);
|
||||
this.sync(callback, options, "onSyncSuccess cursorToken || needsMoreSync");
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else if(this.repeatOnCompletion) {
|
||||
this.repeatOnCompletion = false;
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options);
|
||||
this.sync(callback, options, "onSyncSuccess repeatOnCompletion");
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else {
|
||||
this.writeItemsToLocalStorage(this.allRetreivedItems, false, null);
|
||||
@@ -319,10 +348,11 @@ class SyncManager {
|
||||
this.$rootScope.$broadcast("major-data-change");
|
||||
}
|
||||
|
||||
this.allRetreivedItems = [];
|
||||
|
||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||
this.$rootScope.$broadcast("sync:completed");
|
||||
this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems, savedItems: this.allSavedItems});
|
||||
|
||||
this.allRetreivedItems = [];
|
||||
this.allSavedItems = [];
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
@@ -391,14 +421,13 @@ class SyncManager {
|
||||
console.log("Handle unsaved", unsaved);
|
||||
|
||||
var i = 0;
|
||||
var handleNext = function() {
|
||||
var handleNext = () => {
|
||||
if(i >= unsaved.length) {
|
||||
// Handled all items
|
||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||
return;
|
||||
}
|
||||
|
||||
var handled = false;
|
||||
var mapping = unsaved[i];
|
||||
var itemResponse = mapping.item;
|
||||
EncryptionHelper.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
@@ -414,8 +443,10 @@ class SyncManager {
|
||||
if(error.tag === "uuid_conflict") {
|
||||
// UUID conflicts can occur if a user attempts to
|
||||
// import an old data archive with uuids from the old account into a new account
|
||||
handled = true;
|
||||
this.modelManager.alternateUUIDForItem(item, handleNext, true);
|
||||
this.modelManager.alternateUUIDForItem(item, () => {
|
||||
i++;
|
||||
handleNext();
|
||||
}, true);
|
||||
}
|
||||
|
||||
else if(error.tag === "sync_conflict") {
|
||||
@@ -425,20 +456,16 @@ class SyncManager {
|
||||
itemResponse.uuid = null;
|
||||
|
||||
var dup = this.modelManager.createDuplicateItem(itemResponse, item);
|
||||
if(!itemResponse.deleted && JSON.stringify(item.structureParams()) !== JSON.stringify(dup.structureParams())) {
|
||||
if(!itemResponse.deleted && !item.isItemContentEqualWith(dup)) {
|
||||
this.modelManager.addItem(dup);
|
||||
dup.conflict_of = item.uuid;
|
||||
dup.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
if(!handled) {
|
||||
i++;
|
||||
handleNext();
|
||||
}
|
||||
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
handleNext();
|
||||
}
|
||||
@@ -459,4 +486,4 @@ class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('syncManager', SyncManager);
|
||||
angular.module('app').service('syncManager', SyncManager);
|
||||
|
||||
@@ -1,99 +1,45 @@
|
||||
class ThemeManager {
|
||||
|
||||
constructor(modelManager, syncManager, $rootScope, storageManager) {
|
||||
this.syncManager = syncManager;
|
||||
this.modelManager = modelManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
constructor(componentManager, desktopManager) {
|
||||
this.componentManager = componentManager;
|
||||
|
||||
get themes() {
|
||||
return this.modelManager.itemsForContentType("SN|Theme");
|
||||
}
|
||||
desktopManager.registerUpdateObserver((component) => {
|
||||
// Reload theme if active
|
||||
if(component.active && component.isTheme()) {
|
||||
this.deactivateTheme(component);
|
||||
setTimeout(() => {
|
||||
this.activateTheme(component);
|
||||
}, 10);
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
activeTheme: computed property that returns saved theme
|
||||
currentTheme: stored variable that allows other classes to watch changes
|
||||
*/
|
||||
|
||||
get activeTheme() {
|
||||
var activeThemeId = this.storageManager.getItem("activeTheme");
|
||||
if(!activeThemeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var theme = _.find(this.themes, {uuid: activeThemeId});
|
||||
return theme;
|
||||
}
|
||||
|
||||
activateInitialTheme() {
|
||||
var theme = this.activeTheme;
|
||||
if(theme) {
|
||||
this.activateTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
submitTheme(url) {
|
||||
var name = this.displayNameForThemeFile(this.fileNameFromPath(url));
|
||||
var theme = this.modelManager.createItem({content_type: "SN|Theme", url: url, name: name});
|
||||
this.modelManager.addItem(theme);
|
||||
theme.setDirty(true);
|
||||
this.syncManager.sync();
|
||||
componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => {
|
||||
if(component.active) {
|
||||
this.activateTheme(component);
|
||||
} else {
|
||||
this.deactivateTheme(component);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
activateTheme(theme) {
|
||||
var activeTheme = this.activeTheme;
|
||||
if(activeTheme) {
|
||||
this.deactivateTheme(activeTheme);
|
||||
}
|
||||
|
||||
var url = this.componentManager.urlForComponent(theme);
|
||||
var link = document.createElement("link");
|
||||
link.href = theme.url;
|
||||
link.href = url;
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.media = "screen,print";
|
||||
link.id = theme.uuid;
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
this.storageManager.setItem("activeTheme", theme.uuid);
|
||||
|
||||
this.currentTheme = theme;
|
||||
this.$rootScope.$broadcast("theme-changed");
|
||||
}
|
||||
|
||||
deactivateTheme(theme) {
|
||||
this.storageManager.removeItem("activeTheme");
|
||||
var element = document.getElementById(theme.uuid);
|
||||
if(element) {
|
||||
element.disabled = true;
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
|
||||
this.currentTheme = null;
|
||||
this.$rootScope.$broadcast("theme-changed");
|
||||
}
|
||||
|
||||
isThemeActive(theme) {
|
||||
return this.storageManager.getItem("activeTheme") === theme.uuid;
|
||||
}
|
||||
|
||||
fileNameFromPath(filePath) {
|
||||
return filePath.replace(/^.*[\\\/]/, '');
|
||||
}
|
||||
|
||||
capitalizeString(string) {
|
||||
return string.replace(/(?:^|\s)\S/g, function(a) { return a.toUpperCase(); });
|
||||
}
|
||||
|
||||
displayNameForThemeFile(fileName) {
|
||||
let fromParam = getParameterByName("name", fileName);
|
||||
if(fromParam) {
|
||||
return fromParam;
|
||||
}
|
||||
let name = fileName.split(".")[0];
|
||||
let cleaned = name.split("-").join(" ");
|
||||
return this.capitalizeString(cleaned);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('themeManager', ThemeManager);
|
||||
angular.module('app').service('themeManager', ThemeManager);
|
||||
|
||||
Reference in New Issue
Block a user