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

test

parent 6f9f4173
Loading
Loading
Loading
Loading
+110 −40
Changes for editor/src/htmlimport.cpp: 110 added lines, 40 removed lines.
Original line number Diff line number Diff line
@@ -437,6 +437,78 @@ std::string blogi::htmlimport::serializeInnerHtml(libhtmlpp::HtmlElement *el) {
    return out.str();
}

std::string blogi::htmlimport::rewriteCssUrls(
    const std::string &css,
    const std::function<std::string(const std::string&)> &fn)
{
    auto ciMatch = [&](size_t pos, const char *lit) {
        size_t len = std::strlen(lit);
        if (pos + len > css.size()) return false;
        for (size_t k = 0; k < len; ++k) {
            if (std::tolower(static_cast<unsigned char>(css[pos + k])) !=
                std::tolower(static_cast<unsigned char>(lit[k])))
                return false;
        }
        return true;
    };

    std::string out;
    out.reserve(css.size());
    size_t i = 0, n = css.size();
    while (i < n) {
        if (ciMatch(i, "url(")) {
            size_t close = css.find(')', i + 4);
            if (close == std::string::npos) { out += css[i]; ++i; continue; }

            std::string inner = css.substr(i + 4, close - (i + 4));
            size_t start = inner.find_first_not_of(" \t\r\n");
            size_t end = inner.find_last_not_of(" \t\r\n");
            std::string ref = (start == std::string::npos) ? ""
                             : inner.substr(start, end - start + 1);

            char quote = '\0';
            if (ref.size() >= 2 && (ref.front() == '"' || ref.front() == '\'') &&
                ref.back() == ref.front()) {
                quote = ref.front();
                ref = ref.substr(1, ref.size() - 2);
            }

            out += "url(";
            if (quote) out += quote;
            out += fn(ref);
            if (quote) out += quote;
            out += ')';
            i = close + 1;
        } else if (ciMatch(i, "@import")) {
            out += css.substr(i, 7);
            i += 7;
            size_t wsStart = i;
            while (i < n && std::isspace(static_cast<unsigned char>(css[i]))) ++i;
            out += css.substr(wsStart, i - wsStart);

            // Bare-string form, e.g. @import "foo.css"; -- the url(...) form
            // (@import url(foo.css);) is handled by the branch above instead,
            // on a later pass through this same loop.
            if (i < n && (css[i] == '"' || css[i] == '\'')) {
                char q = css[i];
                size_t strEnd = css.find(q, i + 1);
                if (strEnd != std::string::npos) {
                    std::string ref = css.substr(i + 1, strEnd - (i + 1));
                    out += q;
                    out += fn(ref);
                    out += q;
                    i = strEnd + 1;
                    continue;
                }
            }
        } else {
            out += css[i];
            ++i;
        }
    }
    return out;
}

void blogi::htmlimport::htmlElementToWidgetXml(
    libhtmlpp::Element *htmlEl,
    tinyxml2::XMLDocument &doc,
@@ -1050,49 +1122,36 @@ std::string resolveUrl(const std::string &baseUrl, const std::string &ref)
    return urlOrigin(base) + path;
}

