Bug 903332 - Make watch/unwatch into proxy hooks and such, and make watching/unwatching work on DOM proxies and windows (or at least work as much as it ever did, which is to say kinda-sorta-ish). r=bhackett, r=efaust

--HG--
extra : rebase_source : 58dc71e7e58208a2cfa12b265adca7b189e5e5bd
This commit is contained in:
Jeff Walden 2013-10-29 16:39:09 -07:00
parent 2eb64113cd
commit 04e9645fc3
20 changed files with 365 additions and 8 deletions

View File

@ -35,6 +35,7 @@ MOCHITEST_FILES = test_bug1682.html \
test_viewport.html \
test_documentAll.html \
test_document-element-inserted.html \
test_document.watch.html \
$(filter disabled-temporarily--bug-559932, test_bug445004.html) \
bug445004-inner.js \
bug445004-outer-rel.html \

View File

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=903332
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 903332</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
/** Test for Bug 903332 **/
var watch1Called;
function watch1(prop, oldValue, newValue)
{
is(watch1Called, false, "watch1Called not reset properly?");
watch1Called = true;
is(prop, "cookie", "wrong property name passed to watch1");
return newValue;
}
var watch2Called;
function watch2(prop, oldValue, newValue)
{
is(watch2Called, false, "watch2Called not reset properly?");
watch2Called = true;
is(prop, "cookie", "wrong property name passed to watch2");
return newValue;
}
// Just in case subsequent tests depend on a particular value...
var originalValue = document.cookie;
ok(true, "originalValue: " + originalValue);
var originalPrefix = originalValue.length > 0 ? originalValue + "; " : "";
try
{
// trial set (no watch) to verify things work
document.cookie = "first=set";
is(document.cookie, originalPrefix + "first=set",
"first value correct");
// add a watch
document.watch("cookie", watch1);
// set, check for watch invoked
watch1Called = false;
document.cookie = "second=set";
is(watch1Called, true, "watch1 function should be called");
is(document.cookie, originalPrefix + "first=set; second=set",
"second value correct");
// and a second time, just in case
watch1Called = false;
document.cookie = "third=set";
is(watch1Called, true, "watch1 function should be called");
is(document.cookie, originalPrefix + "first=set; second=set; third=set",
"third value correct");
// overwrite the current watch with a new one
document.watch("cookie", watch2);
// set, check for watch invoked
watch1Called = false;
watch2Called = false;
document.cookie = "fourth=set";
is(watch1Called, false, "watch1 invoked erroneously");
is(watch2Called, true, "watch2 function should be called");
is(document.cookie, originalPrefix + "first=set; second=set; third=set; fourth=set",
"fourth value correct");
// and a second time, just in case
watch1Called = false;
watch2Called = false;
document.cookie = "fifth=set";
is(watch1Called, false, "watch1 invoked erroneously");
is(watch2Called, true, "watch2 function should be called");
is(document.cookie, originalPrefix + "first=set; second=set; third=set; fourth=set; fifth=set",
"fifth value correct");
// remove the watch
document.unwatch("cookie");
// check for non-invocation now
watch1Called = false;
watch2Called = false;
document.cookie = "sixth=set";
is(watch1Called, false, "watch1 shouldn't be called");
is(watch2Called, false, "watch2 shouldn't be called");
is(document.cookie, originalPrefix + "first=set; second=set; third=set; fourth=set; fifth=set; sixth=set",
"sixth value correct");
}
finally
{
// reset
document.unwatch("cookie"); // harmless, should be no-op except if bugs
var d = new Date();
d.setTime(0);
var suffix = "=; expires=" + d.toGMTString();
document.cookie = "first" + suffix;
document.cookie = "second" + suffix;
document.cookie = "third" + suffix;
document.cookie = "fourth" + suffix;
document.cookie = "fifth" + suffix;
document.cookie = "sixth" + suffix;
}
is(document.cookie, originalValue,
"document.cookie isn't what it was initially! expect bustage further " +
"down the line");
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=903332">Mozilla Bug 903332</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>

View File

