Commit 42012659 authored by jan.koester's avatar jan.koester
Browse files

export now as zip

parent 6879d13e
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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') {
+28 −1
Original line number Diff line number Diff line
@@ -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 || ''));
            });
        });

@@ -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, ''];
+203 −0
Original line number Diff line number Diff line
@@ -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) {
@@ -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);
}