fix: SVGAnimatedString, :has() selectors, and cookie persistence on serve
This commit is contained in:
3 files changed
+107
-7
No files matched your search
@@ -366,11 +366,33 @@ async fn cdp_processor(
|
||||
let mut deferred: std::collections::VecDeque<ServerMessage> =
|
||||
std::collections::VecDeque::new();
|
||||
|
||||
// Subscribe to Ctrl-C once. The future is single-shot, so we break out of
|
||||
// the outer loop when it fires and never poll it again. Without this the
|
||||
// accept loop just exits and any cookies set during the session are lost
|
||||
// before `BrowserContext::save_cookies()` runs.
|
||||
let mut shutdown = Box::pin(tokio::signal::ctrl_c());
|
||||
// Subscribe to graceful-shutdown signals once. The future is single-shot,
|
||||
// so we break out of the outer loop when it fires and never poll it again.
|
||||
// Without this the accept loop just exits and any cookies set during the
|
||||
// session are lost before `BrowserContext::save_cookies()` runs. We watch
|
||||
// SIGTERM as well as Ctrl-C (SIGINT) so `docker stop` / `kill`, which send
|
||||
// SIGTERM, also flush cookies to the storage dir (issue #333).
|
||||
let mut shutdown = Box::pin(async {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
match signal(SignalKind::terminate()) {
|
||||
Ok(mut term) => {
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {}
|
||||
_ = term.recv() => {}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
// Drain any deferred messages from the previous interception window
|
||||
|
||||
@@ -167,6 +167,15 @@ impl<'i> parser::Parser<'i> for ObscuraSelectorParser {
|
||||
type Impl = ObscuraSelector;
|
||||
type Error = SelectorParseErrorKind<'i>;
|
||||
|
||||
// Allow `:has()`. The selectors crate gates relative-selector parsing on
|
||||
// this (default false), so without it `a:has(p)` failed to parse and the
|
||||
// error was swallowed into an empty match set. Matching already passes a
|
||||
// SelectorCaches (which holds the relative-selector cache), so enabling
|
||||
// parsing is sufficient.
|
||||
fn parse_has(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn parse_non_ts_pseudo_class(
|
||||
&self,
|
||||
_location: cssparser::SourceLocation,
|
||||
@@ -219,7 +228,18 @@ impl<'a> Element for DomElement<'a> {
|
||||
type Impl = ObscuraSelector;
|
||||
|
||||
fn opaque(&self) -> OpaqueElement {
|
||||
OpaqueElement::new(self)
|
||||
// Must be stable per node. DomElement is Copy and gets a fresh stack
|
||||
// address on every traversal step, so OpaqueElement::new(self) returns a
|
||||
// different id for the same node each call. That breaks `:has` anchor
|
||||
// matching, which compares the anchor's opaque against the element
|
||||
// reached by walking up the tree. Key off the node's stable slot in the
|
||||
// tree's Vec instead (OpaqueElement only compares the address, never
|
||||
// dereferences it, and the Vec is not mutated during a query).
|
||||
let inner = self.tree.borrow_inner();
|
||||
match inner.nodes.get(self.node_id.index()) {
|
||||
Some(slot) => OpaqueElement::new(slot),
|
||||
None => OpaqueElement::new(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn parent_element(&self) -> Option<Self> {
|
||||
@@ -666,6 +686,21 @@ mod tests {
|
||||
assert_eq!(results.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_parses() {
|
||||
// Isolate parse from match: :has must parse, not error.
|
||||
assert!(super::parse_selector("a:has(p.bt)").is_ok(), ":has failed to parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_selector_has() {
|
||||
let tree = parse_html(r#"<a><p class="bt">x</p></a>"#);
|
||||
let all = tree.query_selector_all("a:has(p.bt)").unwrap();
|
||||
assert_eq!(all.len(), 1, "a:has(p.bt) should match the <a>");
|
||||
let none = tree.query_selector_all("a:has(span.bt)").unwrap();
|
||||
assert_eq!(none.len(), 0, "a:has(span.bt) should match nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_selector_descendant() {
|
||||
let tree =
|
||||
|
||||
Vendored
+44
-1
@@ -959,7 +959,15 @@ class Element extends Node {
|
||||
}
|
||||
get id() { return this.getAttribute("id") || ""; }
|
||||
set id(v) { this.setAttribute("id", v); }
|
||||
get className() { return this.getAttribute("class") || ""; }
|
||||
get className() {
|
||||
// SVG elements reflect class as an SVGAnimatedString (.baseVal/.animVal),
|
||||
// not a plain string. Anti-fraud sensors read el.className.animVal.
|
||||
if (this.namespaceURI === "http://www.w3.org/2000/svg") {
|
||||
if (!this._svgClassName) this._svgClassName = new SVGAnimatedString(this, "class");
|
||||
return this._svgClassName;
|
||||
}
|
||||
return this.getAttribute("class") || "";
|
||||
}
|
||||
set className(v) { this.setAttribute("class", v); }
|
||||
get namespaceURI() {
|
||||
// createElementNS records the requested namespace on _ns; an empty string
|
||||
@@ -1581,6 +1589,16 @@ class Element extends Node {
|
||||
// members use. Other elements reflect the raw attribute.
|
||||
get href() {
|
||||
const ln = this.localName;
|
||||
// SVG href-bearing elements reflect href as an SVGAnimatedString (with the
|
||||
// legacy xlink:href as a fallback), not a resolved URL string. Checked
|
||||
// before the HTML <a> path because an SVG <a> also has localName 'a'.
|
||||
if (this.namespaceURI === "http://www.w3.org/2000/svg" &&
|
||||
(ln === 'a' || ln === 'image' || ln === 'use' || ln === 'script' ||
|
||||
ln === 'pattern' || ln === 'filter' || ln === 'textPath' || ln === 'mpath' ||
|
||||
ln === 'linearGradient' || ln === 'radialGradient' || ln === 'feImage' || ln === 'tref')) {
|
||||
if (!this._svgHref) this._svgHref = new SVGAnimatedString(this, "href", "xlink:href");
|
||||
return this._svgHref;
|
||||
}
|
||||
if (ln === 'a' || ln === 'area') {
|
||||
const raw = this.getAttribute('href');
|
||||
if (raw === null) return '';
|
||||
@@ -4997,6 +5015,31 @@ globalThis.HTMLLegendElement = Element;
|
||||
globalThis.HTMLProgressElement = Element;
|
||||
globalThis.HTMLDetailsElement = Element;
|
||||
globalThis.HTMLDialogElement = Element;
|
||||
// SVGAnimatedString backs the className and href reflections on SVG elements.
|
||||
// baseVal and animVal both read the live attribute (no SMIL animation), and
|
||||
// baseVal is writable. Used by the SVG-aware get className()/get href() above.
|
||||
function SVGAnimatedString(el, attr, fallbackAttr) {
|
||||
this._el = el;
|
||||
this._attr = attr;
|
||||
this._fallback = fallbackAttr || null;
|
||||
}
|
||||
SVGAnimatedString.prototype._read = function() {
|
||||
let v = this._el.getAttribute(this._attr);
|
||||
if (v === null && this._fallback) v = this._el.getAttribute(this._fallback);
|
||||
return v == null ? '' : v;
|
||||
};
|
||||
Object.defineProperty(SVGAnimatedString.prototype, 'baseVal', {
|
||||
get() { return this._read(); },
|
||||
set(v) { this._el.setAttribute(this._attr, String(v)); },
|
||||
configurable: true, enumerable: true,
|
||||
});
|
||||
Object.defineProperty(SVGAnimatedString.prototype, 'animVal', {
|
||||
get() { return this._read(); },
|
||||
configurable: true, enumerable: true,
|
||||
});
|
||||
Object.defineProperty(SVGAnimatedString.prototype, Symbol.toStringTag, { value: 'SVGAnimatedString', configurable: true });
|
||||
_markNative(SVGAnimatedString);
|
||||
|
||||
globalThis.SVGElement = Element;
|
||||
globalThis.SVGSVGElement = Element;
|
||||
globalThis.CharacterData = CharacterData;
|
||||
|
||||
Reference in new issue
Block a user