FirefoxOSのパッケージ型アプリとAngularJSのngSrc/ngHref


FirefoxOSのアプリには、通常のWebサイトと同等のホスト型アプリと、アプリのファイルを一つのzipにまとめて配布されるパッケージ型アプリがあります。
注意するべき点として、パッケージ型アプリの場合はオリジンが`http://<hostname>`などではなく、`app://<uuid>`になります。

AngularJSはFirefoxOSアプリの開発にも力を発揮する強力なフレームワークだと思います。
AngularJSを使ったFirefoxOSのパッケージ型アプリで、ngSrcやngHrefを使ってURLを動的にバインディングする際に起きた問題と対策をシェアします。

# ngSrcとngHref

img要素のsrcやa要素のhrefで指定するURLまたはその一部をcontrollerで変更したりしたい場合、src/href内に直接{{}}でバインディングするのではなく、ngSrc/ngHrefを使うべきとされています。
理由としては、例えば`src="{{url}}`と書いた時に、`{{url}}`という文字列がURLとして解釈される瞬間が存在するため、ブラウザからリクエストが飛んだり、ユーザーが未構築のリンクを踏む可能性があるのを防ぐということです。

簡単な例を示しましょう。

```html
<!DOCTYPE html>
<html ng-app="fos-test">
<head>
<meta charset="utf-8">
<title>FirefoxOS App with AngularJS</title>
</head>
<body>
<div ng-controller="MainController">
  <img ng-src="{{imageUrl}}"/>
  <a ng-href="{{linkUrl}}">link</a>
</div>
<script src="js/libs/angular.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
```

```js
angular.module('fos-test', [])
  .controller('MainController', ['$scope', function($scope) {
    $scope.imageUrl = 'img/icons/icon60x60.png';
    $scope.linkUrl = 'link.html';
  }]);
```

通常のWebサイトであれば何の問題もなく画像とリンクが表示されるでしょう。

# パッケージ型アプリの場合

上の例をFirefoxOSのパッケージ型アプリとして実行してみると、画像が表示されず、リンクのURLもおかしいことがわかります。
Webサイトとして表示した場合には、データバインディングの結果、以下のようなHTMLが構築されます。

```html
<img ng-src="img/icons/icon60x60.png" src="img/icons/icon60x60.png"></img>
<a ng-href="link.html" href="link.html"></a>
```

これに対して、パッケージ型アプリで構築されるHTMLは以下のようになっていて、src/hrefに構築されたURLがおかしいことがわかります。

```html
<img ng-src="img/icons/icon60x60.png" src="unsafe:app://5a6bea7e-1ae6-4045-965c-6b02ac71aa71/img/icons/icon60x60.png"></img>
<a ng-href="link.html" href="unsafe:app://5a6bea7e-1ae6-4045-965c-6b02ac71aa71/link.html"></a>
```

パッケージ型アプリの場合、オリジンが`app://<uuid>`になるという話でした。
src/hrefともに、オリジンからはじまるフルパスの前に`unsafe:`がついた形になっているのがわかります。

# imgSrcSanitizationWhitelistとaHrefSanitizationWhitelist

ということで、次は`unsafe:`がどこから来ているのかという話になります。
AngularJSでは、XSSなどの攻撃を防ぐためにsrc/hrefを解決する際には、URLが一定の規則を守っているかチェックする仕組みが入っています。
許可するURLの規則は正規表現で指定されます。

このときの内部処理のおおまかな流れとしては以下のようになります。
1. {{}}の中身を解決してURLの文字列を組み立てる
2. URLをオリジンから始まる絶対URLに変換する
3. 変換された絶対URLが正規表現にマッチした場合はそのURLをDOMに書き込み、マッチしなかった場合はURLの前に`unsafe:`をつけてDOMに書き込む

上の例のhrefの場合、データバインディングを行った後のURLが`link.html`で、絶対URLとして`app://5a6bea7e-1ae6-4045-965c-6b02ac71aa71/link.html`が組み立てられます。
この絶対URLが正規表現にマッチしないため、HTML上では`href="unsafe:app://5a6bea7e-1ae6-4045-965c-6b02ac71aa71/link.html"`となっていることがわかります。

その正規表現の取得、変更を行うのが、`$compileProvider`の`imgSrcSanitizationWhitelist`と`aHrefSanitizationWhitelist`です。
`imgSrcSanitizationWhitelist`のデフォルトは`/^\s*((https?|ftp|file):|data:image\/)/`, `aHrefSanitizationWhitelist`のデフォルトは`/^\s*(https?|ftp|mailto|tel|file):/`
のようになっています。
確かに`app://`からはじまるURLはマッチしないようになっています。

# 解決策
それでは、`app://`からはじまるURLを許可するために正規表現を変更しましょう。
`module.config`の中で`$compileProvider`を依存性注入し、`imgSrcSanitizationWhitelist`と`aHrefSanitizationWhitelist`の引数にそれぞれ新しい正規表現を指定します。

```js
angular.module('fos-test', [])
  .config(function($compileProvider) {
    $compileProvider.imgSrcSanitizationWhitelist(/^\s*((https?|ftp|file|app):|data:image\/)/);
    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|app):/);
  })
  .controller('MainController', ['$scope', function($scope) {
    $scope.imageUrl = 'img/icons/icon60x60.png';
    $scope.linkUrl = 'link.html';
  }]);
```

これで無事、パッケージ型アプリでも通常のWebサイトと同様の表示ができるようになりました。