/*- * Copyright 2021 Vsevolod Stakhov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "css.hxx" #include "contrib/ankerl/unordered_dense.h" #include "css_parser.hxx" #include "libserver/html/html_tag.hxx" #include "libserver/html/html_block.hxx" /* Keep unit tests implementation here (it'll possibly be moved outside one day) */ #define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL #define DOCTEST_CONFIG_IMPLEMENT #include "doctest/doctest.h" namespace rspamd::css { INIT_LOG_MODULE_PUBLIC(css); class css_style_sheet::impl { public: using sel_shared_hash = smart_ptr_hash; using sel_shared_eq = smart_ptr_equal; using selector_ptr = std::unique_ptr; using selectors_hash = ankerl::unordered_dense::map; using universal_selector_t = std::pair; selectors_hash tags_selector; selectors_hash class_selectors; selectors_hash id_selectors; std::optional universal_selector; }; css_style_sheet::css_style_sheet(rspamd_mempool_t *pool) : pool(pool), pimpl(new impl) { } css_style_sheet::~css_style_sheet() { } auto css_style_sheet::add_selector_rule(std::unique_ptr &&selector, css_declarations_block_ptr decls) -> void { impl::selectors_hash *target_hash = nullptr; switch (selector->type) { case css_selector::selector_type::SELECTOR_ALL: if (pimpl->universal_selector) { /* Another universal selector */ msg_debug_css("redefined universal selector, merging rules"); pimpl->universal_selector->second->merge_block(*decls); } else { msg_debug_css("added universal selector"); pimpl->universal_selector = std::make_pair(std::move(selector), decls); } break; case css_selector::selector_type::SELECTOR_CLASS: target_hash = &pimpl->class_selectors; break; case css_selector::selector_type::SELECTOR_ID: target_hash = &pimpl->id_selectors; break; case css_selector::selector_type::SELECTOR_TAG: target_hash = &pimpl->tags_selector; break; } if (target_hash) { auto found_it = target_hash->find(selector); if (found_it == target_hash->end()) { /* Easy case, new element */ target_hash->insert({std::move(selector), decls}); } else { /* The problem with merging is actually in how to handle selectors chains * For example, we have 2 selectors: * 1. class id tag -> meaning that we first match class, then we ensure that * id is also the same and finally we check the tag * 2. tag class id -> it means that we check first tag, then class and then id * So we have somehow equal path in the xpath terms. * I suppose now, that we merely check parent stuff and handle duplicates * merging when finally resolving paths. */ auto sel_str = selector->to_string().value_or("unknown"); msg_debug_css("found duplicate selector: %*s", (int) sel_str.size(), sel_str.data()); found_it->second->merge_block(*decls); } } } auto css_style_sheet::check_tag_block(const rspamd::html::html_tag *tag) -> rspamd::html::html_block * { std::optional id_comp, class_comp; rspamd::html::html_block *res = nullptr; if (!tag) { return nullptr; } /* First, find id in a tag and a class */ for (const auto ¶m: tag->components) { if (param.type == html::html_component_type::RSPAMD_HTML_COMPONENT_ID) { id_comp = param.value; } else if (param.type == html::html_component_type::RSPAMD_HTML_COMPONENT_CLASS) { class_comp = param.value; } } /* ID part */ if (id_comp && !pimpl->id_selectors.empty()) { auto found_id_sel = pimpl->id_selectors.find(css_selector{id_comp.value()}); if (found_id_sel != pimpl->id_selectors.end()) { const auto &decl = *(found_id_sel->second); res = decl.compile_to_block(pool); } } /* Class part */ if (class_comp && !pimpl->class_selectors.empty()) { auto sv_split = [](auto strv, std::string_view delims = " ") -> std::vector { std::vector ret; std::size_t start = 0; while (start < strv.size()) { const auto last = strv.find_first_of(delims, start); if (start != last) { ret.emplace_back(strv.substr(start, last - start)); } if (last == std::string_view::npos) { break; } start = last + 1; } return ret; }; auto elts = sv_split(class_comp.value()); for (const auto &e: elts) { auto found_class_sel = pimpl->class_selectors.find( css_selector{e, css_selector::selector_type::SELECTOR_CLASS}); if (found_class_sel != pimpl->class_selectors.end()) { const auto &decl = *(found_class_sel->second); auto *tmp = decl.compile_to_block(pool); if (res == nullptr) { res = tmp; } else { res->propagate_block(*tmp); } } } } /* Tags part */ if (!pimpl->tags_selector.empty()) { auto found_tag_sel = pimpl->tags_selector.find( css_selector{static_cast(tag->id)}); if (found_tag_sel != pimpl->tags_selector.end()) { const auto &decl = *(found_tag_sel->second); auto *tmp = decl.compile_to_block(pool); if (res == nullptr) { res = tmp; } else { res->propagate_block(*tmp); } } } /* Finally, universal selector */ if (pimpl->universal_selector) { auto *tmp = pimpl->universal_selector->second->compile_to_block(pool); if (res == nullptr) { res = tmp; } else { res->propagate_block(*tmp); } } return res; } auto css_parse_style(rspamd_mempool_t *pool, std::string_view input, std::shared_ptr &&existing) -> css_return_pair { auto parse_res = rspamd::css::parse_css(pool, input, std::forward>(existing)); if (parse_res.has_value()) { return std::make_pair(parse_res.value(), css_parse_error()); } return std::make_pair(nullptr, parse_res.error()); } }// namespace rspamd::css