EV-WebKit

 view release on metacpan or  search on metacpan

wext/evwk_fp.c  view on Meta::CPAN

     * X is: claiming every negated clause dragged queries with no spoofed content
     * at all into the wrapper, and delegating the whole `(not X)` instead answers
     * a query form the engine may not accept. */
    "    var deleg=function(t){ var r=orig.call(window,t); return !!(r&&r.matches); };"
    "    var bal=function(t){ var d=0, i;"
    "      for(i=0;i<t.length;i++){ var c=t.charAt(i);"
    "        if(c==='(') d++;"
    "        else if(c===')'){ d--; if(d===0) return i===t.length-1; if(d<0) return false; } }"
    "      return false; };"
    /* unit/cond recurse once per paren group and per `not`, on query text the
     * page controls, so unbounded nesting overflows the JS stack and matchMedia
     * throws a RangeError where a real engine simply answers. Two guards, and
     * the ORDER of preference between them matters:
     *
     * First, collapse redundant parens. '((((X))))' is just '(X)', and
     * collapsing costs one pass instead of a stack frame per level, so the
     * cheap pathological case never approaches the cap and still gets the
     * correct SPOOFED answer at any depth.
     *
     * Only a genuinely structured query -- hundreds of real nested conditions,
     * which no page emits -- can still reach the cap. Such a breach must NOT be
     * delegated to the engine: delegation was tried, and it re-opened the exact
     * leak this evaluator exists to close, because the host answers a
     * device-width probe with the REAL geometry. Measured on a pixel-chrome
     * profile: a plain binary search returned the spoofed 412, the same search
     * wrapped in 300 parens returned the host's true 1280. Answering false and
     * claiming it as ours is a tell for an absurd query but discloses nothing,
     * which is the right way round. */
    "    var MAXDEPTH=256, TOODEEP={v:false,s:true};"
    "    var unit=function(t,dep){"
    "      t=t.trim();"
    "      while(t.charAt(0)==='('&&bal(t)){"
    "        var q=t.slice(1,t.length-1).trim();"
    "        if(q.charAt(0)==='('&&bal(q)) t=q; else break; }"
    "      if(dep>MAXDEPTH) return TOODEEP;"
    "      var n=t.match(/^not\\b\\s*([\\s\\S]+)$/);"
    "      if(n){ var r=unit(n[1],dep+1); return {v:!r.v,s:r.s}; }"
    "      if(t.charAt(0)!=='('||!bal(t)) return {v:deleg(t),s:false};"
    "      var inner=t.slice(1,t.length-1).trim();"
    "      if(!inner) return {v:deleg(t),s:false};"
    /* A parenthesised GROUP is a condition, not a feature test: evaluating it
     * recursively is what makes '((A) or (B))' agree with 'A or B' instead of
     * being handed to the host whole. */
    "      if(inner.charAt(0)==='('||/^not\\b/.test(inner)) return cond(inner,dep+1);"
    "      var fv=featVal(inner);"
    "      return fv===null?{v:deleg(t),s:false}:{v:fv,s:true}; };"
    "    var cond=function(s,dep){"
    "      var ps=splitTop(s,/^\\s*\\bor\\b\\s*/), i, r, v, sp=false;"
    "      if(ps.length>1){ v=false;"
    "        for(i=0;i<ps.length;i++){ r=unit(ps[i],dep+1); if(r.s) sp=true; if(r.v) v=true; }"
    "        return {v:v,s:sp}; }"
    "      ps=splitTop(s,/^\\s+and\\s+/);"
    "      if(ps.length>1){ v=true;"
    "        for(i=0;i<ps.length;i++){ r=unit(ps[i],dep+1); if(r.s) sp=true; if(!r.v) v=false; }"
    "        return {v:v,s:sp}; }"
    "      return unit(s,dep); };"
    /* Evaluate a COMPOUND query branch by branch and clause by clause: a spoofed
     * clause uses our answer, any other is delegated to the real engine and the
     * results combined. Matching whole literal strings meant
     * '(pointer:coarse) and (min-width:1px)' and comma lists silently reported the
     * real desktop answer. */
    "    var evalQuery=function(lq){"
    "      var ors=splitTop(lq, /^\\s*(?:,|\\bor\\b)\\s*/), spoofed=false, result=false, i, j;"
    "      for(i=0;i<ors.length;i++){"
    /* `only <type>` is semantically identical to `<type>` (the keyword only hides
     * the query from prehistoric parsers), and `not` negates the whole branch.
     * Treating either as unparseable abandoned the spoof and returned the
     * contradicting host answer for the most common real-world form. */
    "        var branch=ors[i].trim(), negate=false;"
    "        if(/^only\\s+/.test(branch)) branch=branch.replace(/^only\\s+/,'');"
    "        else if(/^not\\s+/.test(branch)){ negate=true; branch=branch.replace(/^not\\s+/,''); }"
    "        var parts=splitTop(branch, /^\\s+and\\s+/), all=true, usable=true, got=[];"
    "        for(j=0;j<parts.length;j++){"
    "          var c=parts[j].trim();"
    "          if(!c){ usable=false; break; }"
    "          if(c.charAt(0)!=='('){"
    "            if(c==='screen'||c==='all') continue;"     /* always true in a screen browsing context */
    "            usable=false; break; }"                    /* print/speech/... -> let the host answer */
    "          got.push(c); }"
    /* A branch we cannot tokenize is delegated ON ITS OWN and OR-ed in. Bailing
     * out for the whole query let one unhandleable alternative disable every
     * spoofed one beside it: appending ', print' to any query handed the real
     * device's answer back, reopening the geometry leak in the very form the
     * tests pin. A disjunction is per-branch, so treat it that way. */
    "        if(!usable){"
    "          var um=orig.call(window, ors[i]);"
    "          if(um && um.matches) result=true;"
    "          continue; }"
    "        for(j=0;j<got.length;j++){ var rv=unit(got[j],0); if(rv.s) spoofed=true; if(!rv.v) all=false; }"
    "        if(negate) all=!all;"
    "        if(all) result=true; }"
    "      return spoofed ? result : null; };"
    "    var orig=window.matchMedia;"
    /* Redefine `matches` ONCE on MediaQueryList.prototype, backed by a WeakMap of
     * spoofed instances. A Proxy minted a fresh bound function per access (so
     * m.addEventListener !== m.addEventListener) and failed the platform-object
     * brand check; an own property on the instance would instead show up under
     * getOwnPropertyDescriptor/hasOwnProperty where the real one is inherited.
     * A prototype accessor has neither tell. */
    "    var mqOver=new WeakMap();"
    "    (function(){ var M=window.MediaQueryList; if(!M||!M.prototype) return;"
    "      var d=Object.getOwnPropertyDescriptor(M.prototype,'matches'); if(!d||!d.get) return;"
    "      var og=d.get;"
    /* Store the QUERY and re-evaluate on every read. Caching the boolean froze a
     * spoofed MediaQueryList at its creation-time answer, so a retained object
     * contradicted a freshly created one for the same string and `change` never
     * fired -- responsive pages stuck at their initial breakpoint. */
    "      var g=({ get matches(){ var q=mqOver.get(this);"
    "        if(q===undefined) return og.call(this);"
    "        var ev=evalQuery(q); return ev===null ? og.call(this) : ev; } });"
    "      var gd=Object.getOwnPropertyDescriptor(g,'matches').get;"
    "      Object.defineProperty(M.prototype,'matches',{enumerable:d.enumerable,configurable:true,"
    "        get:gd}); })();"
    /* The engine serialises an unparseable query to media "not all", and such a
     * MediaQueryList can NEVER match. Our looser tokenizer accepts some of those
     * (e.g. 'only (pointer:coarse)' -- `only` requires a media type; or a
     * trailing type), so without this check we reported matches:true beside
     * media:"not all", a self-contradiction inside one object. */
    "    var mk=function(q,nq){"
    "      var real=orig.call(window,q);"
    "      try{ if(real && real.media==='not all') return real; }catch(e){}"
    "      try{ mqOver.set(real, nq); }catch(e){}"
    "      return real; };"
    "    var w=({ matchMedia(query){"
    "      if(arguments.length===0) throw new TypeError(\"Failed to execute 'matchMedia' on 'Window': 1 argument required, but only 0 present.\");"
    "      var q=query, lq=lite(q);"
    "      if(evalQuery(lq)!==null) return mk(q,lq);"       /* spoofed: re-evaluated live on each .matches read */
    "      return orig.call(window,q);"
    "    }}).matchMedia;"
    "    window.matchMedia=w;"
    "  }"
    /* Event-handler IDL attributes are ALWAYS accessors; a writable data property
     * is a one-line tell on the two properties mobile detection reads first. */
    /* All four touch handlers or none: no real touch-capable browser exposes
     * ontouchstart alone, and a partial set beside maxTouchPoints>0 is a
     * contradiction. */
    "  if(cfg.touch){"
    "    ['ontouchstart','ontouchend','ontouchmove','ontouchcancel'].forEach(function(h){"
    "      try{ if(h in window) return; var slot=null;"
    "        Object.defineProperty(window,h,{enumerable:true,configurable:true,"
    "          get(){ return slot; },"
    "          set(v){ slot = ((typeof v==='object'&&v!==null)||typeof v==='function') ? v : null; }}); }catch(e){} });"
    "  }"
    "  if(cfg.orientation){"
    /* window.orientation is MOBILE-ONLY (and legacy); screen.orientation is
     * universal. Emitting the orientation config for desktop profiles so they
     * get screen.orientation must not also hand them window.orientation beside
     * maxTouchPoints:0 and pointer:fine. Gate it on touch, and give it the
     * onorientationchange sibling no real mobile browser omits. */
    "    if(cfg.touch){"
    "      try{ Object.defineProperty(window,'orientation',{enumerable:true,configurable:true,"
    "        get(){ return cfg.orientation.angle; }}); }catch(e){}"
    "      try{ if(!('onorientationchange' in window)){ var _ooc=null;"
    "        Object.defineProperty(window,'onorientationchange',{enumerable:true,configurable:true,"
    "          get(){ return _ooc; },"
    "          set(v){ _ooc=((typeof v==='object'&&v!==null)||typeof v==='function')?v:null; }}); } }catch(e){}"
    "    }"
    "    try{ var so=window.screen&&window.screen.orientation;"
    "      if(so){ var sp=Object.getPrototypeOf(so);"        /* redefine on the existing ScreenOrientation.prototype */
    "        Object.defineProperty(sp,'type',{get(){return cfg.orientation.type;},enumerable:true,configurable:true});"
    "        Object.defineProperty(sp,'angle',{get(){return cfg.orientation.angle;},enumerable:true,configurable:true});"
    "      } else if(window.screen){"                        /* WebKitGTK exposes no screen.orientation -- stub it (mobile has one) */
    /* Shaped as a real WebIDL interface for the same reasons NavigatorUAData was:
     * a bare literal reported [object Object], exposed 8 own enumerable keys
     * (real: none), was not an EventTarget, and left window.ScreenOrientation
     * undefined -- and it sat as an own property of `screen` rather than an
     * accessor on Screen.prototype. */
    "        var SO=function ScreenOrientation(){ throw new TypeError('Illegal constructor'); };"
    "        Object.setPrototypeOf(SO, EventTarget);"
    "        SO.prototype=Object.create(EventTarget.prototype,{constructor:{value:SO,writable:true,configurable:true}});"
    "        try{ Object.defineProperty(SO.prototype,Symbol.toStringTag,{value:'ScreenOrientation',configurable:true}); }catch(e){}"
    "        Object.defineProperty(SO.prototype,'type',{get(){ return cfg.orientation.type; },enumerable:true,configurable:true});"
    "        Object.defineProperty(SO.prototype,'angle',{get(){ return cfg.orientation.angle; },enumerable:true,configurable:true});"
    "        var _oc=null;"
    "        Object.defineProperty(SO.prototype,'onchange',{enumerable:true,configurable:true,"
    "          get(){ return _oc; }, set(v){ _oc=((typeof v==='object'&&v!==null)||typeof v==='function')?v:null; }});"
    "        var soM={ lock(){ return Promise.reject(new DOMException('lock not available','NotSupportedError')); }, unlock(){} };"
    "        Object.defineProperty(SO.prototype,'lock',{value:soM.lock,writable:true,enumerable:true,configurable:true});"
    "        Object.defineProperty(SO.prototype,'unlock',{value:soM.unlock,writable:true,enumerable:true,configurable:true});"
    "        try{ Object.defineProperty(SO,'prototype',{writable:false}); }catch(e){}"
    "        try{ if(!('ScreenOrientation' in window)) Object.defineProperty(window,'ScreenOrientation',{value:SO,writable:true,enumerable:false,configurable:true}); }catch(e){}"
    "        var soInst=Reflect.construct(EventTarget, [], SO);"
    "        var sproto=window.Screen ? window.Screen.prototype : window.screen;"
    "        Object.defineProperty(sproto,'orientation',{get(){ return soInst; },enumerable:true,configurable:true});"
    "      } }catch(e){}"
    "  }"
    "})();";

