Rewrite images to use local proxy (#4035)
* Add markdown rule to add rel=nofollow for all links * Add markdown image rule to add local image proxy (fixes #1036) * comments * rewrite markdown image links working * add comment * perform markdown image processing in api/apub receivers * clippy * add db table to validate proxied links * rewrite link fields for avatar, banner etc * sql fmt * proxy links received over federation * add config option * undo post.url rewriting, move http route definition * add tests * proxy images through pictrs * testing * cleanup request.rs file * more cleanup (fixes #2611) * include url content type when sending post over apub (fixes #2611) * store post url content type in db * should be media_type * get rid of cache_remote_thumbnails setting, instead automatically take thumbnail from federation data if available. * fix tests * add setting disable_external_link_previews * federate post url as image depending on mime type * change setting again * machete * invert * support custom emoji * clippy * update defaults * add image proxy test, fix test * fix test * clippy * revert accidental changes * address review * clippy * Markdown link rule-dess (#4356) * Extracting opengraph_data to its own type. * A few additions for markdown-link-rule. --------- Co-authored-by: Nutomic <me@nutomic.com> * fix setting * use enum for image proxy setting * fix test configs * add config backwards compat * clippy * machete --------- Co-authored-by: Dessalines <dessalines@users.noreply.github.com>remove_ansible_tagging
parent
1782aafd10
commit
e8a52d3a5c
@ -1,35 +0,0 @@ |
||||
use crate::{ |
||||
newtypes::LocalUserId, |
||||
schema::image_upload::dsl::{image_upload, local_user_id}, |
||||
source::image_upload::{ImageUpload, ImageUploadForm}, |
||||
utils::{get_conn, DbPool}, |
||||
}; |
||||
use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl, Table}; |
||||
use diesel_async::RunQueryDsl; |
||||
|
||||
impl ImageUpload { |
||||
pub async fn create(pool: &mut DbPool<'_>, form: &ImageUploadForm) -> Result<Self, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
insert_into(image_upload) |
||||
.values(form) |
||||
.get_result::<Self>(conn) |
||||
.await |
||||
} |
||||
|
||||
pub async fn get_all_by_local_user_id( |
||||
pool: &mut DbPool<'_>, |
||||
user_id: &LocalUserId, |
||||
) -> Result<Vec<Self>, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
image_upload |
||||
.filter(local_user_id.eq(user_id)) |
||||
.select(image_upload::all_columns()) |
||||
.load::<ImageUpload>(conn) |
||||
.await |
||||
} |
||||
|
||||
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
diesel::delete(image_upload.find(alias)).execute(conn).await |
||||
} |
||||
} |
@ -0,0 +1,78 @@ |
||||
use crate::{ |
||||
newtypes::{DbUrl, LocalUserId}, |
||||
schema::{ |
||||
local_image::dsl::{local_image, local_user_id, pictrs_alias}, |
||||
remote_image::dsl::{link, remote_image}, |
||||
}, |
||||
source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, |
||||
utils::{get_conn, DbPool}, |
||||
}; |
||||
use diesel::{ |
||||
dsl::exists, |
||||
insert_into, |
||||
result::Error, |
||||
select, |
||||
ExpressionMethods, |
||||
NotFound, |
||||
QueryDsl, |
||||
Table, |
||||
}; |
||||
use diesel_async::RunQueryDsl; |
||||
use url::Url; |
||||
|
||||
impl LocalImage { |
||||
pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result<Self, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
insert_into(local_image) |
||||
.values(form) |
||||
.get_result::<Self>(conn) |
||||
.await |
||||
} |
||||
|
||||
pub async fn get_all_by_local_user_id( |
||||
pool: &mut DbPool<'_>, |
||||
user_id: &LocalUserId, |
||||
) -> Result<Vec<Self>, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
local_image |
||||
.filter(local_user_id.eq(user_id)) |
||||
.select(local_image::all_columns()) |
||||
.load::<LocalImage>(conn) |
||||
.await |
||||
} |
||||
|
||||
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
diesel::delete(local_image.filter(pictrs_alias.eq(alias))) |
||||
.execute(conn) |
||||
.await |
||||
} |
||||
} |
||||
|
||||
impl RemoteImage { |
||||
pub async fn create(pool: &mut DbPool<'_>, links: Vec<Url>) -> Result<usize, Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
let forms = links |
||||
.into_iter() |
||||
.map(|url| RemoteImageForm { link: url.into() }) |
||||
.collect::<Vec<_>>(); |
||||
insert_into(remote_image) |
||||
.values(forms) |
||||
.on_conflict_do_nothing() |
||||
.execute(conn) |
||||
.await |
||||
} |
||||
|
||||
pub async fn validate(pool: &mut DbPool<'_>, link_: DbUrl) -> Result<(), Error> { |
||||
let conn = &mut get_conn(pool).await?; |
||||
|
||||
let exists = select(exists(remote_image.filter((link).eq(link_)))) |
||||
.get_result::<bool>(conn) |
||||
.await?; |
||||
if exists { |
||||
Ok(()) |
||||
} else { |
||||
Err(NotFound) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,50 @@ |
||||
use crate::newtypes::{DbUrl, LocalUserId}; |
||||
#[cfg(feature = "full")] |
||||
use crate::schema::{local_image, remote_image}; |
||||
use chrono::{DateTime, Utc}; |
||||
use serde_with::skip_serializing_none; |
||||
use std::fmt::Debug; |
||||
use typed_builder::TypedBuilder; |
||||
|
||||
#[skip_serializing_none] |
||||
#[derive(PartialEq, Eq, Debug, Clone)] |
||||
#[cfg_attr(feature = "full", derive(Queryable, Associations))] |
||||
#[cfg_attr(feature = "full", diesel(table_name = local_image))] |
||||
#[cfg_attr(
|
||||
feature = "full", |
||||
diesel(belongs_to(crate::source::local_user::LocalUser)) |
||||
)] |
||||
pub struct LocalImage { |
||||
pub local_user_id: LocalUserId, |
||||
pub pictrs_alias: String, |
||||
pub pictrs_delete_token: String, |
||||
pub published: DateTime<Utc>, |
||||
} |
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)] |
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] |
||||
#[cfg_attr(feature = "full", diesel(table_name = local_image))] |
||||
pub struct LocalImageForm { |
||||
pub local_user_id: LocalUserId, |
||||
pub pictrs_alias: String, |
||||
pub pictrs_delete_token: String, |
||||
} |
||||
|
||||
/// Stores all images which are hosted on remote domains. When attempting to proxy an image, it
|
||||
/// is checked against this table to avoid Lemmy being used as a general purpose proxy.
|
||||
#[skip_serializing_none] |
||||
#[derive(PartialEq, Eq, Debug, Clone)] |
||||
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))] |
||||
#[cfg_attr(feature = "full", diesel(table_name = remote_image))] |
||||
pub struct RemoteImage { |
||||
pub id: i32, |
||||
pub link: DbUrl, |
||||
pub published: DateTime<Utc>, |
||||
} |
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)] |
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] |
||||
#[cfg_attr(feature = "full", diesel(table_name = remote_image))] |
||||
pub struct RemoteImageForm { |
||||
pub link: DbUrl, |
||||
} |
@ -1,113 +0,0 @@ |
||||
use markdown_it::MarkdownIt; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
mod spoiler_rule; |
||||
|
||||
static MARKDOWN_PARSER: Lazy<MarkdownIt> = Lazy::new(|| { |
||||
let mut parser = MarkdownIt::new(); |
||||
markdown_it::plugins::cmark::add(&mut parser); |
||||
markdown_it::plugins::extra::add(&mut parser); |
||||
spoiler_rule::add(&mut parser); |
||||
|
||||
parser |
||||
}); |
||||
|
||||
/// Replace special HTML characters in API parameters to prevent XSS attacks.
|
||||
///
|
||||
/// Taken from https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md#output-encoding-for-html-contexts
|
||||
///
|
||||
/// `>` is left in place because it is interpreted as markdown quote.
|
||||
pub fn sanitize_html(text: &str) -> String { |
||||
text |
||||
.replace('&', "&") |
||||
.replace('<', "<") |
||||
.replace('\"', """) |
||||
.replace('\'', "'") |
||||
} |
||||
|
||||
/// Converts text from markdown to HTML, while escaping special characters.
|
||||
pub fn markdown_to_html(text: &str) -> String { |
||||
MARKDOWN_PARSER.parse(text).xrender() |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
#![allow(clippy::unwrap_used)] |
||||
#![allow(clippy::indexing_slicing)] |
||||
|
||||
use super::*; |
||||
use pretty_assertions::assert_eq; |
||||
|
||||
#[test] |
||||
fn test_basic_markdown() { |
||||
let tests: Vec<_> = vec![ |
||||
( |
||||
"headings", |
||||
"# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6", |
||||
"<h1>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n" |
||||
), |
||||
( |
||||
"line breaks", |
||||
"First\rSecond", |
||||
"<p>First\nSecond</p>\n"), |
||||
( |
||||
"emphasis", |
||||
"__bold__ **bold** *italic* ***bold+italic***", |
||||
"<p><strong>bold</strong> <strong>bold</strong> <em>italic</em> <em><strong>bold+italic</strong></em></p>\n" |
||||
), |
||||
( |
||||
"blockquotes", |
||||
"> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n", |
||||
"<blockquote>\n<h4>Hello</h4>\n<ul>\n<li>Hola</li>\n<li>안영</li>\n</ul>\n<blockquote>\n<p>Goodbye</p>\n</blockquote>\n</blockquote>\n" |
||||
), |
||||
( |
||||
"lists (ordered, unordered)", |
||||
"1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen", |
||||
"<ol>\n<li>pen</li>\n<li>apple</li>\n<li>apple pen</li>\n</ol>\n<ul>\n<li>pen</li>\n<li>pineapple</li>\n<li>pineapple pen</li>\n</ul>\n" |
||||
), |
||||
( |
||||
"code and code blocks", |
||||
"this is my amazing `code snippet` and my amazing ```code block```", |
||||
"<p>this is my amazing <code>code snippet</code> and my amazing <code>code block</code></p>\n" |
||||
), |
||||
( |
||||
"links", |
||||
"[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")", |
||||
"<p><a href=\"https://join-lemmy.org/\" title=\"Join Lemmy!\">Lemmy</a></p>\n" |
||||
), |
||||
( |
||||
"images", |
||||
"![My linked image](https://image.com \"image alt text\")", |
||||
"<p><img src=\"https://image.com\" alt=\"My linked image\" title=\"image alt text\" /></p>\n" |
||||
), |
||||
// Ensure any custom plugins are added to 'MARKDOWN_PARSER' implementation.
|
||||
( |
||||
"basic spoiler", |
||||
"::: spoiler click to see more\nhow spicy!\n:::\n", |
||||
"<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n" |
||||
), |
||||
( |
||||
"escape html special chars", |
||||
"<script>alert('xss');</script> hello &\"", |
||||
"<p><script>alert(‘xss’);</script> hello &"</p>\n" |
||||
) |
||||
]; |
||||
|
||||
tests.iter().for_each(|&(msg, input, expected)| { |
||||
let result = markdown_to_html(input); |
||||
|
||||
assert_eq!( |
||||
result, expected, |
||||
"Testing {}, with original input '{}'", |
||||
msg, input |
||||
); |
||||
}); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_sanitize_html() { |
||||
let sanitized = sanitize_html("<script>alert('xss');</script> hello &\"'"); |
||||
let expected = "<script>alert('xss');</script> hello &"'"; |
||||
assert_eq!(expected, sanitized) |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
use markdown_it::{generics::inline::full_link, MarkdownIt, Node, NodeValue, Renderer}; |
||||
|
||||
/// Renders markdown links. Copied directly from markdown-it source, unlike original code it also
|
||||
/// sets `rel=nofollow` attribute.
|
||||
///
|
||||
/// TODO: We can set nofollow only if post was not made by mod/admin, but then we have to construct
|
||||
/// new parser for every invocation which might have performance implications.
|
||||
/// https://github.com/markdown-it-rust/markdown-it/blob/master/src/plugins/cmark/inline/link.rs
|
||||
#[derive(Debug)] |
||||
pub struct Link { |
||||
pub url: String, |
||||
pub title: Option<String>, |
||||
} |
||||
|
||||
impl NodeValue for Link { |
||||
fn render(&self, node: &Node, fmt: &mut dyn Renderer) { |
||||
let mut attrs = node.attrs.clone(); |
||||
attrs.push(("href", self.url.clone())); |
||||
attrs.push(("rel", "nofollow".to_string())); |
||||
|
||||
if let Some(title) = &self.title { |
||||
attrs.push(("title", title.clone())); |
||||
} |
||||
|
||||
fmt.open("a", &attrs); |
||||
fmt.contents(&node.children); |
||||
fmt.close("a"); |
||||
} |
||||
} |
||||
|
||||
pub fn add(md: &mut MarkdownIt) { |
||||
full_link::add::<false>(md, |href, title| { |
||||
Node::new(Link { |
||||
url: href.unwrap_or_default(), |
||||
title, |
||||
}) |
||||
}); |
||||
} |
@ -0,0 +1,246 @@ |
||||
use crate::settings::SETTINGS; |
||||
use markdown_it::{plugins::cmark::inline::image::Image, MarkdownIt}; |
||||
use once_cell::sync::Lazy; |
||||
use url::Url; |
||||
use urlencoding::encode; |
||||
|
||||
mod link_rule; |
||||
mod spoiler_rule; |
||||
|
||||
static MARKDOWN_PARSER: Lazy<MarkdownIt> = Lazy::new(|| { |
||||
let mut parser = MarkdownIt::new(); |
||||
markdown_it::plugins::cmark::add(&mut parser); |
||||
markdown_it::plugins::extra::add(&mut parser); |
||||
spoiler_rule::add(&mut parser); |
||||
link_rule::add(&mut parser); |
||||
|
||||
parser |
||||
}); |
||||
|
||||
/// Replace special HTML characters in API parameters to prevent XSS attacks.
|
||||
///
|
||||
/// Taken from https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md#output-encoding-for-html-contexts
|
||||
///
|
||||
/// `>` is left in place because it is interpreted as markdown quote.
|
||||
pub fn sanitize_html(text: &str) -> String { |
||||
text |
||||
.replace('&', "&") |
||||
.replace('<', "<") |
||||
.replace('\"', """) |
||||
.replace('\'', "'") |
||||
} |
||||
|
||||
pub fn markdown_to_html(text: &str) -> String { |
||||
MARKDOWN_PARSER.parse(text).xrender() |
||||
} |
||||
|
||||
/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`.
|
||||
pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec<Url>) { |
||||
let ast = MARKDOWN_PARSER.parse(&src); |
||||
let mut links_offsets = vec![]; |
||||
|
||||
// Walk the syntax tree to find positions of image links
|
||||
ast.walk(|node, _depth| { |
||||
if let Some(image) = node.cast::<Image>() { |
||||
// srcmap is always present for image
|
||||
// https://github.com/markdown-it-rust/markdown-it/issues/36#issuecomment-1777844387
|
||||
let node_offsets = node.srcmap.expect("srcmap is none").get_byte_offsets(); |
||||
// necessary for custom emojis which look like `![name](url "title")`
|
||||
let start_offset = node_offsets.1 |
||||
- image.url.len() |
||||
- 1 |
||||
- image |
||||
.title |
||||
.as_ref() |
||||
.map(|t| t.len() + 3) |
||||
.unwrap_or_default(); |
||||
let end_offset = node_offsets.1 - 1; |
||||
|
||||
links_offsets.push((start_offset, end_offset)); |
||||
} |
||||
}); |
||||
|
||||
let mut links = vec![]; |
||||
// Go through the collected links in reverse order
|
||||
while let Some((start, end)) = links_offsets.pop() { |
||||
let content = src.get(start..end).unwrap_or_default(); |
||||
// necessary for custom emojis which look like `![name](url "title")`
|
||||
let (url, extra) = if content.contains(' ') { |
||||
let split = content.split_once(' ').expect("split is valid"); |
||||
(split.0, Some(split.1)) |
||||
} else { |
||||
(content, None) |
||||
}; |
||||
match Url::parse(url) { |
||||
Ok(parsed) => { |
||||
links.push(parsed.clone()); |
||||
// If link points to remote domain, replace with proxied link
|
||||
if parsed.domain() != Some(&SETTINGS.hostname) { |
||||
let mut proxied = format!( |
||||
"{}/api/v3/image_proxy?url={}", |
||||
SETTINGS.get_protocol_and_hostname(), |
||||
encode(url), |
||||
); |
||||
// restore custom emoji format
|
||||
if let Some(extra) = extra { |
||||
proxied = format!("{proxied} {extra}"); |
||||
} |
||||
src.replace_range(start..end, &proxied); |
||||
} |
||||
} |
||||
Err(_) => { |
||||
// If its not a valid url, replace with empty text
|
||||
src.replace_range(start..end, ""); |
||||
} |
||||
} |
||||
} |
||||
|
||||
(src, links) |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
#![allow(clippy::unwrap_used)] |
||||
#![allow(clippy::indexing_slicing)] |
||||
|
||||
use super::*; |
||||
use pretty_assertions::assert_eq; |
||||
|
||||
#[test] |
||||
fn test_basic_markdown() { |
||||
let tests: Vec<_> = vec![ |
||||
( |
||||
"headings", |
||||
"# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6", |
||||
"<h1>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n" |
||||
), |
||||
( |
||||
"line breaks", |
||||
"First\rSecond", |
||||
"<p>First\nSecond</p>\n"), |
||||
( |
||||
"emphasis", |
||||
"__bold__ **bold** *italic* ***bold+italic***", |
||||
"<p><strong>bold</strong> <strong>bold</strong> <em>italic</em> <em><strong>bold+italic</strong></em></p>\n" |
||||
), |
||||
( |
||||
"blockquotes", |
||||
"> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n", |
||||
"<blockquote>\n<h4>Hello</h4>\n<ul>\n<li>Hola</li>\n<li>안영</li>\n</ul>\n<blockquote>\n<p>Goodbye</p>\n</blockquote>\n</blockquote>\n" |
||||
), |
||||
( |
||||
"lists (ordered, unordered)", |
||||
"1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen", |
||||
"<ol>\n<li>pen</li>\n<li>apple</li>\n<li>apple pen</li>\n</ol>\n<ul>\n<li>pen</li>\n<li>pineapple</li>\n<li>pineapple pen</li>\n</ul>\n" |
||||
), |
||||
( |
||||
"code and code blocks", |
||||
"this is my amazing `code snippet` and my amazing ```code block```", |
||||
"<p>this is my amazing <code>code snippet</code> and my amazing <code>code block</code></p>\n" |
||||
), |
||||
// Links with added nofollow attribute
|
||||
( |
||||
"links", |
||||
"[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")", |
||||
"<p><a href=\"https://join-lemmy.org/\" rel=\"nofollow\" title=\"Join Lemmy!\">Lemmy</a></p>\n" |
||||
), |
||||
// Remote images with proxy
|
||||
( |
||||
"images", |
||||
"![My linked image](https://example.com/image.png \"image alt text\")", |
||||
"<p><img src=\"https://example.com/image.png\" alt=\"My linked image\" title=\"image alt text\" /></p>\n" |
||||
), |
||||
// Local images without proxy
|
||||
( |
||||
"images", |
||||
"![My linked image](https://lemmy-alpha/image.png \"image alt text\")", |
||||
"<p><img src=\"https://lemmy-alpha/image.png\" alt=\"My linked image\" title=\"image alt text\" /></p>\n" |
||||
), |
||||
// Ensure spoiler plugin is added
|
||||
( |
||||
"basic spoiler", |
||||
"::: spoiler click to see more\nhow spicy!\n:::\n", |
||||
"<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n" |
||||
), |
||||
( |
||||
"escape html special chars", |
||||
"<script>alert('xss');</script> hello &\"", |
||||
"<p><script>alert(‘xss’);</script> hello &"</p>\n" |
||||
) |
||||
]; |
||||
|
||||
tests.iter().for_each(|&(msg, input, expected)| { |
||||
let result = markdown_to_html(input); |
||||
|
||||
assert_eq!( |
||||
result, expected, |
||||
"Testing {}, with original input '{}'", |
||||
msg, input |
||||
); |
||||
}); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_markdown_proxy_images() { |
||||
let tests: Vec<_> = |
||||
vec![ |
||||
( |
||||
"remote image proxied", |
||||
"![link](http://example.com/image.jpg)", |
||||
"![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", |
||||
), |
||||
( |
||||
"local image unproxied", |
||||
"![link](http://lemmy-alpha/image.jpg)", |
||||
"![link](http://lemmy-alpha/image.jpg)", |
||||
), |
||||
( |
||||
"multiple image links", |
||||
"![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", |
||||
"![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", |
||||
), |
||||
( |
||||
"empty link handled", |
||||
"![image]()", |
||||
"![image]()" |
||||
), |
||||
( |
||||
"empty label handled", |
||||
"![](http://example.com/image.jpg)", |
||||
"![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" |
||||
), |
||||
( |
||||
"invalid image link removed", |
||||
"![image](http-not-a-link)", |
||||
"![image]()" |
||||
), |
||||
( |
||||
"label with nested markdown handled", |
||||
"![a *b* c](http://example.com/image.jpg)", |
||||
"![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" |
||||
), |
||||
( |
||||
"custom emoji support", |
||||
r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, |
||||
r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# |
||||
) |
||||
]; |
||||
|
||||
tests.iter().for_each(|&(msg, input, expected)| { |
||||
let result = markdown_rewrite_image_links(input.to_string()); |
||||
|
||||
assert_eq!( |
||||
result.0, expected, |
||||
"Testing {}, with original input '{}'", |
||||
msg, input |
||||
); |
||||
}); |
||||
} |
||||
|
||||
#[test] |
||||
fn test_sanitize_html() { |
||||
let sanitized = sanitize_html("<script>alert('xss');</script> hello &\"'"); |
||||
let expected = "<script>alert('xss');</script> hello &"'"; |
||||
assert_eq!(expected, sanitized) |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
DROP TABLE remote_image; |
||||
|
||||
ALTER TABLE local_image RENAME TO image_upload; |
||||
|
@ -0,0 +1,8 @@ |
||||
CREATE TABLE remote_image ( |
||||
id serial PRIMARY KEY, |
||||
link text NOT NULL UNIQUE, |
||||
published timestamptz DEFAULT now() NOT NULL |
||||
); |
||||
|
||||
ALTER TABLE image_upload RENAME TO local_image; |
||||
|
@ -0,0 +1,3 @@ |
||||
ALTER TABLE post |
||||
DROP COLUMN url_content_type; |
||||
|
@ -0,0 +1,3 @@ |
||||
ALTER TABLE post |
||||
ADD COLUMN url_content_type text; |
||||
|
Loading…
Reference in new issue