(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o> 3] >> idx % 8 * 4 & 15; var v = c == 'x' ? r : r & 0x3 | 0x8; return v.toString(16); }); } else { var d = new Date().getTime(); if (window.performance && typeof window.performance.now === "function") { d += performance.now(); //use high-precision timer if available } var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c == 'x' ? r : r & 0x3 | 0x8).toString(16); }); return uuid; } } }, { key: 'decryptText', value: function decryptText(encrypted_content, key) { var keyData = CryptoJS.enc.Hex.parse(key); var ivData = CryptoJS.enc.Hex.parse(""); var decrypted = CryptoJS.AES.decrypt(encrypted_content, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); } }, { key: 'encryptText', value: function encryptText(text, key) { var keyData = CryptoJS.enc.Hex.parse(key); var ivData = CryptoJS.enc.Hex.parse(""); var encrypted = CryptoJS.AES.encrypt(text, keyData, { iv: ivData, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } }, { key: 'generateRandomEncryptionKey', value: function generateRandomEncryptionKey() { var salt = Neeto.crypto.generateRandomKey(); var passphrase = Neeto.crypto.generateRandomKey(); return CryptoJS.PBKDF2(passphrase, salt, { keySize: 512 / 32 }).toString(); } }, { key: 'firstHalfOfKey', value: function firstHalfOfKey(key) { return key.substring(0, key.length / 2); } }, { key: 'secondHalfOfKey', value: function secondHalfOfKey(key) { return key.substring(key.length / 2, key.length); } }, { key: 'base64', value: function base64(text) { return CryptoJS.enc.Utf8.parse(text).toString(CryptoJS.enc.Base64); } }, { key: 'base64Decode', value: function base64Decode(base64String) { return CryptoJS.enc.Base64.parse(base64String).toString(CryptoJS.enc.Utf8); } }, { key: 'sha256', value: function sha256(text) { return CryptoJS.SHA256(text).toString(); } }, { key: 'sha1', value: function sha1(text) { return CryptoJS.SHA1(text).toString(); } }, { key: 'hmac256', value: function hmac256(message, key) { var keyData = CryptoJS.enc.Hex.parse(key); var messageData = CryptoJS.enc.Utf8.parse(message); return CryptoJS.HmacSHA256(messageData, keyData).toString(); } }, { key: 'computeEncryptionKeysForUser', value: function computeEncryptionKeysForUser() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, password = _ref.password, pw_salt = _ref.pw_salt, pw_func = _ref.pw_func, pw_alg = _ref.pw_alg, pw_cost = _ref.pw_cost, pw_key_size = _ref.pw_key_size; var callback = arguments[1]; this.generateSymmetricKeyPair({ password: password, pw_salt: pw_salt, pw_func: pw_func, pw_alg: pw_alg, pw_cost: pw_cost, pw_key_size: pw_key_size }, function (keys) { var pw = keys[0]; var mk = keys[1]; callback({ pw: pw, mk: mk }); }); } }, { key: 'generateInitialEncryptionKeysForUser', value: function generateInitialEncryptionKeysForUser() { var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, email = _ref2.email, password = _ref2.password; var callback = arguments[1]; var defaults = this.defaultPasswordGenerationParams(); var pw_func = defaults.pw_func, pw_alg = defaults.pw_alg, pw_key_size = defaults.pw_key_size, pw_cost = defaults.pw_cost; var pw_nonce = this.generateRandomKey(); var pw_salt = this.sha1(email + "SN" + pw_nonce); _.merge(defaults, { pw_salt: pw_salt, pw_nonce: pw_nonce }); this.generateSymmetricKeyPair(_.merge({ email: email, password: password, pw_salt: pw_salt }, defaults), function (keys) { var pw = keys[0]; var mk = keys[1]; callback({ pw: pw, mk: mk }, defaults); }); } }]); return SNCrypto; }(); exports.SNCrypto = SNCrypto; var SNCryptoJS = function (_SNCrypto) { _inherits(SNCryptoJS, _SNCrypto); function SNCryptoJS() { _classCallCheck(this, SNCryptoJS); return _possibleConstructorReturn(this, (SNCryptoJS.__proto__ || Object.getPrototypeOf(SNCryptoJS)).apply(this, arguments)); } _createClass(SNCryptoJS, [{ key: 'generateSymmetricKeyPair', /** Generates two deterministic keys based on one input */ value: function generateSymmetricKeyPair() { var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, password = _ref3.password, pw_salt = _ref3.pw_salt, pw_func = _ref3.pw_func, pw_alg = _ref3.pw_alg, pw_cost = _ref3.pw_cost, pw_key_size = _ref3.pw_key_size; var callback = arguments[1]; var algMapping = { "sha256": CryptoJS.algo.SHA256, "sha512": CryptoJS.algo.SHA512 }; var fnMapping = { "pbkdf2": CryptoJS.PBKDF2 }; var alg = algMapping[pw_alg]; var kdf = fnMapping[pw_func]; var output = kdf(password, pw_salt, { keySize: pw_key_size / 32, hasher: alg, iterations: pw_cost }).toString(); var outputLength = output.length; var firstHalf = output.slice(0, outputLength / 2); var secondHalf = output.slice(outputLength / 2, outputLength); callback([firstHalf, secondHalf]); } }, { key: 'defaultPasswordGenerationParams', value: function defaultPasswordGenerationParams() { return { pw_func: "pbkdf2", pw_alg: "sha512", pw_key_size: 512, pw_cost: 3000 }; } }]); return SNCryptoJS; }(SNCrypto); exports.SNCryptoJS = SNCryptoJS; var subtleCrypto = window.crypto.subtle; var SNCryptoWeb = function (_SNCrypto2) { _inherits(SNCryptoWeb, _SNCrypto2); function SNCryptoWeb() { _classCallCheck(this, SNCryptoWeb); return _possibleConstructorReturn(this, (SNCryptoWeb.__proto__ || Object.getPrototypeOf(SNCryptoWeb)).apply(this, arguments)); } _createClass(SNCryptoWeb, [{ key: 'defaultPasswordGenerationParams', /** Overrides */ value: function defaultPasswordGenerationParams() { return { pw_func: "pbkdf2", pw_alg: "sha512", pw_key_size: 512, pw_cost: 5000 }; } /** Generates two deterministic keys based on one input */ }, { key: 'generateSymmetricKeyPair', value: function generateSymmetricKeyPair() { var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, password = _ref4.password, pw_salt = _ref4.pw_salt, pw_func = _ref4.pw_func, pw_alg = _ref4.pw_alg, pw_cost = _ref4.pw_cost, pw_key_size = _ref4.pw_key_size; var callback = arguments[1]; this.stretchPassword({ password: password, pw_func: pw_func, pw_alg: pw_alg, pw_salt: pw_salt, pw_cost: pw_cost, pw_key_size: pw_key_size }, function (output) { var outputLength = output.length; var firstHalf = output.slice(0, outputLength / 2); var secondHalf = output.slice(outputLength / 2, outputLength); callback([firstHalf, secondHalf]); }); } /** Internal */ }, { key: 'stretchPassword', value: function stretchPassword() { var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, password = _ref5.password, pw_salt = _ref5.pw_salt, pw_cost = _ref5.pw_cost, pw_func = _ref5.pw_func, pw_alg = _ref5.pw_alg, pw_key_size = _ref5.pw_key_size; var callback = arguments[1]; this.webCryptoImportKey(password, pw_func, function (key) { if (!key) { console.log("Key is null, unable to continue"); callback(null); return; } this.webCryptoDeriveBits({ key: key, pw_func: pw_func, pw_alg: pw_alg, pw_salt: pw_salt, pw_cost: pw_cost, pw_key_size: pw_key_size }, function (key) { if (!key) { callback(null); return; } callback(key); }.bind(this)); }.bind(this)); } }, { key: 'webCryptoImportKey', value: function webCryptoImportKey(input, pw_func, callback) { subtleCrypto.importKey("raw", this.stringToArrayBuffer(input), { name: pw_func.toUpperCase() }, false, ["deriveBits"]).then(function (key) { callback(key); }).catch(function (err) { console.error(err); callback(null); }); } }, { key: 'webCryptoDeriveBits', value: function webCryptoDeriveBits() { var _ref6 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, key = _ref6.key, pw_func = _ref6.pw_func, pw_alg = _ref6.pw_alg, pw_salt = _ref6.pw_salt, pw_cost = _ref6.pw_cost, pw_key_size = _ref6.pw_key_size; var callback = arguments[1]; var algMapping = { "sha256": "SHA-256", "sha512": "SHA-512" }; var alg = algMapping[pw_alg]; subtleCrypto.deriveBits({ "name": pw_func.toUpperCase(), salt: this.stringToArrayBuffer(pw_salt), iterations: pw_cost, hash: { name: alg } }, key, pw_key_size).then(function (bits) { var key = this.arrayBufferToHexString(new Uint8Array(bits)); callback(key); }.bind(this)).catch(function (err) { console.error(err); callback(null); }); } }, { key: 'stringToArrayBuffer', value: function stringToArrayBuffer(string) { var encoder = new TextEncoder("utf-8"); return encoder.encode(string); } }, { key: 'arrayBufferToHexString', value: function arrayBufferToHexString(arrayBuffer) { var byteArray = new Uint8Array(arrayBuffer); var hexString = ""; var nextHexByte; for (var i = 0; i < byteArray.byteLength; i++) { nextHexByte = byteArray[i].toString(16); if (nextHexByte.length < 2) { nextHexByte = "0" + nextHexByte; } hexString += nextHexByte; } return hexString; } }]); return SNCryptoWeb; }(SNCrypto); exports.SNCryptoWeb = SNCryptoWeb; 'use strict'; var Neeto = Neeto || {}; if (window.crypto.subtle) { Neeto.crypto = new SNCryptoWeb(); } else { Neeto.crypto = new SNCryptoJS(); } angular.module('app.frontend', ['ui.router', 'restangular', 'ngDialog']).config(function (RestangularProvider, apiControllerProvider) { RestangularProvider.setDefaultHeaders({ "Content-Type": "application/json" }); var url = apiControllerProvider.defaultServerURL(); RestangularProvider.setBaseUrl(url + "/api"); RestangularProvider.setFullRequestInterceptor(function (element, operation, route, url, headers, params, httpConfig) { var token = localStorage.getItem("jwt"); if (token) { headers = _.extend(headers, { Authorization: "Bearer " + localStorage.getItem("jwt") }); } return { element: element, params: params, headers: headers, httpConfig: httpConfig }; }); });angular.module('app.frontend').config(function ($stateProvider, $urlRouterProvider, $locationProvider) { $stateProvider.state('base', { abstract: true }).state('home', { url: '/', parent: 'base', views: { 'content@': { templateUrl: 'frontend/home.html', controller: 'HomeCtrl' } } }); // Default fall back route $urlRouterProvider.otherwise(function ($injector, $location) { var state = $injector.get('$state'); state.go('home'); return $location.path(); }); // enable HTML5 Mode for SEO $locationProvider.html5Mode(true); }); ; var BaseCtrl = function BaseCtrl($rootScope, modelManager, apiController, dbManager) { _classCallCheck(this, BaseCtrl); apiController.getCurrentUser(function () {}); dbManager.openDatabase(null, function () { // new database, delete syncToken so that items can be refetched entirely from server apiController.clearSyncToken(); apiController.sync(); }); }; angular.module('app.frontend').controller('BaseCtrl', BaseCtrl); ;angular.module('app.frontend').directive("editorSection", function ($timeout) { return { restrict: 'E', scope: { save: "&", remove: "&", note: "=" }, templateUrl: 'frontend/editor.html', replace: true, controller: 'EditorCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) { var handler = function handler(event) { if (event.ctrlKey || event.metaKey) { switch (String.fromCharCode(event.which).toLowerCase()) { case 's': event.preventDefault(); $timeout(function () { ctrl.saveNote(event); }); break; case 'e': event.preventDefault(); $timeout(function () { ctrl.clickedEditNote(); }); break; case 'm': event.preventDefault(); $timeout(function () { ctrl.toggleMarkdown(); }); break; case 'o': event.preventDefault(); $timeout(function () { ctrl.toggleFullScreen(); }); break; } } }; window.addEventListener('keydown', handler); scope.$on('$destroy', function () { window.removeEventListener('keydown', handler); }); scope.$watch('ctrl.note', function (note, oldNote) { if (note) { ctrl.setNote(note, oldNote); } else { ctrl.note = {}; } }); } }; }).controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager) { this.setNote = function (note, oldNote) { this.editorMode = 'edit'; if (note.safeText().length == 0 && note.dummy) { this.focusTitle(100); } if (oldNote && oldNote != note) { if (oldNote.hasChanges) { this.save()(oldNote, null); } else if (oldNote.dummy) { this.remove()(oldNote); } } }; this.hasAvailableExtensions = function () { return extensionManager.extensionsInContextOfItem(this.note).length > 0; }; this.onPreviewDoubleClick = function () { this.editorMode = 'edit'; this.focusEditor(100); }; this.focusEditor = function (delay) { setTimeout(function () { var element = document.getElementById("note-text-editor"); element.focus(); }, delay); }; this.focusTitle = function (delay) { setTimeout(function () { document.getElementById("note-title-editor").focus(); }, delay); }; this.clickedTextArea = function () { this.showMenu = false; }; this.renderedContent = function () { return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.safeText())); }; var statusTimeout; this.saveNote = function ($event) { var note = this.note; note.dummy = false; this.save()(note, function (success) { if (success) { apiController.clearDraft(); if (statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function () { this.noteStatus = "All changes saved"; }.bind(this), 200); } else { if (statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function () { this.noteStatus = "(Offline) — All changes saved"; }.bind(this), 200); } }.bind(this)); }; this.saveTitle = function ($event) { $event.target.blur(); this.saveNote($event); this.focusEditor(); }; var saveTimeout; this.changesMade = function () { this.note.hasChanges = true; this.note.dummy = false; if (apiController.isUserSignedIn()) { // signed out users have local autosave, dont need draft saving apiController.saveDraftToDisk(this.note); } if (saveTimeout) $timeout.cancel(saveTimeout); if (statusTimeout) $timeout.cancel(statusTimeout); saveTimeout = $timeout(function () { this.noteStatus = "Saving..."; this.saveNote(); }.bind(this), 275); }; this.contentChanged = function () { this.changesMade(); }; this.nameChanged = function () { this.changesMade(); }; this.onNameFocus = function () { this.editingName = true; }; this.onContentFocus = function () { this.showSampler = false; $rootScope.$broadcast("editorFocused"); this.editingUrl = false; }; this.onNameBlur = function () { this.editingName = false; }; this.toggleFullScreen = function () { this.fullscreen = !this.fullscreen; if (this.fullscreen) { if (this.editorMode == 'edit') { // refocus this.focusEditor(0); } } else {} }; this.selectedMenuItem = function () { this.showMenu = false; }; this.toggleMarkdown = function () { if (this.editorMode == 'preview') { this.editorMode = 'edit'; } else { this.editorMode = 'preview'; } }; this.editUrlPressed = function () { this.showMenu = false; var url = this.publicUrlForNote(this.note); url = url.replace(this.note.presentation_name, ""); this.url = { base: url, token: this.note.presentation_name }; this.editingUrl = true; }; this.saveUrl = function ($event) { $event.target.blur(); var original = this.note.presentation_name; this.note.presentation_name = this.url.token; this.note.setDirty(true); apiController.sync(function (response) { if (response && response.error) { this.note.presentation_name = original; this.url.token = original; alert("This URL is not available."); } else { this.editingUrl = false; } }.bind(this)); }; this.shareNote = function () { function openInNewTab(url) { var a = document.createElement("a"); a.target = "_blank"; a.href = url; a.click(); } apiController.shareItem(this.note, function (note) { openInNewTab(this.publicUrlForNote(note)); }.bind(this)); this.showMenu = false; }; this.unshareNote = function () { apiController.unshareItem(this.note, function (note) {}); this.showMenu = false; }; this.publicUrlForNote = function () { return this.note.presentationURL(); }; this.clickedMenu = function () { if (this.note.locked) { alert("This note has been shared without an account, and can therefore not be changed."); } else { this.showMenu = !this.showMenu; } }; this.deleteNote = function () { apiController.clearDraft(); this.remove()(this.note); this.showMenu = false; }; this.clickedEditNote = function () { this.editorMode = 'edit'; this.focusEditor(100); }; }); ;angular.module('app.frontend').directive("header", function (apiController, extensionManager) { return { restrict: 'E', scope: {}, templateUrl: 'frontend/header.html', replace: true, controller: 'HeaderCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) { scope.$on("sync:updated_token", function () { ctrl.syncUpdated(); }); } }; }).controller('HeaderCtrl', function ($state, apiController, modelManager, $timeout, extensionManager) { this.user = apiController.user; this.extensionManager = extensionManager; this.changePasswordPressed = function () { this.showNewPasswordForm = !this.showNewPasswordForm; }; this.accountMenuPressed = function () { this.serverData = { url: apiController.getServer() }; this.showAccountMenu = !this.showAccountMenu; this.showFaq = false; this.showNewPasswordForm = false; this.showExtensionsMenu = false; }; this.toggleExtensions = function () { this.showAccountMenu = false; this.showExtensionsMenu = !this.showExtensionsMenu; }; this.toggleExtensionForm = function () { this.newExtensionData = {}; this.showNewExtensionForm = !this.showNewExtensionForm; }; this.submitNewExtensionForm = function () { if (this.newExtensionData.url) { extensionManager.addExtension(this.newExtensionData.url, function (response) { if (!response) { alert("Unable to register this extension. Make sure the link is valid and try again."); } else { this.newExtensionData.url = ""; this.showNewExtensionForm = false; } }.bind(this)); } }; this.selectedAction = function (action, extension) { action.running = true; extensionManager.executeAction(action, extension, null, function (response) { action.running = false; if (response && response.error) { action.error = true; alert("There was an error performing this action. Please try again."); } else { action.error = false; apiController.sync(null); } }); }; this.deleteExtension = function (extension) { if (confirm("Are you sure you want to delete this extension?")) { extensionManager.deleteExtension(extension); } }; this.reloadExtensionsPressed = function () { if (confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) { extensionManager.refreshExtensionsFromServer(); } }; this.changeServer = function () { apiController.setServer(this.serverData.url, true); }; this.signOutPressed = function () { this.showAccountMenu = false; apiController.signout(function () { window.location.reload(); }); }; this.submitPasswordChange = function () { this.passwordChangeData.status = "Generating New Keys..."; $timeout(function () { if (data.password != data.password_confirmation) { alert("Your new password does not match its confirmation."); return; } apiController.changePassword(this.passwordChangeData.current_password, this.passwordChangeData.new_password, function (response) {}); }.bind(this)); }; this.hasLocalData = function () { return modelManager.filteredNotes.length > 0; }; this.mergeLocalChanged = function () { if (!this.user.shouldMerge) { if (!confirm("Unchecking this option means any locally stored tags and notes you have now will be deleted. Are you sure you want to continue?")) { this.user.shouldMerge = true; } } }; this.refreshData = function () { this.isRefreshing = true; apiController.sync(function (response) { $timeout(function () { this.isRefreshing = false; }.bind(this), 200); if (response && response.error) { alert("There was an error syncing. Please try again. If all else fails, log out and log back in."); } else { this.syncUpdated(); } }.bind(this)); }; this.syncUpdated = function () { this.lastSyncDate = new Date(); }; this.loginSubmitPressed = function () { this.loginData.status = "Generating Login Keys..."; $timeout(function () { apiController.login(this.loginData.email, this.loginData.user_password, function (response) { if (!response || response.error) { var error = response ? response.error : { message: "An unknown error occured." }; this.loginData.status = null; alert(error.message); } else { this.onAuthSuccess(response.user); } }.bind(this)); }.bind(this)); }; this.submitRegistrationForm = function () { this.loginData.status = "Generating Account Keys..."; $timeout(function () { apiController.register(this.loginData.email, this.loginData.user_password, function (response) { if (!response || response.error) { var error = response ? response.error : { message: "An unknown error occured." }; this.loginData.status = null; alert(error.message); } else { this.onAuthSuccess(response.user); } }.bind(this)); }.bind(this)); }; this.encryptionStatusForNotes = function () { var allNotes = modelManager.filteredNotes; var countEncrypted = 0; allNotes.forEach(function (note) { if (note.encryptionEnabled()) { countEncrypted++; } }.bind(this)); return countEncrypted + "/" + allNotes.length + " notes encrypted"; }; this.archiveEncryptionFormat = { encrypted: true }; this.downloadDataArchive = function () { var link = document.createElement('a'); link.setAttribute('download', 'notes.json'); link.href = apiController.itemsDataFile(this.archiveEncryptionFormat.encrypted); link.click(); }; this.performImport = function (data, password) { this.importData.loading = true; // allow loading indicator to come up with timeout $timeout(function () { apiController.importJSONData(data, password, function (success, response) { console.log("Import response:", success, response); this.importData.loading = false; if (success) { this.importData = null; } else { alert("There was an error importing your data. Please try again."); } }.bind(this)); }.bind(this)); }; this.submitImportPassword = function () { this.performImport(this.importData.data, this.importData.password); }; this.importFileSelected = function (files) { this.importData = {}; var file = files[0]; var reader = new FileReader(); reader.onload = function (e) { var data = JSON.parse(e.target.result); $timeout(function () { if (data.auth_params) { // request password this.importData.requestPassword = true; this.importData.data = data; } else { this.performImport(data, null); } }.bind(this)); }.bind(this); reader.readAsText(file); }; this.onAuthSuccess = function (user) { // if(this.user.shouldMerge && this.hasLocalData()) { // apiController.mergeLocalDataRemotely(this.user, function(){ // window.location.reload(); // }); // } else { window.location.reload(); // } this.showLogin = false; this.showRegistration = false; }; }); ;angular.module('app.frontend').controller('HomeCtrl', function ($scope, $rootScope, $timeout, apiController, modelManager) { $rootScope.bodyClass = "app-body-class"; apiController.loadLocalItems(function (items) { $scope.$apply(); apiController.sync(null); // refresh every 30s setInterval(function () { apiController.sync(null); }, 2000); }); $scope.allTag = new Tag({ all: true }); $scope.allTag.title = "All"; $scope.tags = modelManager.tags; $scope.allTag.notes = modelManager.notes; /* Tags Ctrl Callbacks */ $scope.updateAllTag = function () { // $scope.allTag.notes = modelManager.notes; }; $scope.tagsWillMakeSelection = function (tag) { if (tag.all) { $scope.updateAllTag(); } }; $scope.tagsSelectionMade = function (tag) { $scope.selectedTag = tag; if ($scope.selectedNote && $scope.selectedNote.dummy) { modelManager.removeItemLocally($scope.selectedNote); } }; $scope.tagsAddNew = function (tag) { modelManager.addItem(tag); }; $scope.tagsSave = function (tag, callback) { tag.setDirty(true); apiController.sync(callback); }; /* Called to update the tag of a note after drag and drop change The note object is a copy of the original */ $scope.tagsUpdateNoteTag = function (noteCopy, newTag, oldTag) { var originalNote = _.find(modelManager.notes, { uuid: noteCopy.uuid }); if (!newTag.all) { modelManager.createRelationshipBetweenItems(newTag, originalNote); } apiController.sync(function () {}); }; /* Notes Ctrl Callbacks */ $scope.notesRemoveTag = function (tag) { var validNotes = Note.filterDummyNotes(tag.notes); if (validNotes == 0) { modelManager.setItemToBeDeleted(tag); // if no more notes, delete tag apiController.sync(function () { // force scope tags to update on sub directives $scope.tags = []; $timeout(function () { $scope.tags = modelManager.tags; }); }); } else { alert("To delete this tag, remove all its notes first."); } }; $scope.notesSelectionMade = function (note) { $scope.selectedNote = note; }; $scope.notesAddNew = function (note) { modelManager.addItem(note); if (!$scope.selectedTag.all) { modelManager.createRelationshipBetweenItems($scope.selectedTag, note); $scope.updateAllTag(); } }; /* Shared Callbacks */ $scope.saveNote = function (note, callback) { note.setDirty(true); apiController.sync(function (response) { if (response && response.error) { if (!$scope.didShowErrorAlert) { $scope.didShowErrorAlert = true; alert("There was an error saving your note. Please try again."); } callback(false); } else { note.hasChanges = false; if (callback) { callback(true); } } }); }; $scope.deleteNote = function (note) { modelManager.setItemToBeDeleted(note); if (note == $scope.selectedNote) { $scope.selectedNote = null; } if (note.dummy) { modelManager.removeItemLocally(note); return; } apiController.sync(null); }; }); ;angular.module('app.frontend').directive("notesSection", function () { return { scope: { addNew: "&", selectionMade: "&", remove: "&", tag: "=", removeTag: "&" }, templateUrl: 'frontend/notes.html', replace: true, controller: 'NotesCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) { scope.$watch('ctrl.tag', function (tag, oldTag) { if (tag) { ctrl.tagDidChange(tag, oldTag); } }); } }; }).controller('NotesCtrl', function (apiController, $timeout, $rootScope, modelManager) { $rootScope.$on("editorFocused", function () { this.showMenu = false; }.bind(this)); var isFirstLoad = true; this.tagDidChange = function (tag, oldTag) { this.showMenu = false; if (this.selectedNote && this.selectedNote.dummy) { _.remove(oldTag.notes, this.selectedNote); } this.noteFilter.text = ""; tag.notes.forEach(function (note) { note.visible = true; }); this.selectFirstNote(false); if (isFirstLoad) { $timeout(function () { var draft = apiController.getDraft(); if (draft) { var note = draft; this.selectNote(note); } else { this.createNewNote(); isFirstLoad = false; } }.bind(this)); } else if (tag.notes.length == 0) { this.createNewNote(); } }; this.selectedTagDelete = function () { this.showMenu = false; this.removeTag()(this.tag); }; this.selectedTagShare = function () { this.showMenu = false; if (!apiController.isUserSignedIn()) { alert("You must be signed in to share a tag."); return; } if (this.tag.all) { alert("You cannot share the 'All' tag."); return; } apiController.shareItem(this.tag, function (response) {}); }; this.selectedTagUnshare = function () { this.showMenu = false; apiController.unshareItem(this.tag, function (response) {}); }; this.selectFirstNote = function (createNew) { var visibleNotes = this.tag.notes.filter(function (note) { return note.visible; }); if (visibleNotes.length > 0) { this.selectNote(visibleNotes[0]); } else if (createNew) { this.createNewNote(); } }; this.selectNote = function (note) { this.selectedNote = note; this.selectionMade()(note); }; this.createNewNote = function () { var title = "New Note" + (this.tag.notes ? " " + (this.tag.notes.length + 1) : ""); this.newNote = modelManager.createItem({ content_type: "Note", dummy: true, text: "" }); this.newNote.title = title; this.selectNote(this.newNote); this.addNew()(this.newNote); }; this.noteFilter = { text: '' }; this.filterNotes = function (note) { var filterText = this.noteFilter.text.toLowerCase(); if (filterText.length == 0) { note.visible = true; } else { note.visible = note.safeTitle().toLowerCase().includes(filterText) || note.safeText().toLowerCase().includes(filterText); } return note.visible; }.bind(this); this.filterTextChanged = function () { $timeout(function () { if (!this.selectedNote.visible) { this.selectFirstNote(false); } }.bind(this), 100); }; }); ;angular.module('app.frontend').directive("tagsSection", function () { return { restrict: 'E', scope: { addNew: "&", selectionMade: "&", willSelect: "&", save: "&", tags: "=", allTag: "=", updateNoteTag: "&" }, templateUrl: 'frontend/tags.html', replace: true, controller: 'TagsCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) { scope.$watch('ctrl.tags', function (newTags) { if (newTags) { ctrl.setTags(newTags); } }); scope.$watch('ctrl.allTag', function (allTag) { if (allTag) { ctrl.setAllTag(allTag); } }); } }; }).controller('TagsCtrl', function (modelManager) { var initialLoad = true; this.setAllTag = function (allTag) { this.selectTag(this.allTag); }; this.setTags = function (tags) { if (initialLoad) { initialLoad = false; this.selectTag(this.allTag); } else { if (tags && tags.length > 0) { this.selectTag(tags[0]); } } }; this.selectTag = function (tag) { this.willSelect()(tag); this.selectedTag = tag; this.selectionMade()(tag); }; this.clickedAddNewTag = function () { if (this.editingTag) { return; } this.newTag = modelManager.createItem({ content_type: "Tag" }); this.selectedTag = this.newTag; this.editingTag = this.newTag; this.addNew()(this.newTag); }; var originalTagName = ""; this.onTagTitleFocus = function (tag) { originalTagName = tag.title; }; this.tagTitleDidChange = function (tag) { this.editingTag = tag; }; this.saveTag = function ($event, tag) { this.editingTag = null; if (tag.title.length == 0) { tag.title = originalTagName; originalTagName = ""; return; } $event.target.blur(); if (!tag.title || tag.title.length == 0) { return; } this.save()(tag, function (savedTag) { // _.merge(tag, savedTag); this.selectTag(tag); this.newTag = null; }.bind(this)); }; this.noteCount = function (tag) { var validNotes = Note.filterDummyNotes(tag.notes); return validNotes.length; }; this.handleDrop = function (e, newTag, note) { this.updateNoteTag()(note, newTag, this.selectedTag); }.bind(this); }); ;angular.module('app.frontend').controller('UsernameModalCtrl', function ($scope, apiController, Restangular, callback, $timeout) { $scope.formData = {}; $scope.saveUsername = function () { apiController.setUsername($scope.formData.username, function (response) { var username = response.username; callback(username); $scope.closeThisDialog(); }); }; }); ; var Item = function () { function Item(json_obj) { _classCallCheck(this, Item); this.updateFromJSON(json_obj); this.observers = []; if (!this.uuid) { this.uuid = Neeto.crypto.generateUUID(); } } _createClass(Item, [{ key: 'updateFromJSON', value: function updateFromJSON(json) { _.merge(this, json); if (this.created_at) { this.created_at = new Date(this.created_at); this.updated_at = new Date(this.updated_at); } else { this.created_at = new Date(); this.updated_at = new Date(); } if (json.content) { this.mapContentToLocalProperties(this.contentObject); } } }, { key: 'alternateUUID', value: function alternateUUID() { this.uuid = Neeto.crypto.generateUUID(); } }, { key: 'setDirty', value: function setDirty(dirty) { this.dirty = dirty; if (dirty) { this.notifyObserversOfChange(); } } }, { key: 'markAllReferencesDirty', value: function markAllReferencesDirty() { this.allReferencedObjects().forEach(function (reference) { reference.setDirty(true); }); } }, { key: 'addObserver', value: function addObserver(observer, callback) { if (!_.find(this.observers, observer)) { this.observers.push({ observer: observer, callback: callback }); } } }, { key: 'removeObserver', value: function removeObserver(observer) { _.remove(this.observers, { observer: observer }); } }, { key: 'notifyObserversOfChange', value: function notifyObserversOfChange() { var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = this.observers[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var observer = _step.value; observer.callback(this); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } }, { key: 'mapContentToLocalProperties', value: function mapContentToLocalProperties(contentObj) {} }, { key: 'createContentJSONFromProperties', value: function createContentJSONFromProperties() { return this.structureParams(); } }, { key: 'referenceParams', value: function referenceParams() { // must override } }, { key: 'structureParams', value: function structureParams() { return { references: this.referenceParams() }; } }, { key: 'addItemAsRelationship', value: function addItemAsRelationship(item) { // must override } }, { key: 'removeItemAsRelationship', value: function removeItemAsRelationship(item) { // must override } }, { key: 'removeAllRelationships', value: function removeAllRelationships() { // must override } }, { key: 'mergeMetadataFromItem', value: function mergeMetadataFromItem(item) { _.merge(this, _.omit(item, ["content"])); } }, { key: 'allReferencedObjects', value: function allReferencedObjects() { // must override return []; } }, { key: 'referencesAffectedBySharingChange', value: function referencesAffectedBySharingChange() { // should be overriden to determine which references should be decrypted/encrypted return []; } }, { key: 'isPublic', value: function isPublic() { return this.presentation_name; } }, { key: 'isEncrypted', value: function isEncrypted() { return this.encryptionEnabled() && this.content.substring(0, 3) === '001' ? true : false; } }, { key: 'encryptionEnabled', value: function encryptionEnabled() { return this.enc_item_key; } }, { key: 'presentationURL', value: function presentationURL() { return this.presentation_url; } }, { key: 'contentObject', get: function get() { if (!this.content) { return {}; } if (this.content !== null && _typeof(this.content) === 'object') { // this is the case when mapping localStorage content, in which case the content is already parsed return this.content; } return JSON.parse(this.content); } }], [{ key: 'sortItemsByDate', value: function sortItemsByDate(items) { items.sort(function (a, b) { return new Date(b.created_at) - new Date(a.created_at); }); } }]); return Item; }(); ; var Action = function () { function Action(json) { _classCallCheck(this, Action); _.merge(this, json); this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory this.error = false; if (this.lastExecuted) { // is string this.lastExecuted = new Date(this.lastExecuted); } } _createClass(Action, [{ key: 'permissionsString', get: function get() { if (!this.permissions) { return ""; } var permission = this.permissions.charAt(0).toUpperCase() + this.permissions.slice(1); // capitalize first letter permission += ": "; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = this.content_types[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var contentType = _step2.value; if (contentType == "*") { permission += "All items"; } else { permission += contentType; } permission += " "; } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } return permission; } }, { key: 'encryptionModeString', get: function get() { if (this.verb != "post") { return null; } var encryptionMode = "This action accepts data "; if (this.accepts_encrypted && this.accepts_decrypted) { encryptionMode += "encrypted or decrypted."; } else { if (this.accepts_encrypted) { encryptionMode += "encrypted."; } else { encryptionMode += "decrypted."; } } return encryptionMode; } }]); return Action; }(); var Extension = function (_Item) { _inherits(Extension, _Item); function Extension(json) { _classCallCheck(this, Extension); var _this3 = _possibleConstructorReturn(this, (Extension.__proto__ || Object.getPrototypeOf(Extension)).call(this, json)); _.merge(_this3, json); _this3.encrypted = true; _this3.content_type = "Extension"; return _this3; } _createClass(Extension, [{ key: 'actionsInGlobalContext', value: function actionsInGlobalContext() { return this.actions.filter(function (action) { return action.context == "global"; }); } }, { key: 'actionsWithContextForItem', value: function actionsWithContextForItem(item) { return this.actions.filter(function (action) { return action.context == item.content_type || action.context == "Item"; }); } }, { key: 'mapContentToLocalProperties', value: function mapContentToLocalProperties(contentObject) { _get(Extension.prototype.__proto__ || Object.getPrototypeOf(Extension.prototype), 'mapContentToLocalProperties', this).call(this, contentObject); this.name = contentObject.name; this.url = contentObject.url; this.actions = contentObject.actions.map(function (action) { return new Action(action); }); } }, { key: 'updateFromExternalResponseItem', value: function updateFromExternalResponseItem(externalResponseItem) { _.merge(this, externalResponseItem); this.actions = externalResponseItem.actions.map(function (action) { return new Action(action); }); } }, { key: 'referenceParams', value: function referenceParams() { return null; } }, { key: 'structureParams', value: function structureParams() { var params = { name: this.name, url: this.url, actions: this.actions }; _.merge(params, _get(Extension.prototype.__proto__ || Object.getPrototypeOf(Extension.prototype), 'structureParams', this).call(this)); return params; } }]); return Extension; }(Item); ; var Note = function (_Item2) { _inherits(Note, _Item2); function Note(json_obj) { _classCallCheck(this, Note); var _this4 = _possibleConstructorReturn(this, (Note.__proto__ || Object.getPrototypeOf(Note)).call(this, json_obj)); if (!_this4.tags) { _this4.tags = []; } return _this4; } _createClass(Note, [{ key: 'mapContentToLocalProperties', value: function mapContentToLocalProperties(contentObject) { _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'mapContentToLocalProperties', this).call(this, contentObject); this.title = contentObject.title; this.text = contentObject.text; } }, { key: 'referenceParams', value: function referenceParams() { var references = _.map(this.tags, function (tag) { return { uuid: tag.uuid, content_type: tag.content_type }; }); return references; } }, { key: 'structureParams', value: function structureParams() { var params = { title: this.title, text: this.text }; _.merge(params, _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'structureParams', this).call(this)); return params; } }, { key: 'addItemAsRelationship', value: function addItemAsRelationship(item) { if (item.content_type == "Tag") { if (!_.find(this.tags, item)) { this.tags.push(item); } } _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'addItemAsRelationship', this).call(this, item); } }, { key: 'removeItemAsRelationship', value: function removeItemAsRelationship(item) { if (item.content_type == "Tag") { _.pull(this.tags, item); } _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'removeItemAsRelationship', this).call(this, item); } }, { key: 'removeAllRelationships', value: function removeAllRelationships() { this.tags.forEach(function (tag) { _.pull(tag.notes, this); tag.setDirty(true); }.bind(this)); this.tags = []; } }, { key: 'allReferencedObjects', value: function allReferencedObjects() { return this.tags; } }, { key: 'referencesAffectedBySharingChange', value: function referencesAffectedBySharingChange() { return _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'referencesAffectedBySharingChange', this).call(this); } }, { key: 'safeText', value: function safeText() { return this.text || ""; } }, { key: 'safeTitle', value: function safeTitle() { return this.title || ""; } }, { key: 'toJSON', value: function toJSON() { return { uuid: this.uuid }; } }, { key: 'isSharedIndividually', value: function isSharedIndividually() { return this.presentation_name; } }, { key: 'isPublic', value: function isPublic() { return _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'isPublic', this).call(this) || this.hasOnePublicTag; } }, { key: 'hasOnePublicTag', get: function get() { var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = this.tags[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var tag = _step3.value; if (tag.isPublic()) { return true; } } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } return false; } }, { key: 'content_type', get: function get() { return "Note"; } }], [{ key: 'filterDummyNotes', value: function filterDummyNotes(notes) { var filtered = notes.filter(function (note) { return note.dummy == false || note.dummy == null; }); return filtered; } }]); return Note; }(Item); ; var Tag = function (_Item3) { _inherits(Tag, _Item3); function Tag(json_obj) { _classCallCheck(this, Tag); var _this5 = _possibleConstructorReturn(this, (Tag.__proto__ || Object.getPrototypeOf(Tag)).call(this, json_obj)); if (!_this5.notes) { _this5.notes = []; } return _this5; } _createClass(Tag, [{ key: 'mapContentToLocalProperties', value: function mapContentToLocalProperties(contentObject) { _get(Tag.prototype.__proto__ || Object.getPrototypeOf(Tag.prototype), 'mapContentToLocalProperties', this).call(this, contentObject); this.title = contentObject.title; } }, { key: 'referenceParams', value: function referenceParams() { var references = _.map(this.notes, function (note) { return { uuid: note.uuid, content_type: note.content_type }; }); return references; } }, { key: 'structureParams', value: function structureParams() { var params = { title: this.title }; _.merge(params, _get(Tag.prototype.__proto__ || Object.getPrototypeOf(Tag.prototype), 'structureParams', this).call(this)); return params; } }, { key: 'addItemAsRelationship', value: function addItemAsRelationship(item) { if (item.content_type == "Note") { if (!_.find(this.notes, item)) { this.notes.unshift(item); } } _get(Tag.prototype.__proto__ || Object.getPrototypeOf(Tag.prototype), 'addItemAsRelationship', this).call(this, item); } }, { key: 'removeItemAsRelationship', value: function removeItemAsRelationship(item) { if (item.content_type == "Note") { _.pull(this.notes, item); } _get(Tag.prototype.__proto__ || Object.getPrototypeOf(Tag.prototype), 'removeItemAsRelationship', this).call(this, item); } }, { key: 'removeAllRelationships', value: function removeAllRelationships() { this.notes.forEach(function (note) { _.pull(note.tags, this); note.setDirty(true); }.bind(this)); this.notes = []; } }, { key: 'allReferencedObjects', value: function allReferencedObjects() { return this.notes; } }, { key: 'referencesAffectedBySharingChange', value: function referencesAffectedBySharingChange() { return this.notes; } }, { key: 'content_type', get: function get() { return "Tag"; } }]); return Tag; }(Item); ;angular.module('app.frontend').provider('apiController', function () { function domainName() { var domain_comps = location.hostname.split("."); var domain = domain_comps[domain_comps.length - 2] + "." + domain_comps[domain_comps.length - 1]; return domain; } var url; this.defaultServerURL = function () { if (!url) { url = localStorage.getItem("server"); if (!url) { url = "https://n3.standardnotes.org"; } } return url; }; this.$get = function ($rootScope, Restangular, modelManager, ngDialog, dbManager) { return new ApiController($rootScope, Restangular, modelManager, ngDialog, dbManager); }; function ApiController($rootScope, Restangular, modelManager, ngDialog, dbManager) { this.user = {}; this.syncToken = localStorage.getItem("syncToken"); /* Config */ this.getServer = function () { if (!url) { url = localStorage.getItem("server"); if (!url) { url = "https://n3.standardnotes.org"; this.setServer(url); } } return url; }; this.setServer = function (url, refresh) { localStorage.setItem("server", url); if (refresh) { window.location.reload(); } }; /* Auth */ this.getAuthParams = function () { return JSON.parse(localStorage.getItem("auth_params")); }; this.isUserSignedIn = function () { return localStorage.getItem("jwt"); }; this.userId = function () { return localStorage.getItem("uuid"); }; this.getAuthParamsForEmail = function (email, callback) { var request = Restangular.one("auth", "params"); request.get({ email: email }).then(function (response) { callback(response.plain()); }).catch(function (response) { console.log("Error getting current user", response); callback(response.data); }); }; this.getCurrentUser = function (callback) { if (!localStorage.getItem("jwt")) { callback(null); return; } Restangular.one("users/current").get().then(function (response) { var user = response.plain(); _.merge(this.user, user); callback(); }.bind(this)).catch(function (response) { console.log("Error getting current user", response); callback(response.data); }); }; this.login = function (email, password, callback) { this.getAuthParamsForEmail(email, function (authParams) { if (!authParams) { callback(null); return; } Neeto.crypto.computeEncryptionKeysForUser(_.merge({ password: password }, authParams), function (keys) { this.setMk(keys.mk); var request = Restangular.one("auth/sign_in"); var params = { password: keys.pw, email: email }; _.merge(request, params); request.post().then(function (response) { localStorage.setItem("jwt", response.token); localStorage.setItem("uuid", response.uuid); localStorage.setItem("auth_params", JSON.stringify(authParams)); callback(response); }).catch(function (response) { callback(response.data); }); }.bind(this)); }.bind(this)); }; this.register = function (email, password, callback) { Neeto.crypto.generateInitialEncryptionKeysForUser({ password: password, email: email }, function (keys, authParams) { this.setMk(keys.mk); keys.mk = null; var request = Restangular.one("auth"); var params = _.merge({ password: keys.pw, email: email }, authParams); _.merge(request, params); request.post().then(function (response) { localStorage.setItem("jwt", response.token); localStorage.setItem("uuid", response.uuid); localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); callback(response); }).catch(function (response) { callback(response.data); }); }.bind(this)); }; // this.changePassword = function(current_password, new_password) { // this.getAuthParamsForEmail(email, function(authParams){ // if(!authParams) { // callback(null); // return; // } // Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: current_password, email: user.email}, authParams), function(currentKeys) { // Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: new_password, email: user.email}, authParams), function(newKeys){ // var data = {}; // data.current_password = currentKeys.pw; // data.password = newKeys.pw; // data.password_confirmation = newKeys.pw; // // var user = this.user; // // this._performPasswordChange(currentKeys, newKeys, function(response){ // if(response && !response.error) { // // this.showNewPasswordForm = false; // // reencrypt data with new mk // this.reencryptAllItemsAndSave(user, newKeys.mk, currentKeys.mk, function(success){ // if(success) { // this.setMk(newKeys.mk); // alert("Your password has been changed and your data re-encrypted."); // } else { // // rollback password // this._performPasswordChange(newKeys, currentKeys, function(response){ // alert("There was an error changing your password. Your password has been rolled back."); // window.location.reload(); // }) // } // }.bind(this)); // } else { // // this.showNewPasswordForm = false; // alert("There was an error changing your password. Please try again."); // } // }.bind(this)) // }.bind(this)); // }.bind(this)); // }.bind(this)); // } this._performPasswordChange = function (email, current_keys, new_keys, callback) { var request = Restangular.one("auth"); var params = { password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email }; _.merge(request, params); request.patch().then(function (response) { callback(response); }); }; /* User */ this.setUsername = function (username, callback) { var request = Restangular.one("users", this.userId()); request.username = username; request.patch().then(function (response) { this.user.username = response.username; callback(response.plain()); }.bind(this)); }; /* Items */ this.setSyncToken = function (syncToken) { this.syncToken = syncToken; localStorage.setItem("syncToken", this.syncToken); }; this.syncWithOptions = function (callback) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (this.syncOpInProgress) { // will perform anoter sync after current completes this.repeatSync = true; return; } this.syncOpInProgress = true; var allDirtyItems = modelManager.getDirtyItems(); var submitLimit = 100; var dirtyItems = allDirtyItems.slice(0, submitLimit); if (dirtyItems.length < allDirtyItems.length) { // more items left to be synced, repeat this.repeatSync = true; } else { this.repeatSync = false; } if (!this.isUserSignedIn()) { this.writeItemsToLocalStorage(dirtyItems, function (responseItems) { // delete anything needing to be deleted dirtyItems.forEach(function (item) { if (item.deleted) { modelManager.removeItemLocally(item); } }.bind(this)); modelManager.clearDirtyItems(dirtyItems); if (callback) { callback(); } }.bind(this)); return; } var request = Restangular.one("items/sync"); request.limit = 150; request.sync_token = this.syncToken; request.cursor_token = this.cursorToken; request.items = _.map(dirtyItems, function (item) { return this.createRequestParamsForItem(item, options.additionalFields); }.bind(this)); request.post().then(function (response) { modelManager.clearDirtyItems(dirtyItems); // handle sync token this.setSyncToken(response.sync_token); $rootScope.$broadcast("sync:updated_token", this.syncToken); // handle cursor token (more results waiting, perform another sync) this.cursorToken = response.cursor_token; var retrieved = this.handleItemsResponse(response.retrieved_items, null); // merge only metadata for saved items var omitFields = ["content", "enc_item_key", "auth_hash"]; var saved = this.handleItemsResponse(response.saved_items, omitFields); this.handleUnsavedItemsResponse(response.unsaved); this.writeItemsToLocalStorage(saved, null); this.writeItemsToLocalStorage(retrieved, null); this.syncOpInProgress = false; if (this.cursorToken || this.repeatSync == true) { this.syncWithOptions(callback, options); } else { if (callback) { callback(response); } } }.bind(this)).catch(function (response) { console.log("Sync error: ", response); if (callback) { callback({ error: "Sync error" }); } }); }; this.sync = function (callback) { this.syncWithOptions(callback, undefined); }; this.handleUnsavedItemsResponse = function (unsaved) { if (unsaved.length == 0) { return; } console.log("Handle unsaved", unsaved); var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = unsaved[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var mapping = _step4.value; var itemResponse = mapping.item; var item = modelManager.findItem(itemResponse.uuid); var error = mapping.error; if (error.tag == "uuid_conflict") { item.alternateUUID(); item.setDirty(true); item.markAllReferencesDirty(); } } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } this.syncWithOptions(null, { additionalFields: ["created_at", "updated_at"] }); }; this.handleItemsResponse = function (responseItems, omitFields) { this.decryptItems(responseItems); return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); }; this.createRequestParamsForItem = function (item, additionalFields) { return this.paramsForItem(item, !item.isPublic(), additionalFields, false); }; this.paramsForExportFile = function (item, encrypted) { return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]); }; this.paramsForExtension = function (item, encrypted) { return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]); }; this.paramsForItem = function (item, encrypted, additionalFields, forExportFile) { var itemCopy = _.cloneDeep(item); console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy); var params = { uuid: item.uuid, content_type: item.content_type, presentation_name: item.presentation_name, deleted: item.deleted }; if (encrypted) { this.encryptSingleItem(itemCopy, this.retrieveMk()); params.content = itemCopy.content; params.enc_item_key = itemCopy.enc_item_key; params.auth_hash = itemCopy.auth_hash; } else { params.content = forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties())); if (!forExportFile) { params.enc_item_key = null; params.auth_hash = null; } } if (additionalFields) { _.merge(params, _.pick(item, additionalFields)); } return params; }; this.shareItem = function (item, callback) { if (!this.isUserSignedIn()) { alert("You must be signed in to share."); return; } var shareFn = function () { item.presentation_name = "_auto_"; var needsUpdate = [item].concat(item.referencesAffectedBySharingChange() || []); needsUpdate.forEach(function (needingUpdate) { needingUpdate.setDirty(true); }); this.sync(); }.bind(this); if (!this.user.username) { ngDialog.open({ template: 'frontend/modals/username.html', controller: 'UsernameModalCtrl', resolve: { callback: function callback() { return shareFn; } }, className: 'ngdialog-theme-default', disableAnimation: true }); } else { shareFn(); } }; this.unshareItem = function (item, callback) { item.presentation_name = null; var needsUpdate = [item].concat(item.referencesAffectedBySharingChange() || []); needsUpdate.forEach(function (needingUpdate) { needingUpdate.setDirty(true); }); this.sync(null); }; /* Import */ this.clearSyncToken = function () { this.syncToken = null; localStorage.removeItem("syncToken"); }; this.importJSONData = function (data, password, callback) { console.log("Importing data", data); var onDataReady = function () { var items = modelManager.mapResponseItemsToLocalModels(data.items); items.forEach(function (item) { item.setDirty(true); item.markAllReferencesDirty(); }); this.syncWithOptions(callback, { additionalFields: ["created_at", "updated_at"] }); }.bind(this); if (data.auth_params) { Neeto.crypto.computeEncryptionKeysForUser(_.merge({ password: password }, data.auth_params), function (keys) { var mk = keys.mk; try { this.decryptItemsWithKey(data.items, mk); // 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; }); onDataReady(); } 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(false, null); return; } }.bind(this)); } else { onDataReady(); } }; /* Export */ this.itemsDataFile = function (encrypted) { var textFile = null; var makeTextFile = function (text) { var data = new Blob([text], { type: 'text/json' }); // 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; }.bind(this); var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function (item) { return this.paramsForExportFile(item, encrypted); }.bind(this)); var data = { items: items }; if (encrypted) { data["auth_params"] = this.getAuthParams(); } return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */)); }; /* Merging */ // this.mergeLocalDataRemotely = function(user, callback) { // var request = Restangular.one("users", this.userId()).one("merge"); // var tags = user.tags; // request.items = user.items; // request.items.forEach(function(item){ // if(item.tag_id) { // var tag = tags.filter(function(tag){return tag.uuid == item.tag_id})[0]; // item.tag_name = tag.title; // } // }) // request.post().then(function(response){ // callback(); // localStorage.removeItem('user'); // }) // } this.staticifyObject = function (object) { return JSON.parse(JSON.stringify(object)); }; this.writeItemsToLocalStorage = function (items, callback) { var params = items.map(function (item) { return this.paramsForItem(item, this.isUserSignedIn(), ["created_at", "updated_at", "presentation_url", "dirty"], false); }.bind(this)); dbManager.saveItems(params, callback); }; this.loadLocalItems = function (callback) { var params = dbManager.getAllItems(function (items) { var items = this.handleItemsResponse(items, null); Item.sortItemsByDate(items); callback(items); }.bind(this)); }; /* Drafts */ this.saveDraftToDisk = function (draft) { localStorage.setItem("draft", JSON.stringify(draft)); }; this.clearDraft = function () { localStorage.removeItem("draft"); }; this.getDraft = function () { var draftString = localStorage.getItem("draft"); if (!draftString || draftString == 'undefined') { return null; } var jsonObj = _.merge({ content_type: "Note" }, JSON.parse(draftString)); return modelManager.createItem(jsonObj); }; /* Encrpytion */ this.retrieveMk = function () { if (!this.mk) { this.mk = localStorage.getItem("mk"); } return this.mk; }; this.setMk = function (mk) { localStorage.setItem('mk', mk); }; this.signout = function (callback) { dbManager.clearAllItems(function () { localStorage.clear(); callback(); }); }; this.encryptSingleItem = function (item, masterKey) { var item_key = null; if (item.enc_item_key) { item_key = Neeto.crypto.decryptText(item.enc_item_key, masterKey); } else { item_key = Neeto.crypto.generateRandomEncryptionKey(); item.enc_item_key = Neeto.crypto.encryptText(item_key, masterKey); } var ek = Neeto.crypto.firstHalfOfKey(item_key); var ak = Neeto.crypto.secondHalfOfKey(item_key); var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek); var authHash = Neeto.crypto.hmac256(encryptedContent, ak); item.content = encryptedContent; item.auth_hash = authHash; item.local_encryption_scheme = "1.0"; }; this.decryptSingleItem = function (item, masterKey) { var item_key = Neeto.crypto.decryptText(item.enc_item_key, masterKey); var ek = Neeto.crypto.firstHalfOfKey(item_key); var ak = Neeto.crypto.secondHalfOfKey(item_key); var authHash = Neeto.crypto.hmac256(item.content, ak); if (authHash !== item.auth_hash || !item.auth_hash) { console.log("Authentication hash does not match."); return; } var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek); item.content = content; }; this.decryptItems = function (items) { var masterKey = this.retrieveMk(); this.decryptItemsWithKey(items, masterKey); }; this.decryptItemsWithKey = function (items, key) { var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = items[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var item = _step5.value; if (item.deleted == true) { continue; } var isString = typeof item.content === 'string' || item.content instanceof String; if (isString) { if (item.content.substring(0, 3) == "001" && item.enc_item_key) { // is encrypted this.decryptSingleItem(item, key); } else { // is base64 encoded item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)); } } } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } }; this.reencryptAllItemsAndSave = function (user, newMasterKey, oldMasterKey, callback) { var items = modelManager.allItems(); items.forEach(function (item) { if (item.content.substring(0, 3) == "001" && item.enc_item_key) { // first decrypt item_key with old key var item_key = Neeto.crypto.decryptText(item.enc_item_key, oldMasterKey); // now encrypt item_key with new key item.enc_item_key = Neeto.crypto.encryptText(item_key, newMasterKey); } }); this.saveBatchItems(user, items, function (success) { callback(success); }.bind(this)); }; } }); ; var DBManager = function () { function DBManager() { _classCallCheck(this, DBManager); } _createClass(DBManager, [{ key: 'openDatabase', value: function openDatabase(callback, onUgradeNeeded) { var request = window.indexedDB.open("standardnotes", 1); request.onerror = function (event) { alert("Please enable permissions for Standard Notes to use IndexedDB for offline mode."); if (callback) { callback(null); } }; request.onsuccess = function (event) { // console.log("Successfully opened database", event.target.result); var db = event.target.result; db.onerror = function (errorEvent) { console.log("Database error: " + errorEvent.target.errorCode); }; if (callback) { callback(db); } }; request.onupgradeneeded = function (event) { var db = event.target.result; if (db.version === 1) { if (onUgradeNeeded) { onUgradeNeeded(); } } // Create an objectStore for this database var objectStore = db.createObjectStore("items", { keyPath: "uuid" }); objectStore.createIndex("title", "title", { unique: false }); objectStore.createIndex("uuid", "uuid", { unique: true }); objectStore.transaction.oncomplete = function (event) { // Ready to store values in the newly created objectStore. }; }; } }, { key: 'getAllItems', value: function getAllItems(callback) { this.openDatabase(function (db) { var objectStore = db.transaction("items").objectStore("items"); var items = []; objectStore.openCursor().onsuccess = function (event) { var cursor = event.target.result; if (cursor) { items.push(cursor.value); cursor.continue(); } else { callback(items); } }; }, null); } }, { key: 'saveItem', value: function saveItem(item) { saveItems([item]); } }, { key: 'saveItems', value: function saveItems(items, callback) { if (items.length == 0) { if (callback) { callback(); } return; } this.openDatabase(function (db) { var transaction = db.transaction("items", "readwrite"); transaction.oncomplete = function (event) {}; transaction.onerror = function (event) { console.log("Transaction error:", event.target.errorCode); }; var itemObjectStore = transaction.objectStore("items"); var i = 0; putNext(); function putNext() { if (i < items.length) { var item = items[i]; itemObjectStore.put(item).onsuccess = putNext; ++i; } else { if (callback) { callback(); } } } }, null); } }, { key: 'deleteItem', value: function deleteItem(item) { this.openDatabase(function (db) { var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid); request.onsuccess = function (event) { console.log("Successfully deleted item", item.uuid); }; }, null); } }, { key: 'getItemByUUID', value: function getItemByUUID(uuid, callback) { this.openDatabase(function (db) { var request = db.transaction("items", "readonly").objectStore("items").get(uuid); request.onsuccess = function (event) { callback(event.result); }; }, null); } }, { key: 'clearAllItems', value: function clearAllItems(callback) { this.openDatabase(function (db) { var request = db.transaction("items", "readwrite").objectStore("items").clear(); request.onsuccess = function (event) { console.log("Successfully cleared items"); callback(); }; }, null); } }]); return DBManager; }(); angular.module('app.frontend').service('dbManager', DBManager); ;angular.module('app.frontend').directive('mbAutofocus', ['$timeout', function ($timeout) { return { restrict: 'A', scope: { shouldFocus: "=" }, link: function link($scope, $element) { $timeout(function () { if ($scope.shouldFocus) { $element[0].focus(); } }); } }; }]); ; var ContextualExtensionsMenu = function () { function ContextualExtensionsMenu() { _classCallCheck(this, ContextualExtensionsMenu); this.restrict = "E"; this.templateUrl = "frontend/directives/contextual-menu.html"; this.scope = { item: "=" }; } _createClass(ContextualExtensionsMenu, [{ key: 'controller', value: function controller($scope, modelManager, extensionManager) { 'ngInject'; $scope.extensions = extensionManager.extensionsInContextOfItem($scope.item); $scope.executeAction = function (action, extension) { action.running = true; extensionManager.executeAction(action, extension, $scope.item, function (response) { action.running = false; }); }; $scope.accessTypeForExtension = function (extension) { return extensionManager.extensionUsesEncryptedData(extension) ? "encrypted" : "decrypted"; }; } }]); return ContextualExtensionsMenu; }(); angular.module('app.frontend').directive('contextualExtensionsMenu', function () { return new ContextualExtensionsMenu(); }); ;angular.module('app.frontend').directive('draggable', function () { return { scope: { note: "=" }, link: function link(scope, element) { // this gives us the native JS object var el = element[0]; el.draggable = true; el.addEventListener('dragstart', function (e) { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('Note', JSON.stringify(scope.note)); this.classList.add('drag'); return false; }, false); el.addEventListener('dragend', function (e) { this.classList.remove('drag'); return false; }, false); } }; }); angular.module('app.frontend').directive('droppable', function () { return { scope: { drop: '&', bin: '=', tag: "=" }, link: function link(scope, element) { // again we need the native object var el = element[0]; el.addEventListener('dragover', function (e) { e.dataTransfer.dropEffect = 'move'; // allows us to drop if (e.preventDefault) e.preventDefault(); this.classList.add('over'); return false; }, false); var counter = 0; el.addEventListener('dragenter', function (e) { counter++; this.classList.add('over'); return false; }, false); el.addEventListener('dragleave', function (e) { counter--; if (counter === 0) { this.classList.remove('over'); } return false; }, false); el.addEventListener('drop', function (e) { // Stops some browsers from redirecting. if (e.stopPropagation) e.stopPropagation(); this.classList.remove('over'); var binId = this.uuid; var note = new Note(JSON.parse(e.dataTransfer.getData('Note'))); scope.$apply(function (scope) { var fn = scope.drop(); if ('undefined' !== typeof fn) { fn(e, scope.tag, note); } }); return false; }, false); } }; }); ;angular.module('app.frontend').directive('fileChange', function () { return { restrict: 'A', scope: { handler: '&' }, link: function link(scope, element) { element.on('change', function (event) { scope.$apply(function () { scope.handler({ files: event.target.files }); }); }); } }; }); ;angular.module('app.frontend').directive('lowercase', function () { return { require: 'ngModel', link: function link(scope, element, attrs, modelCtrl) { var lowercase = function lowercase(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]); } }; }); ;angular.module('app.frontend').directive('selectOnClick', ['$window', function ($window) { return { restrict: 'A', link: function link(scope, element, attrs) { element.on('focus', function () { if (!$window.getSelection().toString()) { // Required for mobile Safari this.setSelectionRange(0, this.value.length); } }); } }; }]); ; var ExtensionManager = function () { function ExtensionManager(Restangular, modelManager, apiController) { _classCallCheck(this, ExtensionManager); this.Restangular = Restangular; this.modelManager = modelManager; this.apiController = apiController; this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || []; this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || []; modelManager.addItemSyncObserver("extensionManager", "Extension", function (items) { var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = items[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { var ext = _step6.value; ext.encrypted = this.extensionUsesEncryptedData(ext); var _iteratorNormalCompletion7 = true; var _didIteratorError7 = false; var _iteratorError7 = undefined; try { for (var _iterator7 = ext.actions[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { var action = _step7.value; if (this.enabledRepeatActionUrls.includes(action.url)) { this.enableRepeatAction(action, ext); } } } catch (err) { _didIteratorError7 = true; _iteratorError7 = err; } finally { try { if (!_iteratorNormalCompletion7 && _iterator7.return) { _iterator7.return(); } } finally { if (_didIteratorError7) { throw _iteratorError7; } } } } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } }.bind(this)); } _createClass(ExtensionManager, [{ key: 'extensionsInContextOfItem', value: function extensionsInContextOfItem(item) { return this.extensions.filter(function (ext) { return ext.actionsWithContextForItem(item).length > 0; }); } }, { key: 'actionWithURL', value: function actionWithURL(url) { var _iteratorNormalCompletion8 = true; var _didIteratorError8 = false; var _iteratorError8 = undefined; try { for (var _iterator8 = this.extensions[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { var extension = _step8.value; return _.find(extension.actions, { url: url }); } } catch (err) { _didIteratorError8 = true; _iteratorError8 = err; } finally { try { if (!_iteratorNormalCompletion8 && _iterator8.return) { _iterator8.return(); } } finally { if (_didIteratorError8) { throw _iteratorError8; } } } } }, { key: 'extensionUsesEncryptedData', value: function extensionUsesEncryptedData(extension) { return !this.decryptedExtensions.includes(extension.url); } }, { key: 'changeExtensionEncryptionFormat', value: function changeExtensionEncryptionFormat(encrypted, extension) { if (encrypted) { _.pull(this.decryptedExtensions, extension.url); } else { this.decryptedExtensions.push(extension.url); } localStorage.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions)); extension.encrypted = this.extensionUsesEncryptedData(extension); } }, { key: 'addExtension', value: function addExtension(url, callback) { this.retrieveExtensionFromServer(url, callback); } }, { key: 'deleteExtension', value: function deleteExtension(extension) { var _iteratorNormalCompletion9 = true; var _didIteratorError9 = false; var _iteratorError9 = undefined; try { for (var _iterator9 = extension.actions[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { var action = _step9.value; _.pull(this.decryptedExtensions, extension); if (action.repeat_mode) { if (this.isRepeatActionEnabled(action)) { this.disableRepeatAction(action); } } } } catch (err) { _didIteratorError9 = true; _iteratorError9 = err; } finally { try { if (!_iteratorNormalCompletion9 && _iterator9.return) { _iterator9.return(); } } finally { if (_didIteratorError9) { throw _iteratorError9; } } } this.modelManager.setItemToBeDeleted(extension); this.apiController.sync(null); } }, { key: 'retrieveExtensionFromServer', value: function retrieveExtensionFromServer(url, callback) { this.Restangular.oneUrl(url, url).get().then(function (response) { var ext = this.handleExtensionLoadExternalResponseItem(url, response.plain()); if (callback) { callback(ext); } }.bind(this)).catch(function (response) { console.log("Error registering extension", response); callback(null); }); } }, { key: 'handleExtensionLoadExternalResponseItem', value: function handleExtensionLoadExternalResponseItem(url, externalResponseItem) { var extension = _.find(this.extensions, { url: url }); if (extension) { extension.updateFromExternalResponseItem(externalResponseItem); } else { extension = new Extension(externalResponseItem); extension.url = url; extension.setDirty(true); this.modelManager.addItem(extension); this.apiController.sync(null); } return extension; } }, { key: 'refreshExtensionsFromServer', value: function refreshExtensionsFromServer() { var _iteratorNormalCompletion10 = true; var _didIteratorError10 = false; var _iteratorError10 = undefined; try { for (var _iterator10 = this.enabledRepeatActionUrls[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { var url = _step10.value; var action = this.actionWithURL(url); if (action) { this.disableRepeatAction(action); } } } catch (err) { _didIteratorError10 = true; _iteratorError10 = err; } finally { try { if (!_iteratorNormalCompletion10 && _iterator10.return) { _iterator10.return(); } } finally { if (_didIteratorError10) { throw _iteratorError10; } } } var _iteratorNormalCompletion11 = true; var _didIteratorError11 = false; var _iteratorError11 = undefined; try { for (var _iterator11 = this.extensions[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { var ext = _step11.value; this.retrieveExtensionFromServer(ext.url, function (extension) { extension.setDirty(true); }); } } catch (err) { _didIteratorError11 = true; _iteratorError11 = err; } finally { try { if (!_iteratorNormalCompletion11 && _iterator11.return) { _iterator11.return(); } } finally { if (_didIteratorError11) { throw _iteratorError11; } } } } }, { key: 'executeAction', value: function executeAction(action, extension, item, callback) { if (this.extensionUsesEncryptedData(extension) && !this.apiController.isUserSignedIn()) { alert("To send data encrypted, you must have an encryption key, and must therefore be signed in."); callback(null); return; } switch (action.verb) { case "get": { this.Restangular.oneUrl(action.url, action.url).get().then(function (response) { action.error = false; var items = response.items; this.modelManager.mapResponseItemsToLocalModels(items); callback(items); }.bind(this)).catch(function (response) { action.error = true; }); break; } case "show": { var win = window.open(action.url, '_blank'); win.focus(); callback(); 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) { callback(response); }); break; } default: {} } action.lastExecuted = new Date(); } }, { key: 'isRepeatActionEnabled', value: function isRepeatActionEnabled(action) { return this.enabledRepeatActionUrls.includes(action.url); } }, { key: 'disableRepeatAction', value: function disableRepeatAction(action, extension) { _.pull(this.enabledRepeatActionUrls, action.url); localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls)); this.modelManager.removeItemChangeObserver(action.url); console.assert(this.isRepeatActionEnabled(action) == false); } }, { key: 'enableRepeatAction', value: function enableRepeatAction(action, extension) { if (!_.find(this.enabledRepeatActionUrls, action.url)) { this.enabledRepeatActionUrls.push(action.url); localStorage.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 } } } }, { key: 'queueAction', value: function queueAction(action, extension, delay, changedItems) { this.actionQueue = this.actionQueue || []; if (_.find(this.actionQueue, { url: action.url })) { return; } // console.log("Successfully queued", action, this.actionQueue.length); this.actionQueue.push(action); setTimeout(function () { // console.log("Performing queued action", action); this.triggerWatchAction(action, extension, changedItems); _.pull(this.actionQueue, action); }.bind(this), delay * 1000); } }, { key: 'triggerWatchAction', value: function 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(); console.log("Performing action."); if (action.verb == "post") { var params = {}; params.items = changedItems.map(function (item) { var params = this.outgoingParamsForItem(item, extension); return params; }.bind(this)); this.performPost(action, extension, params, null); } else { // todo } } }, { key: 'outgoingParamsForItem', value: function outgoingParamsForItem(item, extension) { return this.apiController.paramsForExtension(item, this.extensionUsesEncryptedData(extension)); } }, { key: 'performPost', value: function performPost(action, extension, params, callback) { var request = this.Restangular.oneUrl(action.url, action.url); if (this.extensionUsesEncryptedData(extension)) { request.auth_params = this.apiController.getAuthParams(); } _.merge(request, params); request.post().then(function (response) { action.error = false; if (callback) { callback(response.plain()); } }).catch(function (response) { action.error = true; console.log("Action error response:", response); if (callback) { callback({ error: "Request error" }); } }); } }, { key: 'extensions', get: function get() { return this.modelManager.extensions; } }]); return ExtensionManager; }(); angular.module('app.frontend').service('extensionManager', ExtensionManager); ;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') : ''; }; }); ;angular.module('app.frontend').service('markdownRenderer', function ($sce) { marked.setOptions({ breaks: true, sanitize: true }); this.renderedContentForText = function (text) { if (!text || text.length == 0) { return ""; } return marked(text); }; this.renderHtml = function (html_code) { return $sce.trustAsHtml(html_code); }; }); ; var ModelManager = function () { function ModelManager(dbManager) { _classCallCheck(this, ModelManager); this.dbManager = dbManager; this.notes = []; this.tags = []; this.itemSyncObservers = []; this.itemChangeObservers = []; this.items = []; this._extensions = []; } _createClass(ModelManager, [{ key: 'allItemsMatchingTypes', value: function allItemsMatchingTypes(contentTypes) { return this.items.filter(function (item) { return (contentTypes.includes(item.content_type) || contentTypes.includes("*")) && !item.dummy; }); } }, { key: 'findItem', value: function findItem(itemId) { return _.find(this.items, { uuid: itemId }); } }, { key: 'mapResponseItemsToLocalModels', value: function mapResponseItemsToLocalModels(items) { return this.mapResponseItemsToLocalModelsOmittingFields(items, null); } }, { key: 'mapResponseItemsToLocalModelsOmittingFields', value: function mapResponseItemsToLocalModelsOmittingFields(items, omitFields) { var models = []; var _iteratorNormalCompletion12 = true; var _didIteratorError12 = false; var _iteratorError12 = undefined; try { for (var _iterator12 = items[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) { var json_obj = _step12.value; json_obj = _.omit(json_obj, omitFields || []); var item = this.findItem(json_obj["uuid"]); if (json_obj["deleted"] == true) { if (item) { this.removeItemLocally(item); } continue; } _.omit(json_obj, omitFields); if (!item) { item = this.createItem(json_obj); } else { item.updateFromJSON(json_obj); } this.addItem(item); if (json_obj.content) { this.resolveReferencesForItem(item); } models.push(item); } } catch (err) { _didIteratorError12 = true; _iteratorError12 = err; } finally { try { if (!_iteratorNormalCompletion12 && _iterator12.return) { _iterator12.return(); } } finally { if (_didIteratorError12) { throw _iteratorError12; } } } this.notifySyncObserversOfModels(models); this.sortItems(); return models; } }, { key: 'notifySyncObserversOfModels', value: function notifySyncObserversOfModels(models) { var _iteratorNormalCompletion13 = true; var _didIteratorError13 = false; var _iteratorError13 = undefined; try { for (var _iterator13 = this.itemSyncObservers[Symbol.iterator](), _step13; !(_iteratorNormalCompletion13 = (_step13 = _iterator13.next()).done); _iteratorNormalCompletion13 = true) { var observer = _step13.value; var relevantItems = models.filter(function (item) { return item.content_type == observer.type; }); if (relevantItems.length > 0) { observer.callback(relevantItems); } } } catch (err) { _didIteratorError13 = true; _iteratorError13 = err; } finally { try { if (!_iteratorNormalCompletion13 && _iterator13.return) { _iterator13.return(); } } finally { if (_didIteratorError13) { throw _iteratorError13; } } } } }, { key: 'notifyItemChangeObserversOfModels', value: function notifyItemChangeObserversOfModels(models) { var _iteratorNormalCompletion14 = true; var _didIteratorError14 = false; var _iteratorError14 = undefined; try { for (var _iterator14 = this.itemChangeObservers[Symbol.iterator](), _step14; !(_iteratorNormalCompletion14 = (_step14 = _iterator14.next()).done); _iteratorNormalCompletion14 = true) { var observer = _step14.value; var relevantItems = models.filter(function (item) { return observer.content_types.includes(item.content_type) || observer.content_types.includes("*"); }); if (relevantItems.length > 0) { observer.callback(relevantItems); } } } catch (err) { _didIteratorError14 = true; _iteratorError14 = err; } finally { try { if (!_iteratorNormalCompletion14 && _iterator14.return) { _iterator14.return(); } } finally { if (_didIteratorError14) { throw _iteratorError14; } } } } }, { key: 'createItem', value: function createItem(json_obj) { var item; if (json_obj.content_type == "Note") { item = new Note(json_obj); } else if (json_obj.content_type == "Tag") { item = new Tag(json_obj); } else if (json_obj.content_type == "Extension") { item = new Extension(json_obj); } else { item = new Item(json_obj); } item.addObserver(this, function (changedItem) { this.notifyItemChangeObserversOfModels([changedItem]); }.bind(this)); return item; } }, { key: 'addItems', value: function addItems(items) { this.items = _.uniq(this.items.concat(items)); items.forEach(function (item) { if (item.content_type == "Tag") { if (!_.find(this.tags, { uuid: item.uuid })) { this.tags.unshift(item); } } else if (item.content_type == "Note") { if (!_.find(this.notes, { uuid: item.uuid })) { this.notes.unshift(item); } } else if (item.content_type == "Extension") { if (!_.find(this._extensions, { uuid: item.uuid })) { this._extensions.unshift(item); } } }.bind(this)); } }, { key: 'addItem', value: function addItem(item) { this.addItems([item]); } }, { key: 'itemsForContentType', value: function itemsForContentType(contentType) { return this.items.filter(function (item) { return item.content_type == contentType; }); } }, { key: 'resolveReferencesForItem', value: function resolveReferencesForItem(item) { var contentObject = item.contentObject; if (!contentObject.references) { return; } var _iteratorNormalCompletion15 = true; var _didIteratorError15 = false; var _iteratorError15 = undefined; try { for (var _iterator15 = contentObject.references[Symbol.iterator](), _step15; !(_iteratorNormalCompletion15 = (_step15 = _iterator15.next()).done); _iteratorNormalCompletion15 = true) { var reference = _step15.value; var referencedItem = this.findItem(reference.uuid); if (referencedItem) { item.addItemAsRelationship(referencedItem); referencedItem.addItemAsRelationship(item); } else { // console.log("Unable to find item:", reference.uuid); } } } catch (err) { _didIteratorError15 = true; _iteratorError15 = err; } finally { try { if (!_iteratorNormalCompletion15 && _iterator15.return) { _iterator15.return(); } } finally { if (_didIteratorError15) { throw _iteratorError15; } } } } }, { key: 'sortItems', value: function sortItems() { Item.sortItemsByDate(this.notes); this.tags.forEach(function (tag) { Item.sortItemsByDate(tag.notes); }); } }, { key: 'addItemSyncObserver', value: function addItemSyncObserver(id, type, callback) { this.itemSyncObservers.push({ id: id, type: type, callback: callback }); } }, { key: 'removeItemSyncObserver', value: function removeItemSyncObserver(id) { _.remove(this.itemSyncObservers, _.find(this.itemSyncObservers, { id: id })); } }, { key: 'addItemChangeObserver', value: function addItemChangeObserver(id, content_types, callback) { this.itemChangeObservers.push({ id: id, content_types: content_types, callback: callback }); } }, { key: 'removeItemChangeObserver', value: function removeItemChangeObserver(id) { _.remove(this.itemChangeObservers, _.find(this.itemChangeObservers, { id: id })); } }, { key: 'getDirtyItems', value: function getDirtyItems() { return this.items.filter(function (item) { return item.dirty == true && !item.dummy; }); } }, { key: 'clearDirtyItems', value: function clearDirtyItems(items) { var _iteratorNormalCompletion16 = true; var _didIteratorError16 = false; var _iteratorError16 = undefined; try { for (var _iterator16 = items[Symbol.iterator](), _step16; !(_iteratorNormalCompletion16 = (_step16 = _iterator16.next()).done); _iteratorNormalCompletion16 = true) { var item = _step16.value; item.setDirty(false); } } catch (err) { _didIteratorError16 = true; _iteratorError16 = err; } finally { try { if (!_iteratorNormalCompletion16 && _iterator16.return) { _iterator16.return(); } } finally { if (_didIteratorError16) { throw _iteratorError16; } } } } }, { key: 'clearAllDirtyItems', value: function clearAllDirtyItems() { this.clearDirtyItems(this.getDirtyItems()); } }, { key: 'setItemToBeDeleted', value: function setItemToBeDeleted(item) { item.deleted = true; if (!item.dummy) { item.setDirty(true); } item.removeAllRelationships(); } }, { key: 'removeItemLocally', value: function removeItemLocally(item) { _.pull(this.items, item); if (item.content_type == "Tag") { _.pull(this.tags, item); } else if (item.content_type == "Note") { _.pull(this.notes, item); } else if (item.content_type == "Extension") { _.pull(this._extensions, item); } this.dbManager.deleteItem(item); } /* Relationships */ }, { key: 'createRelationshipBetweenItems', value: function createRelationshipBetweenItems(itemOne, itemTwo) { itemOne.addItemAsRelationship(itemTwo); itemTwo.addItemAsRelationship(itemOne); itemOne.setDirty(true); itemTwo.setDirty(true); } }, { key: 'removeRelationshipBetweenItems', value: function removeRelationshipBetweenItems(itemOne, itemTwo) { itemOne.removeItemAsRelationship(itemTwo); itemTwo.removeItemAsRelationship(itemOne); itemOne.setDirty(true); itemTwo.setDirty(true); } }, { key: 'allItems', get: function get() { return this.items.filter(function (item) { return !item.dummy; }); } }, { key: 'extensions', get: function get() { return this._extensions.filter(function (ext) { return !ext.deleted; }); } }, { key: 'filteredNotes', get: function get() { return Note.filterDummyNotes(this.notes); } }]); return ModelManager; }(); angular.module('app.frontend').service('modelManager', ModelManager); },{}]},{},[1]);