收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目 - 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线 - 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径 验证: - cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1 - cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1 - git diff --check - git diff --cached --check - codegraph index . --force && codegraph status . - codegraph sync . && codegraph status .
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use super::onlyoffice_bridge;
|
||||
use crate::app::AppConfig;
|
||||
use crate::app::AppState;
|
||||
use crate::error::WebError;
|
||||
@@ -147,6 +148,9 @@ pub struct OnlyOfficeCallbackQuery {
|
||||
asset_id: Option<String>,
|
||||
#[serde(rename = "userId")]
|
||||
user_id: Option<String>,
|
||||
#[serde(rename = "sessionId")]
|
||||
session_id: Option<String>,
|
||||
token: Option<String>,
|
||||
#[serde(rename = "rootUri")]
|
||||
root_uri: Option<String>,
|
||||
path: Option<String>,
|
||||
@@ -506,10 +510,12 @@ pub async fn page(Query(query): Query<OnlyOfficePageQuery>) -> Result<Response,
|
||||
return null;
|
||||
}}
|
||||
}}
|
||||
function buildCallbackUrl(assetId, userId, localFile) {{
|
||||
function buildCallbackUrl(assetId, userId, localFile, bridgeSessionId, bridgeToken) {{
|
||||
const callback = new URL("/api/onlyoffice/callback", callbackOrigin || location.origin);
|
||||
if (assetId) callback.searchParams.set("assetId", assetId);
|
||||
if (userId) callback.searchParams.set("userId", userId);
|
||||
if (bridgeSessionId) callback.searchParams.set("sessionId", bridgeSessionId);
|
||||
if (bridgeToken) callback.searchParams.set("token", bridgeToken);
|
||||
if (localFile && localFile.rootUri && localFile.path) {{
|
||||
callback.searchParams.set("rootUri", localFile.rootUri);
|
||||
callback.searchParams.set("path", localFile.path);
|
||||
@@ -598,6 +604,11 @@ pub async fn page(Query(query): Query<OnlyOfficePageQuery>) -> Result<Response,
|
||||
if (initial.fileUrl && initial.fileUrl.indexOf("/api/local-folder/files/open") !== -1) return true;
|
||||
return false;
|
||||
}}
|
||||
function editModeLocationHref() {{
|
||||
const next = new URL(location.href);
|
||||
next.searchParams.set("mode", "edit");
|
||||
return next.toString();
|
||||
}}
|
||||
async function resolveAssetUrlAndKey() {{
|
||||
let effectiveFileUrl = initial.fileUrl;
|
||||
let storageId = "";
|
||||
@@ -639,6 +650,18 @@ pub async fn page(Query(query): Query<OnlyOfficePageQuery>) -> Result<Response,
|
||||
const localFile = localFolderOpenParams(fileState.effectiveFileUrl || initial.fileUrl);
|
||||
const displayUserId = String(userId || initial.userId || "mnote-local-user").trim() || "mnote-local-user";
|
||||
const displayUserName = displayUserId === "mnote-local-user" ? "MNote" : displayUserId;
|
||||
const bridgeSessionSalt = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2));
|
||||
const bridgeSessionId = "mnote-oo-" + fileState.docKey + "-" + bridgeSessionSalt;
|
||||
const bridgeToken = (crypto && crypto.randomUUID) ? crypto.randomUUID() : ("mnote-oo-token-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2));
|
||||
const bridgePluginConfigUrl = new URL("/api/onlyoffice/bridge/plugin/config", location.origin);
|
||||
bridgePluginConfigUrl.searchParams.set("sessionId", bridgeSessionId);
|
||||
bridgePluginConfigUrl.searchParams.set("token", bridgeToken);
|
||||
bridgePluginConfigUrl.searchParams.set("apiBase", location.origin);
|
||||
bridgePluginConfigUrl.searchParams.set("documentId", initial.documentId || "");
|
||||
bridgePluginConfigUrl.searchParams.set("assetId", initial.assetId || "");
|
||||
bridgePluginConfigUrl.searchParams.set("fileType", initial.fileType || "");
|
||||
bridgePluginConfigUrl.searchParams.set("docKey", fileState.docKey);
|
||||
bridgePluginConfigUrl.searchParams.set("pageOrigin", location.origin);
|
||||
const config = {{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
@@ -658,11 +681,15 @@ pub async fn page(Query(query): Query<OnlyOfficePageQuery>) -> Result<Response,
|
||||
editorConfig: {{
|
||||
mode: resolvedMode,
|
||||
lang: "zh-CN",
|
||||
callbackUrl: buildCallbackUrl(initial.assetId, userId, localFile),
|
||||
callbackUrl: buildCallbackUrl(initial.assetId, userId, localFile, bridgeSessionId, bridgeToken),
|
||||
user: {{
|
||||
id: displayUserId,
|
||||
name: displayUserName
|
||||
}},
|
||||
plugins: {{
|
||||
autostart: [MNOTE_AGENT_PLUGIN_GUID],
|
||||
pluginsData: [bridgePluginConfigUrl.toString()]
|
||||
}},
|
||||
customization: {{
|
||||
feedback: {{ visible: false }},
|
||||
anonymous: {{ request: false, label: "Guest" }},
|
||||
@@ -673,6 +700,14 @@ pub async fn page(Query(query): Query<OnlyOfficePageQuery>) -> Result<Response,
|
||||
events: {{
|
||||
onDocumentReady: () => {{ window.__MNOTE_ONLYOFFICE_READY__ = true; }},
|
||||
onAppReady: () => {{ window.__MNOTE_ONLYOFFICE_READY__ = true; }},
|
||||
onRequestEditRights: () => {{
|
||||
const editHref = editModeLocationHref();
|
||||
window.__MNOTE_ONLYOFFICE_REQUEST_EDIT_RIGHTS__ = {{
|
||||
requested: true,
|
||||
targetUrl: editHref
|
||||
}};
|
||||
window.location.replace(editHref);
|
||||
}},
|
||||
onError: (event) => showError(JSON.stringify(event))
|
||||
}}
|
||||
}};
|
||||
@@ -690,7 +725,52 @@ pub async fn page(Query(query): Query<OnlyOfficePageQuery>) -> Result<Response,
|
||||
resolvedMode,
|
||||
assetId: initial.assetId,
|
||||
documentId: initial.documentId,
|
||||
docKey: fileState.docKey
|
||||
docKey: fileState.docKey,
|
||||
bridgeSessionId,
|
||||
bridgeToken,
|
||||
bridgePluginConfigUrl: bridgePluginConfigUrl.toString()
|
||||
}};
|
||||
try {{
|
||||
if (window.parent && window.parent !== window) {{
|
||||
window.parent.postMessage({{
|
||||
type: "mnote:onlyoffice-bridge-ready",
|
||||
bridgeSessionId,
|
||||
documentId: initial.documentId,
|
||||
assetId: initial.assetId,
|
||||
fileType: initial.fileType
|
||||
}}, location.origin);
|
||||
}}
|
||||
}} catch (_) {{}}
|
||||
window.__MNOTE_ONLYOFFICE_BRIDGE__ = {{
|
||||
sessionId: bridgeSessionId,
|
||||
run: async function(action, payload, timeoutMs) {{
|
||||
const enqueue = await fetch("/api/onlyoffice/bridge/commands", {{
|
||||
method: "POST",
|
||||
headers: {{ "content-type": "application/json" }},
|
||||
body: JSON.stringify({{
|
||||
sessionId: bridgeSessionId,
|
||||
token: bridgeToken,
|
||||
action,
|
||||
payload: payload || {{}}
|
||||
}})
|
||||
}});
|
||||
const enqueued = await enqueue.json().catch(() => null);
|
||||
if (!enqueue.ok) throw new Error(enqueued && enqueued.message || "ONLYOFFICE bridge command enqueue failed");
|
||||
const commandId = enqueued && enqueued.command && enqueued.command.id;
|
||||
if (!commandId) throw new Error("ONLYOFFICE bridge command id missing");
|
||||
const resultUrl = new URL("/api/onlyoffice/bridge/results", location.origin);
|
||||
resultUrl.searchParams.set("sessionId", bridgeSessionId);
|
||||
resultUrl.searchParams.set("token", bridgeToken);
|
||||
resultUrl.searchParams.set("commandId", commandId);
|
||||
resultUrl.searchParams.set("timeoutMs", String(timeoutMs || 25000));
|
||||
const resultResponse = await fetch(resultUrl.toString());
|
||||
if (resultResponse.status === 204) throw new Error("ONLYOFFICE bridge command timed out");
|
||||
const result = await resultResponse.json().catch(() => null);
|
||||
if (!resultResponse.ok || !result || result.ok === false) {{
|
||||
throw new Error(result && result.error || result && result.message || "ONLYOFFICE bridge command failed");
|
||||
}}
|
||||
return result.result;
|
||||
}}
|
||||
}};
|
||||
const readyDeadline = Date.now() + 120000;
|
||||
const timer = window.setInterval(() => {{
|
||||
@@ -1070,12 +1150,15 @@ fn onlyoffice_callback_success(extra: Value) -> Response {
|
||||
}
|
||||
|
||||
fn onlyoffice_callback_failure(error: WebError) -> Response {
|
||||
Json(json!({
|
||||
(
|
||||
error.status(),
|
||||
Json(json!({
|
||||
"error": 1,
|
||||
"code": error.code(),
|
||||
"message": error.message(),
|
||||
}))
|
||||
.into_response()
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn download_onlyoffice_callback_body(download_url: &str) -> Result<Bytes, WebError> {
|
||||
@@ -1135,6 +1218,53 @@ async fn local_folder_onlyoffice_callback(
|
||||
)
|
||||
})?;
|
||||
let target = resolve_onlyoffice_local_file_path(root_uri, relative_path)?;
|
||||
let session_id = query
|
||||
.session_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"onlyoffice_local_callback_session_required",
|
||||
"OnlyOffice 本地保存缺少 bridge sessionId",
|
||||
)
|
||||
})?;
|
||||
let token = query
|
||||
.token
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"onlyoffice_local_callback_token_required",
|
||||
"OnlyOffice 本地保存缺少 bridge token",
|
||||
)
|
||||
})?;
|
||||
if !onlyoffice_bridge::session_token_matches(session_id, token) {
|
||||
return Err(WebError::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"onlyoffice_local_callback_token_invalid",
|
||||
"OnlyOffice 本地保存 bridge token 无效",
|
||||
));
|
||||
}
|
||||
let session = onlyoffice_bridge::session_info(session_id).ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"onlyoffice_local_callback_session_unregistered",
|
||||
"OnlyOffice 本地保存 bridge session 未注册",
|
||||
)
|
||||
})?;
|
||||
if let Some(session_asset_id) = session.asset_id.as_deref() {
|
||||
if !session_asset_id.trim().is_empty() && session_asset_id.trim() != asset_id {
|
||||
return Err(WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"onlyoffice_local_callback_asset_mismatch",
|
||||
"OnlyOffice 本地保存 session 与资源不匹配",
|
||||
));
|
||||
}
|
||||
}
|
||||
let onlyoffice_internal_url = resolve_onlyoffice_internal_url().await;
|
||||
let prepared = prepare_callback(OnlyOfficeCallbackPreparationInput {
|
||||
asset_id: asset_id.to_string(),
|
||||
@@ -1840,6 +1970,22 @@ mod tests {
|
||||
));
|
||||
assert!(html.contains("if (proxyOrigin && isLocalFolderFileOpen)"));
|
||||
assert!(html.contains("if (proxyOrigin && (isLocal || url.searchParams.has(\"token\")))"));
|
||||
assert!(html.contains(
|
||||
"const MNOTE_AGENT_PLUGIN_GUID = \"asc.{05F87DDF-7B42-4C6F-9F2B-9C77A8D5F4E2}\";"
|
||||
));
|
||||
assert!(html.contains(
|
||||
"const bridgePluginConfigUrl = new URL(\"/api/onlyoffice/bridge/plugin/config\", location.origin);"
|
||||
));
|
||||
assert!(
|
||||
html.contains("bridgePluginConfigUrl.searchParams.set(\"apiBase\", location.origin);")
|
||||
);
|
||||
assert!(html.contains("const bridgeSessionSalt ="));
|
||||
assert!(html.contains(
|
||||
"const bridgeSessionId = \"mnote-oo-\" + fileState.docKey + \"-\" + bridgeSessionSalt;"
|
||||
));
|
||||
assert!(html.contains("/api/onlyoffice/bridge/plugin/config"));
|
||||
assert!(html.contains("pluginsData: [bridgePluginConfigUrl.toString()]"));
|
||||
assert!(html.contains("window.__MNOTE_ONLYOFFICE_BRIDGE__"));
|
||||
}
|
||||
|
||||
fn test_state(legacy_next_base_url: Option<String>) -> AppState {
|
||||
@@ -1897,6 +2043,8 @@ mod tests {
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("asset_1".into()),
|
||||
user_id: None,
|
||||
session_id: None,
|
||||
token: None,
|
||||
root_uri: None,
|
||||
path: None,
|
||||
}),
|
||||
@@ -1917,9 +2065,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_local_callback_writes_status_two_body_to_original_file() {
|
||||
async fn onlyoffice_local_callback_rejects_unauthenticated_local_write() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-onlyoffice-local-callback-{}",
|
||||
"mnote-onlyoffice-local-callback-unauth-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
@@ -1938,6 +2086,74 @@ mod tests {
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
user_id: None,
|
||||
session_id: None,
|
||||
token: None,
|
||||
root_uri: Some(format!("file://{}", root.display())),
|
||||
path: Some("Page/report.docx".into()),
|
||||
}),
|
||||
Json(json!({
|
||||
"status": 2,
|
||||
"key": "doc_key",
|
||||
"url": download_url
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["error"], 1);
|
||||
assert_eq!(
|
||||
payload["code"],
|
||||
"onlyoffice_local_callback_session_required"
|
||||
);
|
||||
assert_eq!(fs::read(&target).expect("read target"), b"old");
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_local_callback_writes_status_two_body_to_original_file() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-onlyoffice-local-callback-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||
let target = root.join("Page").join("report.docx");
|
||||
fs::write(&target, b"old").expect("write old docx");
|
||||
let (download_url, _captured) = spawn_legacy_json_server("new docx bytes").await;
|
||||
let session_id = format!("mnote-oo-local-callback-{}", std::process::id());
|
||||
let token = format!("token-{session_id}");
|
||||
let registered =
|
||||
onlyoffice_bridge::register_session(Json(onlyoffice_bridge::BridgeSessionPayload {
|
||||
session_id: session_id.clone(),
|
||||
token: Some(token.clone()),
|
||||
editor_type: Some("word".into()),
|
||||
document_id: Some("local-md:Page".into()),
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
file_type: Some("docx".into()),
|
||||
doc_key: None,
|
||||
page_origin: None,
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(registered.status(), StatusCode::OK);
|
||||
let response = callback(
|
||||
State(test_state(None)),
|
||||
format!(
|
||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||
session_id,
|
||||
token,
|
||||
root.display(),
|
||||
)
|
||||
.parse::<Uri>()
|
||||
.expect("uri"),
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
user_id: None,
|
||||
session_id: Some(session_id),
|
||||
token: Some(token),
|
||||
root_uri: Some(format!("file://{}", root.display())),
|
||||
path: Some("Page/report.docx".into()),
|
||||
}),
|
||||
@@ -1958,6 +2174,147 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_local_callback_writes_status_six_body_to_original_file() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-onlyoffice-local-callback-status-six-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||
let target = root.join("Page").join("report.docx");
|
||||
fs::write(&target, b"old").expect("write old docx");
|
||||
let (download_url, _captured) = spawn_legacy_json_server("status six bytes").await;
|
||||
let session_id = format!("mnote-oo-local-callback-six-{}", std::process::id());
|
||||
let token = format!("token-{session_id}");
|
||||
let registered =
|
||||
onlyoffice_bridge::register_session(Json(onlyoffice_bridge::BridgeSessionPayload {
|
||||
session_id: session_id.clone(),
|
||||
token: Some(token.clone()),
|
||||
editor_type: Some("word".into()),
|
||||
document_id: Some("local-md:Page".into()),
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
file_type: Some("docx".into()),
|
||||
doc_key: None,
|
||||
page_origin: None,
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(registered.status(), StatusCode::OK);
|
||||
let response = callback(
|
||||
State(test_state(None)),
|
||||
format!(
|
||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||
session_id,
|
||||
token,
|
||||
root.display(),
|
||||
)
|
||||
.parse::<Uri>()
|
||||
.expect("uri"),
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
user_id: None,
|
||||
session_id: Some(session_id),
|
||||
token: Some(token),
|
||||
root_uri: Some(format!("file://{}", root.display())),
|
||||
path: Some("Page/report.docx".into()),
|
||||
}),
|
||||
Json(json!({
|
||||
"status": 6,
|
||||
"key": "doc_key",
|
||||
"url": download_url
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
|
||||
assert_eq!(payload["error"], 0);
|
||||
assert_eq!(fs::read(&target).expect("read target"), b"status six bytes");
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_local_callback_rejects_root_escape_path() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-onlyoffice-local-callback-root-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let outside = std::env::temp_dir().join(format!(
|
||||
"mnote-onlyoffice-local-callback-outside-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
let _ = fs::remove_dir_all(&outside);
|
||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||
fs::create_dir_all(&outside).expect("create outside");
|
||||
fs::write(outside.join("report.docx"), b"outside").expect("write outside");
|
||||
let (download_url, _captured) = spawn_legacy_json_server("should not write").await;
|
||||
let session_id = format!("mnote-oo-local-callback-escape-{}", std::process::id());
|
||||
let token = format!("token-{session_id}");
|
||||
let registered =
|
||||
onlyoffice_bridge::register_session(Json(onlyoffice_bridge::BridgeSessionPayload {
|
||||
session_id: session_id.clone(),
|
||||
token: Some(token.clone()),
|
||||
editor_type: Some("word".into()),
|
||||
document_id: Some("local-md:Page".into()),
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
file_type: Some("docx".into()),
|
||||
doc_key: None,
|
||||
page_origin: None,
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(registered.status(), StatusCode::OK);
|
||||
let response = callback(
|
||||
State(test_state(None)),
|
||||
format!(
|
||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=..%2F{}%2Freport.docx",
|
||||
session_id,
|
||||
token,
|
||||
root.display(),
|
||||
outside.file_name().and_then(|value| value.to_str()).unwrap_or_default(),
|
||||
)
|
||||
.parse::<Uri>()
|
||||
.expect("uri"),
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
user_id: None,
|
||||
session_id: Some(session_id),
|
||||
token: Some(token),
|
||||
root_uri: Some(format!("file://{}", root.display())),
|
||||
path: Some(format!(
|
||||
"../{}/report.docx",
|
||||
outside
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or_default()
|
||||
)),
|
||||
}),
|
||||
Json(json!({
|
||||
"status": 2,
|
||||
"key": "doc_key",
|
||||
"url": download_url
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let status = response.status();
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(payload["error"], 1);
|
||||
assert_eq!(payload["code"], "onlyoffice_local_file_root_escape");
|
||||
assert_eq!(
|
||||
fs::read(outside.join("report.docx")).expect("read outside"),
|
||||
b"outside"
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
let _ = fs::remove_dir_all(&outside);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_local_callback_ignores_non_write_status() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
@@ -1968,17 +2325,36 @@ mod tests {
|
||||
fs::create_dir_all(root.join("Page")).expect("create page");
|
||||
let target = root.join("Page").join("report.docx");
|
||||
fs::write(&target, b"old").expect("write old docx");
|
||||
let session_id = format!("mnote-oo-local-callback-ignore-{}", std::process::id());
|
||||
let token = format!("token-{session_id}");
|
||||
let registered =
|
||||
onlyoffice_bridge::register_session(Json(onlyoffice_bridge::BridgeSessionPayload {
|
||||
session_id: session_id.clone(),
|
||||
token: Some(token.clone()),
|
||||
editor_type: Some("word".into()),
|
||||
document_id: Some("local-md:Page".into()),
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
file_type: Some("docx".into()),
|
||||
doc_key: None,
|
||||
page_origin: None,
|
||||
}))
|
||||
.await;
|
||||
assert_eq!(registered.status(), StatusCode::OK);
|
||||
let response = callback(
|
||||
State(test_state(None)),
|
||||
format!(
|
||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||
root.display()
|
||||
"/api/onlyoffice/callback?assetId=local%3Aasset%3APage%2Freport.docx&sessionId={}&token={}&rootUri=file%3A%2F%2F{}&path=Page%2Freport.docx",
|
||||
session_id,
|
||||
token,
|
||||
root.display(),
|
||||
)
|
||||
.parse::<Uri>()
|
||||
.expect("uri"),
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("local:asset:Page/report.docx".into()),
|
||||
user_id: None,
|
||||
session_id: Some(session_id),
|
||||
token: Some(token),
|
||||
root_uri: Some(format!("file://{}", root.display())),
|
||||
path: Some("Page/report.docx".into()),
|
||||
}),
|
||||
@@ -2006,6 +2382,8 @@ mod tests {
|
||||
Query(OnlyOfficeCallbackQuery {
|
||||
asset_id: Some("asset_1".into()),
|
||||
user_id: Some("user_1".into()),
|
||||
session_id: None,
|
||||
token: None,
|
||||
root_uri: None,
|
||||
path: None,
|
||||
}),
|
||||
@@ -2128,4 +2506,32 @@ mod tests {
|
||||
assert!(html.contains("anonymous: { request: false, label: \"Guest\" }"));
|
||||
assert!(html.contains("features: { featuresTips: false }"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn onlyoffice_page_reinitializes_edit_url_on_request_edit_rights() {
|
||||
let response = page(Query(OnlyOfficePageQuery {
|
||||
file_url: Some(
|
||||
"http://localhost:3000/api/local-folder/files/open?rootUri=file:///tmp&path=Page/report.docx"
|
||||
.into(),
|
||||
),
|
||||
file_name: Some("report.docx".into()),
|
||||
file_type: Some("docx".into()),
|
||||
asset_id: Some("local-file:Page/report.docx".into()),
|
||||
document_id: Some("local-md:Page".into()),
|
||||
user_id: None,
|
||||
mode: Some("view".into()),
|
||||
}))
|
||||
.await
|
||||
.expect("onlyoffice page");
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("html");
|
||||
|
||||
assert!(html.contains("function editModeLocationHref()"));
|
||||
assert!(html.contains("next.searchParams.set(\"mode\", \"edit\");"));
|
||||
assert!(html.contains("onRequestEditRights: () =>"));
|
||||
assert!(html.contains("window.__MNOTE_ONLYOFFICE_REQUEST_EDIT_RIGHTS__"));
|
||||
assert!(html.contains("window.location.replace(editHref);"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user