diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js index f6db24482f44966263512c399afdcf79bdefa09f..3aa487d2ec9f0526716e7979e7391922382c4e8d 100644 --- a/frontend/src/metabase/lib/formatting.js +++ b/frontend/src/metabase/lib/formatting.js @@ -103,18 +103,19 @@ export function formatTimeWithUnit(value, unit, options = {}) { } } -const EMAIL_WHITELIST_REGEX = /.+@.+/; +// https://github.com/angular/angular.js/blob/v1.6.3/src/ng/directive/input.js#L27 +const EMAIL_WHITELIST_REGEX = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; export function formatEmail(value, { jsx } = {}) { if (jsx && EMAIL_WHITELIST_REGEX.test(value)) { return <ExternalLink href={"mailto:" + value}>{value}</ExternalLink>; } else { - value; + return value; } } -// prevent `javascript:` etc URLs -const URL_WHITELIST_REGEX = /^(https?|mailto):/; +// based on https://github.com/angular/angular.js/blob/v1.6.3/src/ng/directive/input.js#L25 +const URL_WHITELIST_REGEX = /^(https?|mailto):\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; export function formatUrl(value, { jsx } = {}) { if (jsx && URL_WHITELIST_REGEX.test(value)) { @@ -124,6 +125,15 @@ export function formatUrl(value, { jsx } = {}) { } } +// fallback for formatting a string without a column special_type +function formatStringFallback(value, options = {}) { + value = formatUrl(value, options); + if (typeof value === 'string') { + value = formatEmail(value, options); + } + return value; +} + export function formatValue(value, options = {}) { let column = options.column; options = { @@ -142,7 +152,7 @@ export function formatValue(value, options = {}) { } else if (isDate(column) || moment.isDate(value) || moment.isMoment(value) || moment(value, ["YYYY-MM-DD'T'HH:mm:ss.SSSZ"], true).isValid()) { return parseTimestamp(value, column && column.unit).format("LLLL"); } else if (typeof value === "string") { - return value; + return formatStringFallback(value, options); } else if (typeof value === "number") { if (isCoordinate(column)) { return DECIMAL_DEGREES_FORMATTER(value); diff --git a/frontend/test/unit/lib/formatting.spec.js b/frontend/test/unit/lib/formatting.spec.js index f2d01c59fd073588165f9cd86d444632e3448de5..3898e3b737cf2a86570bd87664617e415907022c 100644 --- a/frontend/test/unit/lib/formatting.spec.js +++ b/frontend/test/unit/lib/formatting.spec.js @@ -57,6 +57,12 @@ describe('formatting', () => { expect(formatValue(37.7749, { column: { base_type: TYPE.Number, special_type: TYPE.Latitude }})).toEqual("37.77490000"); expect(formatValue(-122.4194, { column: { base_type: TYPE.Number, special_type: TYPE.Longitude }})).toEqual("-122.41940000"); }); + it("should return a component for links in jsx mode", () => { + expect(isElementOfType(formatValue("http://metabase.com/", { jsx: true }), ExternalLink)).toEqual(true); + }); + it("should return a component for email addresses in jsx mode", () => { + expect(isElementOfType(formatValue("tom@metabase.com", { jsx: true }), ExternalLink)).toEqual(true); + }); }); describe("formatUrl", () => { @@ -68,6 +74,10 @@ describe('formatting', () => { expect(isElementOfType(formatUrl("https://metabase.com/", { jsx: true }), ExternalLink)).toEqual(true); expect(isElementOfType(formatUrl("mailto:tom@metabase.com", { jsx: true }), ExternalLink)).toEqual(true); }); + it("should not return a link component for unrecognized links in jsx mode", () => { + expect(isElementOfType(formatUrl("nonexistent://metabase.com/", { jsx: true }), ExternalLink)).toEqual(false); + expect(isElementOfType(formatUrl("metabase.com", { jsx: true }), ExternalLink)).toEqual(false); + }); it("should return a string for javascript:, data:, and other links in jsx mode", () => { expect(formatUrl("javascript:alert('pwnd')", { jsx: true })).toEqual("javascript:alert('pwnd')"); expect(formatUrl("data:text/plain;charset=utf-8,hello%20world", { jsx: true })).toEqual("data:text/plain;charset=utf-8,hello%20world");