Bug 1176280 - make links in Hello chat clickable, r=mikedeboer, r=gerv for license.html changes

This commit is contained in:
Dan Mosedale 2015-08-03 14:53:16 -07:00
Родитель 557d9030a3
Коммит d55fa0f2d7
17 изменённых файлов: 786 добавлений и 35 удалений

Просмотреть файл

@ -22,6 +22,7 @@ content/js/panel.js
content/js/roomViews.js
content/js/feedbackViews.js
content/shared/js/textChatView.js
content/shared/js/linkifiedTextView.js
content/shared/js/views.js
standalone/content/js/fxOSMarketplace.js
standalone/content/js/standaloneRoomViews.js

Просмотреть файл

@ -39,6 +39,8 @@
<script type="text/javascript" src="loop/js/feedbackViews.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
<script type="text/javascript" src="loop/shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="loop/shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>

Просмотреть файл

@ -0,0 +1,114 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.LinkifiedTextView = (function(mozL10n) {
"use strict";
/**
* Given a rawText property, renderer a version of that text with any
* links starting with http://, https://, or ftp:// as actual clickable
* links inside a <p> container.
*/
var LinkifiedTextView = React.createClass({displayName: "LinkifiedTextView",
propTypes: {
// Call this instead of allowing the default <a> click semantics, if
// given. Also causes sendReferrer and suppressTarget attributes to be
// ignored.
linkClickHandler: React.PropTypes.func,
// The text to be linkified.
rawText: React.PropTypes.string.isRequired,
// Should the links send a referrer? Defaults to false.
sendReferrer: React.PropTypes.bool,
// Should we suppress target="_blank" on the link? Defaults to false.
// Mostly for testing use.
suppressTarget: React.PropTypes.bool
},
mixins: [
React.addons.PureRenderMixin
],
_handleClickEvent: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.linkClickHandler(e.currentTarget.href);
},
_generateLinkAttributes: function(href) {
var linkAttributes = {
href: href
};
if (this.props.linkClickHandler) {
linkAttributes.onClick = this._handleClickEvent;
// if this is specified, we short-circuit return to avoid unnecessarily
// creating target and rel attributes.
return linkAttributes;
}
if (!this.props.suppressTarget) {
linkAttributes.target = "_blank";
}
if (!this.props.sendReferrer) {
linkAttributes.rel = "noreferrer";
}
return linkAttributes;
},
/** a
* Parse the given string into an array of strings and React <a> elements
* in the order in which they should be rendered (i.e. FIFO).
*
* @param {String} s the raw string to be parsed
*
* @returns {Array} of strings and React <a> elements in order.
*/
parseStringToElements: function(s) {
var elements = [];
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
var reactElementsCounter = 0; // For giving keys to each ReactElement.
while (result) {
// If there's text preceding the first link, push it onto the array
// and update the string pointer.
if (result.index) {
elements.push(s.substr(0, result.index));
s = s.substr(result.index);
}
// Push the first link itself, and advance the string pointer again.
elements.push(
React.createElement("a", React.__spread({}, this._generateLinkAttributes(result[0]) ,
{key: reactElementsCounter++}),
result[0]
)
);
s = s.substr(result[0].length);
// Check for another link, and perhaps continue...
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
}
if (s) {
elements.push(s);
}
return elements;
},
render: function () {
return ( React.createElement("p", null, this.parseStringToElements(this.props.rawText) ) );
}
});
return LinkifiedTextView;
})(navigator.mozL10n || document.mozL10n);

Просмотреть файл

