Skip to content

Commit

Permalink
BREAKING CHANGE: Use angular 1.3+ $templateRequest service to fetch…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
christopherthielen committed Jan 4, 2017
1 parent 01f7d22 commit 7e1f36e
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 41 deletions.
30 changes: 30 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <a href="https://docs.angularjs.org/api/ng/service/$sce#impact-on-loading-templates">
* Impact on loading templates</a> 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);
}
12 changes: 5 additions & 7 deletions src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -109,13 +107,13 @@ export function watchDigests($rootScope: IRootScopeService) {
mod_init .provider("$uiRouter", <any> $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);

Expand Down
13 changes: 8 additions & 5 deletions src/statebuilders/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
42 changes: 28 additions & 14 deletions src/templateFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -76,12 +84,12 @@ export class TemplateFactory {
if (isFunction(url)) url = (<any> 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);
};

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(",")})'`;
}
Expand Down
29 changes: 26 additions & 3 deletions test/templateFactorySpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
Expand All @@ -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();
}));
});
});
18 changes: 6 additions & 12 deletions test/viewSpec.ts
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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));
Expand Down

0 comments on commit 7e1f36e

Please sign in to comment.