Skip to content

Commit 7e1f36e

Browse files
BREAKING CHANGE: Use angular 1.3+ $templateRequest service to fetch templates
We now fetch templates using `$templateRequest` when it is available (angular 1.3+). You can revert to previous template fetching behavior using `$http` by configuring the ui-router `$templateFactoryProvider`. ```js .config(function($templateFactoryProvider) { $templateFactoryProvider.shouldUnsafelyUseHttp(true); }); ``` There are security ramifications to using `$http` to fetch templates. Read [Impact on loading templates](https://docs.angularjs.org/api/ng/service/$sce#impact-on-loading-templates) for more details Closes #3193 Closes #1882
1 parent 01f7d22 commit 7e1f36e

File tree

6 files changed

+103
-41
lines changed

6 files changed

+103
-41
lines changed

src/interface.ts

+30
Original file line numberDiff line numberDiff line change
@@ -618,3 +618,33 @@ export interface Ng1Controller {
618618
*/
619619
uiCanExit(): HookResult;
620620
}
621+
622+
/**
623+
* Manages which template-loading mechanism to use.
624+
*
625+
* Defaults to `$templateRequest` on Angular versions starting from 1.3, `$http` otherwise.
626+
*/
627+
export interface TemplateFactoryProvider {
628+
/**
629+
* Forces $templateFactory to use $http instead of $templateRequest.
630+
*
631+
* UI-Router uses `$templateRequest` by default on angular 1.3+.
632+
* Use this method to choose to use `$http` instead.
633+
*
634+
* ---
635+
*
636+
* ## Security warning
637+
*
638+
* This might cause XSS, as $http doesn't enforce the regular security checks for
639+
* templates that have been introduced in Angular 1.3.
640+
*
641+
* See the $sce documentation, section
642+
* <a href="https://docs.angularjs.org/api/ng/service/$sce#impact-on-loading-templates">
643+
* Impact on loading templates</a> for more details about this mechanism.
644+
*
645+
* *Note: forcing this to `false` on Angular 1.2.x will crash, because `$templateRequest` is not implemented.*
646+
*
647+
* @param useUnsafeHttpService `true` to use `$http` to fetch templates
648+
*/
649+
useHttpService(useUnsafeHttpService: boolean);
650+
}

src/services.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@
88
* @module ng1
99
* @preferred
1010
*/
11-
1211
/** for typedoc */
1312
import { ng as angular } from "./angular";
1413
import { TypedMap } from "ui-router-core"; // has or is using
1514
import {
16-
IRootScopeService, IQService, ILocationService, ILocationProvider, IHttpService, ITemplateCacheService
15+
IRootScopeService, IQService, ILocationService, ILocationProvider, IHttpService, ITemplateCacheService
1716
} from "angular";
1817
import {
19-
services, applyPairs, prop, isString, trace, extend, UIRouter, StateService, UrlRouter, UrlMatcherFactory,
20-
ResolveContext
18+
services, applyPairs, isString, trace, extend, UIRouter, StateService, UrlRouter, UrlMatcherFactory, ResolveContext
2119
} from "ui-router-core";
22-
import { ng1ViewsBuilder, ng1ViewConfigFactory } from "./statebuilders/views";
20+
import { ng1ViewsBuilder, getNg1ViewConfigFactory } from "./statebuilders/views";
2321
import { TemplateFactory } from "./templateFactory";
2422
import { StateProvider } from "./stateProvider";
2523
import { getStateHookBuilder } from "./statebuilders/onEnterExitRetain";
@@ -60,7 +58,7 @@ function $uiRouter($locationProvider: ILocationProvider) {
6058
router.stateRegistry.decorator("onRetain", getStateHookBuilder("onRetain"));
6159
router.stateRegistry.decorator("onEnter", getStateHookBuilder("onEnter"));
6260

63-
router.viewService._pluginapi._viewConfigFactory('ng1', ng1ViewConfigFactory);
61+
router.viewService._pluginapi._viewConfigFactory('ng1', getNg1ViewConfigFactory());
6462

6563
let ng1LocationService = router.locationService = router.locationConfig = new Ng1LocationServices($locationProvider);
6664

@@ -109,13 +107,13 @@ export function watchDigests($rootScope: IRootScopeService) {
109107
mod_init .provider("$uiRouter", <any> $uiRouter);
110108
mod_rtr .provider('$urlRouter', ['$uiRouterProvider', getUrlRouterProvider]);
111109
mod_util .provider('$urlMatcherFactory', ['$uiRouterProvider', () => router.urlMatcherFactory]);
110+
mod_util .provider('$templateFactory', () => new TemplateFactory());
112111
mod_state.provider('$stateRegistry', getProviderFor('stateRegistry'));
113112
mod_state.provider('$uiRouterGlobals', getProviderFor('globals'));
114113
mod_state.provider('$transitions', getProviderFor('transitionService'));
115114
mod_state.provider('$state', ['$uiRouterProvider', getStateProvider]);
116115

117116
mod_state.factory ('$stateParams', ['$uiRouter', ($uiRouter: UIRouter) => $uiRouter.globals.params]);
118-
mod_util .factory ('$templateFactory', ['$uiRouter', () => new TemplateFactory()]);
119117
mod_main .factory ('$view', () => router.viewService);
120118
mod_main .service ("$trace", () => trace);
121119

src/statebuilders/views.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ import { Ng1ViewDeclaration } from "../interface";
99
import { TemplateFactory } from "../templateFactory";
1010
import IInjectorService = angular.auto.IInjectorService;
1111

12-
export const ng1ViewConfigFactory: ViewConfigFactory = (path, view) =>
13-
[new Ng1ViewConfig(path, view)];
12+
export function getNg1ViewConfigFactory(): ViewConfigFactory {
13+
let templateFactory: TemplateFactory = null;
14+
return (path, view) => {
15+
templateFactory = templateFactory || services.$injector.get("$templateFactory");
16+
return [new Ng1ViewConfig(path, view, templateFactory)];
17+
};
18+
}
1419

1520
const hasAnyKey = (keys, obj) =>
1621
keys.reduce((acc, key) => acc || isDefined(obj[key]), false);
@@ -69,10 +74,8 @@ export class Ng1ViewConfig implements ViewConfig {
6974
template: string;
7075
component: string;
7176
locals: any; // TODO: delete me
72-
factory = new TemplateFactory();
7377

74-
constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration) {
75-
}
78+
constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration, public factory: TemplateFactory) { }
7679

7780
load() {
7881
let $q = services.$q;

src/templateFactory.ts

+28-14
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,28 @@ import {
66
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext,
77
Resolvable, RawParams, prop
88
} from "ui-router-core";
9-
import { Ng1ViewDeclaration } from "./interface";
10-
11-
const service = (token) => {
12-
const $injector = services.$injector;
13-
return $injector.has ? ($injector.has(token) && $injector.get(token)) : $injector.get(token);
14-
};
9+
import { Ng1ViewDeclaration, TemplateFactoryProvider } from "./interface";
1510

1611
/**
1712
* Service which manages loading of templates from a ViewConfig.
1813
*/
19-
export class TemplateFactory {
20-
private $templateRequest = service('$templateRequest');
21-
private $templateCache = service('$templateCache');
22-
private $http = service('$http');
14+
export class TemplateFactory implements TemplateFactoryProvider {
15+
/** @hidden */ private _useHttp = angular.version.minor < 3;
16+
/** @hidden */ private $templateRequest;
17+
/** @hidden */ private $templateCache;
18+
/** @hidden */ private $http;
19+
20+
/** @hidden */ $get = ['$http', '$templateCache', '$injector', ($http, $templateCache, $injector) => {
21+
this.$templateRequest = $injector.has && $injector.has('$templateRequest') && $injector.get('$templateRequest');
22+
this.$http = $http;
23+
this.$templateCache = $templateCache;
24+
return this;
25+
}];
26+
27+
/** @hidden */
28+
useHttpService(value: boolean) {
29+
this._useHttp = value;
30+
};
2331

2432
/**
2533
* Creates a template from a configuration object.
@@ -76,12 +84,12 @@ export class TemplateFactory {
7684
if (isFunction(url)) url = (<any> url)(params);
7785
if (url == null) return null;
7886

79-
if(this.$templateRequest) {
80-
return this.$templateRequest(url);
87+
if (this._useHttp) {
88+
return this.$http.get(url, { cache: this.$templateCache, headers: { Accept: 'text/html' }})
89+
.then(function(response) { return response.data; });
8190
}
8291

83-
return this.$http.get(url, { cache: this.$templateCache, headers: { Accept: 'text/html' }})
84-
.then(function(response) { return response.data; });
92+
return this.$templateRequest(url);
8593
};
8694

8795
/**
@@ -116,6 +124,11 @@ export class TemplateFactory {
116124
/**
117125
* Creates a template from a component's name
118126
*
127+
* This implements route-to-component.
128+
* It works by retrieving the component (directive) metadata from the injector.
129+
* It analyses the component's bindings, then constructs a template that instantiates the component.
130+
* The template wires input and output bindings to resolves or from the parent component.
131+
*
119132
* @param uiView {object} The parent ui-view (for binding outputs to callbacks)
120133
* @param context The ResolveContext (for binding outputs to callbacks returned from resolves)
121134
* @param component {string} Component's name in camel case.
@@ -150,6 +163,7 @@ export class TemplateFactory {
150163
let res = context.getResolvable(resolveName);
151164
let fn = res && res.data;
152165
let args = fn && services.$injector.annotate(fn) || [];
166+
// account for array style injection, i.e., ['foo', function(foo) {}]
153167
let arrayIdxStr = isArray(fn) ? `[${fn.length - 1}]` : '';
154168
return `${attrName}='$resolve.${resolveName}${arrayIdxStr}(${args.join(",")})'`;
155169
}

test/templateFactorySpec.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ declare let inject;
55
let module = angular['mock'].module;
66

77
describe('templateFactory', function () {
8-
98
beforeEach(module('ui.router'));
109

1110
it('exists', inject(function ($templateFactory) {
1211
expect($templateFactory).toBeDefined();
1312
}));
1413

15-
if (angular.version.major >= 1 && angular.version.minor >= 3) {
14+
if (angular.version.minor >= 3) {
1615
// Post 1.2, there is a $templateRequest and a $sce service
1716
describe('should follow $sce policy and', function() {
1817
it('accepts relative URLs', inject(function($templateFactory, $httpBackend, $sce) {
@@ -40,7 +39,9 @@ describe('templateFactory', function () {
4039
$httpBackend.flush();
4140
}));
4241
});
43-
} else { // 1.2 and before will use directly $http
42+
}
43+
44+
if (angular.version.minor <= 2) { // 1.2 and before will use directly $http
4445
it('does not restrict URL loading', inject(function($templateFactory, $httpBackend) {
4546
$httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!');
4647
$templateFactory.fromUrl('http://evil.com/views/view.html');
@@ -60,4 +61,26 @@ describe('templateFactory', function () {
6061
$httpBackend.flush();
6162
}));
6263
}
64+
65+
describe('templateFactory with forced use of $http service', function () {
66+
beforeEach(function() {
67+
angular
68+
.module('forceHttpInTemplateFactory', [])
69+
.config(function($templateFactoryProvider) {
70+
$templateFactoryProvider.useHttpService(true);
71+
});
72+
module('ui.router');
73+
module('forceHttpInTemplateFactory');
74+
});
75+
76+
it('does not restrict URL loading', inject(function($templateFactory, $httpBackend) {
77+
$httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!');
78+
$templateFactory.fromUrl('http://evil.com/views/view.html');
79+
$httpBackend.flush();
80+
81+
$httpBackend.expectGET('data:text/html,foo').respond(200, 'template!');
82+
$templateFactory.fromUrl('data:text/html,foo');
83+
$httpBackend.flush();
84+
}));
85+
});
6386
});

test/viewSpec.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
import * as angular from "angular";
22
import "./util/matchers";
3+
import {
4+
inherit, extend, tail, curry, PathNode, PathFactory, ViewService, StateMatcher, StateBuilder, State
5+
} from "ui-router-core";
6+
import { ng1ViewsBuilder, getNg1ViewConfigFactory } from "../src/statebuilders/views";
7+
import { Ng1StateDeclaration } from "../src/interface";
38
declare var inject;
49

5-
import {inherit, extend, tail} from "ui-router-core";
6-
import {curry} from "ui-router-core";
7-
import {PathNode} from "ui-router-core";
8-
import {ResolveContext} from "ui-router-core";
9-
import {PathFactory} from "ui-router-core";
10-
import {ng1ViewsBuilder, ng1ViewConfigFactory} from "../src/statebuilders/views";
11-
import {ViewService} from "ui-router-core";
12-
import {StateMatcher, StateBuilder} from "ui-router-core";
13-
import {State} from "ui-router-core";
14-
import {Ng1StateDeclaration} from "../src/interface";
15-
1610
describe('view', function() {
1711
var scope, $compile, $injector, elem, $controllerProvider, $urlMatcherFactoryProvider;
1812
let root: State, states: {[key: string]: State};
@@ -64,7 +58,7 @@ describe('view', function() {
6458

6559
state = register(stateDeclaration);
6660
let $view = new ViewService();
67-
$view._pluginapi._viewConfigFactory("ng1", ng1ViewConfigFactory);
61+
$view._pluginapi._viewConfigFactory("ng1", getNg1ViewConfigFactory());
6862

6963
let states = [root, state];
7064
path = states.map(_state => new PathNode(_state));

0 commit comments

Comments
 (0)