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> =
|
let mut deferred: std::collections::VecDeque<ServerMessage> =
|
||||||
std::collections::VecDeque::new();
|
std::collections::VecDeque::new();
|
||||||
|
|
||||||
// Subscribe to Ctrl-C once. The future is single-shot, so we break out of
|
// Subscribe to graceful-shutdown signals once. The future is single-shot,
|
||||||
// the outer loop when it fires and never poll it again. Without this the
|
// so we break out of the outer loop when it fires and never poll it again.
|
||||||
// accept loop just exits and any cookies set during the session are lost
|
// Without this the accept loop just exits and any cookies set during the
|
||||||
// before `BrowserContext::save_cookies()` runs.
|
// session are lost before `BrowserContext::save_cookies()` runs. We watch
|
||||||
let mut shutdown = Box::pin(tokio::signal::ctrl_c());
|
// 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 {
|
loop {
|
||||||
// Drain any deferred messages from the previous interception window
|
// Drain any deferred messages from the previous interception window
|
||||||
|
|||||||
@@ -167,6 +167,15 @@ impl<'i> parser::Parser<'i> for ObscuraSelectorParser {
|
|||||||
type Impl = ObscuraSelector;
|
type Impl = ObscuraSelector;
|
||||||
type Error = SelectorParseErrorKind<'i>;
|
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(
|
fn parse_non_ts_pseudo_class(
|
||||||
&self,
|
&self,
|
||||||
_location: cssparser::SourceLocation,
|
_location: cssparser::SourceLocation,
|
||||||
@@ -219,7 +228,18 @@ impl<'a> Element for DomElement<'a> {
|
|||||||
type Impl = ObscuraSelector;
|
type Impl = ObscuraSelector;
|
||||||
|
|
||||||
fn opaque(&self) -> OpaqueElement {
|
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> {
|
fn parent_element(&self) -> Option<Self> {
|
||||||
@@ -666,6 +686,21 @@ mod tests {
|
|||||||
assert_eq!(results.len(), 3);
|
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]
|
#[test]
|
||||||
fn test_query_selector_descendant() {
|
fn test_query_selector_descendant() {
|
||||||
let tree =
|
let tree =
|
||||||
|
|||||||
Vendored
+44
-1
@@ -959,7 +959,15 @@ class Element extends Node {
|
|||||||
}
|
}
|
||||||
get id() { return this.getAttribute("id") || ""; }
|
get id() { return this.getAttribute("id") || ""; }
|
||||||
set id(v) { this.setAttribute("id", v); }
|
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); }
|
set className(v) { this.setAttribute("class", v); }
|
||||||
get namespaceURI() {
|
get namespaceURI() {
|
||||||
// createElementNS records the requested namespace on _ns; an empty string
|
// 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.
|
// members use. Other elements reflect the raw attribute.
|
||||||
get href() {
|
get href() {
|
||||||
const ln = this.localName;
|
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') {
|
if (ln === 'a' || ln === 'area') {
|
||||||
const raw = this.getAttribute('href');
|
const raw = this.getAttribute('href');
|
||||||
if (raw === null) return '';
|
if (raw === null) return '';
|
||||||
@@ -4997,6 +5015,31 @@ globalThis.HTMLLegendElement = Element;
|
|||||||
globalThis.HTMLProgressElement = Element;
|
globalThis.HTMLProgressElement = Element;
|
||||||
globalThis.HTMLDetailsElement = Element;
|
globalThis.HTMLDetailsElement = Element;
|
||||||
globalThis.HTMLDialogElement = 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.SVGElement = Element;
|
||||||
globalThis.SVGSVGElement = Element;
|
globalThis.SVGSVGElement = Element;
|
||||||
globalThis.CharacterData = CharacterData;
|
globalThis.CharacterData = CharacterData;
|
||||||
|
|||||||
Reference in new issue
Block a user