Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 04224c7

Browse files
committed
feat($compile): add support for arbitrary property and event bindings
Fixes #16428
1 parent 43d3f62 commit 04224c7

File tree

3 files changed

+763
-53
lines changed

3 files changed

+763
-53
lines changed

src/ng/compile.js

+161-23
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,84 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15871587
return cssClassDirectivesEnabledConfig;
15881588
};
15891589

1590+
1591+
/**
1592+
* The security context of DOM Properties.
1593+
* @private
1594+
*/
1595+
var PROP_CONTEXTS = Object.create(null);
1596+
1597+
/**
1598+
* @ngdoc method
1599+
* @name $compileProvider#addPropertyContext
1600+
* @description
1601+
*
1602+
* Defines the security context for HTML properties bound by ng-prop-*
1603+
*
1604+
* @param {string} the context type
1605+
* @param {string} the element name or '*' to match any element
1606+
* @param {string} the property name
1607+
*/
1608+
this.addPropertyContext = function(ctx, elementName, propertyName) {
1609+
PROP_CONTEXTS[(elementName.toLowerCase() + '|' + propertyName.toLowerCase())] = ctx;
1610+
};
1611+
1612+
/* Default property contexts.
1613+
*
1614+
* Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58
1615+
* Changing:
1616+
* - SecurityContext.* => $sce.*
1617+
* - STYLE => CSS
1618+
* - various URL => MEDIA_URL
1619+
*/
1620+
(function setupPropertyContexts() {
1621+
function registerContext(ctx, values) {
1622+
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; });
1623+
}
1624+
1625+
registerContext(SCE_CONTEXTS.HTML, [
1626+
'iframe|srcdoc',
1627+
'*|innerHTML',
1628+
'*|outerHTML',
1629+
]);
1630+
registerContext(SCE_CONTEXTS.CSS, ['*|style']);
1631+
registerContext(SCE_CONTEXTS.URL, [
1632+
'*|formAction',
1633+
'area|href', 'area|ping',
1634+
'a|href', 'a|ping',
1635+
'blockquote|cite',
1636+
'body|background',
1637+
'del|cite',
1638+
'form|action',
1639+
'input|src',
1640+
'ins|cite',
1641+
'q|cite',
1642+
'script|src',
1643+
'video|poster'
1644+
]);
1645+
registerContext(SCE_CONTEXTS.MEDIA_URL, [
1646+
'audio|src',
1647+
'img|src', 'img|srcset',
1648+
'source|src', 'source|srcset',
1649+
'track|src',
1650+
'video|src'
1651+
]);
1652+
registerContext(SCE_CONTEXTS.RESOURCE_URL, [
1653+
'applet|code', 'applet|codebase',
1654+
'base|href',
1655+
'embed|src',
1656+
'frame|src',
1657+
'head|profile',
1658+
'html|manifest',
1659+
'iframe|src',
1660+
'link|href',
1661+
'media|src',
1662+
'object|codebase', 'object|data',
1663+
'script|src',
1664+
]);
1665+
})();
1666+
1667+
15901668
this.$get = [
15911669
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
15921670
'$controller', '$rootScope', '$sce', '$animate',
@@ -1632,12 +1710,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16321710
}
16331711

16341712

