From b153a8a8c8676c63a355613b029af74d0ee63e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski-Owczarek?= Date: Mon, 7 Apr 2025 23:27:27 +0200 Subject: [PATCH 1/2] Data: Patch `$._data` as well Also, extract the util to patch a prototype as we'll need it in the event module as well. --- src/jquery/data.js | 77 ++++++++++------------------------------ src/utils.js | 59 ++++++++++++++++++++++++++++++ test/unit/jquery/data.js | 8 +++-- 3 files changed, 83 insertions(+), 61 deletions(-) diff --git a/src/jquery/data.js b/src/jquery/data.js index be86342f..612deafd 100644 --- a/src/jquery/data.js +++ b/src/jquery/data.js @@ -1,86 +1,45 @@ -import { migratePatchFunc, migrateWarn } from "../main.js"; +import { migratePatchFunc } from "../main.js"; +import { patchProto } from "../utils.js"; function patchDataProto( original, options ) { - var i, + var warningId = options.warningId, apiName = options.apiName, - isInstanceMethod = options.isInstanceMethod, + isInstanceMethod = options.isInstanceMethod; - // `Object.prototype` keys are not enumerable so list the - // official ones here. An alternative would be wrapping - // data objects with a Proxy but that creates additional issues - // like breaking object identity on subsequent calls. - objProtoKeys = [ - "__proto__", - "__defineGetter__", - "__defineSetter__", - "__lookupGetter__", - "__lookupSetter__", - "hasOwnProperty", - "isPrototypeOf", - "propertyIsEnumerable", - "toLocaleString", - "toString", - "valueOf" - ], - - // Use a null prototype at the beginning so that we can define our - // `__proto__` getter & setter. We'll reset the prototype afterwards. - intermediateDataObj = Object.create( null ); - - for ( i = 0; i < objProtoKeys.length; i++ ) { - ( function( key ) { - Object.defineProperty( intermediateDataObj, key, { - get: function() { - migrateWarn( "data-null-proto", - "Accessing properties from " + apiName + - " inherited from Object.prototype is removed" ); - return ( key + "__cache" ) in intermediateDataObj ? - intermediateDataObj[ key + "__cache" ] : - Object.prototype[ key ]; - }, - set: function( value ) { - migrateWarn( "data-null-proto", - "Setting properties from " + apiName + - " inherited from Object.prototype is removed" ); - intermediateDataObj[ key + "__cache" ] = value; - } - } ); - } )( objProtoKeys[ i ] ); - } - - Object.setPrototypeOf( intermediateDataObj, Object.prototype ); - - return function jQueryDataProtoPatched() { + return function apiWithProtoPatched() { var result = original.apply( this, arguments ); if ( arguments.length !== ( isInstanceMethod ? 0 : 1 ) || result === undefined ) { return result; } - // Insert an additional object in the prototype chain between `result` - // and `Object.prototype`; that intermediate object proxies properties - // to `Object.prototype`, warning about their usage first. - Object.setPrototypeOf( result, intermediateDataObj ); + patchProto( result, { + warningId: warningId, + apiName: apiName + } ); return result; }; } -// Yes, we are patching jQuery.data twice; here & above. This is necessary -// so that each of the two patches can be independently disabled. migratePatchFunc( jQuery, "data", patchDataProto( jQuery.data, { + warningId: "data-null-proto", apiName: "jQuery.data()", - isPrivateData: false, + isInstanceMethod: false + } ), + "data-null-proto" ); +migratePatchFunc( jQuery, "_data", + patchDataProto( jQuery._data, { + warningId: "data-null-proto", + apiName: "jQuery._data()", isInstanceMethod: false } ), "data-null-proto" ); migratePatchFunc( jQuery.fn, "data", patchDataProto( jQuery.fn.data, { + warningId: "data-null-proto", apiName: "jQuery.fn.data()", - isPrivateData: true, isInstanceMethod: true } ), "data-null-proto" ); - -// TODO entry in warnings.md diff --git a/src/utils.js b/src/utils.js index d51891c1..5aa5f0a0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,64 @@ +import { migrateWarn } from "./main.js"; + export function camelCase( string ) { return string.replace( /-([a-z])/g, function( _, letter ) { return letter.toUpperCase(); } ); } + +// Make `object` inherit from `Object.prototype` via an additional object +// in between; that intermediate object proxies properties +// to `Object.prototype`, warning about their usage first. +export function patchProto( object, options ) { + var i, + warningId = options.warningId, + apiName = options.apiName, + + // `Object.prototype` keys are not enumerable so list the + // official ones here. An alternative would be wrapping + // objects with a Proxy but that creates additional issues + // like breaking object identity on subsequent calls. + objProtoKeys = [ + "__proto__", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf" + ], + + // Use a null prototype at the beginning so that we can define our + // `__proto__` getter & setter. We'll reset the prototype afterward. + intermediateObj = Object.create( null ); + + for ( i = 0; i < objProtoKeys.length; i++ ) { + ( function( key ) { + Object.defineProperty( intermediateObj, key, { + get: function() { + migrateWarn( warningId, + "Accessing properties from " + apiName + + " inherited from Object.prototype is removed" ); + return ( key + "__cache" ) in intermediateObj ? + intermediateObj[ key + "__cache" ] : + Object.prototype[ key ]; + }, + set: function( value ) { + migrateWarn( warningId, + "Setting properties from " + apiName + + " inherited from Object.prototype is removed" ); + intermediateObj[ key + "__cache" ] = value; + } + } ); + } )( objProtoKeys[ i ] ); + } + + Object.setPrototypeOf( intermediateObj, Object.prototype ); + Object.setPrototypeOf( object, intermediateObj ); + + return object; +} diff --git a/test/unit/jquery/data.js b/test/unit/jquery/data.js index 97f00f25..895829f3 100644 --- a/test/unit/jquery/data.js +++ b/test/unit/jquery/data.js @@ -1,21 +1,25 @@ QUnit.module( "data" ); QUnit.test( "properties from Object.prototype", function( assert ) { - assert.expect( 6 ); + assert.expect( 8 ); var div = jQuery( "
" ).appendTo( "#qunit-fixture" ); div.data( "foo", "bar" ); + jQuery._data( div[ 0 ], "baz", "qaz" ); expectNoMessage( assert, "Regular properties", function() { assert.strictEqual( div.data( "foo" ), "bar", "data access" ); assert.strictEqual( jQuery.data( div[ 0 ], "foo" ), "bar", "data access (static method)" ); + assert.strictEqual( jQuery._data( div[ 0 ], "baz" ), "qaz", "private data access" ); } ); - expectMessage( assert, "Properties from Object.prototype", 2, function() { + expectMessage( assert, "Properties from Object.prototype", 3, function() { assert.ok( div.data().hasOwnProperty( "foo" ), "hasOwnProperty works" ); assert.ok( jQuery.data( div[ 0 ] ).hasOwnProperty( "foo" ), "hasOwnProperty works (static method)" ); + assert.ok( jQuery._data( div[ 0 ] ).hasOwnProperty( "baz" ), + "hasOwnProperty works (private data)" ); } ); } ); From 868906477a8424c7d5ff5ce894df198c13dc6b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski-Owczarek?= Date: Mon, 14 Apr 2025 18:45:45 +0200 Subject: [PATCH 2/2] Event: Patch jQuery.event.special's prototype Allow to use common `Object.prototype` properties on `jQuery.event.special` but warn as well. Fixes gh-542 --- src/jquery/event.js | 11 ++++++++++- test/unit/jquery/event.js | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/jquery/event.js b/src/jquery/event.js index e13a5085..33178d4b 100644 --- a/src/jquery/event.js +++ b/src/jquery/event.js @@ -1,9 +1,11 @@ import { migrateWarn, migratePatchAndInfoFunc, - migratePatchFunc + migratePatchFunc, + migratePatchProp } from "../main.js"; import "../disablePatches.js"; +import { patchProto } from "../utils.js"; var oldEventAdd = jQuery.event.add; @@ -41,3 +43,10 @@ migratePatchAndInfoFunc( jQuery.fn, "undelegate", jQuery.fn.undelegate, migratePatchAndInfoFunc( jQuery.fn, "hover", jQuery.fn.hover, "hover", "jQuery.fn.hover() is deprecated" ); + +migratePatchProp( jQuery.event, "special", + patchProto( jQuery.extend( Object.create( null ), jQuery.event.special ), { + warningId: "event-special-null-proto", + apiName: "jQuery.event.special" + } ), + "event-special-null-proto" ); diff --git a/test/unit/jquery/event.js b/test/unit/jquery/event.js index 519c1e01..3c4021f2 100644 --- a/test/unit/jquery/event.js +++ b/test/unit/jquery/event.js @@ -84,3 +84,25 @@ TestManager.runIframeTest( "Load within a ready handler", "event-lateload.html", JSON.stringify( jQuery.migrateMessages ) ); assert.ok( /load/.test( jQuery.migrateMessages[ 0 ] ), "message ok" ); } ); + +QUnit.test( "jQuery.event.special: properties from Object.prototype", function( assert ) { + assert.expect( 4 ); + + try { + expectNoMessage( assert, "Regular properties", function() { + jQuery.event.special.fakeevent = {}; + + // eslint-disable-next-line no-unused-expressions + jQuery.event.special.fakeevent; + } ); + + expectMessage( assert, "Properties from Object.prototype", 2, function() { + assert.ok( jQuery.event.special.hasOwnProperty( "fakeevent" ), + "hasOwnProperty works (property present)" ); + assert.ok( !jQuery.event.special.hasOwnProperty( "fakeevent2" ), + "hasOwnProperty works (property missing)" ); + } ); + } finally { + delete jQuery.event.special.fakeevent; + } +} );