diff --git a/src/interface.ts b/src/interface.ts
index 14dc069bb..76a1c91ee 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -618,3 +618,33 @@ export interface Ng1Controller {
*/
uiCanExit(): HookResult;
}
+
+/**
+ * Manages which template-loading mechanism to use.
+ *
+ * Defaults to `$templateRequest` on Angular versions starting from 1.3, `$http` otherwise.
+ */
+export interface TemplateFactoryProvider {
+ /**
+ * Forces $templateFactory to use $http instead of $templateRequest.
+ *
+ * UI-Router uses `$templateRequest` by default on angular 1.3+.
+ * Use this method to choose to use `$http` instead.
+ *
+ * ---
+ *
+ * ## Security warning
+ *
+ * This might cause XSS, as $http doesn't enforce the regular security checks for
+ * templates that have been introduced in Angular 1.3.
+ *
+ * See the $sce documentation, section
+ *
+ * Impact on loading templates for more details about this mechanism.
+ *
+ * *Note: forcing this to `false` on Angular 1.2.x will crash, because `$templateRequest` is not implemented.*
+ *
+ * @param useUnsafeHttpService `true` to use `$http` to fetch templates
+ */
+ useHttpService(useUnsafeHttpService: boolean);
+}
\ No newline at end of file
diff --git a/src/services.ts b/src/services.ts
index 47178ee96..aa3fa21c2 100644
--- a/src/services.ts
+++ b/src/services.ts
@@ -8,18 +8,16 @@
* @module ng1
* @preferred
*/
-
/** for typedoc */
import { ng as angular } from "./angular";
import { TypedMap } from "ui-router-core"; // has or is using
import {
- IRootScopeService, IQService, ILocationService, ILocationProvider, IHttpService, ITemplateCacheService
+ IRootScopeService, IQService, ILocationService, ILocationProvider, IHttpService, ITemplateCacheService
} from "angular";
import {
- services, applyPairs, prop, isString, trace, extend, UIRouter, StateService, UrlRouter, UrlMatcherFactory,
- ResolveContext
+ services, applyPairs, isString, trace, extend, UIRouter, StateService, UrlRouter, UrlMatcherFactory, ResolveContext
} from "ui-router-core";
-import { ng1ViewsBuilder, ng1ViewConfigFactory } from "./statebuilders/views";
+import { ng1ViewsBuilder, getNg1ViewConfigFactory } from "./statebuilders/views";
import { TemplateFactory } from "./templateFactory";
import { StateProvider } from "./stateProvider";
import { getStateHookBuilder } from "./statebuilders/onEnterExitRetain";
@@ -60,7 +58,7 @@ function $uiRouter($locationProvider: ILocationProvider) {
router.stateRegistry.decorator("onRetain", getStateHookBuilder("onRetain"));
router.stateRegistry.decorator("onEnter", getStateHookBuilder("onEnter"));
- router.viewService._pluginapi._viewConfigFactory('ng1', ng1ViewConfigFactory);
+ router.viewService._pluginapi._viewConfigFactory('ng1', getNg1ViewConfigFactory());
let ng1LocationService = router.locationService = router.locationConfig = new Ng1LocationServices($locationProvider);
@@ -109,13 +107,13 @@ export function watchDigests($rootScope: IRootScopeService) {
mod_init .provider("$uiRouter", $uiRouter);
mod_rtr .provider('$urlRouter', ['$uiRouterProvider', getUrlRouterProvider]);
mod_util .provider('$urlMatcherFactory', ['$uiRouterProvider', () => router.urlMatcherFactory]);
+mod_util .provider('$templateFactory', () => new TemplateFactory());
mod_state.provider('$stateRegistry', getProviderFor('stateRegistry'));
mod_state.provider('$uiRouterGlobals', getProviderFor('globals'));
mod_state.provider('$transitions', getProviderFor('transitionService'));
mod_state.provider('$state', ['$uiRouterProvider', getStateProvider]);
mod_state.factory ('$stateParams', ['$uiRouter', ($uiRouter: UIRouter) => $uiRouter.globals.params]);
-mod_util .factory ('$templateFactory', ['$uiRouter', () => new TemplateFactory()]);
mod_main .factory ('$view', () => router.viewService);
mod_main .service ("$trace", () => trace);
diff --git a/src/statebuilders/views.ts b/src/statebuilders/views.ts
index c5795d2e7..ce69ce358 100644
--- a/src/statebuilders/views.ts
+++ b/src/statebuilders/views.ts
@@ -9,8 +9,13 @@ import { Ng1ViewDeclaration } from "../interface";
import { TemplateFactory } from "../templateFactory";
import IInjectorService = angular.auto.IInjectorService;
-export const ng1ViewConfigFactory: ViewConfigFactory = (path, view) =>
- [new Ng1ViewConfig(path, view)];
+export function getNg1ViewConfigFactory(): ViewConfigFactory {
+ let templateFactory: TemplateFactory = null;
+ return (path, view) => {
+ templateFactory = templateFactory || services.$injector.get("$templateFactory");
+ return [new Ng1ViewConfig(path, view, templateFactory)];
+ };
+}
const hasAnyKey = (keys, obj) =>
keys.reduce((acc, key) => acc || isDefined(obj[key]), false);
@@ -69,10 +74,8 @@ export class Ng1ViewConfig implements ViewConfig {
template: string;
component: string;
locals: any; // TODO: delete me
- factory = new TemplateFactory();
- constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration) {
- }
+ constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration, public factory: TemplateFactory) { }
load() {
let $q = services.$q;
diff --git a/src/templateFactory.ts b/src/templateFactory.ts
index e586616f3..6f63d23df 100644
--- a/src/templateFactory.ts
+++ b/src/templateFactory.ts
@@ -6,20 +6,28 @@ import {
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext,
Resolvable, RawParams, prop
} from "ui-router-core";
-import { Ng1ViewDeclaration } from "./interface";
-
-const service = (token) => {
- const $injector = services.$injector;
- return $injector.has ? ($injector.has(token) && $injector.get(token)) : $injector.get(token);
-};
+import { Ng1ViewDeclaration, TemplateFactoryProvider } from "./interface";
/**
* Service which manages loading of templates from a ViewConfig.
*/
-export class TemplateFactory {
- private $templateRequest = service('$templateRequest');
- private $templateCache = service('$templateCache');
- private $http = service('$http');
+export class TemplateFactory implements TemplateFactoryProvider {
+ /** @hidden */ private _useHttp = angular.version.minor < 3;
+ /** @hidden */ private $templateRequest;
+ /** @hidden */ private $templateCache;
+ /** @hidden */ private $http;
+
+ /** @hidden */ $get = ['$http', '$templateCache', '$injector', ($http, $templateCache, $injector) => {
+ this.$templateRequest = $injector.has && $injector.has('$templateRequest') && $injector.get('$templateRequest');
+ this.$http = $http;
+ this.$templateCache = $templateCache;
+ return this;
+ }];
+
+ /** @hidden */
+ useHttpService(value: boolean) {
+ this._useHttp = value;
+ };
/**
* Creates a template from a configuration object.
@@ -76,12 +84,12 @@ export class TemplateFactory {
if (isFunction(url)) url = ( url)(params);
if (url == null) return null;
- if(this.$templateRequest) {
- return this.$templateRequest(url);
+ if (this._useHttp) {
+ return this.$http.get(url, { cache: this.$templateCache, headers: { Accept: 'text/html' }})
+ .then(function(response) { return response.data; });
}
- return this.$http.get(url, { cache: this.$templateCache, headers: { Accept: 'text/html' }})
- .then(function(response) { return response.data; });
+ return this.$templateRequest(url);
};
/**
@@ -116,6 +124,11 @@ export class TemplateFactory {
/**
* Creates a template from a component's name
*
+ * This implements route-to-component.
+ * It works by retrieving the component (directive) metadata from the injector.
+ * It analyses the component's bindings, then constructs a template that instantiates the component.
+ * The template wires input and output bindings to resolves or from the parent component.
+ *
* @param uiView {object} The parent ui-view (for binding outputs to callbacks)
* @param context The ResolveContext (for binding outputs to callbacks returned from resolves)
* @param component {string} Component's name in camel case.
@@ -150,6 +163,7 @@ export class TemplateFactory {
let res = context.getResolvable(resolveName);
let fn = res && res.data;
let args = fn && services.$injector.annotate(fn) || [];
+ // account for array style injection, i.e., ['foo', function(foo) {}]
let arrayIdxStr = isArray(fn) ? `[${fn.length - 1}]` : '';
return `${attrName}='$resolve.${resolveName}${arrayIdxStr}(${args.join(",")})'`;
}
diff --git a/test/templateFactorySpec.ts b/test/templateFactorySpec.ts
index 70027a8ba..d42caaf94 100644
--- a/test/templateFactorySpec.ts
+++ b/test/templateFactorySpec.ts
@@ -5,14 +5,13 @@ declare let inject;
let module = angular['mock'].module;
describe('templateFactory', function () {
-
beforeEach(module('ui.router'));
it('exists', inject(function ($templateFactory) {
expect($templateFactory).toBeDefined();
}));
- if (angular.version.major >= 1 && angular.version.minor >= 3) {
+ if (angular.version.minor >= 3) {
// Post 1.2, there is a $templateRequest and a $sce service
describe('should follow $sce policy and', function() {
it('accepts relative URLs', inject(function($templateFactory, $httpBackend, $sce) {
@@ -40,7 +39,9 @@ describe('templateFactory', function () {
$httpBackend.flush();
}));
});
- } else { // 1.2 and before will use directly $http
+ }
+
+ if (angular.version.minor <= 2) { // 1.2 and before will use directly $http
it('does not restrict URL loading', inject(function($templateFactory, $httpBackend) {
$httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!');
$templateFactory.fromUrl('http://evil.com/views/view.html');
@@ -60,4 +61,26 @@ describe('templateFactory', function () {
$httpBackend.flush();
}));
}
+
+ describe('templateFactory with forced use of $http service', function () {
+ beforeEach(function() {
+ angular
+ .module('forceHttpInTemplateFactory', [])
+ .config(function($templateFactoryProvider) {
+ $templateFactoryProvider.useHttpService(true);
+ });
+ module('ui.router');
+ module('forceHttpInTemplateFactory');
+ });
+
+ it('does not restrict URL loading', inject(function($templateFactory, $httpBackend) {
+ $httpBackend.expectGET('http://evil.com/views/view.html').respond(200, 'template!');
+ $templateFactory.fromUrl('http://evil.com/views/view.html');
+ $httpBackend.flush();
+
+ $httpBackend.expectGET('data:text/html,foo').respond(200, 'template!');
+ $templateFactory.fromUrl('data:text/html,foo');
+ $httpBackend.flush();
+ }));
+ });
});
\ No newline at end of file
diff --git a/test/viewSpec.ts b/test/viewSpec.ts
index c9c38b668..f642ebd5a 100644
--- a/test/viewSpec.ts
+++ b/test/viewSpec.ts
@@ -1,18 +1,12 @@
import * as angular from "angular";
import "./util/matchers";
+import {
+ inherit, extend, tail, curry, PathNode, PathFactory, ViewService, StateMatcher, StateBuilder, State
+} from "ui-router-core";
+import { ng1ViewsBuilder, getNg1ViewConfigFactory } from "../src/statebuilders/views";
+import { Ng1StateDeclaration } from "../src/interface";
declare var inject;
-import {inherit, extend, tail} from "ui-router-core";
-import {curry} from "ui-router-core";
-import {PathNode} from "ui-router-core";
-import {ResolveContext} from "ui-router-core";
-import {PathFactory} from "ui-router-core";
-import {ng1ViewsBuilder, ng1ViewConfigFactory} from "../src/statebuilders/views";
-import {ViewService} from "ui-router-core";
-import {StateMatcher, StateBuilder} from "ui-router-core";
-import {State} from "ui-router-core";
-import {Ng1StateDeclaration} from "../src/interface";
-
describe('view', function() {
var scope, $compile, $injector, elem, $controllerProvider, $urlMatcherFactoryProvider;
let root: State, states: {[key: string]: State};
@@ -64,7 +58,7 @@ describe('view', function() {
state = register(stateDeclaration);
let $view = new ViewService();
- $view._pluginapi._viewConfigFactory("ng1", ng1ViewConfigFactory);
+ $view._pluginapi._viewConfigFactory("ng1", getNg1ViewConfigFactory());
let states = [root, state];
path = states.map(_state => new PathNode(_state));