Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use Proxy with native browser objects (HTMLElement, Canvas2DRenderingContext ,...)?

Tags:

javascript

What I was trying to accomplish. I wanted to share a single canvas (because what I'm doing is very heavy) and so I thought I'd make a limited resource manager. You'd ask it for the resource via promise, in this case a Canvas2DRenderingContext. It would wrap the context in a revokable proxy. When you're finished you are required to call release which both returns the canvas to the limited resource manager so it can give it to someone else AND it revokes the proxy so the user can't accidentally use the resource again.

Except when I make a proxy of a Canvas2DRenderingContext it fails.

const ctx = document.createElement('canvas').getContext('2d');
const proxy = new Proxy(ctx, {});

// try to change the width of the canvas via the proxy
test(() => { proxy.canvas.width = 100; });  // ERROR

// try to translate the origin of via the proxy
test(() => { proxy.translate(1, 2); });     // ERROR


function test(fn) {
  try {
    fn();
  } catch (e) {
    console.log("FAILED:", e, fn);
  }
}

The code above generates Uncaught TypeError: Illegal invocation in Chrome and TypeError: 'get canvas' called on an object that does not implement interface CanvasRenderingContext2D. in Firefox

Is that an expected limitation of Proxy or is it a bug?

note: of course there are other solutions. I can remove the proxy and just not worry about it. I can also wrap the canvas in some JavaScript object that just exposes the functions I need and proxy that. I'm just more curious if this is supposed to work or not. This Mozilla blog post kind of indirectly suggests it's supposed to be possbile since it actually mentions using a proxy with an HTMLElement if only to point out it would certainly fail if you called someElement.appendChild(proxiedElement) but given the simple code above I'd expect it's actually not possible to meanfully wrap any DOM elements or other native objects.

Below is proof that Proxies work with plain JS objects. They work with class based (as in the functions are on the prototype chain). And they don't work with native objects.

const img = document.createElement('img')
const proxy = new Proxy(img, {});
console.log(proxy.src);

Also fails with the same error. where as they don't with JavaScript objects

function testNoOpProxy(obj, msg) {
  log(msg, '------');
  const proxy = new Proxy(obj, {});
  check("get property:", () => proxy.width);
  check("set property:", () => proxy.width = 456);
  check("get property:", () => proxy.width);
  check("call fn on object:", () => proxy.getContext('2d'));
}

function check(msg, fn) {
  let success = true;
  let r;
  try {
    r = fn();
  } catch (e) {
    success = false;
  }
  log('   ', success ? "pass" : "FAIL", msg, r, fn);
}


const test = {
  width: 123,
  getContext: function() {
    return "test";
  },
};

class Test {
  constructor() {
    this.width = 123;
  }
  getContext() {
    return `Test width = ${this.width}`;
  }
}

const testInst = new Test();
const canvas = document.createElement('canvas');

testNoOpProxy(test, 'plain object');
testNoOpProxy(testInst, 'class object');
testNoOpProxy(canvas, 'native object');



function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = [...args].join(' ');
  document.body.appendChild(elem);
}
pre { margin: 0; }

Well FWIW the solution I choose was to wrap the canvas in a small class that does the thing I was using it for. Advantage is it's easier to test (since I can pass in a mock) and I can proxy that object no problem. Still, I'd like to know

  1. Why doesn't Proxy work for native object?
  2. Do any of the reasons Proxy doesn't work with native objects apply to situations with JavaScript objects?
  3. Is it possible to get Proxy to work with native objects.
like image 264
gman Avatar asked Jan 31 '18 06:01

gman


People also ask

What is the use of proxy object in JavaScript?

The Proxy object allows you to create an object that can be used in place of the original object, but which may redefine fundamental Object operations like getting, setting, and defining properties.

What is use of proxies in ES6?

ES6 proxies sit between your code and an object. A proxy allows you to perform meta-programming operations such as intercepting a call to inspect or change an object's property. The original object the proxy will virtualize.

What is proxy array?

Proxy arrays for distributed caching enable multiple proxies to serve as a single cache. Each proxy in the array will contain different cached URLs that can be retrieved by a browser or downstream proxy server.


1 Answers

const handlers = {
  get: (target, key) => key in target ? target[key] : undefined,
  set: (target, key, value) => {
    if (key in target) {
      target[key] = value;
    }
    return value;
  }
};

const { revoke, proxy } = Proxy.revocable(ctx, handlers);

// elsewhere
try {
  proxy.canvas.width = 500;
} catch (e) { console.log("Access has been revoked", e); }

Something like that should do what you're expecting.
A revocable proxy, with handlers for get and set traps, for the context.

Just keep in mind that when an instance of Proxy.revocable() is revoked, any subsequent access of that proxy will throw, and thus everything now needs to use try/catch, in the case that it has, indeed, been revoked.

Just for fun, here's how you can do the exact same thing without fear of throwing (in terms of simply using the accessor; no guarantee for doing something wrong while you still have access):

const RevocableAccess = (item, revoked = false) => ({
  access: f => revoked ? undefined : f(item),
  revoke: () => { revoked = true; }
});

const { revoke, access: useContext } = RevocableAccess(ctx);

useContext(ctx => ctx.canvas.width = 500);
revoke();
useContext(ctx => ctx.canvas.width = 200); // never fires

Edit

As pointed out in the comments below, I completely neglected to test for the method calls on the host object, which, it turns out, are all protected. This comes down to weirdness in the host objects, which get to play by their own rules.

With a proxy as above, proxy.drawImage.apply(ctx, args) would work just fine. This, however, is counter-intuitive.

Cases that I'm assuming fail here, are Canvas, Image, Audio, Video, Promise (for instance based methods) and the like. I haven't conferred with the spec on this part of Proxies, and whether this is a property-descriptor thing, or a host-bindings thing, but I'm going to assume that it's the latter, if not both.

That said, you should be able to override it with the following change:

const { proxy, revoke } = Proxy.revocable(ctx, {
  get(object, key) {
    if (!(key in object)) {
      return undefined;
    }
    const value = object[key];
    return typeof value === "function"
      ? (...args) => value.apply(object, args)
      : value;
  }
});

Here, I am still "getting" the method off of the original object, to call it. It just so happens that in the case of the value being a function, I call bind to return a function that maintains the this relationship to the original context. Proxies usually handle this common JS issue.

...this causes its own security concern; someone could cache the value out, now, and have permanent access to, say, drawImage, by saying const draw = proxy.drawImage;... Then again, they already had the ability to save the real render context, just by saying const ctx = proxy.canvas.getContext("2d"); ...so I'm assuming some level of good-faith, here.

For a more secure solution, there are other fixes, though with canvas, unless it's in-memory only, the context is ultimately going to be available to anyone who can read the DOM.

like image 112
Norguard Avatar answered Oct 11 '22 20:10

Norguard