(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" }, { title: "NPM install without sudo", content: "sudo chown -R $(whoami) ~/.npm" }, { title: "Email validation regex", content: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" }, { title: "Ruby generate 256 bit key", content: "Digest::SHA256.hexdigest(SecureRandom.random_bytes(32))" }, { title: "Mac add user to user group", content: "sudo dseditgroup -o edit -a USERNAME -t user GROUPNAME" }, { title: "Kill Mac OS System Apache", content: "sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist" }, { title: "Docker run with mount binding and port", content: "docker run -v /home/vagrant/www/app:/var/www/app -p 8080:80 -d kpi/s3" }, { title: "MySQL grant privileges", content: "GRANT [type of permission] ON [database name].[table name] TO ‘[username]’@'%’;" }, { title: "MySQL list users", content: "SELECT User FROM mysql.user;" }]; this.showSampler = !this.user.id && this.user.filteredNotes().length == 0; this.demoNoteNames = _.map(this.demoNotes, function (note) { return note.title; }); this.currentDemoContent = { text: null }; this.prebeginFn = function () { this.currentDemoContent.text = null; }.bind(this); this.callback = function (index) { this.currentDemoContent.text = this.demoNotes[index].text; }.bind(this); this.contentCallback = function (index) {}; this.setNote = function (note, oldNote) { this.editorMode = 'edit'; if (note.content.text.length == 0) { this.focusTitle(100); } if (oldNote && oldNote != note) { if (oldNote.hasChanges) { this.save()(oldNote, null); } else if (oldNote.dummy) { this.remove()(oldNote); } } }; 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.content.text)); }; 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); } }.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; apiController.saveDraftToDisk(this.note); if (saveTimeout) $timeout.cancel(saveTimeout); if (statusTimeout) $timeout.cancel(statusTimeout); saveTimeout = $timeout(function () { this.noteStatus = "Saving..."; this.saveNote(); }.bind(this), 150); }; 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.root_path, ""); this.url = { base: url, token: this.note.presentation.root_path }; this.editingUrl = true; }; this.saveUrl = function ($event) { $event.target.blur(); var original = this.note.presentation.relative_path; this.note.presentation.relative_path = this.url.token; apiController.updatePresentation(this.note, this.note.presentation, function (response) { if (!response) { this.note.presentation.relative_path = 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.user, this.note, function (note) { openInNewTab(this.publicUrlForNote(note)); }.bind(this)); this.showMenu = false; }; this.unshareNote = function () { apiController.unshareItem(this.user, 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("groupsSection", function () { return { restrict: 'E', scope: { addNew: "&", selectionMade: "&", willSelect: "&", save: "&", groups: "=", allGroup: "=", user: "=", updateNoteGroup: "&" }, templateUrl: 'frontend/groups.html', replace: true, controller: 'GroupsCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) { scope.$watch('ctrl.groups', function (newGroups) { if (newGroups) { ctrl.setGroups(newGroups); } }); } }; }).controller('GroupsCtrl', function () { var initialLoad = true; this.setGroups = function (groups) { if (initialLoad) { initialLoad = false; this.selectGroup(this.allGroup); } else { if (groups && groups.length > 0) { this.selectGroup(groups[0]); } } }; this.selectGroup = function (group) { this.willSelect()(group); this.selectedGroup = group; this.selectionMade()(group); }; this.clickedAddNewGroup = function () { if (this.editingGroup) { return; } this.newGroup = new Group({ notes: [] }); if (!this.user.uuid) { this.newGroup.uuid = Neeto.crypto.generateRandomKey(); } this.selectedGroup = this.newGroup; this.editingGroup = this.newGroup; this.addNew()(this.newGroup); }; var originalGroupName = ""; this.onGroupTitleFocus = function (group) { originalGroupName = group.name; }; this.groupTitleDidChange = function (group) { this.editingGroup = group; }; this.saveGroup = function ($event, group) { this.editingGroup = null; if (group.name.length == 0) { group.name = originalGroupName; originalGroupName = ""; return; } $event.target.blur(); if (!group.name || group.name.length == 0) { return; } this.save()(group, function (savedGroup) { _.merge(group, savedGroup); this.selectGroup(group); this.newGroup = null; }.bind(this)); }; this.noteCount = function (group) { var validNotes = Note.filterDummyNotes(group.notes); return validNotes.length; }; this.handleDrop = function (e, newGroup, note) { this.updateNoteGroup()(note, newGroup, this.selectedGroup); }.bind(this); }); ;angular.module('app.frontend').directive("header", function () { return { restrict: 'E', scope: { user: "=", logout: "&" }, templateUrl: 'frontend/header.html', replace: true, controller: 'HeaderCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) {} }; }).controller('HeaderCtrl', function ($auth, $state, apiController, serverSideValidation, $timeout) { 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.changeServer = function () { apiController.setServer(this.serverData.url, true); }; this.signOutPressed = function () { this.showAccountMenu = false; this.logout()(); apiController.signout(); 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.user, this.passwordChangeData.current_password, this.passwordChangeData.new_password, function (response) {}); }.bind(this)); }; this.hasLocalData = function () { return this.user.filteredNotes().length > 0; }; this.mergeLocalChanged = function () { if (!this.user.shouldMerge) { if (!confirm("Unchecking this option means any locally stored groups and notes you have now will be deleted. Are you sure you want to continue?")) { this.user.shouldMerge = true; } } }; this.loginSubmitPressed = function () { this.loginData.status = "Generating Login Keys..."; $timeout(function () { apiController.login(this.loginData.email, this.loginData.user_password, function (response) { if (response.errors) { this.loginData.status = response.errors[0]; } 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.errors) { this.loginData.status = response.errors[0]; } else { this.onAuthSuccess(response.user); } }.bind(this)); }.bind(this)); }; this.forgotPasswordSubmit = function () { $auth.requestPasswordReset(this.resetData).then(function (resp) { this.resetData.response = "Success"; // handle success response }.bind(this)).catch(function (resp) { // handle error response this.resetData.response = "Error"; }.bind(this)); }; this.encryptionStatusForNotes = function () { var allNotes = this.user.filteredNotes(); var countEncrypted = 0; allNotes.forEach(function (note) { if (note.encryptionEnabled()) { countEncrypted++; } }.bind(this)); return countEncrypted + "/" + allNotes.length + " notes encrypted"; }; this.downloadDataArchive = function () { var link = document.createElement('a'); link.setAttribute('download', 'neeto.json'); link.href = apiController.notesDataFile(this.user); link.click(); }; this.importFileSelected = function (files) { var file = files[0]; var reader = new FileReader(); reader.onload = function (e) { apiController.importJSONData(e.target.result, function (success, response) { console.log("import response", success, response); if (success) { // window.location.reload(); } else { alert("There was an error importing your data. Please try again."); } }); }; reader.readAsText(file); }; this.onAuthSuccess = function (user) { this.user.id = user.id; 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"; $rootScope.title = "Notes — Neeto, a secure code box for developers"; $rootScope.description = "A secure code box for developers to store common commands and useful notes."; var onUserSet = function onUserSet() { $scope.allGroup = new Group({ name: "All", all: true }); $scope.groups = modelManager.groups; apiController.verifyEncryptionStatusOfAllItems($scope.defaultUser, function (success) {}); }; apiController.getCurrentUser(function (response) { if (response && !response.errors) { $scope.defaultUser = new User(response); modelManager.items = response.items; $rootScope.title = "Notes — Neeto"; onUserSet(); } else { $scope.defaultUser = new User(apiController.localUser()); onUserSet(); } }); /* Groups Ctrl Callbacks */ $scope.updateAllGroup = function () { $scope.allGroup.notes = modelManager.filteredNotes; }; $scope.groupsWillMakeSelection = function (group) { if (group.all) { $scope.updateAllGroup(); } }; $scope.groupsSelectionMade = function (group) { if (!group.notes) { group.notes = []; } $scope.selectedGroup = group; }; $scope.groupsAddNew = function (group) { modelManager.addTag(group); }; $scope.groupsSave = function (group, callback) { apiController.saveItems([group], callback); }; /* Called to update the group of a note after drag and drop change The note object is a copy of the original */ $scope.groupsUpdateNoteGroup = function (noteCopy, newGroup, oldGroup) { var originalNote = _.find($scope.defaultUser.notes, { uuid: noteCopy.uuid }); modelManager.removeTagFromNote(oldGroup, originalNote); if (!newGroup.all) { modelManager.addTagToNote(newGroup, originalNote); } apiController.saveDirtyItems(function () {}); }; /* Notes Ctrl Callbacks */ $scope.notesRemoveGroup = function (group) { var validNotes = Note.filterDummyNotes(group.notes); if (validNotes == 0) { // if no more notes, delete group apiController.deleteItem($scope.defaultUser, group, function () { // force scope groups to update on sub directives $scope.groups = []; $timeout(function () { $scope.groups = modelManager.groups; }); }); } else { alert("To delete this group, remove all its notes first."); } }; $scope.notesSelectionMade = function (note) { $scope.selectedNote = note; }; $scope.notesAddNew = function (note) { if (!$scope.defaultUser.id) { // generate local id for note note.id = Neeto.crypto.generateRandomKey(); } modelManager.addNote(note); if (!$scope.selectedGroup.all) { modelManager.addTagToNote($scope.selectedGroup, note); } }; /* Shared Callbacks */ $scope.saveNote = function (note, callback) { apiController.saveItems([note], function () { modelManager.addNote(note); note.hasChanges = false; if (callback) { callback(true); } }); }; $scope.deleteNote = function (note) { modelManager.deleteNote(note); if (note == $scope.selectedNote) { $scope.selectedNote = null; } if (note.dummy) { return; } apiController.deleteItem($scope.defaultUser, note, function (success) {}); apiController.saveDirtyItems(function () {}); }; /* Header Ctrl Callbacks */ $scope.headerLogout = function () { $scope.defaultUser = apiController.localUser(); $scope.groups = $scope.defaultUser.groups; }; }); ;angular.module('app.frontend').directive("notesSection", function () { return { scope: { addNew: "&", selectionMade: "&", remove: "&", group: "=", user: "=", removeGroup: "&" }, templateUrl: 'frontend/notes.html', replace: true, controller: 'NotesCtrl', controllerAs: 'ctrl', bindToController: true, link: function link(scope, elem, attrs, ctrl) { scope.$watch('ctrl.group', function (group, oldGroup) { if (group) { ctrl.groupDidChange(group, oldGroup); } }); } }; }).controller('NotesCtrl', function (apiController, $timeout, ngDialog, $rootScope) { $rootScope.$on("editorFocused", function () { this.showMenu = false; }.bind(this)); var isFirstLoad = true; this.groupDidChange = function (group, oldGroup) { this.showMenu = false; if (this.selectedNote && this.selectedNote.dummy) { _.remove(oldGroup.notes, this.selectedNote); } this.noteFilter.text = ""; this.setNotes(group.notes, 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 (group.notes.length == 0) { this.createNewNote(); } }; this.selectedGroupDelete = function () { this.showMenu = false; this.removeGroup()(this.group); }; this.selectedGroupShare = function () { this.showMenu = false; if (!this.user.id) { alert("You must be signed in to share a group."); return; } if (this.group.all) { alert("You cannot share the 'All' group."); return; } var _callback = function (username) { apiController.shareItem(this.user, this.group, function (response) {}); }.bind(this); if (!this.user.username) { ngDialog.open({ template: 'frontend/modals/username.html', controller: 'UsernameModalCtrl', resolve: { user: function () { return this.user; }.bind(this), callback: function callback() { return _callback; } }, className: 'ngdialog-theme-default', disableAnimation: true }); } else { _callback(this.user.username); } }; this.selectedGroupUnshare = function () { this.showMenu = false; apiController.unshareItem(this.user, this.group, function (response) {}); }; this.publicUrlForGroup = function () { return this.group.presentation.url; }; this.setNotes = function (notes, createNew) { this.notes = notes; notes.forEach(function (note) { note.visible = true; }); apiController.decryptNotesWithLocalKey(notes); this.selectFirstNote(createNew); }; this.selectFirstNote = function (createNew) { var visibleNotes = this.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.notes ? " " + (this.notes.length + 1) : ""); this.newNote = new Note({ dummy: true }); this.newNote.content.title = title; modelManager.addTagToNote(this.group, this.newNote); this.selectNote(this.newNote); this.addNew()(this.newNote); }; this.noteFilter = { text: '' }; this.filterNotes = function (note) { if (this.noteFilter.text.length == 0) { note.visible = true; } else { note.visible = note.title.toLowerCase().includes(this.noteFilter.text) || note.text.toLowerCase().includes(this.noteFilter.text); } return note.visible; }.bind(this); this.filterTextChanged = function () { $timeout(function () { if (!this.selectedNote.visible) { this.selectFirstNote(false); } }.bind(this), 100); }; }); ;angular.module('app.frontend').controller('UsernameModalCtrl', function ($scope, apiController, Restangular, user, callback, $timeout) { $scope.formData = {}; $scope.saveUsername = function () { apiController.setUsername(user, $scope.formData.username, function (response) { var username = response.root_path; user.presentation = response; callback(username); $scope.closeThisDialog(); }); }; }); ; var Item = function () { function Item(json_obj) { _classCallCheck(this, Item); var content; Object.defineProperty(this, "content", { get: function get() { return content; }, set: function set(value) { var finalValue = value; if (typeof value === 'string') { try { decodedValue = JSON.parse(value); finalValue = decodedValue; } catch (e) { finalValue = value; } } content = finalValue; }, enumerable: true }); _.merge(this, json_obj); this.setContentRaw = function (rawContent) { content = rawContent; }; } _createClass(Item, [{ key: 'referencesMatchingContentType', value: function referencesMatchingContentType(contentType) { return this.references.filter(function (reference) { return reference.content_type == content_type; }); } }, { key: 'updateReferencesLocalMapping', value: function updateReferencesLocalMapping() {} // should be overriden to manage local properties /* Returns true if note is shared individually or via group */ }, { key: 'isPublic', value: function isPublic() { return this.presentation; } }, { key: 'isEncrypted', value: function isEncrypted() { return this.encryptionEnabled() && typeof this.content === 'string' ? true : false; } }, { key: 'encryptionEnabled', value: function encryptionEnabled() { return this.loc_eek; } }, { key: 'presentationURL', value: function presentationURL() { return this.presentation.url; } }]); return Item; }(); ; var Note = function (_Item) { _inherits(Note, _Item); function Note(json_obj) { _classCallCheck(this, Note); var _this = _possibleConstructorReturn(this, (Note.__proto__ || Object.getPrototypeOf(Note)).call(this, json_obj)); if (!_this.content) { _this.content = { title: "", text: "" }; } return _this; } _createClass(Note, [{ key: 'filterDummyNotes', value: function filterDummyNotes(notes) { var filtered = notes.filter(function (note) { return note.dummy == false || note.dummy == null; }); return filtered; } }, { key: 'isPublic', value: function isPublic() { return _get(Note.prototype.__proto__ || Object.getPrototypeOf(Note.prototype), 'isPublic', this).call(this) || this.hasOnePublicGroup; } }, { key: 'hasOnePublicGroup', get: function get() { var hasPublicGroup = false; this.groups.forEach(function (group) { if (group.isPublic()) { hasPublicGroup = true; return; } }); return hasPublicGroup; } }, { key: 'content_type', get: function get() { return "Note"; } }]); return Note; }(Item); ; var Tag = function (_Item2) { _inherits(Tag, _Item2); function Tag(json_obj) { _classCallCheck(this, Tag); return _possibleConstructorReturn(this, (Tag.__proto__ || Object.getPrototypeOf(Tag)).call(this, json_obj)); } _createClass(Tag, [{ key: 'content_type', get: function get() { return "Tag"; } }]); return Tag; }(Item); ; var User = function User(json_obj) { _classCallCheck(this, User); _.merge(this, json_obj); }; ;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 = location.protocol + "//" + domainName() + (location.port ? ':' + location.port : ''); } } return url; }; this.$get = function (Restangular) { return new ApiController(Restangular); }; function ApiController(Restangular) { /* Config */ this.getServer = function () { if (!url) { url = localStorage.getItem("server"); if (!url) { url = location.protocol + "//" + domainName() + (location.port ? ':' + location.port : ''); this.setServer(url); } } return url; }; this.setServer = function (url, refresh) { localStorage.setItem("server", url); if (refresh) { window.location.reload(); } }; /* Auth */ this.getCurrentUser = function (callback) { if (!localStorage.getItem("jwt")) { callback(null); return; } Restangular.one("users/current").get().then(function (response) { callback(response.plain()); }); }; this.login = function (email, password, callback) { var keys = Neeto.crypto.generateEncryptionKeysForUser(password, email); this.setGk(keys.gk); var request = Restangular.one("auth/sign_in.json"); request.user = { password: keys.pw, email: email }; request.post().then(function (response) { localStorage.setItem("jwt", response.token); callback(response); }); }; this.register = function (email, password, callback) { var keys = Neeto.crypto.generateEncryptionKeysForUser(password, email); this.setGk(keys.gk); var request = Restangular.one("auth.json"); request.user = { password: keys.pw, email: email }; request.post().then(function (response) { localStorage.setItem("jwt", response.token); callback(response); }); }; this.changePassword = function (user, current_password, new_password) { var current_keys = Neeto.crypto.generateEncryptionKeysForUser(current_password, user.email); var new_keys = Neeto.crypto.generateEncryptionKeysForUser(new_password, user.email); var data = {}; data.current_password = current_keys.pw; data.password = new_keys.pw; data.password_confirmation = new_keys.pw; var user = this.user; this._performPasswordChange(current_keys, new_keys, function (response) { if (response && !response.errors) { // this.showNewPasswordForm = false; // reencrypt data with new gk this.reencryptAllNotesAndSave(user, new_keys.gk, current_keys.gk, function (success) { if (success) { this.setGk(new_keys.gk); alert("Your password has been changed and your data re-encrypted."); } else { // rollback password this._performPasswordChange(new_keys, current_keys, 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."); } }); }; this._performPasswordChange = function (email, current_keys, new_keys, callback) { var request = Restangular.one("auth"); request.user = { password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email }; request.patch().then(function (response) { callback(response); }); }; /* User */ this.setUsername = function (user, username, callback) { var request = Restangular.one("users", user.id).one("set_username"); request.username = username; request.post().then(function (response) { callback(response.plain()); }); }; /* Ensures that if encryption is disabled, all local notes are uncrypted, and that if it's enabled, that all local notes are encrypted */ this.verifyEncryptionStatusOfAllItems = function (user, callback) { var allNotes = user.filteredNotes(); var notesNeedingUpdate = []; allNotes.forEach(function (note) { if (!note.isPublic()) { if (note.encryptionEnabled() && !note.isEncrypted()) { notesNeedingUpdate.push(note); } } else { if (note.isEncrypted()) { notesNeedingUpdate.push(note); } } }.bind(this)); if (notesNeedingUpdate.length > 0) { console.log("verifying encryption, notes need updating", notesNeedingUpdate); this.saveBatchNotes(user, notesNeedingUpdate, callback); } }; /* Items */ this.saveDirtyItems = function (callback) { var dirtyItems = modelManager.dirtyItems; this.saveItems(dirtyItems, function (response) { modelManager.clearDirtyItems(); }); }; this.saveItems = function (items, callback) { var request = Restangular.one("users", user.uuid).one("items"); request.items = _.map(items, function (item) { return this.createRequestParamsFromItem(item, user); }.bind(this)); request.post().then(function (response) { var savedItems = response.items; items.forEach(function (item) { _.merge(item, _.find(savedItems, { uuid: item.uuid })); }); callback(response); }); }; this.createRequestParamsForItem = function (item, user) { var params = { uuid: item.uuid }; if (!item.isPublic()) { // encrypted var itemCopy = _.cloneDeep(item); this.encryptSingleNote(itemCopy, this.retrieveGk()); params.content = itemCopy.content; params.loc_eek = itemCopy.loc_eek; } else { // decrypted params.content = JSON.stringify(item.content); params.loc_eek = null; } return params; }; this.deleteItem = function (user, item, callback) { if (!user.id) { this.writeUserToLocalStorage(user); callback(true); } else { Restangular.one("users", user.uuid).one("items", item.uuid).remove().then(function (response) { callback(true); }); } }; this.shareItem = function (user, item, callback) { if (!user.id) { alert("You must be signed in to share."); } else { Restangular.one("users", user.uuid).one("items", item.uuid).one("presentations").post().then(function (response) { var presentation = response.plain(); _.merge(item, { presentation: presentation }); callback(item); // decrypt references if (item.references.length > 0) { this.saveBatchItems(user, item.references, function (success) {}); } }); } }; this.unshareItem = function (user, item, callback) { var request = Restangular.one("users", user.uuid).one("notes", item.uuid).one("presentations", item.presentation.uuid); request.remove().then(function (response) { item.presentation = null; callback(null); // encrypt references if (item.references.length > 0) { this.saveBatchItems(user, item.references, function (success) {}); } }); }; /* Presentations */ this.updatePresentation = function (resource, presentation, callback) { var request = Restangular.one("users", user.id).one("items", resource.id).one("presentations", resource.presentation.id); _.merge(request, presentation); request.patch().then(function (response) { callback(response.plain()); }).catch(function (error) { callback(nil); }); }; /* Import */ this.importJSONData = function (jsonString, callback) { var data = JSON.parse(jsonString); var user = new User(data); console.log("importing data", JSON.parse(jsonString)); user.notes.forEach(function (note) { if (note.isPublic()) { note.setContentRaw(JSON.stringify(note.content)); } else { this.encryptSingleNoteWithLocalKey(note); } // prevent circular links note.group = null; }.bind(this)); user.groups.forEach(function (group) { // prevent circular links group.notes = null; }); var request = Restangular.one("import"); request.data = { notes: user.notes, groups: user.groups }; request.post().then(function (response) { callback(true, response); }).catch(function (error) { callback(false, error); }); }; /* Export */ this.notesDataFile = function (user) { 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 presentationParams = function presentationParams(presentation) { if (!presentation) { return null; } return { id: presentation.id, uuid: presentation.uuid, root_path: presentation.root_path, relative_path: presentation.relative_path, presentable_type: presentation.presentable_type, presentable_id: presentation.presentable_id, created_at: presentation.created_at, modified_at: presentation.modified_at }; }; var notes = _.map(user.filteredNotes(), function (note) { return { id: note.id, uuid: note.uuid, content: note.content, group_id: note.group_id, created_at: note.created_at, modified_at: note.modified_at, presentation: presentationParams(note.presentation) }; }); var groups = _.map(user.groups, function (group) { return { id: group.id, uuid: group.uuid, name: group.name, created_at: group.created_at, modified_at: group.modified_at, presentation: presentationParams(group.presentation) }; }); var data = { notes: notes, groups: groups }; return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */)); }; /* Merging */ this.mergeLocalDataRemotely = function (user, callback) { var request = Restangular.one("users", user.id).one("merge"); var groups = user.groups; request.notes = user.notes; request.notes.forEach(function (note) { if (note.group_id) { var group = groups.filter(function (group) { return group.id == note.group_id; })[0]; note.group_name = group.name; } }); request.post().then(function (response) { callback(); localStorage.removeItem('user'); }); }; this.staticifyObject = function (object) { return JSON.parse(JSON.stringify(object)); }; this.writeUserToLocalStorage = function (user) { var saveUser = _.cloneDeep(user); saveUser.notes = Note.filterDummyNotes(saveUser.notes); saveUser.groups.forEach(function (group) { group.notes = null; }.bind(this)); this.writeToLocalStorage('user', saveUser); }; this.writeToLocalStorage = function (key, value) { localStorage.setItem(key, angular.toJson(value)); }; this.localUser = function () { var user = JSON.parse(localStorage.getItem('user')); if (!user) { user = { notes: [], groups: [] }; } user.shouldMerge = true; return user; }; /* 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; } return new Note(JSON.parse(draftString)); }; /* Encrpytion */ this.retrieveGk = function () { if (!this.gk) { this.gk = localStorage.getItem("gk"); } return this.gk; }; this.setGk = function (gk) { localStorage.setItem('gk', gk); }; this.signout = function () { localStorage.removeItem("jwt"); localStorage.removeItem("gk"); }; this.encryptSingleNote = function (note, key) { var ek = null; if (note.loc_eek) { ek = Neeto.crypto.decryptText(note.loc_eek, key); } else { ek = Neeto.crypto.generateRandomEncryptionKey(); note.loc_eek = Neeto.crypto.encryptText(ek, key); } note.content = Neeto.crypto.encryptText(JSON.stringify(note.content), ek); note.local_encryption_scheme = "1.0"; }; this.encryptNotes = function (notes, key) { notes.forEach(function (note) { this.encryptSingleNote(note, key); }.bind(this)); }; this.encryptSingleNoteWithLocalKey = function (note) { this.encryptSingleNote(note, this.retrieveGk()); }; this.encryptNotesWithLocalKey = function (notes) { this.encryptNotes(notes, this.retrieveGk()); }; this.encryptNonPublicNotesWithLocalKey = function (notes) { var nonpublic = notes.filter(function (note) { return !note.isPublic() && !note.pending_share; }); this.encryptNotes(nonpublic, this.retrieveGk()); }; this.decryptSingleNoteWithLocalKey = function (note) { this.decryptSingleNote(note, this.retrieveGk()); }; this.decryptSingleNote = function (note, key) { var ek = Neeto.crypto.decryptText(note.loc_eek || note.local_eek, key); var content = Neeto.crypto.decryptText(note.content, ek); // console.log("decrypted contnet", content); note.content = content; }; this.decryptNotes = function (notes, key) { notes.forEach(function (note) { // console.log("is encrypted?", note); if (note.isEncrypted()) { this.decryptSingleNote(note, key); } }.bind(this)); }; this.decryptNotesWithLocalKey = function (notes) { this.decryptNotes(notes, this.retrieveGk()); }; this.reencryptAllNotesAndSave = function (user, newKey, oldKey, callback) { var notes = user.filteredNotes(); notes.forEach(function (note) { if (note.isEncrypted()) { // first decrypt eek with old key var ek = Neeto.crypto.decryptText(note.loc_eek, oldKey); // now encrypt ek with new key note.loc_eek = Neeto.crypto.encryptText(ek, newKey); } }); this.saveBatchNotes(user, notes, function (success) { callback(success); }.bind(this)); }; } }); ; var ItemManager = function () { function ItemManager() { _classCallCheck(this, ItemManager); } _createClass(ItemManager, [{ key: 'referencesForItemId', value: function referencesForItemId(itemId) { return _.find(this.items, { uuid: itemId }); } }, { key: 'resolveReferences', value: function resolveReferences() { this.items.forEach(function (item) { // build out references _.map(item.references, function (reference) { return referencesForItemId(reference.uuid); }); }); } }, { key: 'itemsForContentType', value: function itemsForContentType(contentType) { this.items.filter(function (item) { return item.content_type == contentType; }); } // returns dirty item references that need saving }, { key: 'deleteItem', value: function deleteItem(item) { _.remove(this.items, item); item.references.forEach(function (referencedItem) { removeReferencesBetweenItems(referencedItem, item); }); return item.references; } }, { key: 'removeReferencesBetweenItems', value: function removeReferencesBetweenItems(itemOne, itemTwo) { _.remove(itemOne.references, _.find(itemOne.references, { uuid: itemTwo.uuid })); _.remove(itemTwo.references, _.find(itemTwo.references, { uuid: itemOne.uuid })); return [itemOne, itemTwo]; } }, { key: 'createReferencesBetweenItems', value: function createReferencesBetweenItems(itemOne, itemTwo) { itemOne.references.push(itemTwo); itemTwo.references.push(itemOne); return [itemOne, itemTwo]; } }, { key: 'items', set: function set(items) { this.items = items; resolveReferences(); } }]); return ItemManager; }(); angular.module('app.frontend').service('itemManager', ItemManager); ;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 (_ItemManager) { _inherits(ModelManager, _ItemManager); function ModelManager() { _classCallCheck(this, ModelManager); return _possibleConstructorReturn(this, (ModelManager.__proto__ || Object.getPrototypeOf(ModelManager)).apply(this, arguments)); } _createClass(ModelManager, [{ key: 'addDirtyItems', value: function addDirtyItems(items) { if (this.dirtyItems) { this.dirtyItems = []; } this.dirtyItems.concat(items); } }, { key: 'clearDirtyItems', value: function clearDirtyItems() { this.dirtyItems = []; } }, { key: 'addNote', value: function addNote(note) { if (!_.find(this.notes, { uuid: note.uuid })) { this.notes.unshift(note); } } }, { key: 'addTag', value: function addTag(tag) { this.tags.unshift(tag); } }, { key: 'addTagToNote', value: function addTagToNote(tag, note) { var dirty = this.createReferencesBetweenItems(tag, note); this.refreshRelationshipsForTag(tag); this.refreshRelationshipsForNote(note); this.addDirtyItems(dirty); } }, { key: 'refreshRelationshipsForTag', value: function refreshRelationshipsForTag(tag) { tag.notes = tag.referencesMatchingContentType("Note"); tag.notes.sort(function (a, b) { return new Date(b.created_at) - new Date(a.created_at); }); } }, { key: 'refreshRelationshipsForNote', value: function refreshRelationshipsForNote(note) { note.groups = note.referencesMatchingContentType("Group"); } }, { key: 'removeTagFromNote', value: function removeTagFromNote(tag, note) { var dirty = this.removeReferencesBetweenItems(tag, note); this.addDirtyItems(dirty); } }, { key: 'deleteNote', value: function deleteNote(note) { var dirty = this.deleteItem(note); this.addDirtyItems(dirty); } }, { key: 'deleteTag', value: function deleteTag(tag) { var dirty = this.deleteItem(tag); this.addDirtyItems(dirty); } }, { key: 'filteredNotes', value: function filteredNotes() { return Note.filterDummyNotes(this.notes); } }, { key: 'items', set: function set(items) { _set(ModelManager.prototype.__proto__ || Object.getPrototypeOf(ModelManager.prototype), 'items', items, this); this.notes = _.map(this.items.itemsForContentType("Note"), function (json_obj) { return new Note(json_obj); }); this.groups = _.map(this.items.itemsForContentType("Group"), function (json_obj) { var group = Group(json_obj); group.updateReferencesLocalMapping(); return group; }); } }, { key: 'dirtyItems', get: function get() { return this.dirtyItems || []; } }, { key: 'filteredNotes', get: function get() { return Note.filterDummyNotes(this.notes); } }]); return ModelManager; }(ItemManager); angular.module('app.frontend').service('modelManager', ModelManager); ;angular.module('app.frontend').service('serverSideValidation', function ($sce) { // Show validation errors in form. this.showErrors = function (formErrors, form) { angular.forEach(formErrors, function (errors, key) { if (typeof form[key] !== 'undefined') { form[key].$setDirty(); form[key].$setValidity('server', false); form[key].$error.server = $sce.trustAsHtml(errors.join(', ')); } }); }; // Get validation errors from server response and show them in form. this.parseErrors = function (response, form) { if (response.status === 422) { this.showErrors(response.data, form); } }; }); ;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(); } }); } }; }]); ;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: '=', group: "=" }, 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.id; 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.group, 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); } }); } }; }]); ;angular.module('app.frontend').directive('note', function ($timeout) { return { restrict: 'E', controller: 'SingleNoteCtrl', templateUrl: "frontend/directives/note.html", scope: { note: "=" } }; }).controller('SingleNoteCtrl', function ($rootScope, $scope, $state, markdownRenderer) { $scope.renderedContent = function () { return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText($scope.note.text)); }; }); ; /** * AngularJS directive that simulates the effect of typing on a text editor - with a blinking cursor. * This directive works as an attribute to any HTML element, and it changes the speed/delay of its animation. * * There's also a simple less file included for basic styling of the dialog, which can be overridden. * The config object also lets the user define custom CSS classes for the modal. * * How to use: * * Just add the desired text to the 'text' attribute of the element and the directive takes care of the rest. * The 'text' attribute can be a single string or an array of string. In case an array is passed, the string * on each index is erased so the next item can be printed. When the last index is reached, that string stays * on the screen. (So if you want to erase the last string, just push an empty string to the end of the array) * * These are the optional preferences: * - initial delay: set an 'initial-delay' attribute for the element * - type delay: set a 'type-delay' attribute for the element * - erase delay: set a 'erase-delay' attribute for the element * - specify cursor : set a 'cursor' attribute for the element, specifying which cursor to use * - turn off cursor blinking: set the 'blink-cursor' attribute to "false" * - cursor blinking speed: set a 'blink-delay' attribute for the element * - scope callback: pass the desired scope callback as the 'callback-fn' attribute of the element * * Note: * Each time/delay value should be set either on seconds (1s) or milliseconds (1000) * * Dependencies: * The directive needs the css file provided in order to replicate the cursor blinking effect. */ angular.module('app.frontend').directive('typewrite', ['$timeout', function ($timeout) { function linkFunction($scope, $element, $attrs) { var timer = null, initialDelay = $attrs.initialDelay ? getTypeDelay($attrs.initialDelay) : 200, typeDelay = $attrs.typeDelay || 200, eraseDelay = $attrs.eraseDelay || typeDelay / 2, blinkDelay = $attrs.blinkDelay ? getAnimationDelay($attrs.blinkDelay) : false, cursor = $attrs.cursor || '|', blinkCursor = typeof $attrs.blinkCursor !== 'undefined' ? $attrs.blinkCursor === 'true' : true, currentText, textArray, running, auxStyle; if ($scope.text) { if ($scope.text instanceof Array) { textArray = $scope.text; currentText = textArray[0]; } else { currentText = $scope.text; } } if (typeof $scope.start === 'undefined' || $scope.start) { typewrite(); } function typewrite() { timer = $timeout(function () { updateIt($element, 0, 0, currentText); }, initialDelay); } function updateIt(element, charIndex, arrIndex, text) { if (charIndex <= text.length) { updateValue(element, text.substring(0, charIndex) + cursor); charIndex++; timer = $timeout(function () { updateIt(element, charIndex, arrIndex, text); }, typeDelay); return; } else { charIndex--; if ($scope.iterationCallback) { $scope.iterationCallback()(arrIndex); } // check if it's an array if (textArray && arrIndex < textArray.length - 1) { timer = $timeout(function () { cleanAndRestart(element, charIndex, arrIndex, textArray[arrIndex]); }, $scope.iterationDelay); } else { if ($scope.callbackFn) { $scope.callbackFn(); } blinkIt(element, charIndex, currentText); } } } function blinkIt(element, charIndex) { var text = element.text().substring(0, element.text().length - 1); if (blinkCursor) { if (blinkDelay) { auxStyle = '-webkit-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-moz-animation:blink-it steps(1) ' + blinkDelay + ' infinite ' + '-ms-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-o-animation:blink-it steps(1) ' + blinkDelay + ' infinite; ' + 'animation:blink-it steps(1) ' + blinkDelay + ' infinite;'; updateValue(element, text.substring(0, charIndex) + '' + cursor + ''); } else { updateValue(element, text.substring(0, charIndex) + '' + cursor + ''); } } else { updateValue(element, text.substring(0, charIndex)); } } function cleanAndRestart(element, charIndex, arrIndex, currentText) { if (charIndex == 0) { if ($scope.prebeginFn) { $scope.prebeginFn()(); } } if (charIndex > 0) { currentText = currentText.slice(0, -1); // element.html(currentText.substring(0, currentText.length - 1) + cursor); updateValue(element, currentText + cursor); charIndex--; timer = $timeout(function () { cleanAndRestart(element, charIndex, arrIndex, currentText); }, eraseDelay); return; } else { arrIndex++; currentText = textArray[arrIndex]; timer = $timeout(function () { updateIt(element, 0, arrIndex, currentText); }, typeDelay); } } function getTypeDelay(delay) { if (typeof delay === 'string') { return delay.charAt(delay.length - 1) === 's' ? parseInt(delay.substring(0, delay.length - 1), 10) * 1000 : +delay; } else { return false; } } function getAnimationDelay(delay) { if (typeof delay === 'string') { return delay.charAt(delay.length - 1) === 's' ? delay : parseInt(delay.substring(0, delay.length - 1), 10) / 1000; } } function updateValue(element, value) { if (element.prop('nodeName').toUpperCase() === 'INPUT') { return element.val(value); } return element.html(value); } $scope.$on('$destroy', function () { if (timer) { $timeout.cancel(timer); } }); $scope.$watch('start', function (newVal) { if (!running && newVal) { running = !running; typewrite(); } }); $scope.$watch('text', function (newVal, oldVal) { if (!(newVal instanceof Array)) { currentText = newVal; typewrite(); } }); } return { restrict: 'A', link: linkFunction, replace: true, scope: { text: '=', callbackFn: '&', iterationCallback: '&', iterationDelay: '=', prebeginFn: '&', start: '=' } }; }]); ;var Neeto = Neeto || {}; Neeto.crypto = { generateRandomKey: function generateRandomKey() { return CryptoJS.lib.WordArray.random(256 / 8).toString(); }, decryptText: function decryptText(encrypted_content, key) { return CryptoJS.AES.decrypt(encrypted_content, key).toString(CryptoJS.enc.Utf8); }, encryptText: function encryptText(text, key) { return CryptoJS.AES.encrypt(text, key).toString(); }, generateRandomEncryptionKey: function generateRandomEncryptionKey() { var salt = Neeto.crypto.generateRandomKey(); var passphrase = Neeto.crypto.generateRandomKey(); return CryptoJS.PBKDF2(passphrase, salt, { keySize: 256 / 32 }).toString(); }, sha256: function sha256(text) { return CryptoJS.SHA256(text).toString(); }, /** Generates two deterministic 256 bit keys based on one input */ generateAsymmetricKeyPair: function generateAsymmetricKeyPair(input, salt) { var output = CryptoJS.PBKDF2(input, salt, { keySize: 512 / 32, hasher: CryptoJS.algo.SHA512, iterations: 3000 }); var firstHalf = _.clone(output); var secondHalf = _.clone(output); var sigBytes = output.sigBytes / 2; var outputLength = output.words.length; firstHalf.words = output.words.slice(0, outputLength / 2); secondHalf.words = output.words.slice(outputLength / 2, outputLength); firstHalf.sigBytes = sigBytes; secondHalf.sigBytes = sigBytes; return [firstHalf.toString(), secondHalf.toString()]; }, generateEncryptionKeysForUser: function generateEncryptionKeysForUser(password, email) { var keys = Neeto.crypto.generateAsymmetricKeyPair(password, email); var pw = keys[0]; var gk = keys[1]; return { pw: pw, gk: gk }; } }; },{}]},{},[1]);