@ -604,6 +604,11 @@ public:
virtual bool enumerate(JSContext *cx, JS::Handle<JSObject*> proxy,
JS::AutoIdVector &props) MOZ_OVERRIDE;
virtual bool watch(JSContext *cx, JS::Handle<JSObject*> proxy,
JS::Handle<jsid> id, JS::Handle<JSObject*> callable) MOZ_OVERRIDE;
virtual bool unwatch(JSContext *cx, JS::Handle<JSObject*> proxy,
JS::Handle<jsid> id) MOZ_OVERRIDE;
// Derived traps
virtual bool has(JSContext *cx, JS::Handle<JSObject*> proxy,
JS::Handle<jsid> id, bool *bp) MOZ_OVERRIDE;
@ -953,6 +958,20 @@ nsOuterWindowProxy::AppendIndexedPropertyNames(JSContext *cx, JSObject *proxy,
return true;
}
bool
nsOuterWindowProxy::watch(JSContext *cx, JS::Handle<JSObject*> proxy,
JS::Handle<jsid> id, JS::Handle<JSObject*> callable)
{
return js::WatchGuts(cx, proxy, id, callable);
}
bool
nsOuterWindowProxy::unwatch(JSContext *cx, JS::Handle<JSObject*> proxy,
JS::Handle<jsid> id)
{
return js::UnwatchGuts(cx, proxy, id);
}
nsOuterWindowProxy
nsOuterWindowProxy::singleton;

View File

@ -235,6 +235,19 @@ BaseDOMProxyHandler::enumerate(JSContext* cx, JS::Handle<JSObject*> proxy,
(!proto || js::GetPropertyNames(cx, proto, 0, &props));
}
bool
BaseDOMProxyHandler::watch(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
JS::Handle<JSObject*> callable)
{
return js::WatchGuts(cx, proxy, id, callable);
}
bool
BaseDOMProxyHandler::unwatch(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id)
{
return js::UnwatchGuts(cx, proxy, id);
}
bool
DOMProxyHandler::has(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id, bool* bp)
{

View File

@ -58,6 +58,11 @@ public:
JS::Handle<jsid> id,
JS::MutableHandle<JSPropertyDescriptor> desc,
unsigned flags) MOZ_OVERRIDE;
bool watch(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
JS::Handle<JSObject*> callable) MOZ_OVERRIDE;
bool unwatch(JSContext* cx, JS::Handle<JSObject*> proxy,
JS::Handle<jsid> id) MOZ_OVERRIDE;
};
class DOMProxyHandler : public BaseDOMProxyHandler

View File

@ -377,6 +377,11 @@ typedef bool
typedef bool
(* DeleteSpecialOp)(JSContext *cx, JS::HandleObject obj, HandleSpecialId sid, bool *succeeded);
typedef bool
(* WatchOp)(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable);
typedef bool
(* UnwatchOp)(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
typedef JSObject *
(* ObjectOp)(JSContext *cx, JS::HandleObject obj);
@ -465,6 +470,8 @@ struct ObjectOps
DeletePropertyOp deleteProperty;
DeleteElementOp deleteElement;
DeleteSpecialOp deleteSpecial;
WatchOp watch;
UnwatchOp unwatch;
JSNewEnumerateOp enumerate;
ObjectOp thisObject;
@ -473,7 +480,7 @@ struct ObjectOps
#define JS_NULL_OBJECT_OPS \
{nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr, \
nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr, \
nullptr,nullptr,nullptr,nullptr,nullptr,nullptr}
nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr}
} // namespace js
@ -502,7 +509,7 @@ struct JSClass {
JSNative construct;
JSTraceOp trace;
void *reserved[40];
void *reserved[42];
};
#define JSCLASS_HAS_PRIVATE (1<<0) // objects have private slot

View File

@ -525,9 +525,9 @@ obj_getPrototypeOf(JSContext *cx, unsigned argc, Value *vp)
#if JS_HAS_OBJ_WATCHPOINT
static bool
obj_watch_handler(JSContext *cx, JSObject *obj_, jsid id_, jsval old,
jsval *nvp, void *closure)
bool
js::WatchHandler(JSContext *cx, JSObject *obj_, jsid id_, JS::Value old,
JS::Value *nvp, void *closure)
{
RootedObject obj(cx, obj_);
RootedId id(cx, id_);
@ -579,7 +579,7 @@ obj_watch(JSContext *cx, unsigned argc, Value *vp)
if (!CheckAccess(cx, obj, propid, JSACC_WATCH, &tmp, &attrs))
return false;
if (!JS_SetWatchPoint(cx, obj, propid, obj_watch_handler, callable))
if (!JSObject::watch(cx, obj, propid, callable))
return false;
args.rval().setUndefined();
@ -608,7 +608,7 @@ obj_unwatch(JSContext *cx, unsigned argc, Value *vp)
id = JSID_VOID;
}
if (!JS_ClearWatchPoint(cx, obj, id, nullptr, nullptr))
if (!JSObject::unwatch(cx, obj, id))
return false;
args.rval().setUndefined();

