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));