Pass allowed URI schemes to Markdown outputs and update naming

This commit is contained in:
Jonatan Kłosko 2023-02-15 22:18:13 +01:00
parent 0990ab4cb2
commit 12f2322d08
9 changed files with 42 additions and 28 deletions

View file

@ -229,10 +229,10 @@ The following environment variables configure Livebook:
iframe. Set it to "true" to enable it. If you do enable it, then the application iframe. Set it to "true" to enable it. If you do enable it, then the application
must run with HTTPS. must run with HTTPS.
* LIVEBOOK_ALLOW_URI_SCHEMES - sets addtional extra hyperlink protocols to * LIVEBOOK_ALLOW_URI_SCHEMES - sets additional allowed hyperlink schemes to the
the Markdown content. Livebook sanitizes links in Markdown, allowing only a few Markdown content. Livebook sanitizes links in Markdown, allowing only a few
standard protocols by default (such as http and https). Set a comma-separated list of standard schemes by default (such as http and https). Set it to a comma-separated
protocols to configure additional protocols. list of schemes.
<!-- Environment variables --> <!-- Environment variables -->

View file

@ -125,7 +125,10 @@ const Cell = {
"data-smart-cell-js-view-ref", "data-smart-cell-js-view-ref",
null null
), ),
protocols: getAttributeOrThrow(this.el, "data-protocols"), allowedUriSchemes: getAttributeOrThrow(
this.el,
"data-allowed-uri-schemes"
),
}; };
}, },
@ -217,7 +220,7 @@ const Cell = {
const markdown = new Markdown(markdownContainer, source, { const markdown = new Markdown(markdownContainer, source, {
baseUrl: this.props.sessionPath, baseUrl: this.props.sessionPath,
emptyText: "Empty markdown cell", emptyText: "Empty markdown cell",
extraProtocol: this.props.protocols.replace(/\s+/g, "").split(","), allowedUriSchemes: this.props.allowedUriSchemes.split(","),
}); });
liveEditor.onChange((newSource) => { liveEditor.onChange((newSource) => {

View file

@ -15,6 +15,7 @@ const MarkdownRenderer = {
const markdown = new Markdown(this.el, "", { const markdown = new Markdown(this.el, "", {
baseUrl: this.props.sessionPath, baseUrl: this.props.sessionPath,
allowedUriSchemes: this.props.allowedUriSchemes.split(","),
}); });
this.handleEvent( this.handleEvent(
@ -29,6 +30,10 @@ const MarkdownRenderer = {
return { return {
id: getAttributeOrThrow(this.el, "data-id"), id: getAttributeOrThrow(this.el, "data-id"),
sessionPath: getAttributeOrThrow(this.el, "data-session-path"), sessionPath: getAttributeOrThrow(this.el, "data-session-path"),
allowedUriSchemes: getAttributeOrThrow(
this.el,
"data-allowed-uri-schemes"
),
}; };
}, },
}; };

View file

@ -28,13 +28,13 @@ class Markdown {
constructor( constructor(
container, container,
content, content,
{ baseUrl = null, emptyText = "", extraProtocol = [] } = {} { baseUrl = null, emptyText = "", allowedUriSchemes = [] } = {}
) { ) {
this.container = container; this.container = container;
this.content = content; this.content = content;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.emptyText = emptyText; this.emptyText = emptyText;
this.extraProtocol = extraProtocol; this.allowedUriSchemes = allowedUriSchemes;
this._render(); this._render();
} }
@ -67,7 +67,7 @@ class Markdown {
.use(remarkRehype, { allowDangerousHtml: true }) .use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw) .use(rehypeRaw)
.use(rehypeExpandUrls, { baseUrl: this.baseUrl }) .use(rehypeExpandUrls, { baseUrl: this.baseUrl })
.use(rehypeSanitize, sanitizeSchema(this.extraProtocol)) .use(rehypeSanitize, sanitizeSchema(this.allowedUriSchemes))
.use(rehypeKatex) .use(rehypeKatex)
.use(rehypeMermaid) .use(rehypeMermaid)
.use(rehypeExternalLinks, { baseUrl: this.baseUrl }) .use(rehypeExternalLinks, { baseUrl: this.baseUrl })
@ -103,7 +103,7 @@ export default Markdown;
// Plugins // Plugins
function sanitizeSchema(extraProtocol) { function sanitizeSchema(allowedUriSchemes) {
// Allow class and style attributes on tags for syntax highlighting, // Allow class and style attributes on tags for syntax highlighting,
// remarkMath tags, or user-written styles // remarkMath tags, or user-written styles
@ -115,7 +115,7 @@ function sanitizeSchema(extraProtocol) {
}, },
protocols: { protocols: {
...defaultSchema.protocols, ...defaultSchema.protocols,
href: [...defaultSchema.protocols.href, ...extraProtocol], href: [...defaultSchema.protocols.href, ...allowedUriSchemes],
}, },
}; };
} }

View file

@ -195,9 +195,9 @@ defmodule Livebook.Config do
end end
@doc """ @doc """
Return list of added uri schemes. Return list of additional allowed hyperlink schemes.
""" """
@spec allowed_uri_schemes() :: [] @spec allowed_uri_schemes() :: list(String.t())
def allowed_uri_schemes() do def allowed_uri_schemes() do
Application.fetch_env!(:livebook, :allowed_uri_schemes) Application.fetch_env!(:livebook, :allowed_uri_schemes)
end end
@ -407,6 +407,15 @@ defmodule Livebook.Config do
end end
end end
@doc """
Parses and validates allowed URI schemes from env.
"""
def allowed_uri_schemes!(env) do
if schemes = System.get_env(env) do
String.split(schemes, ",", trim: true)
end
end
@doc """ @doc """
Returns the current version of running Livebook. Returns the current version of running Livebook.
""" """
@ -438,13 +447,4 @@ defmodule Livebook.Config do
IO.puts("\nERROR!!! [Livebook] " <> message) IO.puts("\nERROR!!! [Livebook] " <> message)
System.halt(1) System.halt(1)
end end
@doc """
Parses and validates allowed URI schemes from env.
"""
def allowed_uri_schemes!(env) do
if schemes = System.get_env(env) do
String.split(schemes, ",", trim: true)
end
end
end end

View file

@ -1,6 +1,11 @@
defmodule LivebookWeb.Output.MarkdownComponent do defmodule LivebookWeb.Output.MarkdownComponent do
use LivebookWeb, :live_component use LivebookWeb, :live_component
@impl true
def mount(socket) do
{:ok, assign(socket, allowed_uri_schemes: Livebook.Config.allowed_uri_schemes())}
end
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
socket = assign(socket, assigns) socket = assign(socket, assigns)
@ -20,6 +25,7 @@ defmodule LivebookWeb.Output.MarkdownComponent do
phx-hook="MarkdownRenderer" phx-hook="MarkdownRenderer"
data-id={@id} data-id={@id}
data-session-path={Routes.session_path(@socket, :page, @session_id)} data-session-path={Routes.session_path(@socket, :page, @session_id)}
data-allowed-uri-schemes={Enum.join(@allowed_uri_schemes, ",")}
> >
</div> </div>
""" """

View file

@ -5,7 +5,7 @@ defmodule LivebookWeb.SessionLive do
import LivebookWeb.SessionHelpers import LivebookWeb.SessionHelpers
import Livebook.Utils, only: [format_bytes: 1] import Livebook.Utils, only: [format_bytes: 1]
alias Livebook.{Config, Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, Secrets} alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, Secrets}
alias Livebook.Notebook.{Cell, ContentLoader} alias Livebook.Notebook.{Cell, ContentLoader}
alias Livebook.JSInterop alias Livebook.JSInterop
alias Livebook.Hubs alias Livebook.Hubs
@ -64,7 +64,7 @@ defmodule LivebookWeb.SessionLive do
saved_secrets: get_saved_secrets(), saved_secrets: get_saved_secrets(),
select_secret_ref: nil, select_secret_ref: nil,
select_secret_options: nil, select_secret_options: nil,
protocols: Config.allowed_uri_schemes() |> Enum.join(",") allowed_uri_schemes: Livebook.Config.allowed_uri_schemes()
) )
|> assign_private(data: data) |> assign_private(data: data)
|> prune_outputs() |> prune_outputs()
@ -271,9 +271,9 @@ defmodule LivebookWeb.SessionLive do
session_id={@session.id} session_id={@session.id}
session_pid={@session.pid} session_pid={@session.pid}
client_id={@client_id} client_id={@client_id}
protocols={@protocols}
runtime={@data_view.runtime} runtime={@data_view.runtime}
installing?={@data_view.installing?} installing?={@data_view.installing?}
allowed_uri_schemes={@allowed_uri_schemes}
cell_view={@data_view.setup_cell_view} cell_view={@data_view.setup_cell_view}
/> />
</div> </div>
@ -291,12 +291,12 @@ defmodule LivebookWeb.SessionLive do
id={section_view.id} id={section_view.id}
index={index} index={index}
session_id={@session.id} session_id={@session.id}
protocols={@protocols}
session_pid={@session.pid} session_pid={@session.pid}
client_id={@client_id} client_id={@client_id}
runtime={@data_view.runtime} runtime={@data_view.runtime}
smart_cell_definitions={@data_view.smart_cell_definitions} smart_cell_definitions={@data_view.smart_cell_definitions}
installing?={@data_view.installing?} installing?={@data_view.installing?}
allowed_uri_schemes={@allowed_uri_schemes}
section_view={section_view} section_view={section_view}
/> />
<% end %> <% end %>

