diff options
author | Jon Newson <jon_newson@ieee.org> | 2016-03-15 18:07:42 +1100 |
---|---|---|
committer | Jon Newson <jon_newson@ieee.org> | 2016-03-15 18:07:42 +1100 |
commit | a455353a811d4cf3a9c327750e9d0fb4c7ee229a (patch) | |
tree | 0c42f4a153df882d49b0448209ab0a1937e13685 | |
parent | 0ffeb6b70df00a54a2509179c32104bc7f883196 (diff) | |
parent | cf32471caf75b817b23339166002987726d3d6d8 (diff) |
Merge branch 'master' of https://github.com/pixelated/pixelated-user-agent
# By Felix Hammerl (13) and Thais Siqueira (3)
# Via Christoph (1) and Thais Siqueira (1)
* 'master' of https://github.com/pixelated/pixelated-user-agent:
Sets SSL certifications to false.
Fixes pep8 errors and update requests to 2.9.1.
Update locust test to run after xsrf token implementation.
Issue #620: Adapt unit tests to CSS changes
Issue #620: Refactor palceholder
Issue #620: Remove former main css file
Issue #620: Spike growl CSS modularization
Issue #617: Highlight search terms by altering mail content
Issue #617: Allow only >=3 alphanumeric characters in search field
Issue #617: Restrict searching to alphanumeric characters
Issue #617: Remove highlighting for sandboxed content
Issue #617: Add sandbox to build scripts
Issue #617: Add sandbox to user-agent
Issue #617: Create sandbox resouces
Issue #617: Add iframe-resizer
Issue #617: Serve content from Sandbox resource
48 files changed, 493 insertions, 165 deletions
diff --git a/Vagrantfile b/Vagrantfile index ba925079..828382b9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -30,5 +30,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.provider "virtualbox" do |v, override| v.memory = 1024 override.vm.network :forwarded_port, guest: 3333, host: 3333 # do NOT add host_ip in this line. It is not necessary + override.vm.network :forwarded_port, guest: 8089, host: 8089 end end diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 86435d89..109dc08e 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -20,6 +20,7 @@ from string import Template from pixelated.resources import BaseResource, UnAuthorizedResource from pixelated.resources.attachments_resource import AttachmentsResource +from pixelated.resources.sandbox_resource import SandboxResource from pixelated.resources.contacts_resource import ContactsResource from pixelated.resources.features_resource import FeaturesResource from pixelated.resources.feedback_resource import FeedbackResource @@ -75,6 +76,7 @@ class RootResource(BaseResource): return csrf_input and csrf_input == xsrf_token def initialize(self, portal=None, disclaimer_banner=None): + self._child_resources.add('sandbox', SandboxResource(self._static_folder)) self._child_resources.add('assets', File(self._static_folder)) self._child_resources.add('keys', KeysResource(self._services_factory)) self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) diff --git a/service/pixelated/resources/sandbox_resource.py b/service/pixelated/resources/sandbox_resource.py new file mode 100644 index 00000000..28e8c9be --- /dev/null +++ b/service/pixelated/resources/sandbox_resource.py @@ -0,0 +1,34 @@ +# +# Copyright (c) 2016 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + +from twisted.web.static import File + + +class SandboxResource(File): + CSP_HEADER_VALUES = "sandbox allow-popups allow-scripts;" \ + "default-src 'self';" \ + "style-src *;" \ + "script-src *;" \ + "font-src *;" \ + "img-src *;" \ + "object-src 'none';" \ + "connect-src 'none';" + + def render_GET(self, request): + request.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES) + request.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES) + request.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES) + return super(SandboxResource, self).render_GET(request) diff --git a/service/requirements.txt b/service/requirements.txt index 95a0f4f8..b74b7f94 100644 --- a/service/requirements.txt +++ b/service/requirements.txt @@ -4,7 +4,7 @@ https://launchpad.net/dirspec/stable-13-10/13.10/+download/dirspec-13.10.tar.gz --allow-external dirspec --allow-unverified dirspec https://launchpad.net/u1db/stable-13-10/13.10/+download/u1db-13.10.tar.bz2 pyasn1==0.1.8 -requests==2.0.0 +requests==2.9.1 srp==1.0.4 whoosh==2.5.7 pycryptopp diff --git a/service/test/functional/features/steps/mail_view.py b/service/test/functional/features/steps/mail_view.py index 82fc28af..c0e9e22b 100644 --- a/service/test/functional/features/steps/mail_view.py +++ b/service/test/functional/features/steps/mail_view.py @@ -25,8 +25,11 @@ def impl(context, subject): @then('I see that the body reads \'{expected_body}\'') def impl(context, expected_body): - e = find_element_by_css_selector(context, '#mail-view .bodyArea') + find_element_by_css_selector(context, '#read-sandbox') + context.browser.switch_to_frame('read-sandbox') + e = find_element_by_css_selector(context, 'body') assert e.text == expected_body + context.browser.switch_to_default_content() @then('that email has the \'{tag}\' tag') diff --git a/service/test/load/locustfile.py b/service/test/load/locustfile.py index 68e39433..0c2ed518 100644 --- a/service/test/load/locustfile.py +++ b/service/test/load/locustfile.py @@ -1,6 +1,5 @@ import os import json -import time from random import randint from leap.auth import SRPAuth @@ -9,9 +8,13 @@ from locust import HttpLocust, TaskSet, task from pixelated.resources.login_resource import LoginResource LEAP_PROVIDER = os.environ.get('LEAP_PROVIDER', 'dev.pixelated-project.org') -LEAP_SERVER_HOST = os.environ.get('LEAP_SERVER_HOST', 'https://api.%s:4430' % LEAP_PROVIDER) -LEAP_VERIFY_CERTIFICATE = os.environ.get('LEAP_VERIFY_CERTIFICATE', '~/.leap/ca.crt') -MAX_NUMBER_USER = os.environ.get('MAX_NUMBER_USER', 10000) +LEAP_SERVER_HOST = os.environ.get( + 'LEAP_SERVER_HOST', + 'https://api.%s:4430' % LEAP_PROVIDER) +LEAP_VERIFY_CERTIFICATE = os.environ.get( + 'LEAP_VERIFY_CERTIFICATE', + '~/.leap/ca.crt') +MAX_NUMBER_USER = os.environ.get('MAX_NUMBER_USER', 100) INVITES_FILENAME = os.environ.get('INVITES_FILENAME', '/tmp/invite_codes.txt') INVITES_ENABLED = os.environ.get('INVITES_ENABLED', 'true') == 'true' @@ -23,53 +26,94 @@ def load_invite_from_number(number): class UserBehavior(TaskSet): + def __init__(self, *args, **kwargs): + super(UserBehavior, self).__init__(*args, **kwargs) + self.cookies = {} + def on_start(self): - """ on_start is called when a Locust start before any task is scheduled """ self.login() def _get_or_create_user(self, number): - srp_auth = SRPAuth(LEAP_SERVER_HOST, os.path.expanduser(LEAP_VERIFY_CERTIFICATE)) + srp_auth = SRPAuth( + LEAP_SERVER_HOST, + os.path.expanduser(LEAP_VERIFY_CERTIFICATE)) username, password = ('loadtest%d' % number), ('password_%d' % number) try: srp_auth.authenticate(username, password) except SRPAuthenticationError: - invite_code = load_invite_from_number(number) if INVITES_ENABLED else None + invite_code = None + if INVITES_ENABLED: + invite_code = load_invite_from_number(number) + srp_auth.register(username, password, invite_code) return username, password def login(self): number = randint(1, int(MAX_NUMBER_USER)) username, password = self._get_or_create_user(number) - self.client.post("/%s" % LoginResource.BASE_URL, {"username": username, "password": password}) + response = self.client.post( + "/%s" % LoginResource.BASE_URL, + {"username": username, "password": password}, + verify=False) + self.cookies.update(response.cookies.get_dict()) + resp = self.client.get("/", verify=False) + self.cookies.update(resp.cookies.get_dict()) self.username = username - time.sleep(5) @task(1) def index(self): - self.client.get("/") + self.client.get("/", verify=False) @task(2) def mail_box(self): - self.client.get("/mails?q=tag:'inbox'&p=1&w=25") + self.client.get("/mails?q=tag:'inbox'&p=1&w=25", verify=False) @task(3) def send_mail(self): - payload = {"tags": ["drafts"], "body": "some text lorem ipsum", "attachments": [], "ident": "", - "header": {"to": ["%s@%s" % (self.username, LEAP_PROVIDER)], "cc": [], "bcc": [], "subject": "load testing"}} - with self.client.post('/mails', json=payload, catch_response=True) as email_response: + payload = { + "tags": ["drafts"], + "body": "some text lorem ipsum", + "attachments": [], + "ident": "", + "header": { + "to": ["%s@%s" % (self.username, LEAP_PROVIDER)], + "cc": [], + "bcc": [], + "subject": "load testing"}} + + self.cookies.update( + self.client.get("/", verify=False).cookies.get_dict()) + print(self.cookies) + with self.client.post( + '/mails', + json=payload, + catch_response=True, + cookies=self.cookies, + headers={ + 'X-Requested-With': 'XMLHttpRequest', + 'X-XSRF-TOKEN': self.cookies['XSRF-TOKEN']}) as email_response: if email_response.status_code == 201: email_id = json.loads(email_response.content)['ident'] print email_id self.delete_mail(email_id) else: - email_response.failure('Error: email not Sent, status code: %s' % email_response.status_code) + email_response.failure( + 'Error: email not Sent, status code: %s' % ( + email_response.status_code)) def delete_mail(self, ident): payload = {"idents": [ident]} - self.client.post('/mails/delete', json=payload) + self.client.post( + '/mails/delete', + json=payload, + cookies=self.cookies, + verify=False, + headers={ + 'X-Requested-With': 'XMLHttpRequest', + 'X-XSRF-TOKEN': self.cookies['XSRF-TOKEN']}) class WebsiteUser(HttpLocust): task_set = UserBehavior - min_wait = 3000 + min_wait = 5000 max_wait = 15000 diff --git a/service/test/unit/resources/test_sandbox_resource.py b/service/test/unit/resources/test_sandbox_resource.py new file mode 100644 index 00000000..3db43e12 --- /dev/null +++ b/service/test/unit/resources/test_sandbox_resource.py @@ -0,0 +1,38 @@ +import os +import unittest + +from twisted.internet import defer +from twisted.web.test.requesthelper import DummyRequest + +from pixelated.resources.sandbox_resource import SandboxResource +from test.unit.resources import DummySite + + +class TestSandBoxResource(unittest.TestCase): + def setUp(self): + static_folder = os.path.dirname(os.path.abspath(__file__)) + self.resource = SandboxResource(static_folder) + self.resource.isLeaf = True + self.web = DummySite(self.resource) + + @defer.inlineCallbacks + def test_render_GET_should_set_sandbox_csp_header(self): + request = DummyRequest(['/sandbox']) + request.method = 'GET' + request.isSecure = lambda: True + request.redirect = lambda _: 'irrelevant' + + expected_csp_headers = "sandbox allow-popups allow-scripts;" \ + "default-src 'self';" \ + "style-src *;" \ + "script-src *;" \ + "font-src *;" \ + "img-src *;" \ + "object-src 'none';" \ + "connect-src 'none';" + + yield self.web.get(request) + + self.assertEquals(expected_csp_headers, request.outgoingHeaders.get('X-Content-Security-Policy'.lower())) + self.assertEquals(expected_csp_headers, request.outgoingHeaders.get('Content-Security-Policy'.lower())) + self.assertEquals(expected_csp_headers, request.outgoingHeaders.get('X-Webkit-CSP'.lower())) diff --git a/web-ui/app/index.html b/web-ui/app/index.html index 2d35662c..4d6f3037 100644 --- a/web-ui/app/index.html +++ b/web-ui/app/index.html @@ -9,8 +9,7 @@ <meta name="viewport" content="width=device-width"> <link href="assets/bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="assets/bower_components/jquery-file-upload/css/jquery.fileupload.css" rel="stylesheet" type="text/css"> -<link href="assets/css/opensans.css" rel="stylesheet" type="text/css"> -<link rel="stylesheet" href="assets/css/main.css"> +<link rel="stylesheet" href="assets/css/style.css"> </head> <body> @@ -56,7 +55,7 @@ </div> <div class="off-canvas-wrap content" data-offcanvas> - <header id="main" > + <header class="message-panel-container" > <div id="user-alerts" class="message-panel"></div> </header> @@ -100,6 +99,7 @@ <script src="assets/bower_components/foundation/js/foundation/foundation.reveal.js" ></script> <script src="assets/bower_components/foundation/js/foundation/foundation.offcanvas.js"></script> <script src="assets/js/foundation/initialize_foundation.js"></script> +<script src="assets/bower_components/iframe-resizer/js/iframeResizer.min.js"></script> <script src="assets/bower_components/requirejs/require.js" data-main="assets/js/main.js"></script> <!--usemin_end--> diff --git a/web-ui/app/js/helpers/sanitizer.js b/web-ui/app/js/helpers/sanitizer.js index eea1f0f7..443e8602 100644 --- a/web-ui/app/js/helpers/sanitizer.js +++ b/web-ui/app/js/helpers/sanitizer.js @@ -23,6 +23,16 @@ define(['DOMPurify', 'he'], function (DOMPurify, he) { */ var sanitizer = {}; + sanitizer.whitelist = [{ + // highlight tag open + pre: '<em class="search-highlight">', + post: '<em class="search-highlight">' + }, { + // highlight tag close + pre: '</em>', + post: '</em>' + }]; + /** * Adds html line breaks to a plaintext with line breaks (incl carriage return) * @@ -55,16 +65,24 @@ define(['DOMPurify', 'he'], function (DOMPurify, he) { }; /** - * Runs a given dirty body through he, thereby encoding everything - * as HTML entities. - * - * @param {string} dirtyBody The unsanitized string - * @return {string} Safe-to-display HTML string - */ + * Runs a given dirty body through he, thereby encoding everything + * as HTML entities. + * + * @param {string} dirtyBody The unsanitized string + * @return {string} Safe-to-display HTML string + */ sanitizer.purifyText = function (dirtyBody) { - return he.encode(dirtyBody, { + var escapedBody = he.encode(dirtyBody, { encodeEverything: true }); + + this.whitelist.forEach(function(entry) { + while (escapedBody.indexOf(entry.pre) > -1) { + escapedBody = escapedBody.replace(entry.pre, entry.post); + } + }); + + return escapedBody; }; /** diff --git a/web-ui/app/js/mail_list/ui/mail_list.js b/web-ui/app/js/mail_list/ui/mail_list.js index 18d36049..0f6c4fb5 100644 --- a/web-ui/app/js/mail_list/ui/mail_list.js +++ b/web-ui/app/js/mail_list/ui/mail_list.js @@ -81,7 +81,6 @@ define( this.renderMails = function (mails) { _.each(mails, this.appendMail, this); this.trigger(document, events.search.highlightResults, {where: '#mail-list'}); - this.trigger(document, events.search.highlightResults, {where: '.bodyArea'}); this.trigger(document, events.search.highlightResults, {where: '.subjectArea'}); this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'}); }; diff --git a/web-ui/app/js/mail_view/ui/mail_view.js b/web-ui/app/js/mail_view/ui/mail_view.js index dfc57585..d952fed7 100644 --- a/web-ui/app/js/mail_view/ui/mail_view.js +++ b/web-ui/app/js/mail_view/ui/mail_view.js @@ -71,9 +71,52 @@ define( attachments: attachments })); - this.$node.find('.bodyArea').html(viewHelpers.formatMailBody(data.mail)); + var $iframe = $("#read-sandbox"); + var iframe = $iframe[0]; + + var content = viewHelpers.formatMailBody(data.mail); + + iframe.onload = function() { + // use iframe-resizer to dynamically adapt iframe size to its content + var config = { + resizedCallback: scaleToFit, + checkOrigin: false + }; + $iframe.iFrameResize(config); + + // transform scale iframe to fit container width + // necessary if iframe is wider than container + function scaleToFit() { + var parentWidth = $iframe.parent().width(); + var w = $iframe.width(); + var scale = 'none'; + + // only scale html mails + var mail = data.mail; + if (mail && mail.htmlBody && (w > parentWidth)) { + scale = parentWidth / w; + scale = 'scale(' + scale + ',' + scale + ')'; + } + + $iframe.css({ + '-webkit-transform-origin': '0 0', + '-moz-transform-origin': '0 0', + '-ms-transform-origin': '0 0', + 'transform-origin': '0 0', + '-webkit-transform': scale, + '-moz-transform': scale, + '-ms-transform': scale, + 'transform': scale + }); + } + + iframe.contentWindow.postMessage({ + html: content + }, '*'); + }; + + - this.trigger(document, events.search.highlightResults, {where: '.bodyArea'}); this.trigger(document, events.search.highlightResults, {where: '.subjectArea'}); this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'}); this.trigger(document, events.ui.replyBox.showReplyContainer); @@ -214,9 +257,17 @@ define( this.trigger(events.mail.want, {mail: this.attr.ident, caller: this}); }; + this.highlightMailContent = function (event, data) { + // we can't directly manipulate the iFrame to highlight the content + // so we need to take an indirection where we directly manipulate + // the mail content to accomodate the highlighting + this.trigger(document, events.mail.highlightMailContent, data); + }; + this.after('initialize', function () { - this.on(this, events.mail.here, this.displayMail); this.on(this, events.mail.notFound, this.openNoMessageSelectedPane); + this.on(this, events.mail.here, this.highlightMailContent); + this.on(document, events.mail.display, this.displayMail); this.on(document, events.dispatchers.rightPane.clear, this.teardown); this.on(document, events.mail.tags.updated, this.tagsUpdated); this.on(document, events.mail.deleted, this.mailDeleted); diff --git a/web-ui/app/js/page/events.js b/web-ui/app/js/page/events.js index 7a0dbf9d..ad15e76e 100644 --- a/web-ui/app/js/page/events.js +++ b/web-ui/app/js/page/events.js @@ -121,6 +121,8 @@ define(function () { mail: { here: 'mail:here', want: 'mail:want', + display: 'mail:display', + highlightMailContent: 'mail:highlightMailContent', send: 'mail:send', send_failed: 'mail:send_failed', sent: 'mail:sent', diff --git a/web-ui/app/js/sandbox.js b/web-ui/app/js/sandbox.js new file mode 100644 index 00000000..f9e708d6 --- /dev/null +++ b/web-ui/app/js/sandbox.js @@ -0,0 +1,9 @@ +(function () { + 'use strict'; + + window.onmessage = function (e) { + if (e.data.html) { + document.body.innerHTML = e.data.html; + } + }; +})(); diff --git a/web-ui/app/js/search/results_highlighter.js b/web-ui/app/js/search/results_highlighter.js index 9e3ba167..831be0cd 100644 --- a/web-ui/app/js/search/results_highlighter.js +++ b/web-ui/app/js/search/results_highlighter.js @@ -40,6 +40,7 @@ define( var domIdent = data.where; if(this.attr.keywords) { _.each(this.attr.keywords, function (keyword) { + keyword = escapeRegExp(keyword); $(domIdent).highlightRegex(new RegExp(keyword, 'i'), { tagType: 'em', className: 'search-highlight' @@ -57,12 +58,40 @@ define( }); }; + this.highlightString = function (string) { + _.each(this.attr.keywords, function (keyword) { + keyword = escapeRegExp(keyword); + var regex = new RegExp('(' + keyword + ')', 'ig'); + string = string.replace(regex, '<em class="search-highlight">$1</em>'); + }); + return string; + }; + + /* + * Alter data.mail.textPlainBody to highlight each of this.attr.keywords + * and pass it back to the mail_view when done + */ + this.highlightMailContent = function(ev, data){ + var mail = data.mail; + mail.textPlainBody = this.highlightString(mail.textPlainBody); + this.trigger(document, events.mail.display, data); + }; + + /* + * Escapes the special charaters used regular expressions that + * would cause problems with strings in the RegExp constructor + */ + function escapeRegExp(string){ + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + this.after('initialize', function () { this.on(document, events.search.perform, this.getKeywordsSearch); this.on(document, events.ui.tag.select, this.clearHighlights); this.on(document, events.search.resetHighlight, this.clearHighlights); this.on(document, events.search.highlightResults, this.highlightResults); + this.on(document, events.mail.highlightMailContent, this.highlightMailContent); }); } }); diff --git a/web-ui/app/js/user_alerts/ui/user_alerts.js b/web-ui/app/js/user_alerts/ui/user_alerts.js index b02762aa..e944a7a5 100644 --- a/web-ui/app/js/user_alerts/ui/user_alerts.js +++ b/web-ui/app/js/user_alerts/ui/user_alerts.js @@ -32,20 +32,26 @@ define( dismissTimeout: 3000 }); - this.render = function (message) { + this.render = function(message) { this.$node.html(templates.userAlerts.message(message)); this.show(); setTimeout(this.hide.bind(this), this.attr.dismissTimeout); }; - this.displayMessage = function (ev, data) { - this.render({ message: {content: data.message, class: (data.class || 'success')}}); + this.displayMessage = function(ev, data) { + this.render({ + message: { + content: data.message, + class: 'message-panel__growl--' + (data.class || 'success') + } + }); }; - this.after('initialize', function () { + this.after('initialize', function() { this.on(document, events.ui.userAlerts.displayMessage, this.displayMessage); }); } } ); + diff --git a/web-ui/app/sandbox.html b/web-ui/app/sandbox.html new file mode 100644 index 00000000..13a86f25 --- /dev/null +++ b/web-ui/app/sandbox.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <link href="css/opensans.css" rel="stylesheet" type="text/css"> + <link href="css/sandbox.css" rel="stylesheet" type="text/css"> + + <!--usemin_start--> + <script src="js/sandbox.js"></script> + <script src="bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script> + <!--usemin_end--> +</head> + +<body></body> + +</html> diff --git a/web-ui/app/scss/_alerts.scss b/web-ui/app/scss/_alerts.scss deleted file mode 100644 index cfb31cbe..00000000 --- a/web-ui/app/scss/_alerts.scss +++ /dev/null @@ -1,23 +0,0 @@ -.message-panel { - width: 100%; - margin: 10px auto; - position: fixed; - z-index: 10000; - text-align: center; - span{ - padding: 5px 60px; - &.success { - background: $warning; - color: darken($warning, 50%); - border: 1px solid darken($warning, 10%); - @include box-shadow(1px 1px 3px darken($warning, 60%)); - } - &.error { - font-weight: bold; - color: white; - background: $error; - border: 1px solid darken($error, 10%); - @include box-shadow(1px 1px 3px darken($error, 60%)); - } - } -} diff --git a/web-ui/app/scss/_mascot.scss b/web-ui/app/scss/_mascot.scss deleted file mode 100644 index 74279063..00000000 --- a/web-ui/app/scss/_mascot.scss +++ /dev/null @@ -1,47 +0,0 @@ -#no-message-selected-pane { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100vh; - - z-index: -100; - background: $contrast; - padding: 30px; - vertical-align:middle; - text-align:center; - -webkit-transform: translate3d(0, 0, 0); - &:before{ - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - margin-right: -0.25em; - } - .scene{ - display:inline-block; - vertical-align:middle; - } - - .text{ - color:$medium_dark_grey; - margin-bottom: 40px; - } -} - -#no-mails-available-pane { - text-align: center; - line-height: 100vh; - margin-top: -130px; - - .scene{ - display:inline-block; - vertical-align:middle; - } - - .text{ - color:$medium_dark_grey; - margin-bottom: 40px; - } - -} diff --git a/web-ui/app/scss/_mixins.scss b/web-ui/app/scss/_mixins.scss index 5bb84105..4583c55d 100644 --- a/web-ui/app/scss/_mixins.scss +++ b/web-ui/app/scss/_mixins.scss @@ -1,4 +1,4 @@ -@import 'colors'; +@import 'base/colors'; // SHARED MIXINS @mixin btn-transition { diff --git a/web-ui/app/scss/_others.scss b/web-ui/app/scss/_others.scss new file mode 100644 index 00000000..e73ed33d --- /dev/null +++ b/web-ui/app/scss/_others.scss @@ -0,0 +1,7 @@ +.no-padding { + padding: 0; +} + +.text-right { + text-align: right; +} diff --git a/web-ui/app/scss/_read.scss b/web-ui/app/scss/_read.scss index 2c079408..236a7c7c 100644 --- a/web-ui/app/scss/_read.scss +++ b/web-ui/app/scss/_read.scss @@ -66,6 +66,13 @@ .bodyArea { padding: 10px 30px 0 30px; + box-sizing: border-box; + + iframe { + box-sizing: inherit; + border: none; + width: 100%; + } } .attachmentsAreaWrap { diff --git a/web-ui/app/scss/_colors.scss b/web-ui/app/scss/base/_colors.scss index de5f76b3..de5f76b3 100644 --- a/web-ui/app/scss/_colors.scss +++ b/web-ui/app/scss/base/_colors.scss diff --git a/web-ui/app/scss/opensans.scss b/web-ui/app/scss/base/_fonts.scss index ada6a025..ada6a025 100644 --- a/web-ui/app/scss/opensans.scss +++ b/web-ui/app/scss/base/_fonts.scss diff --git a/web-ui/app/scss/base/_scaffolding.scss b/web-ui/app/scss/base/_scaffolding.scss new file mode 100644 index 00000000..b8b5fa3b --- /dev/null +++ b/web-ui/app/scss/base/_scaffolding.scss @@ -0,0 +1,10 @@ +html { + height: 100% ; +} + +body { + min-height: 100% ; + overflow: hidden; + background: $white; +} + diff --git a/web-ui/app/scss/layout/_message-panel.scss b/web-ui/app/scss/layout/_message-panel.scss new file mode 100644 index 00000000..e311a9bf --- /dev/null +++ b/web-ui/app/scss/layout/_message-panel.scss @@ -0,0 +1,9 @@ +message-panel-container { + overflow: hidden; + position: fixed; + top: 0; + width: 100% ; + position: relative; + margin-bottom: 0; +} + diff --git a/web-ui/app/scss/main.scss b/web-ui/app/scss/main.scss deleted file mode 100644 index b582a5d5..00000000 --- a/web-ui/app/scss/main.scss +++ /dev/null @@ -1,37 +0,0 @@ -@import "compass/css3"; -@import "reset"; -@import "foundation"; -@import "colors"; -@import "mixins"; -@import "alerts"; -@import "read"; -@import "reply"; -@import "compose"; -@import "security"; -@import "mascot"; -@import "styles"; - -html { - height:100%; -} -body { - min-height:100%; - overflow: hidden; - background: $white; -} -header#main { - overflow: hidden; - position: fixed; - top: 0; - width: 100%; - position: relative; - margin-bottom: 0; -} - -.no-padding { - padding: 0; -} - -.text-right { - text-align: right; -} diff --git a/web-ui/app/scss/mixins/_position-helpers.scss b/web-ui/app/scss/mixins/_position-helpers.scss new file mode 100644 index 00000000..254bfc6c --- /dev/null +++ b/web-ui/app/scss/mixins/_position-helpers.scss @@ -0,0 +1,9 @@ +@mixin absolute-center-unknown-height-width() { + margin: auto; + position: absolute; + left: 50%; + top: 50%; + -ms-transform: translate(-50%, -50%); + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} diff --git a/web-ui/app/scss/sandbox.scss b/web-ui/app/scss/sandbox.scss new file mode 100644 index 00000000..3cb4c441 --- /dev/null +++ b/web-ui/app/scss/sandbox.scss @@ -0,0 +1,20 @@ +$search-highlight: #FFEF29; + +body { + font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; + font-size: 13px; + line-height: 1.2em; + background: white; + color: #333; + padding: 0; + margin: 0; + font-weight: normal; + -webkit-font-smoothing: antialiased; + font-style: normal; + box-sizing: border-box; + word-wrap: break-word; +} + +.search-highlight { + background-color: $search-highlight; +} diff --git a/web-ui/app/scss/style.scss b/web-ui/app/scss/style.scss new file mode 100644 index 00000000..d27b3031 --- /dev/null +++ b/web-ui/app/scss/style.scss @@ -0,0 +1,35 @@ +// vendor stylesheets and resets +@import "vendor/reset"; +@import "compass/css3"; +@import "vendor/foundation"; + +// basic configuration +@import "base/fonts"; +@import "base/colors"; +@import "base/scaffolding"; + +// mixins +@import "mixins/position-helpers"; +@import "mixins"; + +// templates +@import "templates/no-content-placeholder"; + +// layout +@import "layout/message-panel"; + +// views +@import "views/message-panel"; +@import "views/no-message-selected"; +@import "views/no-mails-available"; + +// misc stuff +@import "others"; + +// TODO +@import "compose"; +@import "read"; +@import "reply"; +@import "security"; +@import "styles"; + diff --git a/web-ui/app/scss/templates/_no-content-placeholder.scss b/web-ui/app/scss/templates/_no-content-placeholder.scss new file mode 100644 index 00000000..c6807011 --- /dev/null +++ b/web-ui/app/scss/templates/_no-content-placeholder.scss @@ -0,0 +1,5 @@ +.no-content-placeholder { + @include absolute-center-unknown-height-width; + + color: $medium_dark_grey; +} diff --git a/web-ui/app/scss/foundation.scss b/web-ui/app/scss/vendor/_foundation.scss index 7918cf26..7918cf26 100644 --- a/web-ui/app/scss/foundation.scss +++ b/web-ui/app/scss/vendor/_foundation.scss diff --git a/web-ui/app/scss/reset.scss b/web-ui/app/scss/vendor/_reset.scss index 55f8d054..55f8d054 100644 --- a/web-ui/app/scss/reset.scss +++ b/web-ui/app/scss/vendor/_reset.scss diff --git a/web-ui/app/scss/views/_message-panel.scss b/web-ui/app/scss/views/_message-panel.scss new file mode 100644 index 00000000..4a0a7a6b --- /dev/null +++ b/web-ui/app/scss/views/_message-panel.scss @@ -0,0 +1,26 @@ +.message-panel { + width: 100%; + margin: 10px auto; + position: fixed; + z-index: 10000; + text-align: center; + + &__growl { + padding: 5px 60px; + + &--success { + background: $warning; + color: darken($warning, 50%); + border: 1px solid darken($warning, 10%); + @include box-shadow(1px 1px 3px darken($warning, 60%)); + } + + &--error { + font-weight: bold; + color: white; + background: $error; + border: 1px solid darken($error, 10%); + @include box-shadow(1px 1px 3px darken($error, 60%)); + } + } +} diff --git a/web-ui/app/scss/views/_no-mails-available.scss b/web-ui/app/scss/views/_no-mails-available.scss new file mode 100644 index 00000000..bf5d256a --- /dev/null +++ b/web-ui/app/scss/views/_no-mails-available.scss @@ -0,0 +1,3 @@ +.no-mails-available-pane { + @extend .no-content-placeholder; +} diff --git a/web-ui/app/scss/views/_no-message-selected.scss b/web-ui/app/scss/views/_no-message-selected.scss new file mode 100644 index 00000000..0e367bf2 --- /dev/null +++ b/web-ui/app/scss/views/_no-message-selected.scss @@ -0,0 +1,14 @@ +.no-message-selected-pane { + background: $contrast; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + &__text { + @extend .no-content-placeholder; + + margin-bottom: 40px; // aligns label with "no results for XYZ" + } +} diff --git a/web-ui/app/templates/compose/no_mails_available.hbs b/web-ui/app/templates/compose/no_mails_available.hbs index 6388d7db..147f3533 100644 --- a/web-ui/app/templates/compose/no_mails_available.hbs +++ b/web-ui/app/templates/compose/no_mails_available.hbs @@ -1,7 +1,7 @@ -<div class="scene"> +<div class="no-mails-available-pane"> {{#if forSearch }} - <div class="text">{{t 'NO RESULTS FOR'}}: '{{ forSearch }}'.</div> + {{t 'NO RESULTS FOR'}}: '{{ forSearch }}'. {{else}} - <div class="text">{{t 'NO EMAILS IN'}} '{{ tag }}'.</div> + {{t 'NO EMAILS IN'}} '{{ tag }}'. {{/if}} </div> diff --git a/web-ui/app/templates/compose/no_message_selected.hbs b/web-ui/app/templates/compose/no_message_selected.hbs index 0442192d..71aa6267 100644 --- a/web-ui/app/templates/compose/no_message_selected.hbs +++ b/web-ui/app/templates/compose/no_message_selected.hbs @@ -1,3 +1,3 @@ -<div class="scene"> - <div class="text">{{t 'NOTHING SELECTED'}}.</div> +<div class="no-message-selected-pane"> + <div class="no-message-selected-pane__text">{{t 'NOTHING SELECTED'}}.</div> </div> diff --git a/web-ui/app/templates/mails/full_view.hbs b/web-ui/app/templates/mails/full_view.hbs index f9ec084a..3ff109e9 100644 --- a/web-ui/app/templates/mails/full_view.hbs +++ b/web-ui/app/templates/mails/full_view.hbs @@ -64,6 +64,7 @@ </header> <div class="bodyArea column large-12"> + <iframe id="read-sandbox" sandbox="allow-popups allow-scripts" src="sandbox/sandbox.html" scrolling="no"></iframe> </div> {{#if attachments}} diff --git a/web-ui/app/templates/search/search_trigger.hbs b/web-ui/app/templates/search/search_trigger.hbs index 2261d154..f2c410a4 100644 --- a/web-ui/app/templates/search/search_trigger.hbs +++ b/web-ui/app/templates/search/search_trigger.hbs @@ -1,3 +1,3 @@ <form> - <input type="search" placeholder="{{t 'search-placeholder'}}"></input> + <input type="search" pattern="[a-zA-Z0-9\s]{3,}" placeholder="{{t 'search-placeholder'}}"></input> </form> diff --git a/web-ui/app/templates/user_alerts/message.hbs b/web-ui/app/templates/user_alerts/message.hbs index 3a0055eb..abba1f91 100644 --- a/web-ui/app/templates/user_alerts/message.hbs +++ b/web-ui/app/templates/user_alerts/message.hbs @@ -1 +1 @@ -<span class="{{ message.class }}">{{ message.content }}</span> +<span class="message-panel__growl {{ message.class }}">{{ message.content }}</span> diff --git a/web-ui/bower.json b/web-ui/bower.json index 263ac2e4..018a57c4 100644 --- a/web-ui/bower.json +++ b/web-ui/bower.json @@ -17,7 +17,8 @@ "jquery-file-upload": "~9.11.2", "jquery-ui": "~1.11.4", "DOMPurify": "~0.7.4", - "he": "~0.5.0" + "he": "~0.5.0", + "iframe-resizer": "~3.5.3" }, "devDependencies": { "handlebars": "2.0.0", diff --git a/web-ui/config/package.sh b/web-ui/config/package.sh index 9b6bc66c..0bd82367 100644 --- a/web-ui/config/package.sh +++ b/web-ui/config/package.sh @@ -28,16 +28,24 @@ mkdir -p dist ./go handlebars ./go imagemin ./go minify_html +./go minify_sandbox ./go add_git_version ./go buildmain # copy files cd app -cp --parents 404.html fonts/* locales/**/* bower_components/font-awesome/css/font-awesome.min.css bower_components/jquery-file-upload/css/jquery.fileupload.css bower_components/font-awesome/fonts/* ../dist +cp --parents \ +404.html \ +fonts/* \ +locales/**/* \ +bower_components/font-awesome/css/font-awesome.min.css \ +bower_components/jquery-file-upload/css/jquery.fileupload.css \ +bower_components/font-awesome/fonts/* \ +../dist cd - -# concat js files and minify +# concat js files and minify for app.min.js cat \ app/bower_components/modernizr/modernizr.js \ app/bower_components/lodash/dist/lodash.js \ @@ -51,6 +59,14 @@ app/bower_components/foundation/js/foundation.js \ app/bower_components/foundation/js/foundation/foundation.reveal.js \ app/bower_components/foundation/js/foundation/foundation.offcanvas.js \ app/js/foundation/initialize_foundation.js \ +app/bower_components/iframe-resizer/js/iframeResizer.min.js \ .tmp/app.concatenated.js > dist/app.js node_modules/.bin/minify dist/app.js > dist/app.min.js rm dist/app.js + +# concat js files and minify for sandbox.min.js +cat \ +app/js/sandbox.js \ +app/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js > dist/sandbox.js +node_modules/.bin/minify dist/sandbox.js > dist/sandbox.min.js +rm dist/sandbox.js diff --git a/web-ui/package.json b/web-ui/package.json index a49e32d1..0e8b9262 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -38,6 +38,7 @@ "package": "/bin/bash config/package.sh", "imagemin": "node config/imagemin.js", "minify_html": "node_modules/.bin/html-minifier app/index.html --collapse-whitespace | sed 's|<!--usemin_start-->.*<!--usemin_end-->|<script src=\"assets/app.min.js\" type=\"text/javascript\"></script>|' > dist/index.html", + "minify_sandbox": "node_modules/.bin/html-minifier app/sandbox.html --collapse-whitespace | sed 's|<!--usemin_start-->.*<!--usemin_end-->|<script src=\"sandbox.min.js\" type=\"text/javascript\"></script>|' > dist/sandbox.html", "add_git_version": "/bin/bash config/add_git_version.sh" } } diff --git a/web-ui/test/spec/helpers/sanitizer.spec.js b/web-ui/test/spec/helpers/sanitizer.spec.js index acd4b2b2..b553583e 100644 --- a/web-ui/test/spec/helpers/sanitizer.spec.js +++ b/web-ui/test/spec/helpers/sanitizer.spec.js @@ -25,6 +25,12 @@ define(['helpers/sanitizer'], function (sanitizer) { var output = sanitizer.purifyText('123<a>asd</a>'); expect(output).toEqual(expectedOutput); }); + + it('should leave highlighted text untouched', function () { + var expectedOutput = '<em class="search-highlight">123<a>asd</a></em>'; + var output = sanitizer.purifyText('<em class="search-highlight">123<a>asd</a></em>'); + expect(output).toEqual(expectedOutput); + }); }); describe('sanitizer.sanitize', function () { diff --git a/web-ui/test/spec/mail_view/ui/mail_view.spec.js b/web-ui/test/spec/mail_view/ui/mail_view.spec.js index 9ed56023..9f1114a7 100644 --- a/web-ui/test/spec/mail_view/ui/mail_view.spec.js +++ b/web-ui/test/spec/mail_view/ui/mail_view.spec.js @@ -21,6 +21,12 @@ describeComponent('mail_view/ui/mail_view', function () { expect(spyEvent.mostRecentCall.data.mail).toEqual(1); }); + it('triggers mail.highlightMailContent when receiving mail.here', function () { + var hightlightEvent = spyOnEvent(document,Pixelated.events.mail.highlightMailContent); + this.component.trigger(this.component, Pixelated.events.mail.here); + expect(hightlightEvent).toHaveBeenTriggeredOn(document); + }); + it('triggers dispatchers.rightPane.openNoMessageSelected when getting mail.notFound', function () { var openNoMessageSelectedEvent = spyOnEvent(document, Pixelated.events.dispatchers.rightPane.openNoMessageSelected); diff --git a/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js b/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js index 157e6c47..1bf79b96 100644 --- a/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js +++ b/web-ui/test/spec/mail_view/ui/no_mails_available_pane.spec.js @@ -4,17 +4,17 @@ describeComponent('mail_view/ui/no_mails_available_pane', function () { describe('after initialization', function () { it('renders template', function () { this.setupComponent({tag: 'inbox'}); - expect(this.$node.html()).toMatch('<div class="text">NO EMAILS IN \'INBOX\'.</div>'); + expect(this.$node.html()).toMatch('<div class="no-mails-available-pane">\n NO EMAILS IN \'INBOX\'.\n</div>'); }); it('show different message for search with no results', function () { this.setupComponent({tag: 'all', forSearch: 'search'}); - expect(this.$node.html()).toMatch('<div class="text">NO RESULTS FOR: \'SEARCH\'.</div>'); + expect(this.$node.html()).toMatch('<div class="no-mails-available-pane">\n NO RESULTS FOR: \'SEARCH\'.\n</div>'); }); it('show only tag information when listing all mails', function () { this.setupComponent({tag: 'all', forSearch: 'in:all'}); - expect(this.$node.html()).toMatch('<div class="text">NO EMAILS IN \'ALL\'.</div>'); + expect(this.$node.html()).toMatch('<div class="no-mails-available-pane">\n NO EMAILS IN \'ALL\'.\n</div>'); }); }); }); diff --git a/web-ui/test/spec/search/results_highlighter.spec.js b/web-ui/test/spec/search/results_highlighter.spec.js index cfb61e9c..13131a8e 100644 --- a/web-ui/test/spec/search/results_highlighter.spec.js +++ b/web-ui/test/spec/search/results_highlighter.spec.js @@ -1,9 +1,11 @@ describeComponent('search/results_highlighter', function () { 'use strict'; - it('highlights words or parts of words that match with the keywords given', function () { + beforeEach(function () { this.setupComponent('<div id="text">Any one seeing too many open bugs</div>'); + }); + it('highlights words or parts of words that match with the keywords given', function () { this.component.attr = {keywords: ['any']}; this.component.highlightResults(event, {where: '#text'}); @@ -12,9 +14,15 @@ describeComponent('search/results_highlighter', function () { expect(highlightedWords).toEqual(2); }); - it('resets highlights when a new search is performed', function() { - this.setupComponent('<div id="text">Any one seeing too many open bugs</div>'); + it('highlights a string with the keywords given', function () { + this.component.attr = {keywords: ['foo']}; + var expectedString = 'the <em class="search-highlight">foo</em> bar'; + var string = this.component.highlightString('the foo bar'); + + expect(string).toEqual(expectedString); + }); + it('resets highlights when a new search is performed', function() { this.component.attr = {keywords: ['any']}; this.component.highlightResults(event, {where: '#text'}); $(document).trigger(Pixelated.events.search.resetHighlight); diff --git a/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js b/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js index bde3b7fa..f15cda81 100644 --- a/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js +++ b/web-ui/test/spec/user_alerts/ui/user_alerts.spec.js @@ -8,13 +8,13 @@ describeComponent('user_alerts/ui/user_alerts', function () { it('should render message when ui:user_alerts:displayMessage is triggered', function () { this.component.trigger(Pixelated.events.ui.userAlerts.displayMessage, { message: 'a message' }); - expect(this.component.$node.html()).toEqual('<span class="success">a message</span>\n'); + expect(this.component.$node.html()).toEqual('<span class="message-panel__growl message-panel__growl--success">a message</span>\n'); }); it('should render error message', function () { this.component.trigger(Pixelated.events.ui.userAlerts.displayMessage, { message: 'send failed', class: 'error' }); - expect(this.component.$node.html()).toEqual('<span class="error">send failed</span>\n'); + expect(this.component.$node.html()).toEqual('<span class="message-panel__growl message-panel__growl--error">send failed</span>\n'); }); it('should be emptied and hidden when hide is called', function() { @@ -24,6 +24,4 @@ describeComponent('user_alerts/ui/user_alerts', function () { expect(this.$node.html()).toEqual(''); }); - - }); |