Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into bugfix-2549-remove…
Browse files Browse the repository at this point in the history
…-cesium-listeners
  • Loading branch information
rushirajnenuji committed Dec 2, 2024
2 parents 3f1b398 + 5089123 commit 3b8a3b2
Show file tree
Hide file tree
Showing 28 changed files with 1,023 additions and 72 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion helm/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ MetacatUI.AppConfig = {
{{- $ignoreList := list "enabled" "theme" "root" "baseUrl" "metacatContext" "d1CNBaseUrl" -}}
{{- range $key, $value := .Values.appConfig }}
{{- if not (has $key $ignoreList) }}
{{- if eq (typeOf $value) "string" }}
{{- if regexFind "new\\s+[A-Za-z]+\\s*\\(" (printf "%v" $value) -}}
{{/* don't put quotes around JS object declarations -- e.g.: new Date(... */}}
{{- $key | nindent 4 }}: {{ $value }},
{{- else if eq (typeOf $value) "string" }}
{{- $key | nindent 4 }}: {{ $value | quote }},
{{- else }}
{{- $key | nindent 4 }}: {{ $value }},
Expand Down
2 changes: 1 addition & 1 deletion helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ global:
## @param appConfig The MetacatUI.AppConfig optional override settings
##
## Optional configuration. Note you can define any attributes here, to override those that appear
## in config.js, provided they are of the form: 'key: stringValue,' or 'key: intValue,'.
## in config.js, provided they are of the form: 'key: stringValue' or 'key: intValue'.
##
## See full listing in AppModel.js:
## https://github.com/NCEAS/metacatui/blob/main/src/js/models/AppModel.js
Expand Down
159 changes: 159 additions & 0 deletions src/components/showdown/extensions/showdown-iframes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* SHOWDOWN IFRAMES
*
* This extension filters out iframes with src attributes that
* are not from a trusted source
*/

/** List of trusted URL patterns */
const TRUSTED_SOURCES = MetacatUI.appModel.get("trustedContentSources") || [];

/**
* The sandbox to add to iframes from trusted sources. This allows the iframe
* some capabilities, such as running scripts and accessing it's own origin.
*/
const SANDBOX = `sandbox="allow-scripts allow-same-origin"`;

/**
* Regular expression that finds all iframes in the markdown content. The regex
* captures the full iframe tag, the src attribute, the inner content, and the
* closing tag, if it exists.
* @type {RegExp}
*/
const IFRAME_REGEX =
/<iframe[^>]*?\bsrc="([^"]*)"[^>]*?>([\s\S]*?)(<\/iframe>)?/g;

/**
* Function to convert URL patterns with wildcards to regex patterns.
* @param {string} wildcardPattern - The URL pattern with wildcards
* @returns {RegExp} - The regex pattern
*/
function patternToRegex(wildcardPattern) {
// Extract protocol if specified
let protocol = "";
let pattern = wildcardPattern;
const protocolMatch = pattern.match(/^(https?:\/\/)/);
if (protocolMatch) {
[, protocol] = protocolMatch;
pattern = wildcardPattern.slice(protocol.length);
}

// Escape special regex characters except for '*'
let escapedPattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&");
// Replace '*' with '.*'
escapedPattern = escapedPattern.replace(/\*/g, ".*");
// Escape the protocol
const escapedProtocol = protocol.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&");
// Build the full regex pattern
const regexString = `^${escapedProtocol}${escapedPattern}$`;

return new RegExp(regexString, "i"); // Case-insensitive matching
}

/**
* Check if a URL is valid according to the trusted sources. Trusted sources may
* use wildcards (*) to match multiple URLs. For example, the trusted source
* "https://*dataone.org/*" will match any URL that starts with "https://",
* contains "dataone.org", and ends with a path. The trusted source
* "*arcticdata.io*" will match any URL that contains "arcticdata.io". It could
* also include wildcards at any position, such as
* "*arcticdata.io/*\/something".
* @param {string} url - The URL to check
* @returns {boolean} - True if the URL is trusted, false otherwise
*/
function isTrustedUrl(url) {
if (!TRUSTED_SOURCES?.length) return false;

try {
const urlObj = new URL(url);
if (!urlObj.protocol.startsWith("http")) {
return false;
}
} catch (e) {
return false;
}

// Check if the URL matches any of the trusted sources
for (let i = 0; i < TRUSTED_SOURCES.length; i += 1) {
const pattern = TRUSTED_SOURCES[i];
const regex = patternToRegex(pattern);

if (regex.test(url)) {
return true;
}
}

return false;
}