// Rewrites every url(...) reference found anywhere in @p css (declaration
// values -- background/background-image/border-image/list-style-image/mask/
// cursor/@font-face src/... all accept it, so this scans the raw text
// instead of enumerating property names) to absolute, using resolveUrl.
// Quoted ("...", '...') and unquoted forms are both handled; whatever quoting
// (or lack of it) the original used is preserved. Malformed/unterminated
// url(...) is left untouched rather than risk corrupting the stylesheet.
std::string rewriteCssUrls(const std::string &css, const std::string &baseUrl)
// Resolves each URL in an <img>/<source> srcset="..." attribute -- a comma-
// separated list of "<url> [descriptor]" candidates (descriptor being a
// width like "480w" or pixel density like "2x") -- against baseUrl, leaving
// each candidate's descriptor untouched. Doesn't handle a URL that itself
// contains a literal comma (the HTML spec's own parsing algorithm for this
// attribute is itself just a best-effort heuristic around that same case).
std::string resolveSrcset(const std::string &baseUrl, const std::string &srcset)
{
    std::string out;
    out.reserve(css.size());
    size_t i = 0, n = css.size();
    while (i < n) {
        if (i + 4 <= n && std::tolower(static_cast<unsigned char>(css[i])) == 'u' &&
            std::tolower(static_cast<unsigned char>(css[i + 1])) == 'r' &&
            std::tolower(static_cast<unsigned char>(css[i + 2])) == 'l' &&
            css[i + 3] == '(') {
            size_t close = css.find(')', i + 4);
            if (close == std::string::npos) { out += css[i]; ++i; continue; }
    size_t start = 0;
    while (start <= srcset.size()) {
        size_t comma = srcset.find(',', start);
        std::string candidate = srcset.substr(start,
            comma == std::string::npos ? std::string::npos : comma - start);

            std::string inner = css.substr(i + 4, close - (i + 4));
            size_t start = inner.find_first_not_of(" \t\r\n");
            size_t end = inner.find_last_not_of(" \t\r\n");
            std::string ref = (start == std::string::npos) ? ""
                             : inner.substr(start, end - start + 1);
        size_t s = candidate.find_first_not_of(" \t\r\n");
        size_t e = candidate.find_last_not_of(" \t\r\n");
        if (s != std::string::npos) {
            candidate = candidate.substr(s, e - s + 1);
            size_t ws = candidate.find_first_of(" \t\r\n");
            std::string url = (ws == std::string::npos) ? candidate : candidate.substr(0, ws);
            std::string descriptor = (ws == std::string::npos) ? "" : candidate.substr(ws);

            char quote = '\0';
            if (ref.size() >= 2 && (ref.front() == '"' || ref.front() == '\'') &&
                ref.back() == ref.front()) {
                quote = ref.front();
                ref = ref.substr(1, ref.size() - 2);
            if (!out.empty()) out += ", ";
            out += resolveUrl(baseUrl, url);
            out += descriptor;
        }

            out += "url(";
            if (quote) out += quote;
            out += resolveUrl(baseUrl, ref);
            if (quote) out += quote;
            out += ')';
            i = close + 1;
        } else {
            out += css[i];
            ++i;
        }
        if (comma == std::string::npos) break;
        start = comma + 1;
    }
    return out;
}
@@ -1130,12 +1189,19 @@ void rewriteRelativeUrls(libhtmlpp::Element *node, const std::string &baseUrl)
                std::string href = hel->getAtributte("href");
                if (!href.empty()) hel->setAttribute("href", resolveUrl(baseUrl, href));
            }
            if (tag == "img" || tag == "source") {
                std::string srcset = hel->getAtributte("srcset");
                if (!srcset.empty()) hel->setAttribute("srcset", resolveSrcset(baseUrl, srcset));
            }

            // Inline style="...: url(...)" (background-image etc.) -- resolved
            // here rather than where htmlElementToWidgetXml later reads this
            // same attribute, so that code stays unaware baseUrl even exists.
            std::string style = hel->getAtributte("style");
            if (!style.empty()) hel->setAttribute("style", rewriteCssUrls(style, baseUrl));
            if (!style.empty()) {
                hel->setAttribute("style", rewriteCssUrls(style,
                    [&](const std::string &ref) { return resolveUrl(baseUrl, ref); }));
            }

            rewriteRelativeUrls(hel->firstChild(), baseUrl);
        }
