Loading editor/html/js/api.js +4 −0 Original line number Diff line number Diff line Loading @@ -165,6 +165,10 @@ var EditorApi = (function() { return request('GET', '/api/document/export-xml'); }, exportTemplateZip: function() { return request('GET', '/api/document/export-xml'); }, importXml: function(xml, options) { var payload = { xml: xml }; if (options && typeof options === 'object') { Loading editor/html/js/editor.js +28 −1 Original line number Diff line number Diff line Loading @@ -216,9 +216,15 @@ }); document.getElementById('btn-export-xml').addEventListener('click', function() { EditorApi.exportXml().then(function(resp) { EditorApi.exportTemplateZip().then(function(resp) { if (resp && resp.zip_base64) { var blob = base64ToBlob(resp.zip_base64, 'application/zip'); triggerDownload(blob, resp.file_name || 'template.zip'); } document.getElementById('export-xml-content').value = resp.xml || ''; document.getElementById('export-dialog').showModal(); }).catch(function(err) { alert(I18n.t('I18N_EXPORT_FAILED', 'Export fehlgeschlagen') + ': ' + ((err && err.error) || err || '')); }); }); Loading Loading @@ -474,6 +480,27 @@ return btoa(binary); } function base64ToBlob(base64, contentType) { var binary = atob(base64); var len = binary.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary.charCodeAt(i); } return new Blob([bytes], { type: contentType || 'application/octet-stream' }); } function triggerDownload(blob, fileName) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = fileName || 'download.bin'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function() { URL.revokeObjectURL(url); }, 0); } function chooseIndexByPrompt(title, items, toLabel) { if (!items || items.length === 0) return -1; var lines = [title, '']; Loading editor/src/webedit_api.cpp +203 −0 Original line number Diff line number Diff line Loading @@ -159,6 +159,131 @@ struct ZipEntry { std::vector<std::uint8_t> data; }; struct ZipWriteContext { std::vector<std::uint8_t> bytes; }; int zipWriteOpenCallback(struct archive *, void *) { return ARCHIVE_OK; } la_ssize_t zipWriteDataCallback(struct archive *, void *client_data, const void *buffer, size_t length) { auto *ctx = static_cast<ZipWriteContext *>(client_data); const auto *begin = static_cast<const std::uint8_t *>(buffer); ctx->bytes.insert(ctx->bytes.end(), begin, begin + length); return static_cast<la_ssize_t>(length); } int zipWriteCloseCallback(struct archive *, void *) { return ARCHIVE_OK; } bool writeZipEntries(const std::vector<ZipEntry> &entries, std::vector<std::uint8_t> &zipBytes, std::string &error) { zipBytes.clear(); error.clear(); struct archive *aw = archive_write_new(); if (!aw) { error = "archive_write_new failed"; return false; } archive_write_set_format_zip(aw); archive_write_set_options(aw, "zip:compression=deflate"); ZipWriteContext ctx; if (archive_write_open(aw, &ctx, zipWriteOpenCallback, zipWriteDataCallback, zipWriteCloseCallback) != ARCHIVE_OK) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_open failed"; archive_write_free(aw); return false; } for (const auto &entry : entries) { struct archive_entry *ae = archive_entry_new(); archive_entry_set_pathname(ae, entry.name.c_str()); archive_entry_set_filetype(ae, AE_IFREG); archive_entry_set_perm(ae, 0644); archive_entry_set_size(ae, static_cast<la_int64_t>(entry.data.size())); if (archive_write_header(aw, ae) != ARCHIVE_OK) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_header failed"; archive_entry_free(ae); archive_write_close(aw); archive_write_free(aw); return false; } if (!entry.data.empty()) { la_ssize_t written = archive_write_data(aw, entry.data.data(), entry.data.size()); if (written < 0) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_data failed"; archive_entry_free(ae); archive_write_close(aw); archive_write_free(aw); return false; } } archive_entry_free(ae); } if (archive_write_close(aw) != ARCHIVE_OK) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_close failed"; archive_write_free(aw); return false; } archive_write_free(aw); zipBytes = std::move(ctx.bytes); return true; } bool fetchBinaryUrl(const std::string &url, std::vector<std::uint8_t> &out, std::string &error) { out.clear(); error.clear(); try { libhttppp::HttpUrl httpUrl(url); libhttppp::HttpClient client(httpUrl); client.setTimeout(20); libhttppp::HttpRequest req; req.setRequestType(GETREQUEST); req.setRequestURL(httpUrl.getPath()); req.setHeaderData("host")->push_back(httpUrl.getHost()); std::vector<char> resp = client.Get(req); if (resp.empty()) { error = "empty response"; return false; } out.assign(resp.begin(), resp.end()); return true; } catch (const std::exception &e) { error = e.what(); } catch (...) { error = "request failed"; } return false; } std::string makeSafeFileStem(std::string value) { if (value.empty()) return "template"; for (char &ch : value) { unsigned char c = static_cast<unsigned char>(ch); if (!(std::isalnum(c) || c == '-' || c == '_')) ch = '_'; } return value; } bool readZipEntries(const std::vector<std::uint8_t> &zipBytes, std::vector<ZipEntry> &entries, std::string &error) { Loading Loading @@ -1250,8 +1375,86 @@ void webedit::Api::handleExportXml(libhttppp::HttpRequest &curreq, std::string xml = exportTreeXml(doc); // Build ZIP package: template.xml + referenced /media/getimage/<uuid>.<ext> files. std::unordered_map<std::string, std::string> uuidToExt; std::unordered_map<std::string, std::string> uuidToUrl; std::unordered_map<std::string, std::unordered_set<std::string>> uuidToRefs; static const std::regex mediaRefRegex( R"((https?://[^\"'\s<>()]+)?(/media/getimage/([0-9a-fA-F-]{36})\.([A-Za-z0-9]+)))" ); for (std::sregex_iterator it(xml.begin(), xml.end(), mediaRefRegex), end; it != end; ++it) { std::string uuid = (*it)[3].str(); std::string ext = toLowerCopy((*it)[4].str()); if (!isUuidLike(uuid)) continue; if (uuidToExt.find(uuid) == uuidToExt.end()) uuidToExt[uuid] = ext; if (uuidToUrl.find(uuid) == uuidToUrl.end()) uuidToUrl[uuid] = (*it)[0].str(); uuidToRefs[uuid].insert((*it)[0].str()); uuidToRefs[uuid].insert((*it)[2].str()); } std::string blogUrl; { std::lock_guard<std::mutex> clk(_connSessionMtx); if (!_connSessions.empty()) blogUrl = _connSessions.begin()->second.blogUrl; } std::string packageXml = xml; std::vector<ZipEntry> outEntries; for (const auto &it : uuidToExt) { const std::string &uuid = it.first; const std::string &ext = it.second; auto foundUrl = uuidToUrl.find(uuid); if (foundUrl == uuidToUrl.end()) continue; std::string resolvedUrl = foundUrl->second; if (!resolvedUrl.empty() && resolvedUrl[0] == '/') { if (blogUrl.empty()) continue; resolvedUrl = blogUrl + resolvedUrl; } std::vector<std::uint8_t> imageBytes; std::string fetchError; if (!fetchBinaryUrl(resolvedUrl, imageBytes, fetchError) || imageBytes.empty()) continue; std::string imageFileName = uuid + "." + ext; outEntries.push_back({imageFileName, std::move(imageBytes)}); auto refsIt = uuidToRefs.find(uuid); if (refsIt != uuidToRefs.end()) { for (const auto &ref : refsIt->second) { replaceAllInPlace(packageXml, ref, imageFileName); } } } outEntries.insert(outEntries.begin(), ZipEntry{"template.xml", std::vector<std::uint8_t>(packageXml.begin(), packageXml.end())}); std::vector<std::uint8_t> zipData; std::string zipError; if (!writeZipEntries(outEntries, zipData, zipError) || zipData.empty()) { sendJsonError(curreq, 500, "ZIP export failed: " + zipError); return; } std::string fileStem = makeSafeFileStem(doc.currentDocName); std::string zipFileName = fileStem + ".zip"; json_object *resp = json_object_new_object(); json_object_object_add(resp, "xml", json_object_new_string(xml.c_str())); std::string zipB64 = encodeBase64(zipData); json_object_object_add(resp, "zip_base64", json_object_new_string(zipB64.c_str())); json_object_object_add(resp, "file_name", json_object_new_string(zipFileName.c_str())); json_object_object_add(resp, "media_count", json_object_new_int(static_cast<int>(outEntries.size() - 1))); sendJson(curreq, resp); json_object_put(resp); } Loading Loading
editor/html/js/api.js +4 −0 Original line number Diff line number Diff line Loading @@ -165,6 +165,10 @@ var EditorApi = (function() { return request('GET', '/api/document/export-xml'); }, exportTemplateZip: function() { return request('GET', '/api/document/export-xml'); }, importXml: function(xml, options) { var payload = { xml: xml }; if (options && typeof options === 'object') { Loading
editor/html/js/editor.js +28 −1 Original line number Diff line number Diff line Loading @@ -216,9 +216,15 @@ }); document.getElementById('btn-export-xml').addEventListener('click', function() { EditorApi.exportXml().then(function(resp) { EditorApi.exportTemplateZip().then(function(resp) { if (resp && resp.zip_base64) { var blob = base64ToBlob(resp.zip_base64, 'application/zip'); triggerDownload(blob, resp.file_name || 'template.zip'); } document.getElementById('export-xml-content').value = resp.xml || ''; document.getElementById('export-dialog').showModal(); }).catch(function(err) { alert(I18n.t('I18N_EXPORT_FAILED', 'Export fehlgeschlagen') + ': ' + ((err && err.error) || err || '')); }); }); Loading Loading @@ -474,6 +480,27 @@ return btoa(binary); } function base64ToBlob(base64, contentType) { var binary = atob(base64); var len = binary.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary.charCodeAt(i); } return new Blob([bytes], { type: contentType || 'application/octet-stream' }); } function triggerDownload(blob, fileName) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = fileName || 'download.bin'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function() { URL.revokeObjectURL(url); }, 0); } function chooseIndexByPrompt(title, items, toLabel) { if (!items || items.length === 0) return -1; var lines = [title, '']; Loading
editor/src/webedit_api.cpp +203 −0 Original line number Diff line number Diff line Loading @@ -159,6 +159,131 @@ struct ZipEntry { std::vector<std::uint8_t> data; }; struct ZipWriteContext { std::vector<std::uint8_t> bytes; }; int zipWriteOpenCallback(struct archive *, void *) { return ARCHIVE_OK; } la_ssize_t zipWriteDataCallback(struct archive *, void *client_data, const void *buffer, size_t length) { auto *ctx = static_cast<ZipWriteContext *>(client_data); const auto *begin = static_cast<const std::uint8_t *>(buffer); ctx->bytes.insert(ctx->bytes.end(), begin, begin + length); return static_cast<la_ssize_t>(length); } int zipWriteCloseCallback(struct archive *, void *) { return ARCHIVE_OK; } bool writeZipEntries(const std::vector<ZipEntry> &entries, std::vector<std::uint8_t> &zipBytes, std::string &error) { zipBytes.clear(); error.clear(); struct archive *aw = archive_write_new(); if (!aw) { error = "archive_write_new failed"; return false; } archive_write_set_format_zip(aw); archive_write_set_options(aw, "zip:compression=deflate"); ZipWriteContext ctx; if (archive_write_open(aw, &ctx, zipWriteOpenCallback, zipWriteDataCallback, zipWriteCloseCallback) != ARCHIVE_OK) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_open failed"; archive_write_free(aw); return false; } for (const auto &entry : entries) { struct archive_entry *ae = archive_entry_new(); archive_entry_set_pathname(ae, entry.name.c_str()); archive_entry_set_filetype(ae, AE_IFREG); archive_entry_set_perm(ae, 0644); archive_entry_set_size(ae, static_cast<la_int64_t>(entry.data.size())); if (archive_write_header(aw, ae) != ARCHIVE_OK) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_header failed"; archive_entry_free(ae); archive_write_close(aw); archive_write_free(aw); return false; } if (!entry.data.empty()) { la_ssize_t written = archive_write_data(aw, entry.data.data(), entry.data.size()); if (written < 0) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_data failed"; archive_entry_free(ae); archive_write_close(aw); archive_write_free(aw); return false; } } archive_entry_free(ae); } if (archive_write_close(aw) != ARCHIVE_OK) { error = archive_error_string(aw) ? archive_error_string(aw) : "archive_write_close failed"; archive_write_free(aw); return false; } archive_write_free(aw); zipBytes = std::move(ctx.bytes); return true; } bool fetchBinaryUrl(const std::string &url, std::vector<std::uint8_t> &out, std::string &error) { out.clear(); error.clear(); try { libhttppp::HttpUrl httpUrl(url); libhttppp::HttpClient client(httpUrl); client.setTimeout(20); libhttppp::HttpRequest req; req.setRequestType(GETREQUEST); req.setRequestURL(httpUrl.getPath()); req.setHeaderData("host")->push_back(httpUrl.getHost()); std::vector<char> resp = client.Get(req); if (resp.empty()) { error = "empty response"; return false; } out.assign(resp.begin(), resp.end()); return true; } catch (const std::exception &e) { error = e.what(); } catch (...) { error = "request failed"; } return false; } std::string makeSafeFileStem(std::string value) { if (value.empty()) return "template"; for (char &ch : value) { unsigned char c = static_cast<unsigned char>(ch); if (!(std::isalnum(c) || c == '-' || c == '_')) ch = '_'; } return value; } bool readZipEntries(const std::vector<std::uint8_t> &zipBytes, std::vector<ZipEntry> &entries, std::string &error) { Loading Loading @@ -1250,8 +1375,86 @@ void webedit::Api::handleExportXml(libhttppp::HttpRequest &curreq, std::string xml = exportTreeXml(doc); // Build ZIP package: template.xml + referenced /media/getimage/<uuid>.<ext> files. std::unordered_map<std::string, std::string> uuidToExt; std::unordered_map<std::string, std::string> uuidToUrl; std::unordered_map<std::string, std::unordered_set<std::string>> uuidToRefs; static const std::regex mediaRefRegex( R"((https?://[^\"'\s<>()]+)?(/media/getimage/([0-9a-fA-F-]{36})\.([A-Za-z0-9]+)))" ); for (std::sregex_iterator it(xml.begin(), xml.end(), mediaRefRegex), end; it != end; ++it) { std::string uuid = (*it)[3].str(); std::string ext = toLowerCopy((*it)[4].str()); if (!isUuidLike(uuid)) continue; if (uuidToExt.find(uuid) == uuidToExt.end()) uuidToExt[uuid] = ext; if (uuidToUrl.find(uuid) == uuidToUrl.end()) uuidToUrl[uuid] = (*it)[0].str(); uuidToRefs[uuid].insert((*it)[0].str()); uuidToRefs[uuid].insert((*it)[2].str()); } std::string blogUrl; { std::lock_guard<std::mutex> clk(_connSessionMtx); if (!_connSessions.empty()) blogUrl = _connSessions.begin()->second.blogUrl; } std::string packageXml = xml; std::vector<ZipEntry> outEntries; for (const auto &it : uuidToExt) { const std::string &uuid = it.first; const std::string &ext = it.second; auto foundUrl = uuidToUrl.find(uuid); if (foundUrl == uuidToUrl.end()) continue; std::string resolvedUrl = foundUrl->second; if (!resolvedUrl.empty() && resolvedUrl[0] == '/') { if (blogUrl.empty()) continue; resolvedUrl = blogUrl + resolvedUrl; } std::vector<std::uint8_t> imageBytes; std::string fetchError; if (!fetchBinaryUrl(resolvedUrl, imageBytes, fetchError) || imageBytes.empty()) continue; std::string imageFileName = uuid + "." + ext; outEntries.push_back({imageFileName, std::move(imageBytes)}); auto refsIt = uuidToRefs.find(uuid); if (refsIt != uuidToRefs.end()) { for (const auto &ref : refsIt->second) { replaceAllInPlace(packageXml, ref, imageFileName); } } } outEntries.insert(outEntries.begin(), ZipEntry{"template.xml", std::vector<std::uint8_t>(packageXml.begin(), packageXml.end())}); std::vector<std::uint8_t> zipData; std::string zipError; if (!writeZipEntries(outEntries, zipData, zipError) || zipData.empty()) { sendJsonError(curreq, 500, "ZIP export failed: " + zipError); return; } std::string fileStem = makeSafeFileStem(doc.currentDocName); std::string zipFileName = fileStem + ".zip"; json_object *resp = json_object_new_object(); json_object_object_add(resp, "xml", json_object_new_string(xml.c_str())); std::string zipB64 = encodeBase64(zipData); json_object_object_add(resp, "zip_base64", json_object_new_string(zipB64.c_str())); json_object_object_add(resp, "file_name", json_object_new_string(zipFileName.c_str())); json_object_object_add(resp, "media_count", json_object_new_int(static_cast<int>(outEntries.size() - 1))); sendJson(curreq, resp); json_object_put(resp); } Loading