diff --git a/.eslintignore b/.eslintignore index a520c62c910..7e1a7cd5a73 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,7 +5,6 @@ app/assets/javascripts/preload_store.js app/assets/javascripts/pagedown_custom.js app/assets/javascripts/vendor.js app/assets/javascripts/locales/i18n.js -app/assets/javascripts/defer/html-sanitizer-bundle.js app/assets/javascripts/ember-addons/ app/assets/javascripts/discourse/lib/autosize.js.es6 lib/javascripts/locale/ diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index 755dc574b24..7e592e9d80a 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -1,5 +1,6 @@ /* global ace:true */ import loadScript from 'discourse/lib/load-script'; +import escapeExpression from 'discourse/lib/utilities'; export default Ember.Component.extend({ mode: 'css', @@ -16,7 +17,7 @@ export default Ember.Component.extend({ render(buffer) { buffer.push("
"); if (this.get('content')) { - buffer.push(Discourse.Utilities.escapeExpression(this.get('content'))); + buffer.push(escapeExpression(this.get('content'))); } buffer.push("
"); }, diff --git a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 index d26585f3481..8f93884c604 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 @@ -1,3 +1,5 @@ +import { escapeExpression } from 'discourse/lib/utilities'; + export default Ember.Controller.extend({ needs: ['modal'], @@ -22,7 +24,7 @@ export default Ember.Controller.extend({ returned = "
";
 
     _.each(raw, function(linehash) {
-      returned += Discourse.Utilities.escapeExpression(linehash["QUERY PLAN"]);
+      returned += escapeExpression(linehash["QUERY PLAN"]);
       returned += "
"; }); @@ -32,7 +34,7 @@ export default Ember.Controller.extend({ processed_sample: Ember.computed.map('model.sample', function(grant) { var i18nKey = 'admin.badges.preview.grant.with', - i18nParams = { username: Discourse.Utilities.escapeExpression(grant.username) }; + i18nParams = { username: escapeExpression(grant.username) }; if (grant.post_id) { i18nKey += "_post"; @@ -41,7 +43,7 @@ export default Ember.Controller.extend({ if (grant.granted_at) { i18nKey += "_time"; - i18nParams.time = Discourse.Utilities.escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year'))); + i18nParams.time = escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year'))); } return I18n.t(i18nKey, i18nParams); diff --git a/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 b/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 index 5f70a2acc84..73bac433799 100644 --- a/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 +++ b/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 @@ -1,3 +1,4 @@ import { htmlHelper } from 'discourse/lib/helpers'; +import { escapeExpression } from 'discourse/lib/utilities'; -export default htmlHelper(str => Discourse.Utilities.escapeExpression(str).replace(/\n/g, "
")); +export default htmlHelper(str => escapeExpression(str).replace(/\n/g, "
")); diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 index 4f9e5cc9aec..b6e764ce33a 100644 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6 @@ -1,4 +1,5 @@ import AdminUser from 'admin/models/admin-user'; +import { escapeExpression } from 'discourse/lib/utilities'; const StaffActionLog = Discourse.Model.extend({ showFullDetails: false, @@ -19,14 +20,14 @@ const StaffActionLog = Discourse.Model.extend({ formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value'); } if (!this.get('useModalForDetails')) { - if (this.get('details')) formatted += Discourse.Utilities.escapeExpression(this.get('details')) + '
'; + if (this.get('details')) formatted += escapeExpression(this.get('details')) + '
'; } return formatted; }.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'), format: function(label, propertyName) { if (this.get(propertyName)) { - return ('' + I18n.t(label) + ': ' + Discourse.Utilities.escapeExpression(this.get(propertyName)) + '
'); + return ('' + I18n.t(label) + ': ' + escapeExpression(this.get(propertyName)) + '
'); } else { return ''; } diff --git a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 index cb838a48fc6..1929abf5a22 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -1,5 +1,6 @@ import debounce from 'discourse/lib/debounce'; import { renderSpinner } from 'discourse/helpers/loading-spinner'; +import { escapeExpression } from 'discourse/lib/utilities'; export default Ember.View.extend({ classNames: ["admin-backups-logs"], @@ -19,7 +20,7 @@ export default Ember.View.extend({ let formattedLogs = this.get("formattedLogs"); for (let i = this.get("index"), length = logs.length; i < length; i++) { const date = logs[i].get("timestamp"), - message = Discourse.Utilities.escapeExpression(logs[i].get("message")); + message = escapeExpression(logs[i].get("message")); formattedLogs += "[" + date + "] " + message + "\n"; } // update the formatted logs & cache index diff --git a/app/assets/javascripts/defer/html-sanitizer-bundle.js b/app/assets/javascripts/defer/html-sanitizer-bundle.js deleted file mode 100644 index 7529153a857..00000000000 --- a/app/assets/javascripts/defer/html-sanitizer-bundle.js +++ /dev/null @@ -1,2233 +0,0 @@ -// Copyright (C) 2010 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview - * Implements RFC 3986 for parsing/formatting URIs. - * - * @author mikesamuel@gmail.com - * \@provides URI - * \@overrides window - */ - -var URI = (function () { - -/** - * creates a uri from the string form. The parser is relaxed, so special - * characters that aren't escaped but don't cause ambiguities will not cause - * parse failures. - * - * @return {URI|null} - */ -function parse(uriStr) { - var m = ('' + uriStr).match(URI_RE_); - if (!m) { return null; } - return new URI( - nullIfAbsent(m[1]), - nullIfAbsent(m[2]), - nullIfAbsent(m[3]), - nullIfAbsent(m[4]), - nullIfAbsent(m[5]), - nullIfAbsent(m[6]), - nullIfAbsent(m[7])); -} - - -/** - * creates a uri from the given parts. - * - * @param scheme {string} an unencoded scheme such as "http" or null - * @param credentials {string} unencoded user credentials or null - * @param domain {string} an unencoded domain name or null - * @param port {number} a port number in [1, 32768]. - * -1 indicates no port, as does null. - * @param path {string} an unencoded path - * @param query {Array.|string|null} a list of unencoded cgi - * parameters where even values are keys and odds the corresponding values - * or an unencoded query. - * @param fragment {string} an unencoded fragment without the "#" or null. - * @return {URI} - */ -function create(scheme, credentials, domain, port, path, query, fragment) { - var uri = new URI( - encodeIfExists2(scheme, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_), - encodeIfExists2( - credentials, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_), - encodeIfExists(domain), - port > 0 ? port.toString() : null, - encodeIfExists2(path, URI_DISALLOWED_IN_PATH_), - null, - encodeIfExists(fragment)); - if (query) { - if ('string' === typeof query) { - uri.setRawQuery(query.replace(/[^?&=0-9A-Za-z_\-~.%]/g, encodeOne)); - } else { - uri.setAllParameters(query); - } - } - return uri; -} -function encodeIfExists(unescapedPart) { - if ('string' == typeof unescapedPart) { - return encodeURIComponent(unescapedPart); - } - return null; -}; -/** - * if unescapedPart is non null, then escapes any characters in it that aren't - * valid characters in a url and also escapes any special characters that - * appear in extra. - * - * @param unescapedPart {string} - * @param extra {RegExp} a character set of characters in [\01-\177]. - * @return {string|null} null iff unescapedPart == null. - */ -function encodeIfExists2(unescapedPart, extra) { - if ('string' == typeof unescapedPart) { - return encodeURI(unescapedPart).replace(extra, encodeOne); - } - return null; -}; -/** converts a character in [\01-\177] to its url encoded equivalent. */ -function encodeOne(ch) { - var n = ch.charCodeAt(0); - return '%' + '0123456789ABCDEF'.charAt((n >> 4) & 0xf) + - '0123456789ABCDEF'.charAt(n & 0xf); -} - -/** - * {@updoc - * $ normPath('foo/./bar') - * # 'foo/bar' - * $ normPath('./foo') - * # 'foo' - * $ normPath('foo/.') - * # 'foo' - * $ normPath('foo//bar') - * # 'foo/bar' - * } - */ -function normPath(path) { - return path.replace(/(^|\/)\.(?:\/|$)/g, '$1').replace(/\/{2,}/g, '/'); -} - -var PARENT_DIRECTORY_HANDLER = new RegExp( - '' - // A path break - + '(/|^)' - // followed by a non .. path element - // (cannot be . because normPath is used prior to this RegExp) - + '(?:[^./][^/]*|\\.{2,}(?:[^./][^/]*)|\\.{3,}[^/]*)' - // followed by .. followed by a path break. - + '/\\.\\.(?:/|$)'); - -var PARENT_DIRECTORY_HANDLER_RE = new RegExp(PARENT_DIRECTORY_HANDLER); - -var EXTRA_PARENT_PATHS_RE = /^(?:\.\.\/)*(?:\.\.$)?/; - -/** - * Normalizes its input path and collapses all . and .. sequences except for - * .. sequences that would take it above the root of the current parent - * directory. - * {@updoc - * $ collapse_dots('foo/../bar') - * # 'bar' - * $ collapse_dots('foo/./bar') - * # 'foo/bar' - * $ collapse_dots('foo/../bar/./../../baz') - * # 'baz' - * $ collapse_dots('../foo') - * # '../foo' - * $ collapse_dots('../foo').replace(EXTRA_PARENT_PATHS_RE, '') - * # 'foo' - * } - */ -function collapse_dots(path) { - if (path === null) { return null; } - var p = normPath(path); - // Only /../ left to flatten - var r = PARENT_DIRECTORY_HANDLER_RE; - // We replace with $1 which matches a / before the .. because this - // guarantees that: - // (1) we have at most 1 / between the adjacent place, - // (2) always have a slash if there is a preceding path section, and - // (3) we never turn a relative path into an absolute path. - for (var q; (q = p.replace(r, '$1')) != p; p = q) {}; - return p; -} - -/** - * resolves a relative url string to a base uri. - * @return {URI} - */ -function resolve(baseUri, relativeUri) { - // there are several kinds of relative urls: - // 1. //foo - replaces everything from the domain on. foo is a domain name - // 2. foo - replaces the last part of the path, the whole query and fragment - // 3. /foo - replaces the the path, the query and fragment - // 4. ?foo - replace the query and fragment - // 5. #foo - replace the fragment only - - var absoluteUri = baseUri.clone(); - // we satisfy these conditions by looking for the first part of relativeUri - // that is not blank and applying defaults to the rest - - var overridden = relativeUri.hasScheme(); - - if (overridden) { - absoluteUri.setRawScheme(relativeUri.getRawScheme()); - } else { - overridden = relativeUri.hasCredentials(); - } - - if (overridden) { - absoluteUri.setRawCredentials(relativeUri.getRawCredentials()); - } else { - overridden = relativeUri.hasDomain(); - } - - if (overridden) { - absoluteUri.setRawDomain(relativeUri.getRawDomain()); - } else { - overridden = relativeUri.hasPort(); - } - - var rawPath = relativeUri.getRawPath(); - var simplifiedPath = collapse_dots(rawPath); - if (overridden) { - absoluteUri.setPort(relativeUri.getPort()); - simplifiedPath = simplifiedPath - && simplifiedPath.replace(EXTRA_PARENT_PATHS_RE, ''); - } else { - overridden = !!rawPath; - if (overridden) { - // resolve path properly - if (simplifiedPath.charCodeAt(0) !== 0x2f /* / */) { // path is relative - var absRawPath = collapse_dots(absoluteUri.getRawPath() || '') - .replace(EXTRA_PARENT_PATHS_RE, ''); - var slash = absRawPath.lastIndexOf('/') + 1; - simplifiedPath = collapse_dots( - (slash ? absRawPath.substring(0, slash) : '') - + collapse_dots(rawPath)) - .replace(EXTRA_PARENT_PATHS_RE, ''); - } - } else { - simplifiedPath = simplifiedPath - && simplifiedPath.replace(EXTRA_PARENT_PATHS_RE, ''); - if (simplifiedPath !== rawPath) { - absoluteUri.setRawPath(simplifiedPath); - } - } - } - - if (overridden) { - absoluteUri.setRawPath(simplifiedPath); - } else { - overridden = relativeUri.hasQuery(); - } - - if (overridden) { - absoluteUri.setRawQuery(relativeUri.getRawQuery()); - } else { - overridden = relativeUri.hasFragment(); - } - - if (overridden) { - absoluteUri.setRawFragment(relativeUri.getRawFragment()); - } - - return absoluteUri; -} - -/** - * a mutable URI. - * - * This class contains setters and getters for the parts of the URI. - * The getXYZ/setXYZ methods return the decoded part -- so - * uri.parse('/foo%20bar').getPath() will return the decoded path, - * /foo bar. - * - *

The raw versions of fields are available too. - * uri.parse('/foo%20bar').getRawPath() will return the raw path, - * /foo%20bar. Use the raw setters with care, since - * URI::toString is not guaranteed to return a valid url if a - * raw setter was used. - * - *

All setters return this and so may be chained, a la - * uri.parse('/foo').setFragment('part').toString(). - * - *

You should not use this constructor directly -- please prefer the factory - * functions {@link uri.parse}, {@link uri.create}, {@link uri.resolve} - * instead.

- * - *

The parameters are all raw (assumed to be properly escaped) parts, and - * any (but not all) may be null. Undefined is not allowed.

- * - * @constructor - */ -function URI( - rawScheme, - rawCredentials, rawDomain, port, - rawPath, rawQuery, rawFragment) { - this.scheme_ = rawScheme; - this.credentials_ = rawCredentials; - this.domain_ = rawDomain; - this.port_ = port; - this.path_ = rawPath; - this.query_ = rawQuery; - this.fragment_ = rawFragment; - /** - * @type {Array|null} - */ - this.paramCache_ = null; -} - -/** returns the string form of the url. */ -URI.prototype.toString = function () { - var out = []; - if (null !== this.scheme_) { out.push(this.scheme_, ':'); } - if (null !== this.domain_) { - out.push('//'); - if (null !== this.credentials_) { out.push(this.credentials_, '@'); } - out.push(this.domain_); - if (null !== this.port_) { out.push(':', this.port_.toString()); } - } - if (null !== this.path_) { out.push(this.path_); } - if (null !== this.query_) { out.push('?', this.query_); } - if (null !== this.fragment_) { out.push('#', this.fragment_); } - return out.join(''); -}; - -URI.prototype.clone = function () { - return new URI(this.scheme_, this.credentials_, this.domain_, this.port_, - this.path_, this.query_, this.fragment_); -}; - -URI.prototype.getScheme = function () { - // HTML5 spec does not require the scheme to be lowercased but - // all common browsers except Safari lowercase the scheme. - return this.scheme_ && decodeURIComponent(this.scheme_).toLowerCase(); -}; -URI.prototype.getRawScheme = function () { - return this.scheme_; -}; -URI.prototype.setScheme = function (newScheme) { - this.scheme_ = encodeIfExists2( - newScheme, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_); - return this; -}; -URI.prototype.setRawScheme = function (newScheme) { - this.scheme_ = newScheme ? newScheme : null; - return this; -}; -URI.prototype.hasScheme = function () { - return null !== this.scheme_; -}; - - -URI.prototype.getCredentials = function () { - return this.credentials_ && decodeURIComponent(this.credentials_); -}; -URI.prototype.getRawCredentials = function () { - return this.credentials_; -}; -URI.prototype.setCredentials = function (newCredentials) { - this.credentials_ = encodeIfExists2( - newCredentials, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_); - - return this; -}; -URI.prototype.setRawCredentials = function (newCredentials) { - this.credentials_ = newCredentials ? newCredentials : null; - return this; -}; -URI.prototype.hasCredentials = function () { - return null !== this.credentials_; -}; - - -URI.prototype.getDomain = function () { - return this.domain_ && decodeURIComponent(this.domain_); -}; -URI.prototype.getRawDomain = function () { - return this.domain_; -}; -URI.prototype.setDomain = function (newDomain) { - return this.setRawDomain(newDomain && encodeURIComponent(newDomain)); -}; -URI.prototype.setRawDomain = function (newDomain) { - this.domain_ = newDomain ? newDomain : null; - // Maintain the invariant that paths must start with a slash when the URI - // is not path-relative. - return this.setRawPath(this.path_); -}; -URI.prototype.hasDomain = function () { - return null !== this.domain_; -}; - - -URI.prototype.getPort = function () { - return this.port_ && decodeURIComponent(this.port_); -}; -URI.prototype.setPort = function (newPort) { - if (newPort) { - newPort = Number(newPort); - if (newPort !== (newPort & 0xffff)) { - throw new Error('Bad port number ' + newPort); - } - this.port_ = '' + newPort; - } else { - this.port_ = null; - } - return this; -}; -URI.prototype.hasPort = function () { - return null !== this.port_; -}; - - -URI.prototype.getPath = function () { - return this.path_ && decodeURIComponent(this.path_); -}; -URI.prototype.getRawPath = function () { - return this.path_; -}; -URI.prototype.setPath = function (newPath) { - return this.setRawPath(encodeIfExists2(newPath, URI_DISALLOWED_IN_PATH_)); -}; -URI.prototype.setRawPath = function (newPath) { - if (newPath) { - newPath = String(newPath); - this.path_ = - // Paths must start with '/' unless this is a path-relative URL. - (!this.domain_ || /^\//.test(newPath)) ? newPath : '/' + newPath; - } else { - this.path_ = null; - } - return this; -}; -URI.prototype.hasPath = function () { - return null !== this.path_; -}; - - -URI.prototype.getQuery = function () { - // From http://www.w3.org/Addressing/URL/4_URI_Recommentations.html - // Within the query string, the plus sign is reserved as shorthand notation - // for a space. - return this.query_ && decodeURIComponent(this.query_).replace(/\+/g, ' '); -}; -URI.prototype.getRawQuery = function () { - return this.query_; -}; -URI.prototype.setQuery = function (newQuery) { - this.paramCache_ = null; - this.query_ = encodeIfExists(newQuery); - return this; -}; -URI.prototype.setRawQuery = function (newQuery) { - this.paramCache_ = null; - this.query_ = newQuery ? newQuery : null; - return this; -}; -URI.prototype.hasQuery = function () { - return null !== this.query_; -}; - -/** - * sets the query given a list of strings of the form - * [ key0, value0, key1, value1, ... ]. - * - *

uri.setAllParameters(['a', 'b', 'c', 'd']).getQuery() - * will yield 'a=b&c=d'. - */ -URI.prototype.setAllParameters = function (params) { - if (typeof params === 'object') { - if (!(params instanceof Array) - && (params instanceof Object - || Object.prototype.toString.call(params) !== '[object Array]')) { - var newParams = []; - var i = -1; - for (var k in params) { - var v = params[k]; - if ('string' === typeof v) { - newParams[++i] = k; - newParams[++i] = v; - } - } - params = newParams; - } - } - this.paramCache_ = null; - var queryBuf = []; - var separator = ''; - for (var j = 0; j < params.length;) { - var k = params[j++]; - var v = params[j++]; - queryBuf.push(separator, encodeURIComponent(k.toString())); - separator = '&'; - if (v) { - queryBuf.push('=', encodeURIComponent(v.toString())); - } - } - this.query_ = queryBuf.join(''); - return this; -}; -URI.prototype.checkParameterCache_ = function () { - if (!this.paramCache_) { - var q = this.query_; - if (!q) { - this.paramCache_ = []; - } else { - var cgiParams = q.split(/[&\?]/); - var out = []; - var k = -1; - for (var i = 0; i < cgiParams.length; ++i) { - var m = cgiParams[i].match(/^([^=]*)(?:=(.*))?$/); - // From http://www.w3.org/Addressing/URL/4_URI_Recommentations.html - // Within the query string, the plus sign is reserved as shorthand - // notation for a space. - out[++k] = decodeURIComponent(m[1]).replace(/\+/g, ' '); - out[++k] = decodeURIComponent(m[2] || '').replace(/\+/g, ' '); - } - this.paramCache_ = out; - } - } -}; -/** - * sets the values of the named cgi parameters. - * - *

So, uri.parse('foo?a=b&c=d&e=f').setParameterValues('c', ['new']) - * yields foo?a=b&c=new&e=f.

- * - * @param key {string} - * @param values {Array.} the new values. If values is a single string - * then it will be treated as the sole value. - */ -URI.prototype.setParameterValues = function (key, values) { - // be nice and avoid subtle bugs where [] operator on string performs charAt - // on some browsers and crashes on IE - if (typeof values === 'string') { - values = [ values ]; - } - - this.checkParameterCache_(); - var newValueIndex = 0; - var pc = this.paramCache_; - var params = []; - for (var i = 0, k = 0; i < pc.length; i += 2) { - if (key === pc[i]) { - if (newValueIndex < values.length) { - params.push(key, values[newValueIndex++]); - } - } else { - params.push(pc[i], pc[i + 1]); - } - } - while (newValueIndex < values.length) { - params.push(key, values[newValueIndex++]); - } - this.setAllParameters(params); - return this; -}; -URI.prototype.removeParameter = function (key) { - return this.setParameterValues(key, []); -}; -/** - * returns the parameters specified in the query part of the uri as a list of - * keys and values like [ key0, value0, key1, value1, ... ]. - * - * @return {Array.} - */ -URI.prototype.getAllParameters = function () { - this.checkParameterCache_(); - return this.paramCache_.slice(0, this.paramCache_.length); -}; -/** - * returns the values for a given cgi parameter as a list of decoded - * query parameter values. - * @return {Array.} - */ -URI.prototype.getParameterValues = function (paramNameUnescaped) { - this.checkParameterCache_(); - var values = []; - for (var i = 0; i < this.paramCache_.length; i += 2) { - if (paramNameUnescaped === this.paramCache_[i]) { - values.push(this.paramCache_[i + 1]); - } - } - return values; -}; -/** - * returns a map of cgi parameter names to (non-empty) lists of values. - * @return {Object.>} - */ -URI.prototype.getParameterMap = function (paramNameUnescaped) { - this.checkParameterCache_(); - var paramMap = {}; - for (var i = 0; i < this.paramCache_.length; i += 2) { - var key = this.paramCache_[i++], - value = this.paramCache_[i++]; - if (!(key in paramMap)) { - paramMap[key] = [value]; - } else { - paramMap[key].push(value); - } - } - return paramMap; -}; -/** - * returns the first value for a given cgi parameter or null if the given - * parameter name does not appear in the query string. - * If the given parameter name does appear, but has no '=' following - * it, then the empty string will be returned. - * @return {string|null} - */ -URI.prototype.getParameterValue = function (paramNameUnescaped) { - this.checkParameterCache_(); - for (var i = 0; i < this.paramCache_.length; i += 2) { - if (paramNameUnescaped === this.paramCache_[i]) { - return this.paramCache_[i + 1]; - } - } - return null; -}; - -URI.prototype.getFragment = function () { - return this.fragment_ && decodeURIComponent(this.fragment_); -}; -URI.prototype.getRawFragment = function () { - return this.fragment_; -}; -URI.prototype.setFragment = function (newFragment) { - this.fragment_ = newFragment ? encodeURIComponent(newFragment) : null; - return this; -}; -URI.prototype.setRawFragment = function (newFragment) { - this.fragment_ = newFragment ? newFragment : null; - return this; -}; -URI.prototype.hasFragment = function () { - return null !== this.fragment_; -}; - -function nullIfAbsent(matchPart) { - return ('string' == typeof matchPart) && (matchPart.length > 0) - ? matchPart - : null; -} - - - - -/** - * a regular expression for breaking a URI into its component parts. - * - *

http://www.gbiv.com/protocols/uri/rfc/rfc3986.html#RFC2234 says - * As the "first-match-wins" algorithm is identical to the "greedy" - * disambiguation method used by POSIX regular expressions, it is natural and - * commonplace to use a regular expression for parsing the potential five - * components of a URI reference. - * - *

The following line is the regular expression for breaking-down a - * well-formed URI reference into its components. - * - *

- * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
- *  12            3  4          5       6  7        8 9
- * 
- * - *

The numbers in the second line above are only to assist readability; they - * indicate the reference points for each subexpression (i.e., each paired - * parenthesis). We refer to the value matched for subexpression as $. - * For example, matching the above expression to - *

- *     http://www.ics.uci.edu/pub/ietf/uri/#Related
- * 
- * results in the following subexpression matches: - *
- *    $1 = http:
- *    $2 = http
- *    $3 = //www.ics.uci.edu
- *    $4 = www.ics.uci.edu
- *    $5 = /pub/ietf/uri/
- *    $6 = 
- *    $7 = 
- *    $8 = #Related
- *    $9 = Related
- * 
- * where indicates that the component is not present, as is the - * case for the query component in the above example. Therefore, we can - * determine the value of the five components as - *
- *    scheme    = $2
- *    authority = $4
- *    path      = $5
- *    query     = $7
- *    fragment  = $9
- * 
- * - *

msamuel: I have modified the regular expression slightly to expose the - * credentials, domain, and port separately from the authority. - * The modified version yields - *

- *    $1 = http              scheme
- *    $2 =        credentials -\
- *    $3 = www.ics.uci.edu   domain       | authority
- *    $4 =        port        -/
- *    $5 = /pub/ietf/uri/    path
- *    $6 =        query without ?
- *    $7 = Related           fragment without #
- * 
- */ -var URI_RE_ = new RegExp( - "^" + - "(?:" + - "([^:/?#]+)" + // scheme - ":)?" + - "(?://" + - "(?:([^/?#]*)@)?" + // credentials - "([^/?#:@]*)" + // domain - "(?::([0-9]+))?" + // port - ")?" + - "([^?#]+)?" + // path - "(?:\\?([^#]*))?" + // query - "(?:#(.*))?" + // fragment - "$" - ); - -var URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_ = /[#\/\?@]/g; -var URI_DISALLOWED_IN_PATH_ = /[\#\?]/g; - -URI.parse = parse; -URI.create = create; -URI.resolve = resolve; -URI.collapse_dots = collapse_dots; // Visible for testing. - -// lightweight string-based api for loadModuleMaker -URI.utils = { - mimeTypeOf: function (uri) { - var uriObj = parse(uri); - if (/\.html$/.test(uriObj.getPath())) { - return 'text/html'; - } else { - return 'application/javascript'; - } - }, - resolve: function (base, uri) { - if (base) { - return resolve(parse(base), parse(uri)).toString(); - } else { - return '' + uri; - } - } -}; - - -return URI; -})(); - -// Exports for closure compiler. -if (typeof window !== 'undefined') { - window['URI'] = URI; -} -; -// Copyright Google Inc. -// Licensed under the Apache Licence Version 2.0 -// Autogenerated at Mon Oct 21 13:30:08 EDT 2013 -// @overrides window -// @provides html4 -var html4 = {}; -html4.atype = { - 'NONE': 0, - 'URI': 1, - 'URI_FRAGMENT': 11, - 'SCRIPT': 2, - 'STYLE': 3, - 'HTML': 12, - 'ID': 4, - 'IDREF': 5, - 'IDREFS': 6, - 'GLOBAL_NAME': 7, - 'LOCAL_NAME': 8, - 'CLASSES': 9, - 'FRAME_TARGET': 10, - 'MEDIA_QUERY': 13 -}; -html4[ 'atype' ] = html4.atype; -html4.ATTRIBS = { - '*::class': 9, - '*::dir': 0, - '*::draggable': 0, - '*::hidden': 0, - '*::id': 4, - '*::inert': 0, - '*::itemprop': 0, - '*::itemref': 6, - '*::itemscope': 0, - '*::lang': 0, - '*::onblur': 2, - '*::onchange': 2, - '*::onclick': 2, - '*::ondblclick': 2, - '*::onerror': 2, - '*::onfocus': 2, - '*::onkeydown': 2, - '*::onkeypress': 2, - '*::onkeyup': 2, - '*::onload': 2, - '*::onmousedown': 2, - '*::onmousemove': 2, - '*::onmouseout': 2, - '*::onmouseover': 2, - '*::onmouseup': 2, - '*::onreset': 2, - '*::onscroll': 2, - '*::onselect': 2, - '*::onsubmit': 2, - '*::onunload': 2, - '*::spellcheck': 0, - '*::style': 3, - '*::title': 0, - '*::translate': 0, - 'a::accesskey': 0, - 'a::coords': 0, - 'a::href': 1, - 'a::hreflang': 0, - 'a::name': 7, - 'a::onblur': 2, - 'a::onfocus': 2, - 'a::shape': 0, - 'a::tabindex': 0, - 'a::target': 10, - 'a::type': 0, - 'bdo::dir': 0, - 'blockquote::cite': 1, - 'br::clear': 0, - 'caption::align': 0, - 'col::align': 0, - 'col::char': 0, - 'col::charoff': 0, - 'col::span': 0, - 'col::valign': 0, - 'col::width': 0, - 'colgroup::align': 0, - 'colgroup::char': 0, - 'colgroup::charoff': 0, - 'colgroup::span': 0, - 'colgroup::valign': 0, - 'colgroup::width': 0, - 'data::value': 0, - 'del::cite': 1, - 'del::datetime': 0, - 'details::open': 0, - 'dir::compact': 0, - 'div::align': 0, - 'dl::compact': 0, - 'h1::align': 0, - 'h2::align': 0, - 'h3::align': 0, - 'h4::align': 0, - 'h5::align': 0, - 'h6::align': 0, - 'hr::align': 0, - 'hr::noshade': 0, - 'hr::size': 0, - 'hr::width': 0, - 'iframe::align': 0, - 'iframe::frameborder': 0, - 'iframe::height': 0, - 'iframe::marginheight': 0, - 'iframe::marginwidth': 0, - 'iframe::width': 0, - 'iframe::src': 1, - 'img::alt': 0, - 'img::height': 0, - 'img::name': 7, - 'img::src': 1, - 'img::width': 0, - 'ins::cite': 1, - 'ins::datetime': 0, - 'label::accesskey': 0, - 'label::for': 5, - 'label::onblur': 2, - 'label::onfocus': 2, - 'legend::accesskey': 0, - 'legend::align': 0, - 'li::type': 0, - 'li::value': 0, - 'ol::compact': 0, - 'ol::reversed': 0, - 'ol::start': 0, - 'ol::type': 0, - 'p::align': 0, - 'pre::width': 0, - 'q::cite': 1, - 'source::type': 0, - 'ul::compact': 0, - 'ul::type': 0, -}; -html4[ 'ATTRIBS' ] = html4.ATTRIBS; -html4.eflags = { - 'OPTIONAL_ENDTAG': 1, - 'EMPTY': 2, - 'CDATA': 4, - 'RCDATA': 8, - 'UNSAFE': 16, - 'FOLDABLE': 32, - 'SCRIPT': 64, - 'STYLE': 128, - 'VIRTUALIZED': 256 -}; -html4[ 'eflags' ] = html4.eflags; -html4.ELEMENTS = { - 'a': 0, - 'abbr': 0, - 'acronym': 0, - 'address': 0, - 'article': 0, - 'aside': 0, - 'b': 0, - 'base': 274, - 'bdi': 0, - 'bdo': 0, - 'big': 0, - 'blockquote': 0, - 'body': 305, - 'br': 2, - 'caption': 0, - 'cite': 0, - 'code': 0, - 'col': 2, - 'colgroup': 1, - 'data': 0, - 'dd': 1, - 'del': 0, - 'details': 0, - 'dfn': 0, - 'dialog': 272, - 'dir': 0, - 'div': 0, - 'dl': 0, - 'dt': 1, - 'em': 0, - 'figcaption': 0, - 'figure': 0, - 'frame': 274, - 'frameset': 272, - 'h1': 0, - 'h2': 0, - 'h3': 0, - 'h4': 0, - 'h5': 0, - 'h6': 0, - 'head': 305, - 'header': 0, - 'hgroup': 0, - 'hr': 2, - 'html': 305, - 'i': 0, - 'iframe': 4, - 'img': 2, - 'ins': 0, - 'isindex': 274, - 'kbd': 0, - 'keygen': 274, - 'label': 0, - 'legend': 0, - 'li': 1, - 'link': 274, - 'nav': 0, - 'nobr': 0, - 'noembed': 276, - 'noframes': 276, - 'noscript': 276, - 'object': 272, - 'ol': 0, - 'p': 1, - 'param': 274, - 'pre': 0, - 'q': 0, - 's': 0, - 'samp': 0, - 'script': 84, - 'section': 0, - 'small': 0, - 'span': 0, - 'strike': 0, - 'strong': 0, - 'style': 148, - 'sub': 0, - 'summary': 0, - 'sup': 0, - 'table': 272, - 'tbody': 273, - 'td': 273, - 'tfoot': 1, - 'th': 273, - 'thead': 273, - 'time': 0, - 'title': 280, - 'tr': 273, - 'tt': 0, - 'u': 0, - 'ul': 0, - 'var': 0, - 'wbr': 2 -}; -html4[ 'ELEMENTS' ] = html4.ELEMENTS; -html4.ELEMENT_DOM_INTERFACES = { - 'a': 'HTMLAnchorElement', - 'abbr': 'HTMLElement', - 'acronym': 'HTMLElement', - 'address': 'HTMLElement', - 'applet': 'HTMLAppletElement', - 'area': 'HTMLAreaElement', - 'article': 'HTMLElement', - 'aside': 'HTMLElement', - 'audio': 'HTMLAudioElement', - 'b': 'HTMLElement', - 'base': 'HTMLBaseElement', - 'basefont': 'HTMLBaseFontElement', - 'bdi': 'HTMLElement', - 'bdo': 'HTMLElement', - 'big': 'HTMLElement', - 'blockquote': 'HTMLQuoteElement', - 'body': 'HTMLBodyElement', - 'br': 'HTMLBRElement', - 'caption': 'HTMLTableCaptionElement', - 'cite': 'HTMLElement', - 'code': 'HTMLElement', - 'col': 'HTMLTableColElement', - 'colgroup': 'HTMLTableColElement', - 'command': 'HTMLCommandElement', - 'data': 'HTMLElement', - 'datalist': 'HTMLDataListElement', - 'dd': 'HTMLElement', - 'del': 'HTMLModElement', - 'details': 'HTMLDetailsElement', - 'dfn': 'HTMLElement', - 'dialog': 'HTMLDialogElement', - 'dir': 'HTMLDirectoryElement', - 'div': 'HTMLDivElement', - 'dl': 'HTMLDListElement', - 'dt': 'HTMLElement', - 'em': 'HTMLElement', - 'fieldset': 'HTMLFieldSetElement', - 'figcaption': 'HTMLElement', - 'figure': 'HTMLElement', - 'footer': 'HTMLElement', - 'form': 'HTMLFormElement', - 'frame': 'HTMLFrameElement', - 'frameset': 'HTMLFrameSetElement', - 'h1': 'HTMLHeadingElement', - 'h2': 'HTMLHeadingElement', - 'h3': 'HTMLHeadingElement', - 'h4': 'HTMLHeadingElement', - 'h5': 'HTMLHeadingElement', - 'h6': 'HTMLHeadingElement', - 'head': 'HTMLHeadElement', - 'header': 'HTMLElement', - 'hgroup': 'HTMLElement', - 'hr': 'HTMLHRElement', - 'html': 'HTMLHtmlElement', - 'i': 'HTMLElement', - 'iframe': 'HTMLIFrameElement', - 'img': 'HTMLImageElement', - 'input': 'HTMLInputElement', - 'ins': 'HTMLModElement', - 'isindex': 'HTMLUnknownElement', - 'kbd': 'HTMLElement', - 'keygen': 'HTMLKeygenElement', - 'label': 'HTMLLabelElement', - 'legend': 'HTMLLegendElement', - 'li': 'HTMLLIElement', - 'link': 'HTMLLinkElement', - 'map': 'HTMLMapElement', - 'menu': 'HTMLMenuElement', - 'meta': 'HTMLMetaElement', - 'nav': 'HTMLElement', - 'nobr': 'HTMLElement', - 'noembed': 'HTMLElement', - 'noframes': 'HTMLElement', - 'noscript': 'HTMLElement', - 'object': 'HTMLObjectElement', - 'ol': 'HTMLOListElement', - 'optgroup': 'HTMLOptGroupElement', - 'option': 'HTMLOptionElement', - 'output': 'HTMLOutputElement', - 'p': 'HTMLParagraphElement', - 'param': 'HTMLParamElement', - 'pre': 'HTMLPreElement', - 'q': 'HTMLQuoteElement', - 's': 'HTMLElement', - 'samp': 'HTMLElement', - 'script': 'HTMLScriptElement', - 'section': 'HTMLElement', - 'select': 'HTMLSelectElement', - 'small': 'HTMLElement', - 'source': 'HTMLSourceElement', - 'span': 'HTMLSpanElement', - 'strike': 'HTMLElement', - 'strong': 'HTMLElement', - 'style': 'HTMLStyleElement', - 'sub': 'HTMLElement', - 'summary': 'HTMLElement', - 'sup': 'HTMLElement', - 'table': 'HTMLTableElement', - 'tbody': 'HTMLTableSectionElement', - 'td': 'HTMLTableDataCellElement', - 'tfoot': 'HTMLTableSectionElement', - 'th': 'HTMLTableHeaderCellElement', - 'thead': 'HTMLTableSectionElement', - 'time': 'HTMLTimeElement', - 'title': 'HTMLTitleElement', - 'tr': 'HTMLTableRowElement', - 'tt': 'HTMLElement', - 'u': 'HTMLElement', - 'ul': 'HTMLUListElement', - 'var': 'HTMLElement', - 'video': 'HTMLVideoElement', - 'wbr': 'HTMLElement' -}; -html4[ 'ELEMENT_DOM_INTERFACES' ] = html4.ELEMENT_DOM_INTERFACES; -html4.ueffects = { - 'NOT_LOADED': 0, - 'SAME_DOCUMENT': 1, - 'NEW_DOCUMENT': 2 -}; -html4[ 'ueffects' ] = html4.ueffects; -html4.URIEFFECTS = { - 'a::href': 2, - 'area::href': 2, - 'audio::src': 1, - 'blockquote::cite': 0, - 'command::icon': 1, - 'del::cite': 0, - 'form::action': 2, - 'iframe::src': 1, - 'img::src': 1, - 'input::src': 1, - 'ins::cite': 0, - 'q::cite': 0, - 'video::poster': 1, - 'video::src': 1 -}; -html4[ 'URIEFFECTS' ] = html4.URIEFFECTS; -html4.ltypes = { - 'UNSANDBOXED': 2, - 'SANDBOXED': 1, - 'DATA': 0 -}; -html4[ 'ltypes' ] = html4.ltypes; -html4.LOADERTYPES = { - 'a::href': 2, - 'area::href': 2, - 'audio::src': 2, - 'blockquote::cite': 2, - 'command::icon': 1, - 'del::cite': 2, - 'form::action': 2, - 'iframe::src': 2, - 'img::src': 1, - 'input::src': 1, - 'ins::cite': 2, - 'q::cite': 2, - 'video::poster': 1, - 'video::src': 2 -}; -html4[ 'LOADERTYPES' ] = html4.LOADERTYPES; -// NOTE: currently focused only on URI-type attributes -html4.REQUIREDATTRIBUTES = { - "audio" : ["src"], - "form" : ["action"], - "iframe" : ["src"], - "image" : ["src"], - "video" : ["src"] -}; -html4[ 'REQUIREDATTRIBUTES' ] = html4.REQUIREDATTRIBUTES; -// export for Closure Compiler -if (typeof window !== 'undefined') { - window['html4'] = html4; -} -; -// Copyright (C) 2006 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview - * An HTML sanitizer that can satisfy a variety of security policies. - * - *

- * The HTML sanitizer is built around a SAX parser and HTML element and - * attributes schemas. - * - * If the cssparser is loaded, inline styles are sanitized using the - * css property and value schemas. Else they are remove during - * sanitization. - * - * If it exists, uses parseCssDeclarations, sanitizeCssProperty, cssSchema - * - * @author mikesamuel@gmail.com - * @author jasvir@gmail.com - * \@requires html4, URI - * \@overrides window - * \@provides html, html_sanitize - */ - -// The Turkish i seems to be a non-issue, but abort in case it is. -if ('I'.toLowerCase() !== 'i') { throw 'I/i problem'; } - -/** - * \@namespace - */ -var html = (function(html4) { - - // For closure compiler - var parseCssDeclarations, sanitizeCssProperty, cssSchema; - if ('undefined' !== typeof window) { - parseCssDeclarations = window['parseCssDeclarations']; - sanitizeCssProperty = window['sanitizeCssProperty']; - cssSchema = window['cssSchema']; - } - - // The keys of this object must be 'quoted' or JSCompiler will mangle them! - // This is a partial list -- lookupEntity() uses the host browser's parser - // (when available) to implement full entity lookup. - // Note that entities are in general case-sensitive; the uppercase ones are - // explicitly defined by HTML5 (presumably as compatibility). - var ENTITIES = { - 'lt': '<', - 'LT': '<', - 'gt': '>', - 'GT': '>', - 'amp': '&', - 'AMP': '&', - 'quot': '"', - 'apos': '\'', - 'nbsp': '\240' - }; - - // Patterns for types of entity/character reference names. - var decimalEscapeRe = /^#(\d+)$/; - var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/; - // contains every entity per http://www.w3.org/TR/2011/WD-html5-20110113/named-character-references.html - var safeEntityNameRe = /^[A-Za-z][A-za-z0-9]+$/; - // Used as a hook to invoke the browser's entity parsing. "), "hullo"); - equal(sanitize(""), "press me!"); - equal(sanitize("draw me!"), "draw me!"); - equal(sanitize("hello"), "hello"); - equal(sanitize("highlight"), "highlight"); - - cooked("[the answer](javascript:alert(42))", "

the answer

", "it prevents XSS"); - - cooked("\n", "


", "it doesn't circumvent XSS with comments"); - - cooked("a", "

a

", "it sanitizes spans"); - cooked("a", "

a

", "it sanitizes spans"); - cooked("a", "

a

", "it sanitizes spans"); -}); - test("URLs in BBCode tags", function() { cooked("[img]http://eviltrout.com/eviltrout.png[/img][img]http://samsaffron.com/samsaffron.png[/img]", @@ -531,23 +500,6 @@ test("URLs in BBCode tags", function() { }); -test("urlAllowed", function() { - var urlAllowed = Discourse.Markdown.urlAllowed; - - var allowed = function(url, msg) { - equal(urlAllowed(url), url, msg); - }; - - allowed("/foo/bar.html", "allows relative urls"); - allowed("http://eviltrout.com/evil/trout", "allows full urls"); - allowed("https://eviltrout.com/evil/trout", "allows https urls"); - allowed("//eviltrout.com/evil/trout", "allows protocol relative urls"); - - equal(urlAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"), - "http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf", - "escape single quotes"); -}); - test("images", function() { cooked("[![folksy logo](http://folksy.com/images/folksy-colour.png)](http://folksy.com/)", "

\"folksy

", @@ -559,7 +511,6 @@ test("images", function() { }); test("censoring", function() { - Discourse.SiteSettings.censored_words = "shucks|whiz|whizzer"; cooked("aw shucks, golly gee whiz.", "

aw ■■■■■■, golly gee ■■■■.

", "it censors words in the Site Settings"); @@ -583,3 +534,165 @@ test("code blocks/spans hoisting", function() { "

$&

", "it works even when hoisting special replacement patterns"); }); + +test('basic bbcode', function() { + cookedPara("[b]strong[/b]", "strong", "bolds text"); + cookedPara("[i]emphasis[/i]", "emphasis", "italics text"); + cookedPara("[u]underlined[/u]", "underlined", "underlines text"); + cookedPara("[s]strikethrough[/s]", "strikethrough", "strikes-through text"); + cookedPara("[img]http://eviltrout.com/eviltrout.png[/img]", "", "links images"); + cookedPara("[email]eviltrout@mailinator.com[/email]", "eviltrout@mailinator.com", "supports [email] without a title"); + cookedPara("[b]evil [i]trout[/i][/b]", + "evil trout", + "allows embedding of tags"); + cookedPara("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "eviltrout@mailinator.com", "supports upper case bbcode"); + cookedPara("[b]strong [b]stronger[/b][/b]", "strong stronger", "accepts nested bbcode tags"); +}); + +test('urls', function() { + cookedPara("[url]not a url[/url]", "not a url", "supports [url] that isn't a url"); + cookedPara("[url]http://bettercallsaul.com[/url]", "http://bettercallsaul.com", "supports [url] without parameter"); + cookedPara("[url=http://example.com]example[/url]", "example", "supports [url] with given href"); + cookedPara("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]", + "", + "supports [url] with an embedded [img]"); +}); +test('invalid bbcode', function() { + const result = new PrettyText({ lookupAvatar: false }).cook("[code]I am not closed\n\nThis text exists."); + equal(result, "

[code]I am not closed

\n\n

This text exists.

", "does not raise an error with an open bbcode tag."); +}); + +test('code', function() { + cookedPara("[code]\nx++\n[/code]", "
x++
", "makes code into pre"); + cookedPara("[code]\nx++\ny++\nz++\n[/code]", "
x++\ny++\nz++
", "makes code into pre"); + cookedPara("[code]abc\n#def\n[/code]", '
abc\n#def
', 'it handles headings in a [code] block'); + cookedPara("[code]\n s[/code]", + "
   s
", + "it doesn't trim leading whitespace"); +}); + +test('lists', function() { + cookedPara("[ul][li]option one[/li][/ul]", "
  • option one
", "creates an ul"); + cookedPara("[ol][li]option one[/li][/ol]", "
  1. option one
", "creates an ol"); + cookedPara("[ul]\n[li]option one[/li]\n[li]option two[/li]\n[/ul]", "
  • option one
  • option two
", "suppresses empty lines in lists"); +}); + +test('tags with arguments', function() { + cookedPara("[url=http://bettercallsaul.com]better call![/url]", "better call!", "supports [url] with a title"); + cookedPara("[email=eviltrout@mailinator.com]evil trout[/email]", "evil trout", "supports [email] with a title"); + cookedPara("[u][i]abc[/i][/u]", "abc", "can nest tags"); + cookedPara("[b]first[/b] [b]second[/b]", "first second", "can bold two things on the same line"); +}); + + +test("quotes", function() { + const post = Post.create({ + cooked: "

lorem ipsum

", + username: "eviltrout", + post_number: 1, + topic_id: 2 + }); + + function formatQuote(val, expected, text) { + equal(Quote.build(post, val), expected, text); + }; + + formatQuote(undefined, "", "empty string for undefined content"); + formatQuote(null, "", "empty string for null content"); + formatQuote("", "", "empty string for empty string content"); + + formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes"); + + formatQuote(" lorem \t ", + "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", + "trims white spaces before & after the quoted contents"); + + formatQuote("lorem ipsum", + "[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n", + "marks quotes as full when the quote is the full message"); + + formatQuote("**lorem** ipsum", + "[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n", + "keeps BBCode formatting"); + + formatQuote("this is a bug", + "[quote=\"eviltrout, post:1, topic:2\"]\nthis is <not> a bug\n[/quote]\n\n", + "it escapes the contents of the quote"); + + cookedPara("[quote]test[/quote]", + "", + "it supports quotes without params"); + + cookedPara("[quote]\n*test*\n[/quote]", + "", + "it doesn't insert a new line for italics"); + + cookedPara("[quote=,script='a'>"), "
"); + equal(pt.sanitize("

hello

"), "

hello

"); + equal(pt.sanitize("<3 <3"), "<3 <3"); + equal(pt.sanitize("<_<"), "<_<"); + cooked("hello", "

hello

", "it sanitizes while cooking"); + + cooked("disney reddit", + "

disney reddit

", + "we can embed proper links"); + + cooked("
hello
", "

hello

", "it does not allow centering"); + cooked("
hello
\nafter", "

after

", "it does not allow tables"); + cooked("
a\n
\n", "
a\n\n
\n\n
", "it does not double sanitize"); + + cooked("", "", "it does not allow most iframes"); + + cooked("", + "", + "it allows iframe to google maps"); + + cooked("", + "", + "it allows iframe to OpenStreetMap"); + + equal(pt.sanitize(""), "hullo"); + equal(pt.sanitize(""), "press me!"); + equal(pt.sanitize("draw me!"), "draw me!"); + equal(pt.sanitize("hello"), "hello"); + equal(pt.sanitize("highlight"), "highlight"); + + cooked("[the answer](javascript:alert(42))", "

the answer

", "it prevents XSS"); + + cooked("\n", "


", "it doesn't circumvent XSS with comments"); + + cooked("a", "

a

", "it sanitizes spans"); + cooked("a", "

a

", "it sanitizes spans"); + cooked("a", "

a

", "it sanitizes spans"); +}); + +test("urlAllowed", function() { + const allowed = (url, msg) => equal(hrefAllowed(url), url, msg); + + allowed("/foo/bar.html", "allows relative urls"); + allowed("http://eviltrout.com/evil/trout", "allows full urls"); + allowed("https://eviltrout.com/evil/trout", "allows https urls"); + allowed("//eviltrout.com/evil/trout", "allows protocol relative urls"); + + equal(hrefAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"), + "http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf", + "escape single quotes"); +}); + diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index c9f35a948bf..75b8cc194d2 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -1,16 +1,27 @@ /* global Int8Array:true */ import { blank } from 'helpers/qunit-helpers'; +import { + emailValid, + isAnImage, + avatarUrl, + allowsAttachments, + getRawSize, + avatarImg, + defaultHomepage, + validateUploadedFiles, + getUploadMarkdown, + caretRowCol, + setCaretPosition +} from 'discourse/lib/utilities'; -module("Discourse.Utilities"); - -var utils = Discourse.Utilities; +module("lib:utilities"); test("emailValid", function() { - ok(utils.emailValid('Bob@example.com'), "allows upper case in the first part of emails"); - ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain"); + ok(emailValid('Bob@example.com'), "allows upper case in the first part of emails"); + ok(emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain"); }); -var validUpload = utils.validateUploadedFiles; +var validUpload = validateUploadedFiles; test("validateUploadedFiles", function() { not(validUpload(null), "no files are invalid"); @@ -80,8 +91,8 @@ test("allows valid uploads to go through", function() { not(bootbox.alert.calledOnce); }); -var getUploadMarkdown = function(filename) { - return utils.getUploadMarkdown({ +var testUploadMarkdown = function(filename) { + return getUploadMarkdown({ original_filename: filename, filesize: 42, width: 100, @@ -91,26 +102,26 @@ var getUploadMarkdown = function(filename) { }; test("getUploadMarkdown", function() { - ok(getUploadMarkdown("lolcat.gif") === ''); - ok(getUploadMarkdown("important.txt") === 'important.txt (42 Bytes)\n'); + ok(testUploadMarkdown("lolcat.gif") === ''); + ok(testUploadMarkdown("important.txt") === 'important.txt (42 Bytes)\n'); }); test("isAnImage", function() { _.each(["png", "jpg", "jpeg", "bmp", "gif", "tif", "tiff", "ico"], function(extension) { var image = "image." + extension; - ok(utils.isAnImage(image), image + " is recognized as an image"); - ok(utils.isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image"); + ok(isAnImage(image), image + " is recognized as an image"); + ok(isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image"); }); - not(utils.isAnImage("file.txt")); - not(utils.isAnImage("http://foo.bar/path/to/file.txt")); - not(utils.isAnImage("")); + not(isAnImage("file.txt")); + not(isAnImage("http://foo.bar/path/to/file.txt")); + not(isAnImage("")); }); test("avatarUrl", function() { - var rawSize = utils.getRawSize; - blank(utils.avatarUrl('', 'tiny'), "no template returns blank"); - equal(utils.avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url"); - equal(utils.avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size"); + var rawSize = getRawSize; + blank(avatarUrl('', 'tiny'), "no template returns blank"); + equal(avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url"); + equal(avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size"); }); var setDevicePixelRatio = function(value) { @@ -126,19 +137,19 @@ test("avatarImg", function() { setDevicePixelRatio(2); var avatarTemplate = "/path/to/avatar/{size}.png"; - equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}), + equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}), "", "it returns the avatar html"); - equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}), + equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}), "", "it adds a title if supplied"); - equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}), + equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}), "", "it adds extra classes if supplied"); - blank(utils.avatarImg({avatarTemplate: "", size: 'tiny'}), + blank(avatarImg({avatarTemplate: "", size: 'tiny'}), "it doesn't render avatars for invalid avatar template"); setDevicePixelRatio(oldRatio); @@ -146,18 +157,18 @@ test("avatarImg", function() { test("allowsAttachments", function() { Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; - not(utils.allowsAttachments(), "no attachments allowed by default"); + not(allowsAttachments(), "no attachments allowed by default"); Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|*"; - ok(utils.allowsAttachments(), "attachments are allowed when all extensions are allowed"); + ok(allowsAttachments(), "attachments are allowed when all extensions are allowed"); Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|pdf"; - ok(utils.allowsAttachments(), "attachments are allowed when at least one extension is not an image extension"); + ok(allowsAttachments(), "attachments are allowed when at least one extension is not an image extension"); }); test("defaultHomepage", function() { Discourse.SiteSettings.top_menu = "latest|top|hot"; - equal(utils.defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting"); + equal(defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting"); }); test("caretRowCol", () => { @@ -167,9 +178,9 @@ test("caretRowCol", () => { document.body.appendChild(textarea); const assertResult = (setCaretPos, expectedRowNum, expectedColNum) => { - Discourse.Utilities.setCaretPosition(textarea, setCaretPos); + setCaretPosition(textarea, setCaretPos); - const result = Discourse.Utilities.caretRowCol(textarea); + const result = caretRowCol(textarea); equal(result.rowNum, expectedRowNum, "returns the right row of the caret"); equal(result.colNum, expectedColNum, "returns the right col of the caret"); }; diff --git a/test/javascripts/mdtest/mdtest.js.erb b/test/javascripts/mdtest/mdtest.js.es6.erb similarity index 55% rename from test/javascripts/mdtest/mdtest.js.erb rename to test/javascripts/mdtest/mdtest.js.es6.erb index f1c9a713814..a17d5645135 100644 --- a/test/javascripts/mdtest/mdtest.js.erb +++ b/test/javascripts/mdtest/mdtest.js.es6.erb @@ -1,8 +1,9 @@ -module("MDTest", { - setup: function() { - Discourse.SiteSettings.traditional_markdown_linebreaks = false; - } -}); +import { sanitize } from 'pretty-text/sanitizer'; +import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text'; +import { hashString } from 'discourse/lib/hash'; + +// Run the MDTest spec +module("MDTest"); // This is cheating, but the trivial differences between sanitization // do not affect formatting. @@ -15,44 +16,42 @@ function normalize(str) { // We use a custom sanitizer for MD test that hoists out comments. In Discourse // they are stripped, but to be compliant with the spec they should not be. -function hoistingSanitizer(result) { - var hoisted, - m = result.match(//g); +function sanitizer(result, whiteLister) { + let hoisted; + const m = result.match(//g); if (m && m.length) { hoisted = []; - for (var i=0; i result = result.replace(tuple[1], tuple[0])); } return result; } -var md = function(input, expected, text) { - var result = Discourse.Markdown.cook(input, { - sanitizerFunction: hoistingSanitizer, - traditional_markdown_linebreaks: true - }), - resultNorm = normalize(result), - expectedNorm = normalize(expected), - same = (result === expected) || (resultNorm === expectedNorm); +function md(input, expected, text) { + + const opts = buildOptions({ siteSettings: {} }); + opts.traditionalMarkdownLinebreaks = true; + opts.sanitizer = sanitizer; + + const cooker = new PrettyText(opts); + const result = cooker.cook(input); + const resultNorm = normalize(result); + const expectedNorm = normalize(expected); + const same = (result === expected) || (resultNorm === expectedNorm); if (same) { ok(same, text); } else { - console.log(resultNorm); - console.log(expectedNorm); equal(resultNorm, expectedNorm, text); } }; diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index f1d2469b43e..7938b7afd92 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -1,4 +1,6 @@ import { blank, present } from 'helpers/qunit-helpers'; +import { IMAGE_VERSION as v} from 'pretty-text/emoji'; + module("model:topic"); import Topic from 'discourse/models/topic'; @@ -75,7 +77,6 @@ test("recover", function() { test('fancyTitle', function() { var topic = Topic.create({ fancy_title: ":smile: with all :) the emojis :pear::peach:" }); - const v = Discourse.Emoji.ImageVersion; equal(topic.get('fancyTitle'), `smile with all slight_smile the emojis pearpeach`, diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 7362298d159..f867246f2f1 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -22,9 +22,9 @@ //= require htmlparser.js // Stuff we need to load first +//= require pretty-text-bundle //= require main_include //= require admin -//= require_tree ../../app/assets/javascripts/defer //= require sinon-1.7.1 //= require sinon-qunit-1.0.0 @@ -43,12 +43,6 @@ window.inTestEnv = true; -window.assetPath = function(url) { - if (url.indexOf('defer') === 0) { - return "/assets/" + url; - } -}; - // Stop the message bus so we don't get ajax calls window.MessageBus.stop(); @@ -137,3 +131,5 @@ Object.keys(requirejs.entries).forEach(function(entry) { require(entry, null, null, true); } }); +require('mdtest/mdtest', null, null, true); + diff --git a/vendor/assets/javascripts/better_markdown.js b/vendor/assets/javascripts/better_markdown.js index a445aacec5c..836b1c7e313 100644 --- a/vendor/assets/javascripts/better_markdown.js +++ b/vendor/assets/javascripts/better_markdown.js @@ -429,6 +429,7 @@ if ( attrs && attrs.references ) refs = attrs.references; + var html = convert_tree_to_html( input, refs , options ); merge_text_nodes( html ); return html; diff --git a/vendor/assets/javascripts/ember-qunit.js b/vendor/assets/javascripts/ember-qunit.js index c3717ef55bf..ce4a591162f 100644 --- a/vendor/assets/javascripts/ember-qunit.js +++ b/vendor/assets/javascripts/ember-qunit.js @@ -1043,4 +1043,4 @@ window.test = emberQunit.test; window.setResolver = emberQunit.setResolver; })(); -//# sourceMappingURL=ember-qunit.map \ No newline at end of file +//# sourceMappingURL=ember-qunit.map diff --git a/vendor/assets/javascripts/md5.js b/vendor/assets/javascripts/md5.js deleted file mode 100644 index bebcdacc26c..00000000000 --- a/vendor/assets/javascripts/md5.js +++ /dev/null @@ -1,180 +0,0 @@ -/*! - * Joseph Myer's md5() algorithm wrapped in a self-invoked function to prevent - * global namespace polution, modified to hash unicode characters as UTF-8. - * - * Copyright 1999-2010, Joseph Myers, Paul Johnston, Greg Holt, Will Bond - * http://www.myersdaily.org/joseph/javascript/md5-text.html - * http://pajhome.org.uk/crypt/md5 - * - * Released under the BSD license - * http://www.opensource.org/licenses/bsd-license - */ -(function() { - function md5cycle(x, k) { - var a = x[0], b = x[1], c = x[2], d = x[3]; - - a = ff(a, b, c, d, k[0], 7, -680876936); - d = ff(d, a, b, c, k[1], 12, -389564586); - c = ff(c, d, a, b, k[2], 17, 606105819); - b = ff(b, c, d, a, k[3], 22, -1044525330); - a = ff(a, b, c, d, k[4], 7, -176418897); - d = ff(d, a, b, c, k[5], 12, 1200080426); - c = ff(c, d, a, b, k[6], 17, -1473231341); - b = ff(b, c, d, a, k[7], 22, -45705983); - a = ff(a, b, c, d, k[8], 7, 1770035416); - d = ff(d, a, b, c, k[9], 12, -1958414417); - c = ff(c, d, a, b, k[10], 17, -42063); - b = ff(b, c, d, a, k[11], 22, -1990404162); - a = ff(a, b, c, d, k[12], 7, 1804603682); - d = ff(d, a, b, c, k[13], 12, -40341101); - c = ff(c, d, a, b, k[14], 17, -1502002290); - b = ff(b, c, d, a, k[15], 22, 1236535329); - - a = gg(a, b, c, d, k[1], 5, -165796510); - d = gg(d, a, b, c, k[6], 9, -1069501632); - c = gg(c, d, a, b, k[11], 14, 643717713); - b = gg(b, c, d, a, k[0], 20, -373897302); - a = gg(a, b, c, d, k[5], 5, -701558691); - d = gg(d, a, b, c, k[10], 9, 38016083); - c = gg(c, d, a, b, k[15], 14, -660478335); - b = gg(b, c, d, a, k[4], 20, -405537848); - a = gg(a, b, c, d, k[9], 5, 568446438); - d = gg(d, a, b, c, k[14], 9, -1019803690); - c = gg(c, d, a, b, k[3], 14, -187363961); - b = gg(b, c, d, a, k[8], 20, 1163531501); - a = gg(a, b, c, d, k[13], 5, -1444681467); - d = gg(d, a, b, c, k[2], 9, -51403784); - c = gg(c, d, a, b, k[7], 14, 1735328473); - b = gg(b, c, d, a, k[12], 20, -1926607734); - - a = hh(a, b, c, d, k[5], 4, -378558); - d = hh(d, a, b, c, k[8], 11, -2022574463); - c = hh(c, d, a, b, k[11], 16, 1839030562); - b = hh(b, c, d, a, k[14], 23, -35309556); - a = hh(a, b, c, d, k[1], 4, -1530992060); - d = hh(d, a, b, c, k[4], 11, 1272893353); - c = hh(c, d, a, b, k[7], 16, -155497632); - b = hh(b, c, d, a, k[10], 23, -1094730640); - a = hh(a, b, c, d, k[13], 4, 681279174); - d = hh(d, a, b, c, k[0], 11, -358537222); - c = hh(c, d, a, b, k[3], 16, -722521979); - b = hh(b, c, d, a, k[6], 23, 76029189); - a = hh(a, b, c, d, k[9], 4, -640364487); - d = hh(d, a, b, c, k[12], 11, -421815835); - c = hh(c, d, a, b, k[15], 16, 530742520); - b = hh(b, c, d, a, k[2], 23, -995338651); - - a = ii(a, b, c, d, k[0], 6, -198630844); - d = ii(d, a, b, c, k[7], 10, 1126891415); - c = ii(c, d, a, b, k[14], 15, -1416354905); - b = ii(b, c, d, a, k[5], 21, -57434055); - a = ii(a, b, c, d, k[12], 6, 1700485571); - d = ii(d, a, b, c, k[3], 10, -1894986606); - c = ii(c, d, a, b, k[10], 15, -1051523); - b = ii(b, c, d, a, k[1], 21, -2054922799); - a = ii(a, b, c, d, k[8], 6, 1873313359); - d = ii(d, a, b, c, k[15], 10, -30611744); - c = ii(c, d, a, b, k[6], 15, -1560198380); - b = ii(b, c, d, a, k[13], 21, 1309151649); - a = ii(a, b, c, d, k[4], 6, -145523070); - d = ii(d, a, b, c, k[11], 10, -1120210379); - c = ii(c, d, a, b, k[2], 15, 718787259); - b = ii(b, c, d, a, k[9], 21, -343485551); - - x[0] = add32(a, x[0]); - x[1] = add32(b, x[1]); - x[2] = add32(c, x[2]); - x[3] = add32(d, x[3]); - } - - function cmn(q, a, b, x, s, t) { - a = add32(add32(a, q), add32(x, t)); - return add32((a << s) | (a >>> (32 - s)), b); - } - - function ff(a, b, c, d, x, s, t) { - return cmn((b & c) | ((~b) & d), a, b, x, s, t); - } - - function gg(a, b, c, d, x, s, t) { - return cmn((b & d) | (c & (~d)), a, b, x, s, t); - } - - function hh(a, b, c, d, x, s, t) { - return cmn(b ^ c ^ d, a, b, x, s, t); - } - - function ii(a, b, c, d, x, s, t) { - return cmn(c ^ (b | (~d)), a, b, x, s, t); - } - - function md51(s) { - // Converts the string to UTF-8 "bytes" when necessary - if (/[\x80-\xFF]/.test(s)) { - s = unescape(encodeURI(s)); - } - txt = ''; - var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; - for (i = 64; i <= s.length; i += 64) { - md5cycle(state, md5blk(s.substring(i - 64, i))); - } - s = s.substring(i - 64); - var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for (i = 0; i < s.length; i++) - tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); - tail[i >> 2] |= 0x80 << ((i % 4) << 3); - if (i > 55) { - md5cycle(state, tail); - for (i = 0; i < 16; i++) tail[i] = 0; - } - tail[14] = n * 8; - md5cycle(state, tail); - return state; - } - - function md5blk(s) { /* I figured global was faster. */ - var md5blks = [], i; /* Andy King said do it this way. */ - for (i = 0; i < 64; i += 4) { - md5blks[i >> 2] = s.charCodeAt(i) + - (s.charCodeAt(i + 1) << 8) + - (s.charCodeAt(i + 2) << 16) + - (s.charCodeAt(i + 3) << 24); - } - return md5blks; - } - - var hex_chr = '0123456789abcdef'.split(''); - - function rhex(n) { - var s = '', j = 0; - for (; j < 4; j++) - s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + - hex_chr[(n >> (j * 8)) & 0x0F]; - return s; - } - - function hex(x) { - for (var i = 0; i < x.length; i++) - x[i] = rhex(x[i]); - return x.join(''); - } - - md5 = function (s) { - return hex(md51(s)); - } - - /* this function is much faster, so if possible we use it. Some IEs are the - only ones I know of that need the idiotic second function, generated by an - if clause. */ - function add32(a, b) { - return (a + b) & 0xFFFFFFFF; - } - - if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { - function add32(x, y) { - var lsw = (x & 0xFFFF) + (y & 0xFFFF), - msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - } - } -})(); \ No newline at end of file