@@ -1424,7 +1490,10 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
                        // url(...) here is relative to the page itself (this
                        // <style> block is part of the document), unlike an
                        // external stylesheet's own url(...) below.
                        if (!baseUrl.empty()) cssText = rewriteCssUrls(cssText, baseUrl);
                        if (!baseUrl.empty()) {
                            cssText = rewriteCssUrls(cssText,
                                [&](const std::string &ref) { return resolveUrl(baseUrl, ref); });
                        }
                        libhtmlpp::CSSStyleSheet partial;
                        partial.parse(cssText);
                        for (const auto &rule : partial.getRules()) {
@@ -1460,7 +1529,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
                                    // page's -- href is already absolute here
                                    // (isAbsolute above, or rewriteRelativeUrls
                                    // resolved it against the page baseUrl).
                                    cssText = rewriteCssUrls(cssText, href);
                                    cssText = rewriteCssUrls(cssText,
                                        [&](const std::string &ref) { return resolveUrl(href, ref); });
                                    libhtmlpp::CSSStyleSheet partial;
                                    partial.parse(cssText);
                                    for (const auto &rule : partial.getRules()) {
+20 −0
Changes for editor/src/htmlimport.h: 20 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@
#include <vector>
#include <set>
#include <cstdint>
#include <functional>

#include <htmlpp/html.h>
#include <htmlpp/css.h>
@@ -87,6 +88,25 @@ namespace htmlimport {
    std::string serializeElement(const libhtmlpp::Element *el);
    std::string serializeInnerHtml(libhtmlpp::HtmlElement *el);

    /** Finds every url(...) and bare-string @import "..."/'...' (the form
     *  without url()) reference in @p css, in order, and passes each one's
     *  raw reference text (quotes stripped, whitespace trimmed) to @p fn;
     *  @p fn's return value replaces it in the output, with the original
     *  quoting (or lack of it) preserved -- return the input unchanged to
     *  leave that occurrence as-is. Not a CSS tokenizer: it just scans for
     *  these two textual patterns, so e.g. a "url(...)"-shaped substring
     *  inside an unrelated string or comment would also be (mis)matched --
     *  acceptable given where this is used (best-effort import-time
     *  rewriting, never round-tripping already-published content).
     *  Malformed/unterminated url()/@import is left untouched rather than
     *  risk corrupting the stylesheet. Used both to resolve document-
     *  relative references against a fetched page's URL (see baseUrl on
     *  convertHtmlToWidgetXml) and, by webedit_api.cpp's HTML-import
     *  media-download step, to find/rewrite externally hosted CSS
     *  background images the same way <img> sources are re-hosted. */
    std::string rewriteCssUrls(const std::string &css,
                                const std::function<std::string(const std::string&)> &fn);

    void htmlElementToWidgetXml(
        libhtmlpp::Element *htmlEl,
        tinyxml2::XMLDocument &doc,
+45 −4
Changes for editor/src/webedit_api.cpp: 45 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -5103,8 +5103,17 @@ struct UploadedExternalImage {
    std::string mediaExt;
};

static bool isExternalImageUrl(const std::string &s) {
    return s.compare(0, 7, "http://") == 0 || s.compare(0, 8, "https://") == 0;
}

// Recursively collects every distinct absolute http(s) `src` from the
// generated widget XML's Image elements, in document order.
// generated widget XML's Image elements, plus every absolute http(s)
// url(...)/@import reference found in any CustomCss element's text (per-
// widget background-image etc., set by extractCustomCss), in document order.
// The page-level equivalent (doc.globalCss) isn't reachable from here -- it
// lives in DocumentState, not this XML tree -- see its own scan alongside
// this call in handleImportHtml.
static void collectExternalImageSrcs(tinyxml2::XMLElement *el, std::vector<std::string> &urls,
                                      std::set<std::string> &seen) {
    for (auto *child = el->FirstChildElement(); child; child = child->NextSiblingElement()) {
@@ -5112,18 +5121,26 @@ static void collectExternalImageSrcs(tinyxml2::XMLElement *el, std::vector<std::
            const char *src = child->Attribute("src");
            if (src) {
                std::string s = src;
                if ((s.compare(0, 7, "http://") == 0 || s.compare(0, 8, "https://") == 0) &&
                    seen.insert(s).second) {
                if (isExternalImageUrl(s) && seen.insert(s).second) {
                    urls.push_back(s);
                }
            }
        } else if (strcmp(child->Name(), "CustomCss") == 0) {
            if (const char *text = child->GetText()) {
                blogi::htmlimport::rewriteCssUrls(text, [&](const std::string &ref) {
                    if (isExternalImageUrl(ref) && seen.insert(ref).second) urls.push_back(ref);
                    return ref;
                });
            }
        }
        collectExternalImageSrcs(child, urls, seen);
    }
}

// Rewrites every Image element whose (still-external) `src` matches a
// successfully uploaded entry to the new mediadb src/media_id/media_ext.
// successfully uploaded entry to the new mediadb src/media_id/media_ext, and
// every CustomCss element's url(...)/@import reference the same way (see
// collectExternalImageSrcs above for where these were found).
static void rewriteImageSrcs(tinyxml2::XMLElement *el,
                              const std::unordered_map<std::string, UploadedExternalImage> &uploaded) {
    for (auto *child = el->FirstChildElement(); child; child = child->NextSiblingElement()) {
@@ -5139,6 +5156,15 @@ static void rewriteImageSrcs(tinyxml2::XMLElement *el,
                        child->SetAttribute("media_ext", it->second.mediaExt.c_str());
                }
            }
        } else if (strcmp(child->Name(), "CustomCss") == 0) {
            if (const char *text = child->GetText()) {
                std::string rewritten = blogi::htmlimport::rewriteCssUrls(text,
                    [&](const std::string &ref) {
                        auto it = uploaded.find(ref);
                        return it != uploaded.end() ? it->second.url : ref;
                    });
                child->SetText(rewritten.c_str());
            }
        }
        rewriteImageSrcs(child, uploaded);
    }
@@ -5338,6 +5364,14 @@ void webedit::Api::handleImportHtml(libhttppp::HttpRequest &curreq,
        std::vector<std::string> externalUrls;
        std::set<std::string> seenUrls;
        collectExternalImageSrcs(rootEl, externalUrls, seenUrls);
        // Page-level CSS (doc.globalCss, already assigned above) lives outside
        // rootEl's tree, so it needs its own scan for background-image etc.
        if (!doc.globalCss.empty()) {
            blogi::htmlimport::rewriteCssUrls(doc.globalCss, [&](const std::string &ref) {
                if (isExternalImageUrl(ref) && seenUrls.insert(ref).second) externalUrls.push_back(ref);
                return ref;
            });
        }

        std::string mediaAuthid, mediaBlogUrl;
        if (!albumId.empty() && !externalUrls.empty()) {
@@ -5427,6 +5461,13 @@ void webedit::Api::handleImportHtml(libhttppp::HttpRequest &curreq,

            if (!uploaded.empty()) {
                rewriteImageSrcs(rootEl, uploaded);
                if (!doc.globalCss.empty()) {
                    doc.globalCss = blogi::htmlimport::rewriteCssUrls(doc.globalCss,
                        [&](const std::string &ref) {
                            auto it = uploaded.find(ref);
                            return it != uploaded.end() ? it->second.url : ref;
                        });
                }
            }
        }
    }