View File

@ -8,7 +8,8 @@
#define builtin_Object_h
#include "jsapi.h"
#include "js/Value.h"
namespace JS { class Value; }
namespace js {
@ -25,6 +26,10 @@ JSString *
ObjectToSource(JSContext *cx, JS::HandleObject obj);
#endif // JS_HAS_TOSOURCE
extern bool
WatchHandler(JSContext *cx, JSObject *obj, jsid id, JS::Value old,
JS::Value *nvp, void *closure);
} /* namespace js */
#endif /* builtin_Object_h */

View File

@ -2169,6 +2169,7 @@ const Class TypedObject::class_ = {
TypedDatum::obj_deleteProperty,
TypedDatum::obj_deleteElement,
TypedDatum::obj_deleteSpecial,
nullptr, nullptr, // watch/unwatch
TypedDatum::obj_enumerate,
nullptr, /* thisObject */
}
@ -2259,6 +2260,7 @@ const Class TypedHandle::class_ = {
TypedDatum::obj_deleteProperty,
TypedDatum::obj_deleteElement,
TypedDatum::obj_deleteSpecial,
nullptr, nullptr, // watch/unwatch
TypedDatum::obj_enumerate,
nullptr, /* thisObject */
}

View File

@ -1298,6 +1298,33 @@ JS_GetDataViewByteLength(JSObject *obj);
JS_FRIEND_API(void *)
JS_GetDataViewData(JSObject *obj);
namespace js {
/*
* Add a watchpoint -- in the Object.prototype.watch sense -- to |obj| for the
* property |id|, using the callable object |callable| as the function to be
* called for notifications.
*
* This is an internal function exposed -- temporarily -- only so that DOM
* proxies can be watchable. Don't use it! We'll soon kill off the
* Object.prototype.{,un}watch functions, at which point this will go too.
*/
extern JS_FRIEND_API(bool)
WatchGuts(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable);
/*
* Remove a watchpoint -- in the Object.prototype.watch sense -- from |obj| for
* the property |id|.
*
* This is an internal function exposed -- temporarily -- only so that DOM
* proxies can be watchable. Don't use it! We'll soon kill off the
* Object.prototype.{,un}watch functions, at which point this will go too.
*/
extern JS_FRIEND_API(bool)
UnwatchGuts(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
} // namespace js
/*
* A class, expected to be passed by value, which represents the CallArgs for a
* JSJitGetterOp.

View File

@ -21,6 +21,7 @@
#include "jsarray.h"
#include "jsatom.h"
#include "jscntxt.h"
#include "jsfriendapi.h"
#include "jsfun.h"
#include "jsgc.h"
#include "jsiter.h"
@ -35,6 +36,7 @@
#include "jswatchpoint.h"
#include "jswrapper.h"
#include "builtin/Object.h"
#include "frontend/BytecodeCompiler.h"
#include "gc/Marking.h"
#include "jit/AsmJSModule.h"
@ -4984,6 +4986,70 @@ baseops::DeleteSpecial(JSContext *cx, HandleObject obj, HandleSpecialId sid, boo
return baseops::DeleteGeneric(cx, obj, id, succeeded);
}
bool
js::WatchGuts(JSContext *cx, JS::HandleObject origObj, JS::HandleId id, JS::HandleObject callable)
{
RootedObject obj(cx, GetInnerObject(cx, origObj));
if (origObj != obj) {
// If by unwrapping and innerizing, we changed the object, check again
// to make sure that we're allowed to set a watch point.
RootedValue v(cx);
unsigned attrs;
if (!CheckAccess(cx, obj, id, JSACC_WATCH, &v, &attrs))
return false;
}
if (obj->isNative()) {
// Use sparse indexes for watched objects, as dense elements can be
// written to without checking the watchpoint map.
if (!JSObject::sparsifyDenseElements(cx, obj))
return false;
types::MarkTypePropertyConfigured(cx, obj, id);
}
WatchpointMap *wpmap = cx->compartment()->watchpointMap;
if (!wpmap) {
wpmap = cx->runtime()->new_<WatchpointMap>();
if (!wpmap || !wpmap->init()) {
js_ReportOutOfMemory(cx);
return false;
}
cx->compartment()->watchpointMap = wpmap;
}
return wpmap->watch(cx, obj, id, js::WatchHandler, callable);
}
bool
baseops::Watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable)
{
if (!obj->isNative()) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_CANT_WATCH,
obj->getClass()->name);
return false;
}
return WatchGuts(cx, obj, id, callable);
}
bool
js::UnwatchGuts(JSContext *cx, JS::HandleObject origObj, JS::HandleId id)
{
// Looking in the map for an unsupported object will never hit, so we don't
// need to check for nativeness or watchable-ness here.
RootedObject obj(cx, GetInnerObject(cx, origObj));
if (WatchpointMap *wpmap = cx->compartment()->watchpointMap)
wpmap->unwatch(obj, id, nullptr, nullptr);
return true;
}
bool
baseops::Unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id)
{
return UnwatchGuts(cx, obj, id);
}
bool
js::HasDataProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp)
{

View File

@ -155,6 +155,12 @@ DeleteSpecial(JSContext *cx, HandleObject obj, HandleSpecialId sid, bool *succee
extern bool
DeleteGeneric(JSContext *cx, HandleObject obj, HandleId id, bool *succeeded);
extern bool
Watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable);
extern bool
Unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
} /* namespace js::baseops */
extern const Class IntlClass;
@ -1090,6 +1096,10 @@ class JSObject : public js::ObjectImpl
static bool deleteByValue(JSContext *cx, js::HandleObject obj,
const js::Value &property, bool *succeeded);
static inline bool watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id,
JS::HandleObject callable);
static inline bool unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
static bool enumerate(JSContext *cx, JS::HandleObject obj, JSIterateOp iterop,
JS::MutableHandleValue statep, JS::MutableHandleId idp)
{

View File

@ -69,6 +69,21 @@ JSObject::deleteSpecial(JSContext *cx, js::HandleObject obj, js::HandleSpecialId
return (op ? op : js::baseops::DeleteSpecial)(cx, obj, sid, succeeded);
}
/* static */ inline bool
JSObject::watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id,
JS::HandleObject callable)
{
js::WatchOp op = obj->getOps()->watch;
return (op ? op : js::baseops::Watch)(cx, obj, id, callable);
}
/* static */ inline bool
JSObject::unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id)
{
js::UnwatchOp op = obj->getOps()->unwatch;
return (op ? op : js::baseops::Unwatch)(cx, obj, id);
}
inline void
JSObject::finalize(js::FreeOp *fop)
{

View File

@ -371,6 +371,19 @@ BaseProxyHandler::getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandl
return true;
}
bool
BaseProxyHandler::watch(JSContext *cx, HandleObject proxy, HandleId id, HandleObject callable)
{
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_CANT_WATCH,
proxy->getClass()->name);
return false;
}
bool
BaseProxyHandler::unwatch(JSContext *cx, HandleObject proxy, HandleId id)
{
return true;
}
bool
DirectProxyHandler::getPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
@ -2747,6 +2760,20 @@ Proxy::getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject pro
JSObject * const Proxy::LazyProto = reinterpret_cast<JSObject *>(0x1);
/* static */ bool
Proxy::watch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::HandleObject callable)
{
JS_CHECK_RECURSION(cx, return false);
return proxy->as<ProxyObject>().handler()->watch(cx, proxy, id, callable);
}
/* static */ bool
Proxy::unwatch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id)
{
JS_CHECK_RECURSION(cx, return false);
return proxy->as<ProxyObject>().handler()->unwatch(cx, proxy, id);
}
static JSObject *
proxy_innerObject(JSContext *cx, HandleObject obj)
{
@ -3046,6 +3073,18 @@ proxy_Construct(JSContext *cx, unsigned argc, Value *vp)
return Proxy::construct(cx, proxy, args);
}
static bool
proxy_Watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable)
{
return Proxy::watch(cx, obj, id, callable);
}
static bool
proxy_Unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id)
{
return Proxy::unwatch(cx, obj, id);
}
#define PROXY_CLASS_EXT \
{ \
nullptr, /* outerObject */ \
@ -3098,6 +3137,7 @@ proxy_Construct(JSContext *cx, unsigned argc, Value *vp)
proxy_DeleteProperty, \
proxy_DeleteElement, \
proxy_DeleteSpecial, \
proxy_Watch, proxy_Unwatch, \
nullptr, /* enumerate */ \
nullptr, /* thisObject */ \
} \
@ -3155,6 +3195,7 @@ const Class js::OuterWindowProxyObject::class_ = {
proxy_DeleteProperty,
proxy_DeleteElement,
proxy_DeleteSpecial,
proxy_Watch, proxy_Unwatch,
nullptr, /* enumerate */
nullptr, /* thisObject */
}

