angular.js/src/ng/compile.js

1488 lines
57 KiB
JavaScript
Raw Normal View History

'use strict';
/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
*
* DOM-related variables:
*
* - "node" - DOM Node
* - "element" - DOM Element or Node
* - "$node" or "$element" - jqLite-wrapped node or element
*
*
* Compiler related stuff:
*
* - "linkFn" - linking fn of a single directive
* - "nodeLinkFn" - function that aggregates all linking fns for a particular node
* - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
* - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
*/
/**
* @ngdoc function
* @name ng.$compile
* @function
*
* @description
* Compiles a piece of HTML string or DOM into a template and produces a template function, which
* can then be used to link {@link ng.$rootScope.Scope scope} and the template together.
*
* The compilation is a process of walking the DOM tree and trying to match DOM elements to
* {@link ng.$compileProvider#directive directives}. For each match it
* executes corresponding template function and collects the
* instance functions into a single template function which is then returned.
*
* The template function can then be used once to produce the view or as it is the case with
* {@link ng.directive:ngRepeat repeater} many-times, in which
* case each call results in a view that is a DOM clone of the original template.
*
<doc:example module="compile">
<doc:source>
<script>
// declare a new module, and inject the $compileProvider
angular.module('compile', [], function($compileProvider) {
// configure new 'compile' directive by passing a directive
// factory function. The factory function injects the '$compile'
$compileProvider.directive('compile', function($compile) {
// directive factory creates a link function
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
2010-03-19 23:41:02 +00:00
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
})
});
function Ctrl($scope) {
$scope.name = 'Angular';
$scope.html = 'Hello {{name}}';
}
</script>
<div ng-controller="Ctrl">
2012-03-09 08:00:05 +00:00
<input ng-model="name"> <br>
<textarea ng-model="html"></textarea> <br>
<div compile="html"></div>
</div>
</doc:source>
<doc:scenario>
it('should auto compile', function() {
expect(element('div[compile]').text()).toBe('Hello Angular');
input('html').enter('{{name}}!');
expect(element('div[compile]').text()).toBe('Angular!');
});
</doc:scenario>
</doc:example>
2010-03-20 05:18:39 +00:00
*
*
* @param {string|DOMElement} element Element or HTML string to compile into a template function.
* @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives.
* @param {number} maxPriority only apply directives lower then given priority (Only effects the
* root element(s), not their children)
* @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
* (a DOM element/tree) to a scope. Where:
*
* * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
* * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the `template`
* and call the `cloneAttachFn` function allowing the caller to attach the
* cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
* called as: <br> `cloneAttachFn(clonedElement, scope)` where:
*
* * `clonedElement` - is a clone of the original `element` passed into the compiler.
* * `scope` - is the current scope with which the linking function is working with.
*
* Calling the linking function returns the element of the template. It is either the original element
* passed in, or the clone of the element if the `cloneAttachFn` is provided.
*
2012-04-17 21:35:00 +00:00
* After linking the view is not updated until after a call to $digest which typically is done by
* Angular automatically.
*
* If you need access to the bound view, there are two ways to do it:
*
* - If you are not asking the linking function to clone the template, create the DOM element(s)
* before you send them to the compiler and keep this reference around.
* <pre>
* var element = $compile('<p>{{total}}</p>')(scope);
* </pre>
*
* - if on the other hand, you need the element to be cloned, the view reference from the original
* example would not point to the clone, but rather to the original template that was cloned. In
* this case, you can access the clone via the cloneAttachFn:
* <pre>
* var templateHTML = angular.element('<p>{{total}}</p>'),
* scope = ....;
*
* var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) {
* //attach the clone to DOM document at the right place
* });
*
* //now we have reference to the cloned DOM via `clone`
* </pre>
*
*
* For information on how the compiler works, see the
* {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
*/
var $compileMinErr = minErr('$compile');
/**
* @ngdoc service
* @name ng.$compileProvider
* @function
*
* @description
*/
$CompileProvider.$inject = ['$provide'];
function $CompileProvider($provide) {
var hasDirectives = {},
Suffix = 'Directive',
COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//;
// Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes
// The assumption is that future DOM event attribute names will begin with
// 'on' and be composed of only English letters.
var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/;
/**
* @ngdoc function
* @name ng.$compileProvider#directive
* @methodOf ng.$compileProvider
* @function
*
* @description
* Register a new directive with the compiler.
*
* @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
* will match as <code>ng-bind</code>), or an object map of directives where the keys are the
* names and the values are the factories.
* @param {function|Array} directiveFactory An injectable directive factory function. See
* {@link guide/directive} for more info.
* @returns {ng.$compileProvider} Self for chaining.
*/
this.directive = function registerDirective(name, directiveFactory) {
if (isString(name)) {
assertArg(directiveFactory, 'directiveFactory');
if (!hasDirectives.hasOwnProperty(name)) {
hasDirectives[name] = [];
$provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
function($injector, $exceptionHandler) {
var directives = [];
forEach(hasDirectives[name], function(directiveFactory) {
try {
var directive = $injector.invoke(directiveFactory);
if (isFunction(directive)) {
directive = { compile: valueFn(directive) };
} else if (!directive.compile && directive.link) {
directive.compile = valueFn(directive.link);
}
directive.priority = directive.priority || 0;
directive.name = directive.name || name;
directive.require = directive.require || (directive.controller && directive.name);
directive.restrict = directive.restrict || 'A';
directives.push(directive);
} catch (e) {
$exceptionHandler(e);
}
});
return directives;
}]);
}
hasDirectives[name].push(directiveFactory);
} else {
forEach(name, reverseParams(registerDirective));
}
return this;
};
/**
* @ngdoc function
* @name ng.$compileProvider#aHrefSanitizationWhitelist
* @methodOf ng.$compileProvider
* @function
*
* @description
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during a[href] sanitization.
*
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
*
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
* regular expression. If a match is found, the original url is written into the dom. Otherwise,
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
this.aHrefSanitizationWhitelist = function(regexp) {
if (isDefined(regexp)) {
aHrefSanitizationWhitelist = regexp;
return this;
}
return aHrefSanitizationWhitelist;
};
/**
* @ngdoc function
* @name ng.$compileProvider#imgSrcSanitizationWhitelist
* @methodOf ng.$compileProvider
* @function
*
* @description
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during img[src] sanitization.
*
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
*
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into an
* absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` regular
* expression. If a match is found, the original url is written into the dom. Otherwise, the
* absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
* chaining otherwise.
*/
this.imgSrcSanitizationWhitelist = function(regexp) {
if (isDefined(regexp)) {
imgSrcSanitizationWhitelist = regexp;
return this;
}
return imgSrcSanitizationWhitelist;
};
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate',
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
$controller, $rootScope, $document, $sce, $$urlUtils, $animate) {
var Attributes = function(element, attr) {
this.$$element = element;
this.$attr = attr || {};
};
Attributes.prototype = {
$normalize: directiveNormalize,
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$addClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Adds the CSS class value specified by the classVal parameter to the element. If animations
* are enabled then an animation will be triggered for the class addition.
*
* @param {string} classVal The className value that will be added to the element
*/
$addClass : function(classVal) {
if(classVal && classVal.length > 0) {
$animate.addClass(this.$$element, classVal);
}
},
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$removeClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Removes the CSS class value specified by the classVal parameter from the element. If animations
* are enabled then an animation will be triggered for the class removal.
*
* @param {string} classVal The className value that will be removed from the element
*/
$removeClass : function(classVal) {
if(classVal && classVal.length > 0) {
$animate.removeClass(this.$$element, classVal);
}
},
/**
* Set a normalized attribute on the element in a way such that all directives
* can share the attribute. This function properly handles boolean attributes.
* @param {string} key Normalized key. (ie ngAttribute)
* @param {string|boolean} value The value to set. If `null` attribute will be deleted.
* @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
* Defaults to true.
* @param {string=} attrName Optional none normalized name. Defaults to key.
*/
$set: function(key, value, writeAttr, attrName) {
//special case for class attribute addition + removal
//so that class changes can tap into the animation
//hooks provided by the $animate service
if(key == 'class') {
value = value || '';
var current = this.$$element.attr('class') || '';
this.$removeClass(tokenDifference(current, value).join(' '));
this.$addClass(tokenDifference(value, current).join(' '));
} else {
var booleanKey = getBooleanAttrName(this.$$element[0], key),
normalizedVal,
nodeName;
if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
}
this[key] = value;
// translate normalized key to actual key
if (attrName) {
this.$attr[key] = attrName;
} else {
attrName = this.$attr[key];
if (!attrName) {
this.$attr[key] = attrName = snake_case(key, '-');
}
}
nodeName = nodeName_(this.$$element);
// sanitize a[href] and img[src] values
if ((nodeName === 'A' && key === 'href') ||
(nodeName === 'IMG' && key === 'src')) {
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
if (!msie || msie >= 8 ) {
normalizedVal = $$urlUtils.resolve(value);
if (normalizedVal !== '') {
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
this[key] = value = 'unsafe:' + normalizedVal;
}
}
}
}
if (writeAttr !== false) {
if (value === null || value === undefined) {
this.$$element.removeAttr(attrName);
} else {
this.$$element.attr(attrName, value);
}
}
}
// fire observers
var $$observers = this.$$observers;
$$observers && forEach($$observers[key], function(fn) {
try {
fn(value);
} catch (e) {
$exceptionHandler(e);
}
});
function tokenDifference(str1, str2) {
var values = [],
tokens1 = str1.split(/\s+/),
tokens2 = str2.split(/\s+/);
outer:
for(var i=0;i<tokens1.length;i++) {
var token = tokens1[i];
for(var j=0;j<tokens2.length;j++) {
if(token == tokens2[j]) continue outer;
}
values.push(token);
}
return values;
};
},
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$observe
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Observes an interpolated attribute.
*
* The observer function will be invoked once during the next `$digest` following
* compilation. The observer is then invoked whenever the interpolated value
* changes.
*
* @param {string} key Normalized key. (ie ngAttribute) .
* @param {function(interpolatedValue)} fn Function that will be called whenever
the interpolated value of the attribute changes.
* See the {@link guide/directive#Attributes Directives} guide for more info.
* @returns {function()} the `fn` parameter.
*/
$observe: function(key, fn) {
var attrs = this,
$$observers = (attrs.$$observers || (attrs.$$observers = {})),
listeners = ($$observers[key] || ($$observers[key] = []));
listeners.push(fn);
$rootScope.$evalAsync(function() {
if (!listeners.$$inter) {
// no one registered attribute interpolation function, so lets call it manually
fn(attrs[key]);
}
});
return fn;
}
};
var urlSanitizationNode = $document[0].createElement('a'),
startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
? identity
: function denormalizeTemplate(template) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
},
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
return compile;
//================================
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective) {
if (!($compileNodes instanceof jqLite)) {
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
$compileNodes = jqLite($compileNodes);
}
// We can not compile top level text elements since text nodes can be merged and we will
// not be able to attach scope data to them, so we will wrap them in <span>
forEach($compileNodes, function(node, index){
if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
$compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];
}
});
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective);
return function publicLinkFn(scope, cloneConnectFn){
assertArg(scope, 'scope');
// important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
// and sometimes changes the structure of the DOM.
var $linkNode = cloneConnectFn
? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
: $compileNodes;
// Attach scope only to non-text nodes.
for(var i = 0, ii = $linkNode.length; i<ii; i++) {
var node = $linkNode[i];
if (node.nodeType == 1 /* element */ || node.nodeType == 9 /* document */) {
$linkNode.eq(i).data('$scope', scope);
}
}
safeAddClass($linkNode, 'ng-scope');
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
return $linkNode;
};
}
function safeAddClass($element, className) {
try {
$element.addClass(className);
} catch(e) {
// ignore, since it means that we are trying to set class on
// SVG element, where class name is read-only.
}
}
/**
* Compile function matches each node in nodeList against the directives. Once all directives
* for a particular node are collected their compile functions are executed. The compile
* functions return values - the linking functions - are combined into a composite linking
* function, which is the a linking function for the node.
*
* @param {NodeList} nodeList an array of nodes or NodeList to compile
* @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
* scope argument is auto-generated to the new child of the transcluded parent scope.
* @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then the
* rootElement must be set the jqLite collection of the compile root. This is
* needed so that the jqLite collection items can be replaced with widgets.
* @param {number=} max directive priority
* @returns {?function} A composite linking function of all of the matched directives or null.
*/
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective) {
2013-01-10 00:54:29 +00:00
var linkFns = [],
nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
for(var i = 0; i < nodeList.length; i++) {
attrs = new Attributes();
// we must always refer to nodeList[i] since the nodes can be replaced underneath us.
directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective);
2013-01-10 00:54:29 +00:00
nodeLinkFn = (directives.length)
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [])
2013-01-10 00:54:29 +00:00
: null;
childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length)
2013-01-10 00:54:29 +00:00
? null
: compileNodes(nodeList[i].childNodes,
nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
linkFns.push(nodeLinkFn);
linkFns.push(childLinkFn);
linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
}
// return a linking function if we have found anything, null otherwise
return linkFnFound ? compositeLinkFn : null;
function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n;
2013-01-10 00:54:29 +00:00
// copy nodeList so that linking doesn't break due to live list updates.
var stableNodeList = [];
for (i = 0, ii = nodeList.length; i < ii; i++) {
stableNodeList.push(nodeList[i]);
}
for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
node = stableNodeList[n];
2013-01-10 00:54:29 +00:00
nodeLinkFn = linkFns[i++];
childLinkFn = linkFns[i++];
if (nodeLinkFn) {
if (nodeLinkFn.scope) {
childScope = scope.$new(isObject(nodeLinkFn.scope));
jqLite(node).data('$scope', childScope);
} else {
childScope = scope;
}
childTranscludeFn = nodeLinkFn.transclude;
if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
nodeLinkFn(childLinkFn, childScope, node, $rootElement,
(function(transcludeFn) {
return function(cloneFn) {
var transcludeScope = scope.$new();
transcludeScope.$$transcluded = true;
2013-01-10 00:54:29 +00:00
return transcludeFn(transcludeScope, cloneFn).
on('$destroy', bind(transcludeScope, transcludeScope.$destroy));
};
})(childTranscludeFn || transcludeFn)
2013-01-10 00:54:29 +00:00
);
} else {
nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn);
}
} else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
}
}
}
}
/**
* Looks for directives on the given node and adds them to the directive collection which is
* sorted.
*
* @param node Node to search.
* @param directives An array to which the directives are added to. This array is sorted before
* the function returns.
* @param attrs The shared attrs object which is used to populate the normalized attributes.
* @param {number=} maxPriority Max directive priority.
*/
function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
var nodeType = node.nodeType,
attrsMap = attrs.$attr,
match,
className;
switch(nodeType) {
case 1: /* Element */
// use the node name: <directive>
addDirective(directives,
directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective);
// iterate over the attributes
for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
var attrStartName = false;
var attrEndName = false;
attr = nAttrs[j];
if (!msie || msie >= 8 || attr.specified) {
name = attr.name;
// support ngAttr attribute binding
ngAttrName = directiveNormalize(name);
if (NG_ATTR_BINDING.test(ngAttrName)) {
name = snake_case(ngAttrName.substr(6), '-');
}
var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
if (ngAttrName === directiveNName + 'Start') {
attrStartName = name;
attrEndName = name.substr(0, name.length - 5) + 'end';
name = name.substr(0, name.length - 6);
}
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
attrs[nName] = value = trim((msie && name == 'href')
? decodeURIComponent(node.getAttribute(name, 2))
: attr.value);
2012-06-05 04:11:35 +00:00
if (getBooleanAttrName(node, nName)) {
attrs[nName] = true; // presence means true
}
addAttrInterpolateDirective(node, directives, value, nName);
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName);
}
}
// use class as directive
className = node.className;
if (isString(className) && className !== '') {
while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
nName = directiveNormalize(match[2]);
if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) {
attrs[nName] = trim(match[3]);
}
className = className.substr(match.index + match[0].length);
}
}
break;
case 3: /* Text Node */
addTextInterpolateDirective(directives, node.nodeValue);
break;
case 8: /* Comment */
try {
match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
if (match) {
nName = directiveNormalize(match[1]);
if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
attrs[nName] = trim(match[2]);
}
}
} catch (e) {
// turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value.
// Just ignore it and continue. (Can't seem to reproduce in test case.)
}
break;
}
directives.sort(byPriority);
return directives;
}
2010-03-19 23:41:02 +00:00
/**
* Given a node with an directive-start it collects all of the siblings until it find directive-end.
* @param node
* @param attrStart
* @param attrEnd
* @returns {*}
*/
function groupScan(node, attrStart, attrEnd) {
var nodes = [];
var depth = 0;
if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
var startNode = node;
do {
if (!node) {
throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd);
}
if (node.nodeType == 1 /** Element **/) {
if (node.hasAttribute(attrStart)) depth++;
if (node.hasAttribute(attrEnd)) depth--;
}
nodes.push(node);
node = node.nextSibling;
} while (depth > 0);
} else {
nodes.push(node);
}
return jqLite(nodes);
}
/**
* Wrapper for linking function which converts normal linking function into a grouped
* linking function.
* @param linkFn
* @param attrStart
* @param attrEnd
* @returns {Function}
*/
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
return function(scope, element, attrs, controllers) {
element = groupScan(element[0], attrStart, attrEnd);
return linkFn(scope, element, attrs, controllers);
}
}
2010-03-20 05:18:39 +00:00
/**
2013-04-02 21:38:23 +00:00
* Once the directives have been collected, their compile functions are executed. This method
* is responsible for inlining directive templates as well as terminating the application
2013-04-02 21:38:23 +00:00
* of the directives if the terminal directive has been reached.
*
* @param {Array} directives Array of collected directives to execute their compile function.
* this needs to be pre-sorted by priority order.
* @param {Node} compileNode The raw DOM node to apply the compile functions to
* @param {Object} templateAttrs The shared attribute function
* @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
* scope argument is auto-generated to the new child of the transcluded parent scope.
2012-12-03 19:03:24 +00:00
* @param {JQLite} jqCollection If we are working on the root of the compile tree then this
* argument has the root jqLite array so that we can replace nodes on it.
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
* @param {Object=} ignoreDirective An optional directive that will be ignored when compiling
* the transclusion.
* @param {Array.<Function>} preLinkFns
* @param {Array.<Function>} postLinkFns
* @returns linkFn
*/
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection,
originalReplaceDirective, preLinkFns, postLinkFns) {
var terminalPriority = -Number.MAX_VALUE,
newScopeDirective = null,
newIsolateScopeDirective = null,
templateDirective = null,
$compileNode = templateAttrs.$$element = jqLite(compileNode),
directive,
directiveName,
$template,
transcludeDirective,
replaceDirective = originalReplaceDirective,
childTranscludeFn = transcludeFn,
controllerDirectives,
linkFn,
directiveValue;
// executes all directives on the current element
for(var i = 0, ii = directives.length; i < ii; i++) {
directive = directives[i];
var attrStart = directive.$$start;
var attrEnd = directive.$$end;
// collect multiblock sections
if (attrStart) {
$compileNode = groupScan(compileNode, attrStart, attrEnd)
}
$template = undefined;
if (terminalPriority > directive.priority) {
break; // prevent further processing of directives
}
if (directiveValue = directive.scope) {
newScopeDirective = newScopeDirective || directive;
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
// skip the check for directives with async templates, we'll check the derived sync directive when
// the template arrives
if (!directive.templateUrl) {
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode);
if (isObject(directiveValue)) {
safeAddClass($compileNode, 'ng-isolate-scope');
newIsolateScopeDirective = directive;
}
safeAddClass($compileNode, 'ng-scope');
}
}
directiveName = directive.name;
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
if (!directive.templateUrl && directive.controller) {
directiveValue = directive.controller;
controllerDirectives = controllerDirectives || {};
assertNoDuplicate("'" + directiveName + "' controller",
controllerDirectives[directiveName], directive, $compileNode);
controllerDirectives[directiveName] = directive;
}
if (directiveValue = directive.transclude) {
assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
transcludeDirective = directive;
if (directiveValue == 'element') {
terminalPriority = directive.priority;
$template = groupScan(compileNode, attrStart, attrEnd)
$compileNode = templateAttrs.$$element =
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
compileNode = $compileNode[0];
replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode);
childTranscludeFn = compile($template, transcludeFn, terminalPriority,
replaceDirective && replaceDirective.name);
} else {
$template = jqLite(JQLiteClone(compileNode)).contents();
$compileNode.html(''); // clear contents
childTranscludeFn = compile($template, transcludeFn);
}
}
if (directive.template) {
assertNoDuplicate('template', templateDirective, directive, $compileNode);
templateDirective = directive;
directiveValue = (isFunction(directive.template))
? directive.template($compileNode, templateAttrs)
: directive.template;
directiveValue = denormalizeTemplate(directiveValue);
if (directive.replace) {
replaceDirective = directive;
2012-08-11 06:46:42 +00:00
$template = jqLite('<div>' +
trim(directiveValue) +
'</div>').contents();
compileNode = $template[0];
if ($template.length != 1 || compileNode.nodeType !== 1) {
throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, '');
}
2012-12-03 19:03:24 +00:00
replaceWith(jqCollection, $compileNode, compileNode);
var newTemplateAttrs = {$attr: {}};
// combine directives from the original node and from the template:
// - take the array of directives for this element
// - split it into two parts, those that were already applied and those that weren't
// - collect directives from the template, add them to the second group and sort them
// - append the second group with new directives to the first group
directives = directives.concat(
collectDirectives(
compileNode,
directives.splice(i + 1, directives.length - (i + 1)),
newTemplateAttrs
)
);
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
ii = directives.length;
} else {
$compileNode.html(directiveValue);
}
}
if (directive.templateUrl) {
assertNoDuplicate('template', templateDirective, directive, $compileNode);
templateDirective = directive;
if (directive.replace) {
replaceDirective = directive;
}
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns);
ii = directives.length;
} else if (directive.compile) {
try {
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
if (isFunction(linkFn)) {
addLinkFns(null, linkFn, attrStart, attrEnd);
} else if (linkFn) {
addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
}
} catch (e) {
$exceptionHandler(e, startingTag($compileNode));
}
}
2010-03-19 23:41:02 +00:00
if (directive.terminal) {
nodeLinkFn.terminal = true;
terminalPriority = Math.max(terminalPriority, directive.priority);
}
2010-04-30 19:22:07 +00:00
}
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
// might be normal or delayed nodeLinkFn depending on if templateUrl is present
return nodeLinkFn;
2010-03-19 23:41:02 +00:00
////////////////////
function addLinkFns(pre, post, attrStart, attrEnd) {
if (pre) {
if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
pre.require = directive.require;
preLinkFns.push(pre);
}
if (post) {
if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
post.require = directive.require;
postLinkFns.push(post);
}
}
function getControllers(require, $element) {
var value, retrievalMethod = 'data', optional = false;
if (isString(require)) {
while((value = require.charAt(0)) == '^' || value == '?') {
require = require.substr(1);
if (value == '^') {
retrievalMethod = 'inheritedData';
}
optional = optional || value == '?';
}
value = $element[retrievalMethod]('$' + require + 'Controller');
if ($element[0].nodeType == 8 && $element[0].$$controller) { // Transclusion comment node
value = value || $element[0].$$controller;
$element[0].$$controller = null;
}
if (!value && !optional) {
throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName);
}
return value;
} else if (isArray(require)) {
value = [];
forEach(require, function(require) {
value.push(getControllers(require, $element));
});
}
return value;
}
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
var attrs, $element, i, ii, linkFn, controller;
if (compileNode === linkNode) {
attrs = templateAttrs;
} else {
attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
}
$element = attrs.$$element;
if (newIsolateScopeDirective) {
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
var parentScope = scope.$parent || scope;
forEach(newIsolateScopeDirective.scope, function(definition, scopeName) {
var match = definition.match(LOCAL_REGEXP) || [],
attrName = match[3] || scopeName,
optional = (match[2] == '?'),
mode = match[1], // @, =, or &
lastValue,
parentGet, parentSet;
scope.$$isolateBindings[scopeName] = mode + attrName;
switch (mode) {
case '@': {
attrs.$observe(attrName, function(value) {
scope[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = parentScope;
if( attrs[attrName] ) {
// If the attribute has been provided then we trigger an interpolation to ensure the value is there for use in the link fn
scope[scopeName] = $interpolate(attrs[attrName])(parentScope);
}
break;
}
case '=': {
if (optional && !attrs[attrName]) {
return;
}
parentGet = $parse(attrs[attrName]);
parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest
lastValue = scope[scopeName] = parentGet(parentScope);
throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!",
attrs[attrName], newIsolateScopeDirective.name);
};
lastValue = scope[scopeName] = parentGet(parentScope);
scope.$watch(function parentValueWatch() {
var parentValue = parentGet(parentScope);
if (parentValue !== scope[scopeName]) {
// we are out of sync and need to copy
if (parentValue !== lastValue) {
// parent changed and it has precedence
lastValue = scope[scopeName] = parentValue;
} else {
// if the parent can be assigned then do so
parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
}
}
return parentValue;
});
break;
}
case '&': {
parentGet = $parse(attrs[attrName]);
scope[scopeName] = function(locals) {
return parentGet(parentScope, locals);
};
break;
}
default: {
throw $compileMinErr('iscp', "Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}",
newIsolateScopeDirective.name, scopeName, definition);
}
}
});
}
if (controllerDirectives) {
forEach(controllerDirectives, function(directive) {
var locals = {
$scope: scope,
$element: $element,
$attrs: attrs,
$transclude: boundTranscludeFn
}, controllerInstance;
controller = directive.controller;
if (controller == '@') {
controller = attrs[directive.name];
}
controllerInstance = $controller(controller, locals);
// Directives with element transclusion and a controller need to attach controller
// to the comment node created by the compiler, but jQuery .data doesn't support
// attaching data to comment nodes so instead we set it directly on the element and
// remove it after we read it later.
if ($element[0].nodeType == 8) { // Transclusion comment node
$element[0].$$controller = controllerInstance;
} else {
$element.data('$' + directive.name + 'Controller', controllerInstance);
}
if (directive.controllerAs) {
locals.$scope[directive.controllerAs] = controllerInstance;
}
});
}
// PRELINKING
for(i = 0, ii = preLinkFns.length; i < ii; i++) {
try {
linkFn = preLinkFns[i];
linkFn(scope, $element, attrs,
linkFn.require && getControllers(linkFn.require, $element));
} catch (e) {
$exceptionHandler(e, startingTag($element));
}
}
// RECURSION
childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
// POSTLINKING
2013-10-01 14:55:47 +00:00
for(i = postLinkFns.length - 1; i >= 0; i--) {
try {
linkFn = postLinkFns[i];
linkFn(scope, $element, attrs,
linkFn.require && getControllers(linkFn.require, $element));
} catch (e) {
$exceptionHandler(e, startingTag($element));
}
}
}
}
/**
* looks up the directive and decorates it with exception handling and proper parameters. We
* call this the boundDirective.
*
* @param {string} name name of the directive to look up.
* @param {string} location The directive must be found in specific format.
* String containing any of theses characters:
*
* * `E`: element name
* * `A': attribute
* * `C`: class
* * `M`: comment
* @returns true if directive was added.
*/
function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) {
if (name === ignoreDirective) return null;
var match = null;
if (hasDirectives.hasOwnProperty(name)) {
for(var directive, directives = $injector.get(name + Suffix),
i = 0, ii = directives.length; i<ii; i++) {
try {
directive = directives[i];
if ( (maxPriority === undefined || maxPriority > directive.priority) &&
directive.restrict.indexOf(location) != -1) {
if (startAttrName) {
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
}
tDirectives.push(directive);
match = directive;
}
} catch(e) { $exceptionHandler(e); }
}
}
return match;
}
/**
* When the element is replaced with HTML template then the new attributes
* on the template need to be merged with the existing attributes in the DOM.
* The desired effect is to have both of the attributes present.
*
* @param {object} dst destination attributes (original DOM)
* @param {object} src source attributes (from the directive template)
*/
function mergeTemplateAttributes(dst, src) {
var srcAttr = src.$attr,
dstAttr = dst.$attr,
$element = dst.$$element;
// reapply the old attributes to the new element
forEach(dst, function(value, key) {
if (key.charAt(0) != '$') {
if (src[key]) {
value += (key === 'style' ? ';' : ' ') + src[key];
}
dst.$set(key, value, true, srcAttr[key]);
}
});
// copy the new attributes on the old attrs object
forEach(src, function(value, key) {
if (key == 'class') {
safeAddClass($element, value);
dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
} else if (key == 'style') {
$element.attr('style', $element.attr('style') + ';' + value);
} else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
dst[key] = value;
dstAttr[key] = srcAttr[key];
}
});
}
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
function compileTemplateUrl(directives, $compileNode, tAttrs,
$rootElement, childTranscludeFn, preLinkFns, postLinkFns) {
var linkQueue = [],
afterTemplateNodeLinkFn,
afterTemplateChildLinkFn,
beforeTemplateCompileNode = $compileNode[0],
origAsyncDirective = directives.shift(),
// The fact that we have to copy and patch the directive seems wrong!
derivedSyncDirective = extend({}, origAsyncDirective, {
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
templateUrl: null, transclude: null, replace: null
}),
templateUrl = (isFunction(origAsyncDirective.templateUrl))
? origAsyncDirective.templateUrl($compileNode, tAttrs)
: origAsyncDirective.templateUrl;
$compileNode.html('');
feat($sce): new $sce service for Strict Contextual Escaping. $sce is a service that provides Strict Contextual Escaping services to AngularJS. Strict Contextual Escaping -------------------------- Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context One example of such a context is binding arbitrary html controlled by the user via ng-bind-html-unsafe. We refer to these contexts as privileged or SCE contexts. As of version 1.2, Angular ships with SCE enabled by default. Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows one to execute arbitrary javascript by the use of the expression() syntax. Refer http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx to learn more about them. You can ensure your document is in standards mode and not quirks mode by adding <!doctype html> to the top of your HTML document. SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. Here's an example of a binding in a privileged context: <input ng-model="userHtml"> <div ng-bind-html-unsafe="{{userHtml}}"> Notice that ng-bind-html-unsafe is bound to {{userHtml}} controlled by the user. With SCE disabled, this application allows the user to render arbitrary HTML into the DIV. In a more realistic example, one may be rendering user comments, blog articles, etc. via bindings. (HTML is just one example of a context where rendering user controlled input creates security vulnerabilities.) For the case of HTML, you might use a library, either on the client side, or on the server side, to sanitize unsafe HTML before binding to the value and rendering it in the document. How would you ensure that every place that used these types of bindings was bound to a value that was sanitized by your library (or returned as safe for rendering by your server?) How can you ensure that you didn't accidentally delete the line that sanitized the value, or renamed some properties/fields and forgot to update the binding to the sanitized value? To be secure by default, you want to ensure that any such bindings are disallowed unless you can determine that something explicitly says it's safe to use a value for binding in that context. You can then audit your code (a simple grep would do) to ensure that this is only done for those values that you can easily tell are safe - because they were received from your server, sanitized by your library, etc. You can organize your codebase to help with this - perhaps allowing only the files in a specific directory to do this. Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. In the case of AngularJS' SCE service, one uses $sce.trustAs (and shorthand methods such as $sce.trustAsHtml, etc.) to obtain values that will be accepted by SCE / privileged contexts. In privileged contexts, directives and code will bind to the result of $sce.getTrusted(context, value) rather than to the value directly. Directives use $sce.parseAs rather than $parse to watch attribute bindings, which performs the $sce.getTrusted behind the scenes on non-constant literals. As an example, ngBindHtmlUnsafe uses $sce.parseAsHtml(binding expression). Here's the actual code (slightly simplified): var ngBindHtmlUnsafeDirective = ['$sce', function($sce) { return function(scope, element, attr) { scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) { element.html(value || ''); }); }; }]; Impact on loading templates --------------------------- This applies both to the ng-include directive as well as templateUrl's specified by directives. By default, Angular only loads templates from the same domain and protocol as the application document. This is done by calling $sce.getTrustedResourceUrl on the template URL. To load templates from other domains and/or protocols, you may either either whitelist them or wrap it into a trusted value. *Please note*: The browser's Same Origin Policy and Cross-Origin Resource Sharing (CORS) policy apply in addition to this and may further restrict whether the template is successfully loaded. This means that without the right CORS policy, loading templates from a different domain won't work on all browsers. Also, loading templates from file:// URL does not work on some browsers. This feels like too much overhead for the developer? ---------------------------------------------------- It's important to remember that SCE only applies to interpolation expressions. If your expressions are constant literals, they're automatically trusted and you don't need to call $sce.trustAs on them. e.g. <div ng-html-bind-unsafe="'<b>implicitly trusted</b>'"></div> just works. Additionally, a[href] and img[src] automatically sanitize their URLs and do not pass them through $sce.getTrusted. SCE doesn't play a role here. The included $sceDelegate comes with sane defaults to allow you to load templates in ng-include from your application's domain without having to even know about SCE. It blocks loading templates from other domains or loading templates over http from an https served document. You can change these by setting your own custom whitelists and blacklists for matching such URLs. This significantly reduces the overhead. It is far easier to pay the small overhead and have an application that's secure and can be audited to verify that with much more ease than bolting security onto an application later.
2013-05-14 21:51:39 +00:00
$http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}).
success(function(content) {
var compileNode, tempTemplateAttrs, $template;
content = denormalizeTemplate(content);
if (origAsyncDirective.replace) {
$template = jqLite('<div>' + trim(content) + '</div>').contents();
compileNode = $template[0];
if ($template.length != 1 || compileNode.nodeType !== 1) {
throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}",
origAsyncDirective.name, templateUrl);
}
tempTemplateAttrs = {$attr: {}};
replaceWith($rootElement, $compileNode, compileNode);
collectDirectives(compileNode, directives, tempTemplateAttrs);
mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
} else {
compileNode = beforeTemplateCompileNode;
$compileNode.html(content);
}
directives.unshift(derivedSyncDirective);
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs,
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns);
forEach($rootElement, function(node, i) {
if (node == compileNode) {
$rootElement[i] = $compileNode[0];
}
});
afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
while(linkQueue.length) {
var scope = linkQueue.shift(),
beforeTemplateLinkNode = linkQueue.shift(),
linkRootElement = linkQueue.shift(),
controller = linkQueue.shift(),
linkNode = $compileNode[0];
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
// it was cloned therefore we have to clone as well.
linkNode = JQLiteClone(compileNode);
replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
}
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
}
linkQueue = null;
}).
error(function(response, code, headers, config) {
throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url);
});
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) {
if (linkQueue) {
linkQueue.push(scope);
linkQueue.push(node);
linkQueue.push(rootElement);
linkQueue.push(controller);
} else {
fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927
2013-09-08 17:05:25 +00:00
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
}
};
}
/**
* Sorting function for bound directives.
*/
function byPriority(a, b) {
return b.priority - a.priority;
}
function assertNoDuplicate(what, previousDirective, directive, element) {
if (previousDirective) {
throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}',
previousDirective.name, directive.name, what, startingTag(element));
}
}
function addTextInterpolateDirective(directives, text) {
var interpolateFn = $interpolate(text, true);
if (interpolateFn) {
directives.push({
priority: 0,
compile: valueFn(function textInterpolateLinkFn(scope, node) {
var parent = node.parent(),
bindings = parent.data('$binding') || [];
bindings.push(interpolateFn);
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
});
})
});
}
}
feat($sce): new $sce service for Strict Contextual Escaping. $sce is a service that provides Strict Contextual Escaping services to AngularJS. Strict Contextual Escaping -------------------------- Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context One example of such a context is binding arbitrary html controlled by the user via ng-bind-html-unsafe. We refer to these contexts as privileged or SCE contexts. As of version 1.2, Angular ships with SCE enabled by default. Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows one to execute arbitrary javascript by the use of the expression() syntax. Refer http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx to learn more about them. You can ensure your document is in standards mode and not quirks mode by adding <!doctype html> to the top of your HTML document. SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. Here's an example of a binding in a privileged context: <input ng-model="userHtml"> <div ng-bind-html-unsafe="{{userHtml}}"> Notice that ng-bind-html-unsafe is bound to {{userHtml}} controlled by the user. With SCE disabled, this application allows the user to render arbitrary HTML into the DIV. In a more realistic example, one may be rendering user comments, blog articles, etc. via bindings. (HTML is just one example of a context where rendering user controlled input creates security vulnerabilities.) For the case of HTML, you might use a library, either on the client side, or on the server side, to sanitize unsafe HTML before binding to the value and rendering it in the document. How would you ensure that every place that used these types of bindings was bound to a value that was sanitized by your library (or returned as safe for rendering by your server?) How can you ensure that you didn't accidentally delete the line that sanitized the value, or renamed some properties/fields and forgot to update the binding to the sanitized value? To be secure by default, you want to ensure that any such bindings are disallowed unless you can determine that something explicitly says it's safe to use a value for binding in that context. You can then audit your code (a simple grep would do) to ensure that this is only done for those values that you can easily tell are safe - because they were received from your server, sanitized by your library, etc. You can organize your codebase to help with this - perhaps allowing only the files in a specific directory to do this. Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. In the case of AngularJS' SCE service, one uses $sce.trustAs (and shorthand methods such as $sce.trustAsHtml, etc.) to obtain values that will be accepted by SCE / privileged contexts. In privileged contexts, directives and code will bind to the result of $sce.getTrusted(context, value) rather than to the value directly. Directives use $sce.parseAs rather than $parse to watch attribute bindings, which performs the $sce.getTrusted behind the scenes on non-constant literals. As an example, ngBindHtmlUnsafe uses $sce.parseAsHtml(binding expression). Here's the actual code (slightly simplified): var ngBindHtmlUnsafeDirective = ['$sce', function($sce) { return function(scope, element, attr) { scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) { element.html(value || ''); }); }; }]; Impact on loading templates --------------------------- This applies both to the ng-include directive as well as templateUrl's specified by directives. By default, Angular only loads templates from the same domain and protocol as the application document. This is done by calling $sce.getTrustedResourceUrl on the template URL. To load templates from other domains and/or protocols, you may either either whitelist them or wrap it into a trusted value. *Please note*: The browser's Same Origin Policy and Cross-Origin Resource Sharing (CORS) policy apply in addition to this and may further restrict whether the template is successfully loaded. This means that without the right CORS policy, loading templates from a different domain won't work on all browsers. Also, loading templates from file:// URL does not work on some browsers. This feels like too much overhead for the developer? ---------------------------------------------------- It's important to remember that SCE only applies to interpolation expressions. If your expressions are constant literals, they're automatically trusted and you don't need to call $sce.trustAs on them. e.g. <div ng-html-bind-unsafe="'<b>implicitly trusted</b>'"></div> just works. Additionally, a[href] and img[src] automatically sanitize their URLs and do not pass them through $sce.getTrusted. SCE doesn't play a role here. The included $sceDelegate comes with sane defaults to allow you to load templates in ng-include from your application's domain without having to even know about SCE. It blocks loading templates from other domains or loading templates over http from an https served document. You can change these by setting your own custom whitelists and blacklists for matching such URLs. This significantly reduces the overhead. It is far easier to pay the small overhead and have an application that's secure and can be audited to verify that with much more ease than bolting security onto an application later.
2013-05-14 21:51:39 +00:00
function getTrustedContext(node, attrNormalizedName) {
// maction[xlink:href] can source SVG. It's not limited to <maction>.
if (attrNormalizedName == "xlinkHref" ||
(nodeName_(node) != "IMG" && (attrNormalizedName == "src" ||
attrNormalizedName == "ngSrc"))) {
feat($sce): new $sce service for Strict Contextual Escaping. $sce is a service that provides Strict Contextual Escaping services to AngularJS. Strict Contextual Escaping -------------------------- Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context One example of such a context is binding arbitrary html controlled by the user via ng-bind-html-unsafe. We refer to these contexts as privileged or SCE contexts. As of version 1.2, Angular ships with SCE enabled by default. Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows one to execute arbitrary javascript by the use of the expression() syntax. Refer http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx to learn more about them. You can ensure your document is in standards mode and not quirks mode by adding <!doctype html> to the top of your HTML document. SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. Here's an example of a binding in a privileged context: <input ng-model="userHtml"> <div ng-bind-html-unsafe="{{userHtml}}"> Notice that ng-bind-html-unsafe is bound to {{userHtml}} controlled by the user. With SCE disabled, this application allows the user to render arbitrary HTML into the DIV. In a more realistic example, one may be rendering user comments, blog articles, etc. via bindings. (HTML is just one example of a context where rendering user controlled input creates security vulnerabilities.) For the case of HTML, you might use a library, either on the client side, or on the server side, to sanitize unsafe HTML before binding to the value and rendering it in the document. How would you ensure that every place that used these types of bindings was bound to a value that was sanitized by your library (or returned as safe for rendering by your server?) How can you ensure that you didn't accidentally delete the line that sanitized the value, or renamed some properties/fields and forgot to update the binding to the sanitized value? To be secure by default, you want to ensure that any such bindings are disallowed unless you can determine that something explicitly says it's safe to use a value for binding in that context. You can then audit your code (a simple grep would do) to ensure that this is only done for those values that you can easily tell are safe - because they were received from your server, sanitized by your library, etc. You can organize your codebase to help with this - perhaps allowing only the files in a specific directory to do this. Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. In the case of AngularJS' SCE service, one uses $sce.trustAs (and shorthand methods such as $sce.trustAsHtml, etc.) to obtain values that will be accepted by SCE / privileged contexts. In privileged contexts, directives and code will bind to the result of $sce.getTrusted(context, value) rather than to the value directly. Directives use $sce.parseAs rather than $parse to watch attribute bindings, which performs the $sce.getTrusted behind the scenes on non-constant literals. As an example, ngBindHtmlUnsafe uses $sce.parseAsHtml(binding expression). Here's the actual code (slightly simplified): var ngBindHtmlUnsafeDirective = ['$sce', function($sce) { return function(scope, element, attr) { scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) { element.html(value || ''); }); }; }]; Impact on loading templates --------------------------- This applies both to the ng-include directive as well as templateUrl's specified by directives. By default, Angular only loads templates from the same domain and protocol as the application document. This is done by calling $sce.getTrustedResourceUrl on the template URL. To load templates from other domains and/or protocols, you may either either whitelist them or wrap it into a trusted value. *Please note*: The browser's Same Origin Policy and Cross-Origin Resource Sharing (CORS) policy apply in addition to this and may further restrict whether the template is successfully loaded. This means that without the right CORS policy, loading templates from a different domain won't work on all browsers. Also, loading templates from file:// URL does not work on some browsers. This feels like too much overhead for the developer? ---------------------------------------------------- It's important to remember that SCE only applies to interpolation expressions. If your expressions are constant literals, they're automatically trusted and you don't need to call $sce.trustAs on them. e.g. <div ng-html-bind-unsafe="'<b>implicitly trusted</b>'"></div> just works. Additionally, a[href] and img[src] automatically sanitize their URLs and do not pass them through $sce.getTrusted. SCE doesn't play a role here. The included $sceDelegate comes with sane defaults to allow you to load templates in ng-include from your application's domain without having to even know about SCE. It blocks loading templates from other domains or loading templates over http from an https served document. You can change these by setting your own custom whitelists and blacklists for matching such URLs. This significantly reduces the overhead. It is far easier to pay the small overhead and have an application that's secure and can be audited to verify that with much more ease than bolting security onto an application later.
2013-05-14 21:51:39 +00:00
return $sce.RESOURCE_URL;
}
}
function addAttrInterpolateDirective(node, directives, value, name) {
var interpolateFn = $interpolate(value, true);
// no interpolation found -> ignore
if (!interpolateFn) return;
if (name === "multiple" && nodeName_(node) === "SELECT") {
throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}",
startingTag(node));
}
directives.push({
2013-10-01 14:55:47 +00:00
priority: -100,
compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) {
var $$observers = (attr.$$observers || (attr.$$observers = {}));
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
throw $compileMinErr('nodomevents',
"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- " +
"versions (such as ng-click instead of onclick) instead.");
}
// we need to interpolate again, in case the attribute value has been updated
// (e.g. by another directive's compile function)
feat($sce): new $sce service for Strict Contextual Escaping. $sce is a service that provides Strict Contextual Escaping services to AngularJS. Strict Contextual Escaping -------------------------- Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context One example of such a context is binding arbitrary html controlled by the user via ng-bind-html-unsafe. We refer to these contexts as privileged or SCE contexts. As of version 1.2, Angular ships with SCE enabled by default. Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows one to execute arbitrary javascript by the use of the expression() syntax. Refer http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx to learn more about them. You can ensure your document is in standards mode and not quirks mode by adding <!doctype html> to the top of your HTML document. SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. Here's an example of a binding in a privileged context: <input ng-model="userHtml"> <div ng-bind-html-unsafe="{{userHtml}}"> Notice that ng-bind-html-unsafe is bound to {{userHtml}} controlled by the user. With SCE disabled, this application allows the user to render arbitrary HTML into the DIV. In a more realistic example, one may be rendering user comments, blog articles, etc. via bindings. (HTML is just one example of a context where rendering user controlled input creates security vulnerabilities.) For the case of HTML, you might use a library, either on the client side, or on the server side, to sanitize unsafe HTML before binding to the value and rendering it in the document. How would you ensure that every place that used these types of bindings was bound to a value that was sanitized by your library (or returned as safe for rendering by your server?) How can you ensure that you didn't accidentally delete the line that sanitized the value, or renamed some properties/fields and forgot to update the binding to the sanitized value? To be secure by default, you want to ensure that any such bindings are disallowed unless you can determine that something explicitly says it's safe to use a value for binding in that context. You can then audit your code (a simple grep would do) to ensure that this is only done for those values that you can easily tell are safe - because they were received from your server, sanitized by your library, etc. You can organize your codebase to help with this - perhaps allowing only the files in a specific directory to do this. Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. In the case of AngularJS' SCE service, one uses $sce.trustAs (and shorthand methods such as $sce.trustAsHtml, etc.) to obtain values that will be accepted by SCE / privileged contexts. In privileged contexts, directives and code will bind to the result of $sce.getTrusted(context, value) rather than to the value directly. Directives use $sce.parseAs rather than $parse to watch attribute bindings, which performs the $sce.getTrusted behind the scenes on non-constant literals. As an example, ngBindHtmlUnsafe uses $sce.parseAsHtml(binding expression). Here's the actual code (slightly simplified): var ngBindHtmlUnsafeDirective = ['$sce', function($sce) { return function(scope, element, attr) { scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) { element.html(value || ''); }); }; }]; Impact on loading templates --------------------------- This applies both to the ng-include directive as well as templateUrl's specified by directives. By default, Angular only loads templates from the same domain and protocol as the application document. This is done by calling $sce.getTrustedResourceUrl on the template URL. To load templates from other domains and/or protocols, you may either either whitelist them or wrap it into a trusted value. *Please note*: The browser's Same Origin Policy and Cross-Origin Resource Sharing (CORS) policy apply in addition to this and may further restrict whether the template is successfully loaded. This means that without the right CORS policy, loading templates from a different domain won't work on all browsers. Also, loading templates from file:// URL does not work on some browsers. This feels like too much overhead for the developer? ---------------------------------------------------- It's important to remember that SCE only applies to interpolation expressions. If your expressions are constant literals, they're automatically trusted and you don't need to call $sce.trustAs on them. e.g. <div ng-html-bind-unsafe="'<b>implicitly trusted</b>'"></div> just works. Additionally, a[href] and img[src] automatically sanitize their URLs and do not pass them through $sce.getTrusted. SCE doesn't play a role here. The included $sceDelegate comes with sane defaults to allow you to load templates in ng-include from your application's domain without having to even know about SCE. It blocks loading templates from other domains or loading templates over http from an https served document. You can change these by setting your own custom whitelists and blacklists for matching such URLs. This significantly reduces the overhead. It is far easier to pay the small overhead and have an application that's secure and can be audited to verify that with much more ease than bolting security onto an application later.
2013-05-14 21:51:39 +00:00
interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name));
// if attribute was updated so that there is no interpolation going on we don't want to
// register any observers
if (!interpolateFn) return;
2013-10-01 14:55:47 +00:00
// TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the actual attr value
attr[name] = interpolateFn(scope);
($$observers[name] || ($$observers[name] = [])).$$inter = true;
(attr.$$observers && attr.$$observers[name].$$scope || scope).
$watch(interpolateFn, function interpolateFnWatchAction(value) {
attr.$set(name, value);
});
})
});
}
/**
* This is a special jqLite.replaceWith, which can replace items which
* have no parents, provided that the containing jqLite collection is provided.
*
* @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
* in the root of the tree.
* @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell,
* but replace its DOM node reference.
* @param {Node} newNode The new DOM node.
*/
function replaceWith($rootElement, elementsToRemove, newNode) {
var firstElementToRemove = elementsToRemove[0],
removeCount = elementsToRemove.length,
parent = firstElementToRemove.parentNode,
i, ii;
if ($rootElement) {
for(i = 0, ii = $rootElement.length; i < ii; i++) {
if ($rootElement[i] == firstElementToRemove) {
$rootElement[i++] = newNode;
for (var j = i, j2 = j + removeCount - 1,
jj = $rootElement.length;
j < jj; j++, j2++) {
if (j2 < jj) {
$rootElement[j] = $rootElement[j2];
} else {
delete $rootElement[j];
}
}
$rootElement.length -= removeCount - 1;
break;
}
}
}
if (parent) {
parent.replaceChild(newNode, firstElementToRemove);
}
var fragment = document.createDocumentFragment();
fragment.appendChild(firstElementToRemove);
newNode[jqLite.expando] = firstElementToRemove[jqLite.expando];
for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
var element = elementsToRemove[k];
jqLite(element).remove(); // must do this way to clean up expando
fragment.appendChild(element);
delete elementsToRemove[k];
}
elementsToRemove[0] = newNode;
elementsToRemove.length = 1
}
}];
}
var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
/**
* Converts all accepted directives format into proper directive name.
* All of these will become 'myDirective':
* my:Directive
* my-directive
* x-my-directive
* data-my:directive
*
* Also there is special case for Moz prefix starting with upper case letter.
* @param name Name to normalize
*/
function directiveNormalize(name) {
return camelCase(name.replace(PREFIX_REGEXP, ''));
2010-03-30 21:55:04 +00:00
}
/**
* @ngdoc object
* @name ng.$compile.directive.Attributes
* @description
*
* A shared object between directive compile / linking functions which contains normalized DOM element
* attributes. The the values reflect current binding state `{{ }}`. The normalization is needed
* since all of these are treated as equivalent in Angular:
*
* <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
*/
/**
* @ngdoc property
* @name ng.$compile.directive.Attributes#$attr
* @propertyOf ng.$compile.directive.Attributes
* @returns {object} A map of DOM element attribute names to the normalized name. This is
* needed to do reverse lookup from normalized name back to actual name.
*/
/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$set
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Set DOM element attribute value.
*
*
* @param {string} name Normalized element attribute name of the property to modify. The name is
* revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
* property to the original name.
* @param {string} value Value to set the attribute to. The value can be an interpolated string.
*/
/**
* Closure compiler type information
*/
function nodesetLinkingFn(
/* angular.Scope */ scope,
/* NodeList */ nodeList,
/* Element */ rootElement,
/* function(Function) */ boundTranscludeFn
){}
function directiveLinkingFn(
/* nodesetLinkingFn */ nodesetLinkingFn,
/* angular.Scope */ scope,
/* Node */ node,
/* Element */ rootElement,
/* function(Function) */ boundTranscludeFn
){}