new functions and fixes
This commit is contained in:
+123
-6
@@ -130,7 +130,7 @@ pub(super) fn content_references_file(content: &str, filename: &str, url: &str)
|
||||
if content.contains(url) {
|
||||
return true;
|
||||
}
|
||||
for marker in ["[file=", "[image=", "[img="] {
|
||||
for marker in ["[file=", "[image=", "[img=", "[video="] {
|
||||
let mut remaining = content;
|
||||
while let Some(index) = remaining.find(marker) {
|
||||
let after = &remaining[index + marker.len()..];
|
||||
@@ -513,17 +513,38 @@ async fn require_upload_permission(
|
||||
.ok_or_else(|| ApiError::forbidden("File uploads are disabled for guests."))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct FileDownloadQuery {
|
||||
#[serde(default)]
|
||||
download: Option<String>,
|
||||
}
|
||||
|
||||
impl FileDownloadQuery {
|
||||
fn force_download(&self) -> bool {
|
||||
self.download.as_deref().is_some_and(|value| {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "download"
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_file(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path((token, filename)): Path<(String, String)>,
|
||||
Query(query): Query<FileDownloadQuery>,
|
||||
) -> Result<Response, ApiError> {
|
||||
serve_token_file(&state, &token, &filename).await
|
||||
serve_token_file(&state, &headers, &token, &filename, query.force_download()).await
|
||||
}
|
||||
|
||||
async fn serve_token_file(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
token: &str,
|
||||
filename: &str,
|
||||
force_download: bool,
|
||||
) -> Result<Response, ApiError> {
|
||||
let safe = sanitize_filename(filename);
|
||||
if safe != filename {
|
||||
@@ -543,15 +564,44 @@ async fn serve_token_file(
|
||||
.get_local_with_legacy(&key, &legacy_key)
|
||||
.await
|
||||
.map_err(|_| ApiError::not_found_file())?;
|
||||
let total_len = bytes.len();
|
||||
let guessed_mime = mime_guess::from_path(&safe).first_or_octet_stream();
|
||||
let inline_image = is_safe_inline_image_mime(guessed_mime.essence_str());
|
||||
let served_mime = if inline_image {
|
||||
let safe_inline = is_safe_inline_image_mime(guessed_mime.essence_str())
|
||||
|| is_safe_inline_video_mime(guessed_mime.essence_str());
|
||||
let served_mime = if safe_inline {
|
||||
guessed_mime.as_ref()
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
};
|
||||
let disposition = if inline_image { "inline" } else { "attachment" };
|
||||
let mut response = bytes.into_response();
|
||||
let disposition = if safe_inline && !force_download {
|
||||
"inline"
|
||||
} else {
|
||||
"attachment"
|
||||
};
|
||||
|
||||
let requested_range = headers
|
||||
.get(header::RANGE)
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let range = match requested_range {
|
||||
Some(value) => match parse_byte_range(value, total_len) {
|
||||
Ok(range) => range,
|
||||
Err(()) => return Ok(range_not_satisfiable(total_len)),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let (body, status, content_range) = if let Some((start, end)) = range {
|
||||
(
|
||||
bytes.slice(start..end),
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
Some(format!("bytes {start}-{}/{}", end - 1, total_len)),
|
||||
)
|
||||
} else {
|
||||
(bytes, StatusCode::OK, None)
|
||||
};
|
||||
let body_len = body.len();
|
||||
let mut response = body.into_response();
|
||||
*response.status_mut() = status;
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(served_mime)
|
||||
@@ -562,6 +612,20 @@ async fn serve_token_file(
|
||||
HeaderValue::from_str(&format!("{disposition}; filename=\"{safe}\""))
|
||||
.expect("sanitized attachment filename"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::ACCEPT_RANGES,
|
||||
HeaderValue::from_static("bytes"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&body_len.to_string()).expect("valid content length"),
|
||||
);
|
||||
if let Some(content_range) = content_range {
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&content_range).expect("valid content range"),
|
||||
);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static("content-security-policy"),
|
||||
HeaderValue::from_static("default-src 'none'; sandbox"),
|
||||
@@ -584,6 +648,52 @@ async fn serve_token_file(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn range_not_satisfiable(total_len: usize) -> Response {
|
||||
let mut response = StatusCode::RANGE_NOT_SATISFIABLE.into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&format!("bytes */{total_len}")).expect("valid content range"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::ACCEPT_RANGES,
|
||||
HeaderValue::from_static("bytes"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
fn parse_byte_range(value: &str, total_len: usize) -> Result<Option<(usize, usize)>, ()> {
|
||||
if total_len == 0 {
|
||||
return Err(());
|
||||
}
|
||||
let range = value.trim().strip_prefix("bytes=").ok_or(())?;
|
||||
if range.contains(',') {
|
||||
return Err(());
|
||||
}
|
||||
let (start, end) = range.split_once('-').ok_or(())?;
|
||||
if start.is_empty() {
|
||||
let suffix_len = end.parse::<usize>().map_err(|_| ())?;
|
||||
if suffix_len == 0 {
|
||||
return Err(());
|
||||
}
|
||||
let start = total_len.saturating_sub(suffix_len);
|
||||
return Ok(Some((start, total_len)));
|
||||
}
|
||||
|
||||
let start = start.parse::<usize>().map_err(|_| ())?;
|
||||
if start >= total_len {
|
||||
return Err(());
|
||||
}
|
||||
let end_inclusive = if end.is_empty() {
|
||||
total_len - 1
|
||||
} else {
|
||||
end.parse::<usize>().map_err(|_| ())?.min(total_len - 1)
|
||||
};
|
||||
if end_inclusive < start {
|
||||
return Err(());
|
||||
}
|
||||
Ok(Some((start, end_inclusive + 1)))
|
||||
}
|
||||
|
||||
fn is_safe_inline_image_mime(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
@@ -597,6 +707,13 @@ fn is_safe_inline_image_mime(value: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn is_safe_inline_video_mime(value: &str) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
"video/mp4" | "video/webm" | "video/ogg" | "video/quicktime" | "video/x-m4v"
|
||||
)
|
||||
}
|
||||
|
||||
fn sanitize_filename(value: &str) -> String {
|
||||
let name = std::path::Path::new(value)
|
||||
.file_name()
|
||||
|
||||
@@ -432,6 +432,7 @@ pub struct NoteInfo {
|
||||
editor_line_numbers: bool,
|
||||
preview_line_numbers: bool,
|
||||
line_links: bool,
|
||||
toolbar_collapsed: bool,
|
||||
font_family: String,
|
||||
font_size: i64,
|
||||
personal_editor_settings: bool,
|
||||
@@ -461,6 +462,8 @@ pub struct EditorSettingsRequest {
|
||||
#[serde(default)]
|
||||
line_links: Option<bool>,
|
||||
#[serde(default)]
|
||||
toolbar_collapsed: Option<bool>,
|
||||
#[serde(default)]
|
||||
font_family: Option<String>,
|
||||
#[serde(default)]
|
||||
font_size: Option<i64>,
|
||||
@@ -502,6 +505,7 @@ async fn save_editor_settings(
|
||||
|| payload.editor_line_numbers.is_some()
|
||||
|| payload.preview_line_numbers.is_some()
|
||||
|| payload.line_links.is_some()
|
||||
|| payload.toolbar_collapsed.is_some()
|
||||
|| payload.font_family.is_some()
|
||||
|| payload.font_size.is_some();
|
||||
let wants_global_update = payload.authorship_mode.is_some() || payload.colors_enabled.is_some();
|
||||
@@ -553,6 +557,9 @@ async fn save_editor_settings(
|
||||
if let Some(value) = payload.line_links {
|
||||
preferences.line_links = value;
|
||||
}
|
||||
if let Some(value) = payload.toolbar_collapsed {
|
||||
preferences.toolbar_collapsed = value;
|
||||
}
|
||||
if let Some(value) = payload.font_family {
|
||||
preferences.font_family = match value.as_str() {
|
||||
"mono" | "system" | "serif" | "arial" | "georgia" => value,
|
||||
@@ -1084,6 +1091,7 @@ pub async fn note_info(
|
||||
editor_line_numbers: editor_preferences.editor_line_numbers,
|
||||
preview_line_numbers: editor_preferences.preview_line_numbers,
|
||||
line_links: editor_preferences.line_links,
|
||||
toolbar_collapsed: editor_preferences.toolbar_collapsed,
|
||||
font_family: editor_preferences.font_family,
|
||||
font_size: editor_preferences.font_size,
|
||||
personal_editor_settings,
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct PadInfo {
|
||||
editor_line_numbers: bool,
|
||||
preview_line_numbers: bool,
|
||||
line_links: bool,
|
||||
toolbar_collapsed: bool,
|
||||
font_family: String,
|
||||
font_size: i64,
|
||||
personal_editor_settings: bool,
|
||||
@@ -172,6 +173,7 @@ pub async fn pad_info(
|
||||
editor_line_numbers: editor_preferences.editor_line_numbers,
|
||||
preview_line_numbers: editor_preferences.preview_line_numbers,
|
||||
line_links: editor_preferences.line_links,
|
||||
toolbar_collapsed: editor_preferences.toolbar_collapsed,
|
||||
font_family: editor_preferences.font_family,
|
||||
font_size: editor_preferences.font_size,
|
||||
personal_editor_settings,
|
||||
|
||||
Reference in New Issue
Block a user