diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 3254d78f8..cdca7863e 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -15,30 +15,9 @@ angular.module('app.frontend') bindToController: true, link:function(scope, elem, attrs, ctrl) { - - var handler = function(event) { - if (event.ctrlKey || event.metaKey) { - switch (String.fromCharCode(event.which).toLowerCase()) { - 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 = {}; } }); } @@ -47,27 +26,49 @@ angular.module('app.frontend') .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager) { window.addEventListener("message", function(event){ - // console.log("App received message:", event); if(event.data.status) { this.postNoteToExternalEditor(); } else { var id = event.data.id; var text = event.data.text; - if(this.note.uuid == id) { + var data = event.data.data; + + if(this.note.uuid === id) { this.note.text = text; + if(data) { + var changesMade = this.customEditor.setData(id, data); + if(changesMade) { + this.customEditor.setDirty(true); + } + } this.changesMade(); } } }.bind(this), false); this.setNote = function(note, oldNote) { + var currentEditor = this.customEditor; + this.customEditor = null; this.showExtensions = false; this.showMenu = false; this.loadTagsString(); - if(note.editorUrl) { - this.customEditor = this.editorForUrl(note.editorUrl); + var setEditor = function(editor) { + this.customEditor = editor; this.postNoteToExternalEditor(); + }.bind(this) + + var editor = this.editorForNote(note); + if(editor) { + if(currentEditor !== editor) { + // switch after timeout, so that note data isnt posted to current editor + $timeout(function(){ + setEditor(editor); + }.bind(this)); + } else { + // switch immediately + setEditor(editor); + } } else { this.customEditor = null; } @@ -87,23 +88,35 @@ angular.module('app.frontend') this.selectedEditor = function(editor) { this.showEditorMenu = false; + + if(this.customEditor && editor !== this.customEditor) { + this.customEditor.removeItemAsRelationship(this.note); + this.customEditor.setDirty(true); + } + if(editor.default) { this.customEditor = null; } else { this.customEditor = editor; + this.customEditor.addItemAsRelationship(this.note); + this.customEditor.setDirty(true); } - this.note.editorUrl = editor.url; }.bind(this) - this.editorForUrl = function(url) { + this.editorForNote = function(note) { var editors = modelManager.itemsForContentType("SN|Editor"); - return editors.filter(function(editor){return editor.url == url})[0]; + for(var editor of editors) { + if(_.includes(editor.notes, note)) { + return editor; + } + } + return null; } this.postNoteToExternalEditor = function() { var externalEditorElement = document.getElementById("editor-iframe"); if(externalEditorElement) { - externalEditorElement.contentWindow.postMessage({text: this.note.text, id: this.note.uuid}, '*'); + externalEditorElement.contentWindow.postMessage({text: this.note.text, data: this.customEditor.dataForKey(this.note.uuid), id: this.note.uuid}, '*'); } } @@ -214,6 +227,11 @@ angular.module('app.frontend') } } + this.clickedEditNote = function() { + this.editorMode = 'edit'; + this.focusEditor(100); + } + /* Tags */ this.loadTagsString = function() { diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index 936fb7b52..df1f9307f 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -1,7 +1,31 @@ angular.module('app.frontend') -.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager, authManager) { +.controller('HomeCtrl', function ($scope, $stateParams, $rootScope, $timeout, modelManager, syncManager, authManager) { + + function autoSignInFromParams() { + if(!authManager.offline()) { + // check if current account + if(syncManager.serverURL == $stateParams.server && authManager.user.email == $stateParams.email) { + // already signed in, return + return; + } else { + // sign out + syncManager.destroyLocalData(function(){ + window.location.reload(); + }) + } + } else { + authManager.login($stateParams.server, $stateParams.email, $stateParams.pw, function(response){ + window.location.reload(); + }) + } + } + + if($stateParams.server && $stateParams.email) { + autoSignInFromParams(); + } syncManager.loadLocalItems(function(items) { + $scope.allTag.didLoad = true; $scope.$apply(); syncManager.sync(null); @@ -11,7 +35,9 @@ angular.module('app.frontend') }, 30000); }); - $scope.allTag = new Tag({all: true}); + var allTag = new Tag({all: true}); + allTag.needsLoad = true; + $scope.allTag = allTag; $scope.allTag.title = "All"; $scope.tags = modelManager.tags; $scope.allTag.notes = modelManager.notes; @@ -127,6 +153,12 @@ angular.module('app.frontend') this.$apply(fn); }; + $scope.notifyDelete = function() { + $timeout(function() { + $rootScope.$broadcast("noteDeleted"); + }.bind(this), 0); + } + $scope.deleteNote = function(note) { modelManager.setItemToBeDeleted(note); @@ -137,6 +169,7 @@ angular.module('app.frontend') if(note.dummy) { modelManager.removeItemLocally(note); + $scope.notifyDelete(); return; } @@ -144,8 +177,11 @@ angular.module('app.frontend') if(authManager.offline()) { // when deleting items while ofline, we need to explictly tell angular to refresh UI setTimeout(function () { + $scope.notifyDelete(); $scope.safeApply(); }, 50); + } else { + $scope.notifyDelete(); } }); } diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index 76ee180f0..0d3106260 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -4,7 +4,6 @@ angular.module('app.frontend') scope: { addNew: "&", selectionMade: "&", - remove: "&", tag: "=", removeTag: "&" }, @@ -18,7 +17,16 @@ angular.module('app.frontend') link:function(scope, elem, attrs, ctrl) { scope.$watch('ctrl.tag', function(tag, oldTag){ if(tag) { - ctrl.tagDidChange(tag, oldTag); + if(tag.needsLoad) { + scope.$watch('ctrl.tag.didLoad', function(didLoad){ + if(didLoad) { + tag.needsLoad = false; + ctrl.tagDidChange(tag, oldTag); + } + }); + } else { + ctrl.tagDidChange(tag, oldTag); + } } }); } @@ -32,7 +40,9 @@ angular.module('app.frontend') this.showMenu = false; }.bind(this)) - var isFirstLoad = true; + $rootScope.$on("noteDeleted", function() { + this.selectFirstNote(false); + }.bind(this)) this.notesToDisplay = 20; this.paginate = function() { @@ -47,20 +57,16 @@ angular.module('app.frontend') } this.noteFilter.text = ""; + this.setNotes(tag.notes); + } - tag.notes.forEach(function(note){ + this.setNotes = function(notes) { + notes.forEach(function(note){ note.visible = true; }) - this.selectFirstNote(false); - if(isFirstLoad) { - $timeout(function(){ - this.createNewNote(); - isFirstLoad = false; - }.bind(this)) - } else if(tag.notes.length == 0) { - this.createNewNote(); - } + var createNew = notes.length == 0; + this.selectFirstNote(createNew); } this.selectedTagDelete = function() { @@ -69,7 +75,7 @@ angular.module('app.frontend') } this.selectFirstNote = function(createNew) { - var visibleNotes = this.tag.notes.filter(function(note){ + var visibleNotes = this.sortedNotes.filter(function(note){ return note.visible; }); diff --git a/app/assets/javascripts/app/frontend/models/app/editor.js b/app/assets/javascripts/app/frontend/models/app/editor.js index 2b823d3a5..00a09cc0f 100644 --- a/app/assets/javascripts/app/frontend/models/app/editor.js +++ b/app/assets/javascripts/app/frontend/models/app/editor.js @@ -2,24 +2,70 @@ class Editor extends Item { constructor(json_obj) { super(json_obj); + if(!this.notes) { + this.notes = []; + } + if(!this.data) { + this.data = {}; + } } mapContentToLocalProperties(contentObject) { super.mapContentToLocalProperties(contentObject) this.url = contentObject.url; this.name = contentObject.name; + this.data = contentObject.data || {}; } structureParams() { var params = { url: this.url, - name: this.name + name: this.name, + data: this.data }; _.merge(params, super.structureParams()); return params; } + referenceParams() { + var references = _.map(this.notes, function(note){ + return {uuid: note.uuid, content_type: note.content_type}; + }) + + return references; + } + + addItemAsRelationship(item) { + if(item.content_type == "Note") { + if(!_.find(this.notes, item)) { + this.notes.push(item); + } + } + super.addItemAsRelationship(item); + } + + removeItemAsRelationship(item) { + if(item.content_type == "Note") { + _.pull(this.notes, item); + } + super.removeItemAsRelationship(item); + } + + removeAllRelationships() { + super.removeAllRelationships(); + this.notes = []; + } + + locallyClearAllReferences() { + super.locallyClearAllReferences(); + this.notes = []; + } + + allReferencedObjects() { + return this.notes; + } + toJSON() { return {uuid: this.uuid} } @@ -27,4 +73,17 @@ class Editor extends Item { get content_type() { return "SN|Editor"; } + + setData(key, value) { + var dataHasChanged = JSON.stringify(this.data[key]) !== JSON.stringify(value); + if(dataHasChanged) { + this.data[key] = value; + return true; + } + return false; + } + + dataForKey(key) { + return this.data[key] || {}; + } } diff --git a/app/assets/javascripts/app/frontend/models/app/note.js b/app/assets/javascripts/app/frontend/models/app/note.js index e79a64682..58cd84292 100644 --- a/app/assets/javascripts/app/frontend/models/app/note.js +++ b/app/assets/javascripts/app/frontend/models/app/note.js @@ -62,7 +62,7 @@ class Note extends Item { _.pull(tag.notes, this); }.bind(this)) this.tags = []; - } + } isBeingRemovedLocally() { this.tags.forEach(function(tag){ diff --git a/app/assets/javascripts/app/frontend/models/local/itemParams.js b/app/assets/javascripts/app/frontend/models/local/itemParams.js index 22d7405ce..396a51bd1 100644 --- a/app/assets/javascripts/app/frontend/models/local/itemParams.js +++ b/app/assets/javascripts/app/frontend/models/local/itemParams.js @@ -16,7 +16,7 @@ class ItemParams { } paramsForLocalStorage() { - this.additionalFields = ["updated_at", "dirty", "editorUrl"]; + this.additionalFields = ["updated_at", "dirty"]; this.forExportFile = true; return this.__params(); } diff --git a/app/assets/javascripts/app/frontend/routes.js b/app/assets/javascripts/app/frontend/routes.js index f1f9322fa..687d4cab9 100644 --- a/app/assets/javascripts/app/frontend/routes.js +++ b/app/assets/javascripts/app/frontend/routes.js @@ -7,7 +7,7 @@ angular.module('app.frontend') }) .state('home', { - url: '/', + url: '/?server&email&pw', parent: 'base', views: { 'content@' : { diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index f9e57864c..96f001166 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -78,7 +78,7 @@ angular.module('app.frontend') callback(response); }.bind(this), function(response){ console.error("Error logging in", response); - callback(null); + callback(response); }) }.bind(this)); @@ -106,8 +106,8 @@ angular.module('app.frontend') callback(response); }.bind(this), function(response){ console.error("Registration error", response); - callback(null); - }) + callback(response); + }.bind(this)) }.bind(this)); } @@ -120,7 +120,7 @@ angular.module('app.frontend') this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw); callback(response); }.bind(this), function(response){ - var error = response.data; + var error = response; if(!error) { error = {message: "Something went wrong while changing your password. Your password was not changed. Please try again."} } diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index e243a35cb..7fd9ca4c1 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -6,10 +6,10 @@ class AccountMenu { this.scope = {}; } - controller($scope, authManager, modelManager, syncManager, $timeout) { + controller($scope, authManager, modelManager, syncManager, dbManager, $timeout) { 'ngInject'; - $scope.formData = {url: syncManager.serverURL}; + $scope.formData = {mergeLocal: true, url: syncManager.serverURL}; $scope.user = authManager.user; $scope.server = syncManager.serverURL; @@ -82,7 +82,6 @@ class AccountMenu { $scope.loginSubmitPressed = function() { $scope.formData.status = "Generating Login Keys..."; - console.log("logging in with url", $scope.formData.url); $timeout(function(){ authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ if(!response || response.error) { @@ -99,6 +98,11 @@ class AccountMenu { } $scope.submitRegistrationForm = function() { + var confirmation = prompt("Please confirm your password. Note that because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.") + if(confirmation !== $scope.formData.user_password) { + alert("The two passwords you entered do not match. Please try again."); + return; + } $scope.formData.status = "Generating Account Keys..."; $timeout(function(){ @@ -114,10 +118,32 @@ class AccountMenu { }) } + $scope.localNotesCount = function() { + return modelManager.filteredNotes.length; + } + + $scope.mergeLocalChanged = function() { + if(!$scope.formData.mergeLocal) { + if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) { + $scope.formData.mergeLocal = true; + } + } + } + $scope.onAuthSuccess = function() { - syncManager.markAllItemsDirtyAndSaveOffline(function(){ + var block = function() { window.location.reload(); - }) + } + + if($scope.formData.mergeLocal) { + syncManager.markAllItemsDirtyAndSaveOffline(function(){ + block(); + }) + } else { + dbManager.clearAllItems(function(){ + block(); + }) + } } $scope.destroyLocalData = function() { diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index ef67bc86a..f5b5c04ac 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -59,7 +59,9 @@ class ModelManager { } mapResponseItemsToLocalModelsOmittingFields(items, omitFields) { - var models = []; + var models = [], processedObjects = []; + + // first loop should add and process items for (var json_obj of items) { json_obj = _.omit(json_obj, omitFields || []) var item = this.findItem(json_obj["uuid"]); @@ -80,11 +82,16 @@ class ModelManager { this.addItem(item); - if(json_obj.content) { - this.resolveReferencesForItem(item); - } - models.push(item); + processedObjects.push(json_obj); + } + + // second loop should process references + for (var index in processedObjects) { + var json_obj = processedObjects[index]; + if(json_obj.content) { + this.resolveReferencesForItem(models[index]); + } } this.notifySyncObserversOfModels(models); @@ -174,6 +181,7 @@ class ModelManager { return; } + for(var reference of contentObject.references) { var referencedItem = this.findItem(reference.uuid); if(referencedItem) { diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 4736c5b48..54d4c18d8 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -187,7 +187,6 @@ class SyncManager { var saved = this.handleItemsResponse(response.saved_items, omitFields); this.handleUnsavedItemsResponse(response.unsaved) - this.writeItemsToLocalStorage(saved, false, null); this.writeItemsToLocalStorage(retrieved, false, null); @@ -213,7 +212,7 @@ class SyncManager { }.bind(this), function(response){ console.log("Sync error: ", response); - var error = response.data ? response.data.error : {message: "Could not connect to server."}; + var error = response ? response.error : {message: "Could not connect to server."}; this.syncStatus.syncOpInProgress = false; this.syncStatus.error = error; diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 6927bff0f..3c93889a5 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -8,6 +8,10 @@ %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'} %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'} %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'} + .checkbox{"ng-if" => "localNotesCount() > 0"} + %label + %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} + Merge local notes ({{localNotesCount()}} notes) %div{"ng-if" => "!formData.status"} %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} @@ -94,4 +98,4 @@ .spinner.mt-10{"ng-if" => "importData.loading"} - %a.block.mt-25.red{"ng-click" => "destroyLocalData()"} Destroy all local data + %a.block.mt-25.red{"ng-click" => "destroyLocalData()"} {{ user ? "Sign out and clear local data" : "Clear all local data" }} diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index e8956bb3b..933e00c99 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -1,5 +1,5 @@ .section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"} - .section-title-bar.editor-heading{"ng-class" => "{'fullscreen' : ctrl.fullscreen }"} + .section-title-bar.editor-heading{"ng-if" => "ctrl.note", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"} .title %input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()", @@ -8,20 +8,19 @@ .tags %input.tags-input{"type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();", "ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"} - .section-menu + .section-menu{"ng-if" => "ctrl.note"} %ul.nav %li.dropdown.pull-left.mr-10{"click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"} %a.dropdown-toggle{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false;"} - File + Menu %span.caret %span.sr-only %ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu"} %li{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleFullScreen()"} .text Toggle Fullscreen - .shortcut Cmd + O %li{"ng-click" => "ctrl.deleteNote()"} - .text Delete + .text Delete Note %li.sep %li.dropdown.pull-left.mr-10{"click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"} diff --git a/app/assets/templates/frontend/home.html.haml b/app/assets/templates/frontend/home.html.haml index 82642f6f9..28584401b 100644 --- a/app/assets/templates/frontend/home.html.haml +++ b/app/assets/templates/frontend/home.html.haml @@ -4,8 +4,8 @@ "tags" => "tags"} %notes-section{"remove-tag" => "notesRemoveTag", "add-new" => "notesAddNew", "selection-made" => "notesSelectionMade", - "tag" => "selectedTag", "remove" => "deleteNote"} + "tag" => "selectedTag"} - %editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"} + %editor-section{"note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"} %header diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index f259f62c8..63a42b7f0 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -10,13 +10,11 @@ %ul.nav.nav-pills %li.dropdown %a.dropdown-toggle{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"} - Tag options + Menu %span.caret %span.sr-only %ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu"} - %li - %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagDelete()"} Delete Tag %li %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedSortByCreated()"} %span.top.mt-5.mr-5{"ng-if" => "ctrl.sortBy == 'created_at'"} ✓ @@ -25,10 +23,12 @@ %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedSortByUpdated()"} %span.top.mt-5.mr-5{"ng-if" => "ctrl.sortBy == 'updated_at'"} ✓ Sort by date updated + %li + %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagDelete()"} Delete Tag .scrollable .infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"} - .note{"ng-repeat" => "note in ctrl.tag.notes | filter: ctrl.filterNotes | orderBy: ctrl.sortBy:true | limitTo:ctrl.notesToDisplay", + .note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | orderBy: ctrl.sortBy:true | limitTo:ctrl.notesToDisplay))", "ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"} .name{"ng-if" => "note.title"} {{note.title}} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2d6b04dea..43b6f0688 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,10 @@ class ApplicationController < ActionController::Base - # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :null_session instead. protect_from_forgery with: :null_session after_action :set_csrf_cookie + after_action :allow_iframe + layout :false def frontend @@ -13,8 +13,13 @@ class ApplicationController < ActionController::Base rescue_from ActionView::MissingTemplate do |exception| end + protected + def allow_iframe + response.headers.except! 'X-Frame-Options' + end + def set_app_domain @appDomain = request.domain @appDomain << ':' + request.port.to_s unless request.port.blank?