/* DOM feature-presence stubs. Each is installed ONLY if the target does not
 * already expose it (in-guarded), so a WebKitGTK build that ships a real API
 * keeps the real one. Functional where feasible -- but there is no real ICE /
 * device access behind them, and like the other JS-installed layers they are
 * Function.prototype.toString.call-detectable (documented ceiling). Driven by
 * cfg.features, an array of group names chosen per profile in Fingerprint.pm. */
/* navigator.plugins / mimeTypes / pdfViewerEnabled for a profile with NO inline
 * PDF viewer -- currently only Chrome for Android, which had none at 131.
 *
 * The HTML spec hardcodes both states: a browser with inline PDF viewing reports
 * pdfViewerEnabled true and five fixed plugin names, one without reports false
 * and EMPTY lists. WebKitGTK reports the viewer-present state, so a Chrome 131
 * Android profile contradicted itself in one property read.
 *
 * Faking the empty list is fiddly for one reason: PluginArray.prototype.length
 * (and item/namedItem) BRAND-CHECK -- calling them on an Object.create(
 * PluginArray.prototype) throws TypeError, so the obvious fake breaks on the
 * first length read. Nor can length be shadowed as an own property: on a real



( run in 1.455 second using v1.01-cache-2.11-cpan-b16cb0d3907 )