}
}
}
+ if (prop.is_overflow()) {
+ if (parser_block.is_token()) {
+ /* A single token */
+ const auto &tok = parser_block.get_token_or_empty();
+
+ if (tok.type == css_parser_token::token_type::ident_token) {
+ auto sv = tok.get_string_or_default("");
+
+ /* Distinguish only the clipping values, as they hide
+ * the content of a zero sized block entirely */
+ if (sv == "hidden" || sv == "clip") {
+ return css_value{css_display_value::DISPLAY_HIDDEN};
+ }
+
+ return css_value{css_display_value::DISPLAY_INLINE};
+ }
+ }
+ }
return std::nullopt;
}
auto css_declarations_block::compile_to_block(rspamd_mempool_t *pool) const -> rspamd::html::html_block *
{
auto *block = rspamd_mempool_alloc0_type(pool, rspamd::html::html_block);
- auto opacity = -1;
+ auto opacity = -1.0f;
+ std::optional<css_dimension> height, width, max_height, max_width;
const css_rule *font_rule = nullptr, *background_rule = nullptr;
for (const auto &rule: rules) {
if (fs) {
block->set_font_size(fs.value().dim, fs.value().is_percent);
}
+ break;
}
case css_property_type::PROPERTY_OPACITY: {
opacity = vals.back().to_number().value_or(opacity);
break;
}
case css_property_type::PROPERTY_HEIGHT: {
- auto w = vals.back().to_dimension();
- if (w) {
- block->set_width(w.value().dim, w.value().is_percent);
- }
+ height = vals.back().to_dimension();
break;
}
case css_property_type::PROPERTY_WIDTH: {
- auto h = vals.back().to_dimension();
- if (h) {
- block->set_width(h.value().dim, h.value().is_percent);
+ width = vals.back().to_dimension();
+ break;
+ }
+ case css_property_type::PROPERTY_MAX_HEIGHT: {
+ max_height = vals.back().to_dimension();
+ break;
+ }
+ case css_property_type::PROPERTY_MAX_WIDTH: {
+ max_width = vals.back().to_dimension();
+ break;
+ }
+ case css_property_type::PROPERTY_OVERFLOW: {
+ auto disp = vals.back().to_display();
+ if (disp) {
+ block->set_overflow(disp.value() == css_display_value::DISPLAY_HIDDEN);
}
break;
}
}
}
+ /*
+ * max-height/max-width clamp the corresponding dimension; mixed
+ * percent/absolute values cannot be compared, so prefer the zero
+ * limit in that case as it is the only value that matters for
+ * the visibility detection
+ */
+ auto clamp_dim = [](std::optional<css_dimension> dim,
+ std::optional<css_dimension> max_dim) -> std::optional<css_dimension> {
+ if (dim && max_dim) {
+ if (dim->is_percent == max_dim->is_percent) {
+ return max_dim->dim < dim->dim ? max_dim : dim;
+ }
+
+ return max_dim->dim == 0 ? max_dim : dim;
+ }
+
+ return dim ? dim : max_dim;
+ };
+
+ if (auto h = clamp_dim(height, max_height); h) {
+ block->set_height(h->dim, h->is_percent);
+ }
+ if (auto w = clamp_dim(width, max_width); w) {
+ block->set_width(w->dim, w->is_percent);
+ }
+
+ if (opacity >= 0 && opacity < 0.1) {
+ /*
+ * A (nearly) zero opacity makes the block invisible, and, unlike
+ * display, it cannot be reset by descendants; reuse the display
+ * value as it has the desired inheritance semantics
+ */
+ block->set_display(css_display_value::DISPLAY_HIDDEN);
+ }
+
/* Optional properties */
if (!(block->fg_color_mask) && font_rule) {
auto &vals = font_rule->get_values();
}
}
+ TEST_CASE("hidden blocks are compiled as invisible")
+ {
+ /* Real-world hiding techniques: text is removed from the rendered
+ * view to dilute the visible content (e.g. of a phishing message) */
+ const std::vector<const char *> hidden_styles{
+ "display:block; max-width:0; max-height:0; overflow:hidden",
+ "opacity:0; height:0; line-height:0; overflow:hidden",
+ "overflow:hidden; max-height:0",
+ "height:0px; overflow:clip",
+ "width:0; overflow:hidden",
+ "opacity:0.01",
+ "max-height:0; height:50px; overflow:hidden",
+ };
+ const std::vector<const char *> visible_styles{
+ "max-width:600px; width:100%",
+ /* Without overflow:hidden the content of a zero sized block
+ * is rendered outside of the block */
+ "height:0",
+ "max-height:0",
+ "opacity:0.5",
+ "overflow:hidden; max-height:10px",
+ "overflow:hidden",
+ "display:block",
+ };
+
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "css", 0);
+
+ for (const auto *css_text: hidden_styles) {
+ auto res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, css_text));
+ CHECK(res.get() != nullptr);
+ auto *block = res->compile_to_block(pool);
+ CHECK(block != nullptr);
+ block->compute_visibility();
+ CHECK_MESSAGE(!block->is_visible(), css_text);
+ }
+
+ for (const auto *css_text: visible_styles) {
+ auto res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, css_text));
+ CHECK(res.get() != nullptr);
+ auto *block = res->compile_to_block(pool);
+ CHECK(block != nullptr);
+ block->compute_visibility();
+ CHECK_MESSAGE(block->is_visible(), css_text);
+ }
+
+ rspamd_mempool_delete(pool);
+ }
+
+ TEST_CASE("height and width are applied to the proper fields")
+ {
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "css", 0);
+
+ auto res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, "height:10px;width:20px"));
+ CHECK(res.get() != nullptr);
+ auto *block = res->compile_to_block(pool);
+ CHECK(block != nullptr);
+ CHECK(static_cast<int>(block->height_mask) != 0);
+ CHECK(static_cast<int>(block->width_mask) != 0);
+ CHECK(block->height == 10);
+ CHECK(block->width == 20);
+
+ rspamd_mempool_delete(pool);
+ }
+
+ TEST_CASE("hidden parent cannot be reset by child display")
+ {
+ auto *pool = rspamd_mempool_new(rspamd_mempool_suggest_size(),
+ "css", 0);
+
+ auto parent_res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, "display:none"));
+ auto *parent = parent_res->compile_to_block(pool);
+ parent->compute_visibility();
+ CHECK(!parent->is_visible());
+
+ auto child_res = process_declaration_tokens(pool,
+ get_rules_parser_functor(pool, "display:block"));
+ auto *child = child_res->compile_to_block(pool);
+ child->propagate_block(*parent);
+ child->compute_visibility();
+ CHECK(!child->is_visible());
+
+ rspamd_mempool_delete(pool);
+ }
+
TEST_CASE("duplicate color properties - last wins")
{
/* Test case: duplicate color declarations should use the last value
std::int16_t width;
rspamd::css::css_display_value display;
std::int8_t font_size;
+ bool overflow_hidden;
unsigned fg_color_mask : 2;
unsigned bg_color_mask : 2;
unsigned font_mask : 2;
unsigned display_mask : 2;
unsigned visibility_mask : 2;
+ unsigned overflow_mask : 2;
constexpr static const auto unset = 0;
constexpr static const auto inherited = 1;
display_mask = how;
}
+ auto set_overflow(bool hidden, int how = html_block::set) -> void
+ {
+ overflow_hidden = hidden;
+ overflow_mask = how;
+ }
+
auto set_font_size(float fs, bool is_percent = false, int how = html_block::set) -> void
{
fs = is_percent ? (-fs) : fs;
fg_color, other.fg_color);
bg_color_mask = html_block::simple_prop(bg_color_mask, other.bg_color_mask,
bg_color, other.bg_color);
- display_mask = html_block::simple_prop(display_mask, other.display_mask,
- display, other.display);
+
+ if (other.display_mask && other.display == css::css_display_value::DISPLAY_HIDDEN) {
+ /*
+ * A hidden ancestor hides all descendants: a child cannot reset
+ * display of a display:none parent, so ignore the own display
+ * value in this case
+ */
+ display = css::css_display_value::DISPLAY_HIDDEN;
+ display_mask = html_block::inherited;
+ }
+ else {
+ display_mask = html_block::simple_prop(display_mask, other.display_mask,
+ display, other.display);
+ }
height_mask = html_block::size_prop(height_mask, other.height_mask,
height, other.height, static_cast<std::int16_t>(800));
height_mask = set_value(height_mask, other.height_mask, height, other.height);
width_mask = set_value(width_mask, other.width_mask, width, other.width);
font_mask = set_value(font_mask, other.font_mask, font_size, other.font_size);
+ overflow_mask = set_value(overflow_mask, other.overflow_mask, overflow_hidden, other.overflow_hidden);
}
auto compute_visibility(void) -> void
}
}
+ if (overflow_mask && overflow_hidden) {
+ /* Zero height or width with overflow:hidden clips the content entirely */
+ if ((height_mask && height == 0) || (width_mask && width == 0)) {
+ /*
+ * Convert to hidden display as well: clipping cannot be resurrected
+ * by descendants, so it should be propagated to children as
+ * the display value
+ */
+ set_display(css::css_display_value::DISPLAY_HIDDEN);
+ visibility_mask = html_block::invisible_flag;
+
+ return;
+ }
+ }
+
auto is_similar_colors = [](const rspamd::css::css_color &fg, const rspamd::css::css_color &bg) -> bool {
constexpr const auto min_visible_diff = 0.1f;
auto diff_r = ((float) fg.r - bg.r);
.width = 0,
.display = rspamd::css::css_display_value::DISPLAY_INLINE,
.font_size = 12,
+ .overflow_hidden = false,
.fg_color_mask = html_block::inherited,
.bg_color_mask = html_block::inherited,
.height_mask = html_block::unset,
.width_mask = html_block::unset,
.font_mask = html_block::unset,
.display_mask = html_block::inherited,
- .visibility_mask = html_block::unset};
+ .visibility_mask = html_block::unset,
+ .overflow_mask = html_block::unset};
}
/**
* Produces html block with no defined values allocated from the pool