@ -0,0 +1,114 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.LinkifiedTextView = (function(mozL10n) {
"use strict";
/**
* Given a rawText property, renderer a version of that text with any
* links starting with http://, https://, or ftp:// as actual clickable
* links inside a <p> container.
*/
var LinkifiedTextView = React.createClass({
propTypes: {
// Call this instead of allowing the default <a> click semantics, if
// given. Also causes sendReferrer and suppressTarget attributes to be
// ignored.
linkClickHandler: React.PropTypes.func,
// The text to be linkified.
rawText: React.PropTypes.string.isRequired,
// Should the links send a referrer? Defaults to false.
sendReferrer: React.PropTypes.bool,
// Should we suppress target="_blank" on the link? Defaults to false.
// Mostly for testing use.
suppressTarget: React.PropTypes.bool
},
mixins: [
React.addons.PureRenderMixin
],
_handleClickEvent: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.linkClickHandler(e.currentTarget.href);
},
_generateLinkAttributes: function(href) {
var linkAttributes = {
href: href
};
if (this.props.linkClickHandler) {
linkAttributes.onClick = this._handleClickEvent;
// if this is specified, we short-circuit return to avoid unnecessarily
// creating target and rel attributes.
return linkAttributes;
}
if (!this.props.suppressTarget) {
linkAttributes.target = "_blank";
}
if (!this.props.sendReferrer) {
linkAttributes.rel = "noreferrer";
}
return linkAttributes;
},
/** a
* Parse the given string into an array of strings and React <a> elements
* in the order in which they should be rendered (i.e. FIFO).
*
* @param {String} s the raw string to be parsed
*
* @returns {Array} of strings and React <a> elements in order.
*/
parseStringToElements: function(s) {
var elements = [];
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
var reactElementsCounter = 0; // For giving keys to each ReactElement.
while (result) {
// If there's text preceding the first link, push it onto the array
// and update the string pointer.
if (result.index) {
elements.push(s.substr(0, result.index));
s = s.substr(result.index);
}
// Push the first link itself, and advance the string pointer again.
elements.push(
<a { ...this._generateLinkAttributes(result[0]) }
key={reactElementsCounter++}>
{result[0]}
</a>
);
s = s.substr(result[0].length);
// Check for another link, and perhaps continue...
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
}
if (s) {
elements.push(s);
}
return elements;
},
render: function () {
return ( <p>{ this.parseStringToElements(this.props.rawText) }</p> );
}
});
return LinkifiedTextView;
})(navigator.mozL10n || document.mozL10n);

Просмотреть файл

@ -56,9 +56,15 @@ loop.shared.views.chat = (function(mozL10n) {
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
});
var optionalProps = {};
if (navigator.mozLoop) {
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
}
return (
React.createElement("div", {className: classes},
React.createElement("p", null, this.props.message),
React.createElement(sharedViews.LinkifiedTextView, React.__spread({}, optionalProps,
{rawText: this.props.message})),
React.createElement("span", {className: "text-chat-arrow"}),
this.props.showTimestamp ? this._renderTimestamp() : null
)

Просмотреть файл

@ -56,9 +56,15 @@ loop.shared.views.chat = (function(mozL10n) {
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
});
var optionalProps = {};
if (navigator.mozLoop) {
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
}
return (
<div className={classes}>
<p>{this.props.message}</p>
<sharedViews.LinkifiedTextView {...optionalProps}
rawText={this.props.message} />
<span className="text-chat-arrow" />
{this.props.showTimestamp ? this._renderTimestamp() : null}
</div>

Просмотреть файл

@ -0,0 +1,72 @@
// This is derived from Diego Perini's code,
// currently available at https://gist.github.com/dperini/729294
// Regular Expression for URL validation
//
// Original Author: Diego Perini
// License: MIT
//
// Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.urlRegExps = (function() {
"use strict";
// Some https://wiki.mozilla.org/Loop/Development/RegExpDebugging for tools
// if you need to debug changes to this:
var fullUrlMatch = new RegExp(
// Protocol identifier.
"(?:(?:https?|ftp)://)" +
// User:pass authentication.
"((?:\\S+(?::\\S*)?@)?" +
"(?:" +
// IP address dotted notation octets:
// excludes loopback network 0.0.0.0,
// excludes reserved space >= 224.0.0.0,
// excludes network & broadcast addresses,
// (first & last IP address of each class).
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// Host name.
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
// Domain name.
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
// TLD identifier.
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
// Port number.
"(?::\\d{2,5})?" +
// Resource path.
"(?:[/?#]\\S*)?)", "i");
return {
fullUrlMatch: fullUrlMatch
};
})();