/**
* Replace iFrames that are NOT from trusted sources with a link to the source
* URL. Make the iFrames from trusted sources secure by adding the 'sandbox'
* attribute, which restricts the iframe's capabilities. Remove any inner
* content from the iframe.
* @param {string} iframe - The full iframe tag
* @param {string} src - The src attribute of the iframe
* @param {string} _innerContent - The inner content of the iframe tag
* @param {string} closingTag - The closing iframe tag
* @param {number} _index - The index of the match
* @param {string} _markdown - The full markdown content
* @returns {string} - The secure iframe tag
*/
const secureIFrame = (
iframe,
src,
_innerContent,
closingTag,
_index,
_markdown,
) => {
// Return as a link instead of an iframe if the source is not trusted
if (!isTrustedUrl(src)) {
return `<a href="${src}" target="_blank" rel="noopener noreferrer"><b>External Content</b>: ${src}</a>`;
}

// Find the position of the first '>' that ends the opening iframe tag
const openingTagEndIndex = iframe.indexOf(">");

// Add the 'sandbox' attr and strip out any inner content
if (openingTagEndIndex !== -1) {
// Extract the opening tag
let openingTag = iframe.slice(0, openingTagEndIndex);

// Ensure 'sandbox' attribute exists with the correct value
if (!/\bsandbox=/.test(openingTag)) {
// Add the 'sandbox' attribute
openingTag += ` ${SANDBOX}`;
} else {
// Update the existing 'sandbox' attribute to have the correct value
openingTag = openingTag.replace(/\bsandbox="[^"]*"/, SANDBOX);
}

// Close the opening tag
openingTag += ">";

let newIframe;
if (closingTag) {
// Reconstruct the iframe without inner content and include the closing tag
newIframe = `${openingTag}${closingTag}`;
} else {
// If there is no closing tag, self-close the iframe
newIframe = openingTag.replace(">", " />");
}

return newIframe;
}

// If the iframe tag is malformed and doesn't contain '>', return it as is
return iframe;
};

const extension = {
type: "output",
regex: IFRAME_REGEX,
replace: secureIFrame,
};

define(["showdown"], (showdown) => {
showdown.extension("showdown-iframes", () => [extension]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ define(['showdown', 'xss'], function (showdown, xss) {
var options = {
css: false,
allowList: {
iframe: ["src", "width", "height", "frameborder", "allowfullscreen"],
a: ["target", "href", "title", "class", "target"],
abbr: ["title"],
address: [],
Expand Down
6 changes: 3 additions & 3 deletions src/components/showdown/showdown.min.js

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions src/css/metacatui-common.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ a:hover {
.icon.warning {
color: #ffbc00;
}
.icon.info {
color: #3a87ad;
}
.list-group-item.success {
background-color: #dff0d8;
}
Expand Down Expand Up @@ -1963,11 +1966,10 @@ div.analyze.dropdown.open button.dropdown.btn.btn-secondary.dropdown-toggle {
}
.controls-container .info-icons .icon-stack .icon-stack-top {
color: #fff;
font-size: 0.75em;
margin-top: -15px;
font-size: 0.7em;
}
.controls-container .info-icons .icon-stack .icon-stack-base {
font-size: 1.25em;
font-size: 1.5em;
}
.metadata-controls-container {
position: relative;
Expand Down
2 changes: 2 additions & 0 deletions src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ require.config({
"/components/showdown/extensions/showdown-xss-filter/xss.min",
showdownHtags:
MetacatUI.root + "/components/showdown/extensions/showdown-htags",
showdownIframes:
MetacatUI.root + "/components/showdown/extensions/showdown-iframes",
// woofmark - markdown editor
woofmark: MetacatUI.root + "/components/woofmark.min",
// drop zone creates drag and drop areas
Expand Down
18 changes: 18 additions & 0 deletions src/js/models/AppModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1731,6 +1731,24 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
*/
feverUrl: "",

/**
* A list of trusted content sources from which MetacatUI can safely
* embed external content. This property is used to define URLs or URL
* patterns that are considered secure for embedding content in
* iframes, especially when rendering user-generated Markdown content.
*
* Each source in the list can include wildcards (`*`) to match any
* subdomain or path. For example, `"https://*.dataone.org/*"` matches
* any subdomain of `dataone.org` over HTTPS, and `"*arcticdata.io*"`
* matches any URL containing `arcticdata.io`.
*
* Set to an empty array or a falsy value to disable all embedded content.
*
* @type {string[]}
* @since 0.0.0
*/
trustedContentSources: [],

/** If true, then archived content is available in the search index.
* Set to false if this MetacatUI is using a Metacat version before 2.10.0
* @type {boolean}
Expand Down
Loading

0 comments on commit 3b8a3b2

Please sign in to comment.