View File

@ -166,6 +166,12 @@ class JS_FRIEND_API(BaseProxyHandler)
uint32_t index, MutableHandleValue vp, bool *present);
virtual bool getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject protop);
// These two hooks must be overridden, or not overridden, in tandem -- no
// overriding just one!
virtual bool watch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
JS::HandleObject callable);
virtual bool unwatch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id);
/* See comment for weakmapKeyDelegateOp in js/Class.h. */
virtual JSObject *weakmapKeyDelegate(JSObject *proxy);
};
@ -275,6 +281,10 @@ class Proxy
static bool defaultValue(JSContext *cx, HandleObject obj, JSType hint, MutableHandleValue vp);
static bool getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject protop);
static bool watch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
JS::HandleObject callable);
static bool unwatch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id);
/* IC entry path for handling __noSuchMethod__ on access. */
static bool callProp(JSContext *cx, HandleObject proxy, HandleObject reveiver, HandleId id,
MutableHandleValue vp);

View File

@ -536,6 +536,7 @@ const Class WithObject::class_ = {
with_DeleteProperty,
with_DeleteElement,
with_DeleteSpecial,
nullptr, nullptr, /* watch/unwatch */
with_Enumerate,
with_ThisObject,
}

View File

@ -3470,6 +3470,7 @@ const Class ArrayBufferObject::class_ = {
ArrayBufferObject::obj_deleteProperty,
ArrayBufferObject::obj_deleteElement,
ArrayBufferObject::obj_deleteSpecial,
nullptr, nullptr, /* watch/unwatch */
ArrayBufferObject::obj_enumerate,
nullptr, /* thisObject */
}
@ -3632,6 +3633,7 @@ IMPL_TYPED_ARRAY_COMBINED_UNWRAPPERS(Float64, double, double)
_typedArray##Object::obj_deleteProperty, \
_typedArray##Object::obj_deleteElement, \
_typedArray##Object::obj_deleteSpecial, \
nullptr, nullptr, /* watch/unwatch */ \
_typedArray##Object::obj_enumerate, \
nullptr, /* thisObject */ \
} \