Просмотреть файл

@ -86,8 +86,10 @@ browser.jar:
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/linkifiedTextView.js (content/shared/js/linkifiedTextView.js)
content/browser/loop/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
content/browser/loop/shared/js/textChatView.js (content/shared/js/textChatView.js)
content/browser/loop/shared/js/urlRegExps.js (content/shared/js/urlRegExps.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)

Просмотреть файл

@ -144,9 +144,10 @@
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="shared/js/textChatStore.js"></script>
<script type="text/javascript" src="shared/js/textChatView.js"></script>
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>

Просмотреть файл

@ -34,6 +34,8 @@ module.exports = function(config) {
"content/shared/js/views.js",
"content/shared/js/textChatStore.js",
"content/shared/js/textChatView.js",
"content/shared/js/urlRegExps.js",
"content/shared/js/linkifiedTextView.js",
"standalone/content/js/multiplexGum.js",
"standalone/content/js/standaloneAppStore.js",
"standalone/content/js/standaloneClient.js",

Просмотреть файл

@ -63,6 +63,8 @@
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/urlRegExps.js"></script>
<script src="../../content/shared/js/linkifiedTextView.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
@ -80,6 +82,8 @@
<script src="store_test.js"></script>
<script src="textChatStore_test.js"></script>
<script src="textChatView_test.js"></script>
<script src="linkifiedTextView_test.js"></script>
<script>
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {

Просмотреть файл

@ -0,0 +1,378 @@
/*
* Many of these tests are ported from Autolinker.js:
*
* https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
*
* which is released under the MIT license. Thanks to Greg Jacobs for his hard
* work there.
*
* The MIT License (MIT)
* Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("loop.shared.views.LinkifiedTextView", function () {
"use strict";
var expect = chai.expect;
var LinkifiedTextView = loop.shared.views.LinkifiedTextView;
var TestUtils = React.addons.TestUtils;
describe("LinkifiedTextView", function () {
function renderToMarkup(string, extraProps) {
return React.renderToStaticMarkup(
React.createElement(
LinkifiedTextView,
_.extend({rawText: string}, extraProps)));
}
describe("#render", function() {
function testRender(testData) {
it(testData.desc, function() {
var markup = renderToMarkup(testData.rawText,
{suppressTarget: true, sendReferrer: true});
expect(markup).to.equal(testData.markup);
});
}
function testSkip(testData) {
it.skip(testData.desc, function() {
var markup = renderToMarkup(testData.rawText,
{suppressTarget: true, sendReferrer: true});
expect(markup).to.equal(testData.markup);
});
}
describe("this.props.suppressTarget", function() {
it("should make links w/o a target attr if suppressTarget is true",
function() {
var markup = renderToMarkup("http://example.com", {suppressTarget: true});
expect(markup).to.equal(
'<p><a href="http://example.com" rel="noreferrer">http://example.com</a></p>');
});
it("should make links with target=_blank if suppressTarget is not given",
function() {
var markup = renderToMarkup("http://example.com", {});
expect(markup).to.equal(
'<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
});
});
describe("this.props.sendReferrer", function() {
it("should make links w/o rel=noreferrer if sendReferrer is true",
function() {
var markup = renderToMarkup("http://example.com", {sendReferrer: true});
expect(markup).to.equal(
'<p><a href="http://example.com" target="_blank">http://example.com</a></p>');
});
it("should make links with rel=noreferrer if sendReferrer is not given",
function() {
var markup = renderToMarkup("http://example.com", {});
expect(markup).to.equal(
'<p><a href="http://example.com" target="_blank" rel="noreferrer">http://example.com</a></p>');
});
});
describe("this.props.linkClickHandler", function () {
function mountTestComponent(string, extraProps) {
return TestUtils.renderIntoDocument(
React.createElement(
LinkifiedTextView,
_.extend({rawText: string}, extraProps)));
}
it("should be called when a generated link is clicked", function () {
var fakeUrl = "http://example.com";
var linkClickHandler = sinon.stub();
var comp = mountTestComponent(fakeUrl, {linkClickHandler: linkClickHandler});
TestUtils.Simulate.click(comp.getDOMNode().querySelector("a"));
sinon.assert.calledOnce(linkClickHandler);
});
it("should cause sendReferrer and suppressTarget props to be ignored",
function() {
var fakeUrl = "http://example.com";
var linkClickHandler = function() {};
var markup = renderToMarkup("http://example.com", {
linkClickHandler: linkClickHandler,
sendReferrer: false,
suppressTarget: false
});
expect(markup).to.equal(
'<p><a href="http://example.com">http://example.com</a></p>');
});
describe("#_handleClickEvent", function () {
var fakeEvent;
var fakeUrl = "http://example.com";
beforeEach(function() {
fakeEvent = {
currentTarget: { href: fakeUrl },
preventDefault: sinon.stub(),
stopPropagation: sinon.stub()
};
});
it("should call preventDefault on the given event", function () {
function linkClickHandler() {}
var comp = mountTestComponent(
fakeUrl, {linkClickHandler: linkClickHandler});
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(fakeEvent.preventDefault);
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
});
it("should call stopPropagation on the given event", function () {
function linkClickHandler() {}
var comp = mountTestComponent(
fakeUrl, {linkClickHandler: linkClickHandler});
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(fakeEvent.stopPropagation);
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
});
it("should call this.props.linkClickHandler with event.currentTarget.href", function () {
var linkClickHandler = sinon.stub();
var comp = mountTestComponent(
fakeUrl, {linkClickHandler: linkClickHandler});
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(linkClickHandler);
sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);
});
});
});
// Note that these are really integration tests with the parser and React.
// Since we're depending on that integration to provide us with security
// against various injection problems, it feels fairly important. That
// said, these tests are not terribly robust in the face of markup changes
// in the code, and over time, some of them may want to be pushed down
// to only be unit tests against the parser or against
// parseStringToElements. We may also want both unit and integration
// testing for some subset of these.
var tests = [
{
desc: "should only add a container to a string with no URLs",
rawText: "This is a test.",
markup: "<p>This is a test.</p>"
},
{
desc: "should linkify a string containing only a URL",
rawText: "http://example.com/",
markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should linkify a URL with text preceding it",
rawText: "This is a link to http://example.com",
markup: '<p>This is a link to <a href="http://example.com">http://example.com</a></p>'
},
{
desc: "should linkify a URL with text before and after",
rawText: "Look at http://example.com near the bottom",
markup: '<p>Look at <a href="http://example.com">http://example.com</a> near the bottom</p>'
},
{
desc: "should linkify an http URL",
rawText: "This is an http://example.com test",
markup: '<p>This is an <a href="http://example.com">http://example.com</a> test</p>'
},
{
desc: "should linkify an https URL",
rawText: "This is an https://example.com test",
markup: '<p>This is an <a href="https://example.com">https://example.com</a> test</p>'
},
{
desc: "should not linkify a data URL",
rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>"
},
{
desc: "should linkify URLs with a port number",
rawText: "Joe went to http://example.com:8000 today.",
markup: '<p>Joe went to <a href="http://example.com:8000">http://example.com:8000</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a trailing slash",
rawText: "Joe went to http://example.com:8000/ today.",
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a path",
rawText: "Joe went to http://example.com:8000/mysite/page today.",
markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a query string",
rawText: "Joe went to http://example.com:8000?page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000?page=index">http://example.com:8000?page=index</a> today.</p>'
},
{
desc: "should linkify a URL with a port number and a hash string",
rawText: "Joe went to http://example.com:8000#page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000#page=index">http://example.com:8000#page=index</a> today.</p>'
},
{
desc: "should NOT include preceding ':' intros without a space",
rawText: "the link:http://example.com/",
markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should NOT autolink URLs with 'javascript:' URI scheme",
rawText: "do not link javascript:window.alert('hi') please",
markup: "<p>do not link javascript:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
rawText: "do not link JavAscriPt:window.alert('hi') please",
markup: "<p>do not link JavAscriPt:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
rawText: "do not link vbscript:window.alert('hi') please",
markup: "<p>do not link vbscript:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
rawText: "do not link vBsCriPt:window.alert('hi') please",
markup: "<p>do not link vBsCriPt:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink a string in the form of 'version:1.0'",
rawText: "version:1.0",
markup: "<p>version:1.0</p>"
},
{
desc: "should linkify an ftp URL",
rawText: "This is an ftp://example.com test",
markup: '<p>This is an <a href="ftp://example.com">ftp://example.com</a> test</p>'
},
// We don't want to include trailing dots in URLs, even though those
// are valid DNS names, as that should match user intent more of the
// time, as well as avoid this stuff:
//
// http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
//
{
desc: "should linkify 'http://example.com.', w/o a trailing dot",
rawText: "Joe went to http://example.com.",
markup: '<p>Joe went to <a href="http://example.com">http://example.com</a>.</p>'
},
// XXX several more tests like this we could port from Autolinkify.js
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should exclude invalid chars after domain part",
rawText: "Joe went to http://www.example.com's today",
markup: '<p>Joe went to <a href="http://www.example.com">http://www.example.com</a>&#x27;s today</p>'
},
{
desc: "should not linkify protocol-relative URLs",
rawText: "//C/Programs",
markup: "<p>//C/Programs</p>"
},
// do a few tests to convince ourselves that, when our code is handled
// HTML in the input box, the integration of our code with React should
// cause that to rendered to appear as HTML source code, rather than
// getting injected into our real HTML DOM
{
desc: "should linkify simple HTML include an href properly escaped",
rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
markup: '<p>&lt;p&gt;Joe went to &lt;a href=&quot;<a href="http://www.example.com">http://www.example.com</a>&quot;&gt;example&lt;/a&gt;&lt;/p&gt;</p>'
},
{
desc: "should linkify HTML with nested tags and resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com">http://example.com</a>&quot; /&gt;&lt;/a&gt;</p>'
}
];
var skippedTests = [
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should link localhost URLs with an allowed URL scheme",
rawText: "Joe went to http://localhost today",
markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today'
},
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should not include a ? if at the end of a URL",
rawText: "Did Joe go to http://example.com?",
markup: '<p>Did Joe go to <a href="http://example.com">http://example.com</a>?</p>'
},
{
desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
rawText: "check out http://example.com/monkey...",
markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>'
},
// another variant of eating too many trailing characters, it includes
// the trailing ", which it shouldn't. Makes links inside pasted HTML
// source be slightly broken. Not key for our target users, I suspect,
// but still...
{
desc: "should linkify HTML with nested tags and a resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com">http://example.com</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/someImage.jpg&quot;">http://example.com/someImage.jpg&quot;</a> /&gt;&lt;/a&gt;</p>'
},
// XXX handle domains without schemes (bug 1186245)
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should linkify a.museum (known TLD), but not abc.qqqq",
rawText: "a.museum should be linked, but abc.qqqq should not",
markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>'
},
{
desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
rawText: "example.xyz should be linked, example.etc should not",
rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>'
}
];
tests.forEach(testRender);
// XXX Over time, we'll want to make these pass..
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
skippedTests.forEach(testSkip);
});
});
});

Просмотреть файл

@ -250,6 +250,21 @@ describe("loop.shared.views.TextChatView", function () {
expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
});
// note that this is really an integration test to be sure that we don't
// inadvertently regress using LinkifiedTextView.
it("should linkify a URL starting with http", function (){
view = mountTestComponent({
showTimestamp: true,
timestamp: "2015-06-23T22:48:39.738Z",
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Check out http://example.com and see what you think..."
});
var node = view.getDOMNode();
expect(node.querySelector("a")).to.not.eql(null);
});
});
describe("TextChatView", function() {

Просмотреть файл

@ -58,6 +58,8 @@
<script src="../content/shared/js/textChatStore.js"></script>
<script src="../content/js/feedbackViews.js"></script>
<script src="../content/shared/js/textChatView.js"></script>
<script src="../content/shared/js/urlRegExps.js"></script>
<script src="../content/shared/js/linkifiedTextView.js"></script>
<script src="../content/js/roomStore.js"></script>
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>

Просмотреть файл

@ -86,9 +86,11 @@
// Dummy function to stop warnings.
},
sendTextChatMessage: function(message) {
sendTextChatMessage: function(actionData) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: actionData.message,
receivedTimestamp: actionData.sentTimestamp
}));
}
};
@ -394,16 +396,17 @@
message: "Rheet!",
sentTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hi there",
receivedTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hello",
receivedTimestamp: "2015-06-23T23:24:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Check out this menu from DNA Pizza:" +
@ -411,12 +414,6 @@
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "That avocado monkey-brains pie sounds tasty!",
@ -427,10 +424,10 @@
message: "What time should we meet?",
sentTimestamp: "2015-06-23T22:27:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Cool",
sentTimestamp: "2015-06-23T22:27:45.590Z"
message: "8:00 PM",
receivedTimestamp: "2015-06-23T22:27:45.590Z"
}));
loop.store.StoreMixin.register({

Просмотреть файл

@ -86,9 +86,11 @@
// Dummy function to stop warnings.
},
sendTextChatMessage: function(message) {
sendTextChatMessage: function(actionData) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: actionData.message,
receivedTimestamp: actionData.sentTimestamp
}));
}
};
@ -394,16 +396,17 @@
message: "Rheet!",
sentTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hi there",
receivedTimestamp: "2015-06-23T22:21:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Hello",
receivedTimestamp: "2015-06-23T23:24:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Check out this menu from DNA Pizza:" +
@ -411,12 +414,6 @@
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
"linewrappingissuesifthecssiswrong",
sentTimestamp: "2015-06-23T22:23:45.590Z"
}));
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "That avocado monkey-brains pie sounds tasty!",
@ -427,10 +424,10 @@
message: "What time should we meet?",
sentTimestamp: "2015-06-23T22:27:45.590Z"
}));
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Cool",
sentTimestamp: "2015-06-23T22:27:45.590Z"
message: "8:00 PM",
receivedTimestamp: "2015-06-23T22:27:45.590Z"
}));
loop.store.StoreMixin.register({

Просмотреть файл

@ -135,6 +135,7 @@
<li><a href="about:license#superfasthash">SuperFastHash License</a></li>
<li><a href="about:license#unicode">Unicode License</a></li>
<li><a href="about:license#ucal">University of California License</a></li>
<li><a href="about:license#url-validation">URL Validation Regexp License</a></li>
<li><a href="about:license#hunspell-en-US">US English Spellchecking Dictionary Licenses</a></li>
<li><a href="about:license#v8">V8 License</a></li>
<li><a href="about:license#vtune">VTune License</a></li>
@ -4195,6 +4196,43 @@ SUCH DAMAGE.
</pre>
<hr>
<h1><a id="url-validation"></a>URL Validation Regexp License</h1>
<p>This license applies to the following file:</p>
<ul>
<li class="path">
browser/components/loop/content/shared/js/urlRegExps.js
</li>
</ul>
<pre>
Copyright (c) 2010-2013 Diego Perini (http://www.iport.it)
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
</pre>
<hr>
<h1><a id="hunspell-en-US"></a>US English Spellchecking Dictionary Licenses</h1>