View file

@ -10,7 +10,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
id={"cell-#{@cell_view.id}"} id={"cell-#{@cell_view.id}"}
phx-hook="Cell" phx-hook="Cell"
data-cell-id={@cell_view.id} data-cell-id={@cell_view.id}
data-protocols={@protocols}
data-focusable-id={@cell_view.id} data-focusable-id={@cell_view.id}
data-type={@cell_view.type} data-type={@cell_view.type}
data-session-path={Routes.session_path(@socket, :page, @session_id)} data-session-path={Routes.session_path(@socket, :page, @session_id)}
@ -18,6 +17,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
data-eval-validity={get_in(@cell_view, [:eval, :validity])} data-eval-validity={get_in(@cell_view, [:eval, :validity])}
data-js-empty={empty?(@cell_view.source_view)} data-js-empty={empty?(@cell_view.source_view)}
data-smart-cell-js-view-ref={smart_cell_js_view_ref(@cell_view)} data-smart-cell-js-view-ref={smart_cell_js_view_ref(@cell_view)}
data-allowed-uri-schemes={Enum.join(@allowed_uri_schemes, ",")}
> >
<%= render_cell(assigns) %> <%= render_cell(assigns) %>
</div> </div>

View file

@ -142,9 +142,9 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
session_id={@session_id} session_id={@session_id}
session_pid={@session_pid} session_pid={@session_pid}
client_id={@client_id} client_id={@client_id}
protocols={@protocols}
runtime={@runtime} runtime={@runtime}
installing?={@installing?} installing?={@installing?}
allowed_uri_schemes={@allowed_uri_schemes}
cell_view={cell_view} cell_view={cell_view}
/> />
<.live_component <.live_component