View File

@ -739,6 +739,7 @@ const XPCWrappedNativeJSClass XPC_WN_NoHelper_JSClass = {
nullptr, // deleteProperty
nullptr, // deleteElement
nullptr, // deleteSpecial
nullptr, nullptr, // watch/unwatch
XPC_WN_JSOp_Enumerate,
XPC_WN_JSOp_ThisObject,
}

View File

@ -1172,6 +1172,7 @@ XPC_WN_JSOp_ThisObject(JSContext *cx, JS::HandleObject obj);
nullptr, /* deleteProperty */ \
nullptr, /* deleteElement */ \
nullptr, /* deleteSpecial */ \
nullptr, nullptr, /* watch/unwatch */ \
XPC_WN_JSOp_Enumerate, \
XPC_WN_JSOp_ThisObject, \
}
@ -1200,6 +1201,7 @@ XPC_WN_JSOp_ThisObject(JSContext *cx, JS::HandleObject obj);
nullptr, /* deleteProperty */ \
nullptr, /* deleteElement */ \
nullptr, /* deleteSpecial */ \
nullptr, nullptr, /* watch/unwatch */ \
XPC_WN_JSOp_Enumerate, \
XPC_WN_JSOp_ThisObject, \
}

View File

@ -258,6 +258,7 @@
"content/base/test/test_bug424359-2.html":"",
"content/base/test/test_mixed_content_blocker_bug803225.html":"",
"content/html/document/test/test_non-ascii-cookie.html":"",
"content/html/document/test/test_document.watch.html":"expects document.cookie setting to work",
"docshell/test/navigation/test_bug13871.html":"",
"docshell/test/navigation/test_bug270414.html":"",