1635-
function sanitizeSrcset(value) {
1713+
function sanitizeSrcset(value, invokeType) {
16361714
if (!value) {
16371715
return value;
16381716
}
16391717
if (!isString(value)) {
1640-
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
1718+
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `' + invokeType + '`: "{0}"', value.toString());
16411719
}
16421720

16431721
// Such values are a bit too complex to handle automatically inside $sce.
@@ -1820,7 +1898,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18201898

18211899
// Sanitize img[srcset] + source[srcset] values.
18221900
if ((nodeName === 'img' || nodeName === 'source') && key === 'srcset') {
1823-
this[key] = value = sanitizeSrcset(value);
1901+
this[key] = value = sanitizeSrcset(value, '$set(\'srcset\', value)');
18241902
}
18251903

18261904
if (writeAttr !== false) {
@@ -1917,7 +1995,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
19171995
: function denormalizeTemplate(template) {
19181996
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
19191997
},
1920-
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
1998+
NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/;
19211999
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
19222000

19232001
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
@@ -2253,43 +2331,58 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
22532331
directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective);
22542332

22552333
// iterate over the attributes
2256-
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
2334+
for (var attr, name, nName, ngAttrName, value, ngPrefixMatch, nAttrs = node.attributes,
22572335
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
22582336
var attrStartName = false;
22592337
var attrEndName = false;
22602338

2339+
var isNgAttr = false, isNgProp = false, isNgEvent = false;
2340+
var multiElementMatch;
2341+
22612342
attr = nAttrs[j];
22622343
name = attr.name;
22632344
value = attr.value;
22642345

2265-
// support ngAttr attribute binding
2346+
// support ng-attr-*, ng-prop-* and ng-on-*
22662347
ngAttrName = directiveNormalize(name);
2267-
isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
2268-
if (isNgAttr) {
2348+
if (ngPrefixMatch = ngAttrName.match(NG_PREFIX_BINDING)) {
2349+
isNgAttr = ngPrefixMatch[1] === "Attr";
2350+
isNgProp = ngPrefixMatch[1] === "Prop";
2351+
isNgEvent = ngPrefixMatch[1] === "On";
2352+
22692353
name = name.replace(PREFIX_REGEXP, '')
2270-
.substr(8).replace(/_(.)/g, function(match, letter) {
2354+
.substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) {
22712355
return letter.toUpperCase();
22722356
});
2273-
}
22742357

2275-
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
2276-
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
2358+
// support *-start / *-end multi element directives
2359+
} else if ((multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) {
22772360
attrStartName = name;
22782361
attrEndName = name.substr(0, name.length - 5) + 'end';
22792362
name = name.substr(0, name.length - 6);
22802363
}
22812364

22822365
nName = directiveNormalize(name.toLowerCase());
22832366
attrsMap[nName] = name;
2284-
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
2285-
attrs[nName] = value;
2286-
if (getBooleanAttrName(node, nName)) {
2287-
attrs[nName] = true; // presence means true
2288-
}
2367+
2368+
if (isNgProp) {
2369+
attrs[ngAttrName] = value;
2370+
addPropertyDirective(node, directives, ngAttrName, name);
2371+
} else if (isNgEvent) {
2372+
attrs[ngAttrName] = value;
2373+
addEventDirective(directives, ngAttrName, name);
2374+
} else {
2375+
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
2376+
attrs[nName] = value;
2377+
if (getBooleanAttrName(node, nName)) {
2378+
attrs[nName] = true; // presence means true
2379+
}
2380+
}
2381+
2382+
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
2383+
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
2384+
attrEndName);
22892385
}
2290-
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
2291-
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
2292-
attrEndName);
22932386
}
22942387

22952388
if (nodeName === 'input' && node.getAttribute('type') === 'hidden') {
@@ -3325,7 +3418,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
33253418
}
33263419

33273420

3328-
function getTrustedContext(node, attrNormalizedName) {
3421+
function getTrustedAttrContext(node, attrNormalizedName) {
33293422
if (attrNormalizedName === 'srcdoc') {
33303423
return $sce.HTML;
33313424
}
@@ -3358,9 +3451,54 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
33583451
}
33593452
}
33603453

3454+
function getTrustedPropContext(node, propNormalizedName) {
3455+
var tag = nodeName_(node);
3456+
var prop = propNormalizedName.toLowerCase();
3457+
return PROP_CONTEXTS[tag + "|" + prop] || PROP_CONTEXTS["*|" + prop];
3458+
}
3459+
3460+
function addPropertyDirective(node, directives, attrName, propName) {
3461+
if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) {
3462+
throw $compileMinErr('nodomevents',
3463+
'Property bindings for HTML DOM event properties are disallowed. Please use the ' +
3464+
'ng- versions (such as ng-click or ng-on-click instead of ng-prop-onclick) instead.');
3465+
}
3466+
3467+
var nodeName = nodeName_(node);
3468+
var trustedContext = getTrustedPropContext(node, propName);
3469+
3470+
directives.push({
3471+
priority: 100,
3472+
compile: function ngPropCompileFn(_, attr) {
3473+
var fn = $parse(attr[attrName]);
3474+
return {
3475+
pre: function ngPropPreLinkFn(scope, $element) {
3476+
scope.$watch(fn, function propertyWatchActionFn(value) {
3477+
if (value) {
3478+
// Sanitize img[srcset] + source[srcset] values.
3479+
if ((nodeName === 'img' || nodeName === 'source') && propName === 'srcset') {
3480+
value = sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset');
3481+
} else if (trustedContext) {
3482+
value = $sce.getTrusted(trustedContext, value);
3483+
}
3484+
}
3485+
3486+
$element.prop(propName, value);
3487+
});
3488+
}
3489+
};
3490+
}
3491+
});
3492+
}
3493+
3494+
function addEventDirective(directives, attrName, eventName) {
3495+
directives.push(
3496+
createEventDirective($parse, $rootScope, attrName, eventName, /*forceAsync=*/false)
3497+
);
3498+
}
33613499

33623500
function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
3363-
var trustedContext = getTrustedContext(node, name);
3501+
var trustedContext = getTrustedAttrContext(node, name);
33643502
var mustHaveExpression = !isNgAttr;
33653503
var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr;
33663504

@@ -3378,7 +3516,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
33783516
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
33793517
throw $compileMinErr('nodomevents',
33803518
'Interpolations for HTML DOM event attributes are disallowed. Please use the ' +
3381-
'ng- versions (such as ng-click instead of onclick) instead.');
3519+
'ng- versions (such as ng-click or ng-on-click instead of onclick) instead.');
33823520
}
33833521

33843522
directives.push({

src/ng/directive/ngEventDirs.js

+27-22
Original file line numberDiff line numberDiff line change
@@ -51,32 +51,37 @@ forEach(
5151
function(eventName) {
5252
var directiveName = directiveNormalize('ng-' + eventName);
5353
ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
54-
return {
55-
restrict: 'A',
56-
compile: function($element, attr) {
57-
// NOTE:
58-
// We expose the powerful `$event` object on the scope that provides access to the Window,
59-
// etc. This is OK, because expressions are not sandboxed any more (and the expression
60-
// sandbox was never meant to be a security feature anyway).
61-
var fn = $parse(attr[directiveName]);
62-
return function ngEventHandler(scope, element) {
63-
element.on(eventName, function(event) {
64-
var callback = function() {
65-
fn(scope, {$event: event});
66-
};
67-
if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
68-
scope.$evalAsync(callback);
69-
} else {
70-
scope.$apply(callback);
71-
}
72-
});
73-
};
74-
}
75-
};
54+
return createEventDirective($parse, $rootScope, directiveName, eventName, forceAsyncEvents[eventName]);
7655
}];
7756
}
7857
);
7958

59+
function createEventDirective($parse, $rootScope, attrName, eventName, forceAsync) {
60+
return {
61+
restrict: 'A',
62+
priority: 0,
63+
compile: function($element, attr) {
64+
// NOTE:
65+
// We expose the powerful `$event` object on the scope that provides access to the Window,
66+
// etc. This is OK, because expressions are not sandboxed any more (and the expression
67+
// sandbox was never meant to be a security feature anyway).
68+
var fn = $parse(attr[attrName]);
69+
return function ngEventHandler(scope, element) {
70+
element.on(eventName, function(event) {
71+
var callback = function() {
72+
fn(scope, {$event: event});
73+
};
74+
if (forceAsync && $rootScope.$$phase) {
75+
scope.$evalAsync(callback);
76+
} else {
77+
scope.$apply(callback);
78+
}
79+
});
80+
};
81+
}
82+
};
83+
}
84+
8085
/**
8186
* @ngdoc directive
8287
* @name ngDblclick

0 commit comments

Comments
 (0)