iOS6でメガピクセル画像をCanvasに描画するとおかしくなってしまう件と、その対処

iOS6によるアップデート

まず最初に、iOS6において、Safari上のWebアプリから簡単にカメラ&フォトライブラリの写真にアクセスできるようになりました。いままでカメラにアクセスするにはPhoneGapなり何なりでネイティブ化する必要があったので、写真共有サービスなどにはかなり有用なアップデートです。

File API、HTML Media Capture への対応

HTML Media Capture に対応し、Safari から カメラを起動してのファイルアップロードが可能になりました。

type 属性値に file を指定すれば単体ファイルのアップロードが簡単に。
(略)
ファイルを選択したあとは何ができるか...

HTML5 開発者向け iPhone 5 / iOS 6 での変更点等まとめ | WWW WATCH

発生する問題

さて、フォトライブラリ内のカメラ画像にアクセス可能になりましたが、iPhone5などの高解像度カメラを備えた端末では、写真として保存されるファイルは数メガピクセルに軽く達してしまいます。そして、iOSの制限として、2Mピクセルを超えるJPEG画像はサブサンプルの対象となり、情報が間引かれてしまいます。

JPEG images can be up to 32 megapixels due to subsampling, which allows JPEG images to decode to a size that has one sixteenth the number of pixels. JPEG images larger than 2 megapixels are subsampled―that is, decoded to a reduced size. JPEG subsampling allows the user to view images from the latest digital cameras.

Know iOS Resource Limits

情報が間引かれて表示されるだけならまだいいですが、この画像をCanvasに対してdrawImageなどしてリサイズする場合、不都合が起きます。サブサンプルされる場合とされない画像で描画の結果が異なってしまいます。

例)iOS6Safariで2Mピクセル以下の写真をdrawImageで描画した時:

例)iOS6Safariで2Mピクセル以上の写真をdrawImageで描画した時

テスト環境:
http://jsbin.com/upivad/4/

回避方法

いくつかのテストの結果、メガピクセル画像をCanvasにdrawImageする際にリサイズを伴う場合、つまりContext#drawImage(img, x, y, width, height) のwidth, height に元画像と異なる値を設定していると発生することがわかりました。そのため、回避策として、一旦小さなCanvasに元画像の一部をコピーし、それを張り合わせて対象のCanvasにdrawImageすることで対応しました。またその際、サブサンプルされている画像とそうでないものを描画結果から判別し、サブサンプルされているものには縮尺を補正してあげるようにしています。

ライブラリ化しましたので、ご参考まで。

ソース:
http://github.com/stomita/ios-imagefile-megapixel

テスト環境:
http://jsbin.com/ovupil/1/

上記ソースでは、画像ファイルにEXIFのOrientation(画像の向き)が90度回転方向に設定されている際、縦方向にひしゃげて表示されるバグについても対処しています(これは画像サイズに関係なく発生する模様)。

なお、こちらはiOSの問題の解決のために作成しましたが、同じソースをそれ以外のブラウザで利用しても特に挙動は変わりません。PCのChrome, Safari, Firefoxなどで正常に動作します。

Heroku上でスクリーンショットサーバを動かす

前回PhantomJSのbuildpackを作成したので、Heroku上でPhantomJSのプロセスを自由に稼働させることができるようになった。

PhantomJSはGUI環境のない(Headless) WebKitブラウザであるため、ブラウザ上のJavaScript単体テストor結合テストをサーバ上で走らせて、継続的インテグレーションに組み込むなどの利用方法もあるだろう。これも興味深いトピックではあるけど、ここでは触れない。

今回はPhantomJSの画面レンダリングの機能を使って、スクリーンショットサーバをHeroku上に構築する。

構成と処理フロー

PhantomJSにはwebサーバ機能も含まれており、単独でHTTPサーバとしてリクエストを受け取る事もできるが、実サービスで利用するようなシロモノではない。そのためクライアントからリクエストを受け取るNode.jsのサーバを別に立てる。今回はこれもHeroku上で行なっている。

Node.jsのアプリケーションは、Socket.IOサーバとして稼働し、2つのchannelを持つ。
1つめのrequest channelでは、ブラウザからのスクリーンショット取得リクエストを受け取り、ページのURLをキューに入れる。
もう1つのrender channelでは、PhantomJSのスクリーンショットサーバと接続する。PhantomJSはスクリーンショットサーバであるが、Socket.IO的にはクライアントになる。スクリーンショットのリクエストは、接続中のPhantomJSクライアントのうちの1つにdispatchされる。

PhantomJSのアプリケーションは、PushサーバとしてのNode.jsにSocket.IOで接続し、スクリーンショットのリクエストを待つ。ページのURLがプッシュされた時点で、PhantomJSはページにアクセスし、スクリーンショットレンダリングを開始する。

スクリーンショットは画像ファイルとしてPhantomJSのローカルファイルシステムに保存されるが、そのままではどこからもアクセスできないので、イメージサーバであるAmazon S3にアップロードする。ここで、ファイルデータをNode.jsに戻すことなく、PhantomJSから直接Browser Post Formを利用してS3にアップロードする(postの際に必要になるsignatureは事前にNode.js側で生成されており、プッシュ通知の際にページURLと共に同時に渡ってくる)

最後に、PhantomJSはアップロードしたスクリーンショットのURLをNode.jsに通知する。その後、Node.jsはrequest channelに接続しているクライアント全てに対してスクリーンショットのURLをブロードキャストする(ブラウザはそれを受け取って描画する)

利点

スクリーンショットサーバをクラウド上で構築する場合、今まではEC2などのIaaS上でGUI環境を含むOSを稼働させることが多かったが、常時起動によるリソースの有効利用およびスケール時のVMの上げ下げのコストが問題点としてあった。

これを、HeadlessのPhantomJSでスクリーンショットを撮り、それをHeroku上で動作させることにより、スクリーンショットサーバのスケールアウトがスピーディかつ容易になっている。

たとえば、以下のコマンドでインスタンスを瞬時に*1上げ下げ可能である。

$ heroku ps:scale renderer=4

資料

*1:VMの起動停止に比べて

Heroku の PhantomJS buildpack

つくりました。

GitHub - stomita/heroku-buildpack-phantomjs

何をする?

PhantomJS (www.phantomjs.org) はheadless(GUI環境を必要としない)のWebKitで、JavaScriptおよびCoffeeScriptスクリプトを書くことができます。

このPhantomJSをheroku cedar stackで実行可能にしたものが、heroku-buildpack-phantomjs です。

※ heroku devcenter ドキュメントの 3rd party buildpack リストに載っけていただきました
https://devcenter.heroku.com/articles/third-party-buildpacks

利用方法

READMEに書いてあるように、以下のコマンドでheroku cedar上にアプリを作成できます。

$ heroku create --stack cedar --buildpack http://github.com/stomita/heroku-buildpack-phantomjs.git

とりあえず試しにスクリーンショットを取得するサーバを書いてみます。

screenshot-server.js:

var fs = require('fs');

var formHtml = [
  '<html><body>',
  '<form method="get">',
  'URL: <input type="text" name="url"><br>',
  'Width: <input type="text" name="width" value="320"><br>',
  'Height: <input type="text" name="height" value="480"><br>',
  'Filetype: <select name="ftype">',
  '<option>jpg</option><option>png</option><option>pdf</option>',
  '</select><br>',
  'Wait: <input type="text" name="sleep" value="400"><br>',
  '<input type="submit" value="Get Screenshot">',
  '</form>',
  '</body></html>'
].join('');

function toNumber(str, defaultValue) {
  var n = Number(str);
  if (isNaN(n)) { return defaultValue; }
  return n;
}

function parseQueryString(url) {
  var params = {};
  var qstr = url.split('?', 2)[1];
  if (qstr) {
    var pairs = qstr.split('&');
    for (var i=0; i<pairs.length; i++) {
      var pair = pairs[i].split('=');
      params[pair[0]] = decodeURIComponent(pair[1]);
    }
  }
  return params;
}


var port = phantom.args[0];

var server = require('webserver').create();
console.log('server listening port: '+port);
var service = server.listen(port, function (request, response) {
  var params = parseQueryString(request.url);
  if (!params.url) {
    response.statusCode = 200;
    response.write(formHtml);
    response.close();
    return;
  }
  var url = params.url;
  var ftype = params.ftype || 'jpg';
  var width = toNumber(params.width, 320);
  var height = toNumber(params.height, 480);
  var sleep = toNumber(params.sleep, 400);
  var page = new WebPage();
  page.viewportSize = { width: width, height: height };
  page.open(url, function (status) {
    if (status !== 'success') {
      response.statusCode = 500;
      response.write('Unable to load the address!');
      response.close();
    } else {
      setTimeout(function() {
        var file = "/tmp/image_"+ Math.random().toString(36).substring(2, 10) + "." + ftype;
        page.render(file);
        var contentType =
          ftype === 'jpg' ? 'image/jpeg' :
          ftype === 'png' ? 'image/png' :
          ftype === 'pdf' ? 'application/pdf' : 'application/octet-stream';
        response.statusCode = 200;
        response.setHeader("Content-Type", contentType);
        var stream = fs.open(file, "rb");
        response.write(stream.read());
        stream.close();
        fs.remove(file);
        response.close();
      }, sleep);
    }
  });
});


Procfile:

web:  phantomjs screenshot-server.js $PORT

heroku上に配布したものがこちらになります。

http://growing-lightning-3614.herokuapp.com/

これまで

実はすでにPhantomJS buildpackは PhantomJS 1.4.1ベースで作成していましたが、Xvfb(X11のheadless)を必要としていました。そのため、buildpack内にXvfb環境を埋め込んで提供していたので、slug sizeが90MB近くになっていました(これでも結構削った結果)。

今回、1.5.0ベースにしたことで、pure headless (X11を必要としない)となり、slug sizeは17MBほどまで減りました。

制限

まだ日本語フォントが使えません。headlessなPhantomJSでフォント追加する方法を調査中です。
なお @font-face でWeb Font なら使えるかな、と思いましたが、対応してないようです。

(追記)
fontconfigを使う環境では追加のTTFフォントを ~/.fonts に入れておくだけで大丈夫な模様。(参考:書体関係 Wiki - unixuser200403-2
なので、日本語フォント対応するには、IPAフォントなりをダウンロードし、.fontsディレクトリを作成してTTFファイルを突っ込んだ上でherokuにpushしておけばOK。
(容量の問題もあるのでbuildpackには含めていません)

ビルド方法メモ

以下、個人的なメモとしてのbuildpack作成の手順。

https://gist.github.com/2725125
https://gist.github.com/2725122

vulcanに以上の2つのスクリプトを載せてビルドプロセスを走らせれば良いわけだけど、どうもvulcanはビルドの途中で落ちてしまうことが多い。PhantomJSのソースコードコンパイルはQtを含んでおりheroku上で優に1時間くらいはかかるので、落とされると大変困る。heroku run bash で対話的にやるほうがよいかとおもう。ビルド結果は自分でアーカイブしてscpで適当なサーバに転送した。

BaaS (Backend as a Service) について

正直またXaaSかよ、って感じでしょうが、SaaSとかPaaSとかに比べてまだそれほど流行ってはいないのかもしれないけど、スマフォがこれだけ普及してくると、それに特化した形態としてこのレイヤーでのサービスの切断もあり得る、ということらしいです。

モバイル向けの新クラウド、BaaS(Backend as a Service)とは何か。「Parse」が正式サービス開始 - Publickey

BaaSって何か新しいの?

取り組みとしては、2006年ごろの初期型PaaSや一部のDaaS (Database as a Service) にもアーキテクチャ的に似たのがあったような記憶があります。しかしながら、これらが理念先行型ニーズ模索中だったのに対し、今回のBaaSはすでにアピールするべき確固たる市場(=スマフォアプリ開発者)があるのがやはり異なります。

ちなみに、この時代(2006-2008年)の中間レイヤのプラットフォームサービスは、UIを切り離して開発者のバックエンドDBに向くか、UIを統合してExcel的なエンドユーザ向けを目指すかで分けられましたが、どちらも茨の道の歴史で死屍累々で、前者はHTTP経由のデータベース以上の何者でもなく、開発者には結局MySQL自前で持つかホスティングで持つかのような話と比べられて、じゃあ慣れたMySQLでいいじゃん、と冷めた目で見られていたような気がしますし、後者は後者で、SaaSから発展したSalesforceなどに競合してもユーザに訴える飛び道具としてのアプリケーションもなく、結局CRMのおもちゃみたいなテンプレートをつけてみたはいいが、実際の業務に適用なんて作ってる当人も現実味を帯びて考えられないような代物だったりして、結局どちらもサービスの利用者を獲得できずに潰れたりどこかに吸収されたりしましたとさ。

まあとにかく、今度はUIは全部スマフォ側でやってくれと割り切れるので幸せです。あとはユーザ管理や認証があればほんとにサーバ側で何かしなきゃいけないなんてことはないので、クライアント側しか書けないアプリ開発者は楽ちんなものですね。

BaaSで何ができるの?

だいたいどのBaaSにも以下のような機能が提供されています。

その他に、スマフォ特化なところで、以下のようなものも揃っています

あとは、本番/開発切り替えとか、APIバージョン管理とか、アクセス解析とか、サービスによっていろいろあります。サーバでカスタムロジックのホスティングもできたりして、PaaSとの垣根の部分が若干低くなってるところもあるようです。

データストア

JSONデータストアはだいたいCouchDB/MongoDB的なドキュメントデータベースがRESTでアクセス可能になっていると想像すれば間違いはないとおもいます。コレクション(BaaSによってクラス、オブジェクトなどと呼び方が異なる)が同じタイプのレコードを管理する単位になり、そのスキーマ情報は自由にUIから定義でき、簡単なバリデーションが組み込まれます。MongoDB的に使うには十分でしょうが、その中でもStackMobなどはRelationshipのデータモデルをサポートしていたりしており、関連性を持ったコレクションデータをより簡単に扱えるようです。

じゃあこれらはMongoDBみたいなのにREST APIくっつけたの*1と全く同じなのか、というと、明確な違いが一つあって、それはユーザ管理とアクセスコントロールが組み込まれているということです。MongoDBにはMongoDBの接続アカウント+パスワードを持った管理者orサーバプログラムしかアクセスできなかったですが、BaaSにはエンドユーザ管理とその認証およびレコードレベルのアクセスコントロールが組み込まれているので、直接インターネットに晒しても大丈夫です。適切にアクセスコントロールが設定されていれば、誰かが違う人の投稿を書き換えちゃうとか消しちゃうとかできないわけです。

なお、アクセスコントロールの実装というのは、サーバプログラマが一番間違えやすい所の一つではあります。現在のBaaSが達成できているのは非常にシンプルなコントロールですが、それらが全部宣言的にできるということは、かなりのメリットだと思います。特に、UI側の表示による制御のみに依存してサーバプログラム上でのチェックを怠っていたような脆弱なダメスマフォアプリであっても、ちゃんとしたレベルに平準化できるでしょう。

余談ですが、JSONEngineプロジェクトは似たようなところを目指していたように思います。BaaSというにはちょっとまだまだかもしれませんが、開発が止まっているように見えるのでちょっと残念ですね。

ユーザ管理とアクセスコントロール

データストアでアクセスコントロールをする以上、どのユーザがリクエストしているのか認証する必要がありますし、レコードの作成者(所有者)をはっきりさせておく必要もあるため、ユーザ管理の仕組みが組み込まれます。ユーザのアカウント情報は、他のデータストア内のレコードと同様ではありますが、ユーザ管理用の特別なコレクションにJSON形式で保存されます。

アクセスコントロールはレコードレベルで達成されるようになっています。BaaSのデータストア内にそのレコードを作成したアカウントの情報が紐付けられており、その作成者アカウントと現在認証されているユーザアカウントの関係性においてアクセスをコントロールするというものです。より具体的には、そのレコードが保存されているコレクションに対して「作成者のみ/ログイン済みユーザ/すべて(匿名含む)」が「読み/書き」権限を持っている、という表を設定しておくような形になります。BaaSの中にはより細かい条件で制御が可能なものもあるかと思います。

認証とソーシャル連携

認証については、通常のユーザアカウント名+パスワードのようなものはもちろん、「ソーシャル連携」、つまり「Facebookでログイン」「Twitterでログイン」を簡単に実現してくれる機能が提供される場合が多いです。

残念ながら、現在のBaaSのほとんどが、この「Facebookでログイン」「Twitterでログイン」を正しく実装できていません。具体的に言うと、トークンリプレースという攻撃によって他人になりすますことが簡単にできてしまいます。回避するワークアラウンドはあるのですが、対処されていません。そもそもすべてのBaaSベンダーがこの問題を正しく認識しているかどうか微妙です。まだBaaSサービス自体が大きくないために被害例は出てないのでしょうが、これらが改善されるまでは主要BaaSの「ソーシャル連携」の機能については利用しないほうが良いのではないかと思います。

BaaSによってサーバエンジニアは必要なくなる?

僕はもともとサーバエンジニアのキャリアを積んでいましたが、ここ5-6年ほどはできるだけサーバプログラム書きたくないなーというメンタリティで過ごしてきました。それは単にスケールとか運用とかめんどくさいという理由なのですが、そういう点で、このBaaSについてはとても期待を持っています(ソーシャル連携のところだけが残念…)。そんなことくらいめんどくさがるな、という意見は重々承知ですが、怠惰はエンジニアの美徳ということですのでまあ許してください。

ともあれ、同じような人ってのはそれなりにいるはずで、クライアント専業のエンジニアはもちろんのこと、Webエンジニアでもサーバの運用に疲れてしまった人がPaaSに流れてきている現況をみてると、Webのサーバ側を書くのはもう流行んねーよ、くらいのことを言っても小声ならバチは当たらないんじゃないかなー、と思ってきています。

ただ、僕がエンジニアとして育ってきたころはなんだかんだやっぱりサーバを書かなきゃいけないことは多かったので、こうなってきたのは時代の流れなのでしょう。もちろんそういった「怠惰な」エンジニアがそのまま次の時代に生き残れるなどという保証はないのですが、その時にはむしろ次の先端に向かってクロールしていけるフットワークでいればいいんじゃないですかね。

とりあえず、IaaSがインフラエンジニアを不要にし、PaaSがアプリケーションサーバの運用を不要にしたように、BaaSはWebアプリケーションのサーバ側開発者を不要にします。実際に完全に世の中から不要にできるかどうかはさておき、そこを目指しています。サーバ側しか書けないアプリケーションエンジニアは青ざめていたらいいと思います(嘘)

追記

「ソーシャル連携」のところで触れた、トークンリプレース攻撃(トークン置き換え攻撃)が可能な場合とその対策方法について触れている記事へのリンク。

単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる | @_Nat Zone
http://oauth.jp/post/43684110055/oauth-20-implicit-flow
http://oauth.jp/post/43684108710/ios-sdk

要は、FBのSDKとかでGETしたaccess tokenをそのままクライアントからサーバに投げて認証しちゃダメ、ってことなんですが、なんでだめなのかいまいちわかってもらえないケースが多いです。この記事はBaaSについてのものなので詳細は省きますが、興味があれば上記記事をじっくり読んでみてください。

*1:Sleepy.Mongooseとかもありましたね

Node.js からSalesforceを使う

一応誰かがすでにやっていることですが、ちゃんとしたのが無いので作りました。
GitHub - stomita/node-salesforce: Salesforce API Connection Library for Node.js Applications

インストール

npm 登録済みです。

$ npm install node-salesforce

利用方法

接続
var sf = require('node-salesforce');
var conn = new sf.Connection({
  serverUrl : 'https://na1.salesforce.com',
  accessToken : '<your oauth2 access token is here>'
});

Connectionオブジェクトの引数に、OAuth2の認証フローで得られた情報をそのまま渡してやります。今のところOAuth2のトークンやりとりについてはモジュールに含まれていません。

テスト用にユーザ名/パスワードでログインすることも可能です。

var sf = require('node-salesforce');
var conn = new sf.Connection({
  loginUrl : 'https://login.salesforce.com'
});
conn.login(username, password, function(err) {
  if (!err) {
    // console.log(conn.accessToken);

    //
    // ... logics after authentication ...
    //
  }
});

本来ならこの loginメソッドの実装は OAuth2に則って Resource Owner Password Credential でやるべきかもしれませんが、SOAP APIからだとClient ID/Client Secretが必要ないので、そちらを利用しています。

余談ですが、Salesforce API においては OAuth2 Access Token ≒ Session IDです。SOAP API や埋め込みタブのURLから得たSession ID は、そのままOAuth2 Access Tokenとして利用できます(また逆に、scopeを指定しないで取得したOAuth2 Access Token は SOAP APIのSession IDとしても使えます)。

検索

SOQLでの検索。
検索結果のレコードをイベント駆動的に扱うオプションがあります。autoFetchオプションを渡せばレコード件数が多い場合は自動的にqueryMoreするので、結果セットの取得をアプリケーション側で意識しなくても大丈夫です。

var records = [];
conn.query("SELECT Id, Name FROM Account")
  .on("record", function(record) {
    records.push(record);
  })
  .on("end", function(query) {
    console.log("total in database : " + query.totalSize);
    console.log("total fetched : " + query.totalFetched);
  })
  .run({ autoFetch : true, maxFetch : 4000 });

通常のコールバックスタイルでも利用できます。

var records = [];
conn.query("SELECT Id, Name FROM Account", function(err, result) {
  if (!err) {
    console.log("total : " + result.totalSize);
    console.log("fetched : " + result.records.length);
  }
});
CRUD操作

対応してます。

conn.sobject("Account").retrieve("0017000000hOMChAAO", function(err, account) {
  if (!err) {
    console.log("Name : " + account.Name);
  }
});
conn.sobject("Account").create({ Name : 'My Account #1' }, function(err, ret) {
  if (!err && ret.success) {
    console.log("Created record id : " + ret.id);
  }
});
conn.sobject("Account").update({ 
  Id : '0017000000hOMChAAO',
  Name : 'Updated Account #1'
}, function(err, ret) {
  if (!err && ret.success) {
    console.log('Updated Successfully : ' + ret.id);
  }
});
conn.sobject("Account").del('0017000000hOMChAAO', function(err, ret) {
  if (!err && ret.success) {
    console.log('Deleted Successfully : ' + ret.id);
  }
});

配列指定で複数レコードの一括操作もありますが、SOAP APIと違いレコードごとに一つ一つAPIリクエストを投げるので、注意。

// Multiple records creation consumes one API request per record.
// Be careful for the API quota.
conn.sobject("Account").create([{
  Name : 'My Account #1'
}, {
  Name : 'My Account #2'
}], 
function(err, rets) {
  if (!err) {
    for (var i=0; i<rets.length; i++) {
      if (rets[i].success) {
        console.log("Created record id : " + rets[i].id);
      }
    }
  }
});

未対応

describe系はまだです。

参考

なお、githubにある既存のライブラリでは、以下のものがあります。

https://github.com/gidzone/sfdc-node
=> express依存&あまり再利用考えてなさそうです。

https://github.com/durchblicker/SalesforceAPI
=> 作りかけっぽいかんじ。なんでnpmに登録したかな…

https://github.com/joshbirk/FDC-NODEJS-HEROKU
=> 多分これが一番まともだけど、なぜわざわざHEROKUってつけてるかな?


※ このエントリーはForce.com Advent Calendarに参加しています http://atnd.org/events/22909

Force.com ブックマークレット

Salesforceを使って多言語対応の開発をしていると、英語⇔日本語の設定を行き来することが多いですが、毎回設定画面に行って変更するのって結構めんどくさいですよね?
そんなとき、僕はブックマークレットで一発変更しています。

説明

Ajax Toolkit経由でSOAP APIを使って現在のログインユーザの言語属性を変更しています(なので実行する組織のAPIが有効化されている必要があります)。

ソースはこちら
https://gist.github.com/1436661#file_toggle_language.js
英語⇔日本語のトグルですが、他の言語必要だったら適当にForkして増やしてください。


なおこのブックマークレットは拡張可能なLoaderを経由して実行しており、上記のスクリプトだけでなくGistに書いた任意のJavaScriptスニペットを簡単に呼び出し可能になっています。APIデバッグは同梱のAjax Toolkit Shellで。うん、もうローカルPCにIDEなんて必要ないですね!
http://stomita-lab.s3.amazonaws.com/gist/1436661/sfdc-bookmarklet.html

注意事項

詳細は言いませんが、このブックマークレットはちょっと問題を抱えています。本稿執筆時現在は動きますが、将来にわたって動くことを保証はしません。詳しくはソースみてね。


※ このエントリーはForce.com Advent Calendarに参加しています http://atnd.org/events/22909

(とりあえず軽めにジャブ)

SalesforceのWeb APIに対する意見

残念ながら個人的な意見なので殆どの人には響かないに違いないが、できるだけ一般の技術者にわかるように書く。なお実はそれほど現在の業務とは関係ない。

自分がやりたいこと

Salesforce(あるいはDatabase.com)のデータにアクセスするHTML5Ajaxアプリ(iPhone対応)を書きたい。なおVisualforceは管理者にセットアップさせるから×。外部サーバ経由は通信ポイントがいたずらに増えるから&IP制限してたときに使えないから×。直接API EndpointにJavaScriptから通信するのが一番よろしい。

現状

REST APICORS (Cross-Origin Resource Sharing) に対応してない。SOAP APIの方は(CORSではなくcrossdomain.xmlだが)OKしてるのになんでなのかまったくよくわからない。せっかくImplicit Grant (User-Agent flow) も対応してるのにほぼ意味ないじゃない。SOAP APIでやるにはFlash Proxyを経由するしか無いので、PCブラウザならいいが、Flashの動かないモバイル用途だとこれも×。え、PhoneGap?アプリじゃなくてWebじゃないとだめなんですよ。

結論

Patのブログのコメントを見るかぎりCORS対応はロードマップには入っているっぽいが、いつになるのかわからない。Winter '12では入らなかった。Spring '12で対応しなかったら見限ろうかと思う。CORS対応を見据えてのDatabase.comに対するよいしょ記事を用意する予定だったがもう書かない。拗ねる。お蔵入り。なので中の人は調べてこっそりいつ頃になるのか教えてくれるとありがたい。

補遺:想定問答集

「セキュリティのことを考えてあえてCORSはOFFにしてるのでは?」

⇒ セキュリティというのが「IPアドレスによるAPIのアクセス制御が難しいからクロスドメインでの直接アクセスを禁じたい」という主旨であるとして、それは現在Flashアプリが(AIRでなくWeb埋め込みでも)SOAP API経由でクロスドメインで直接APIコールが送れるようになっている実態と矛盾する。

「Herokuでやったらいいじゃない」

⇒ サーバを介することは不必要な通信ノードを増やす。クラウドとはいえサービス提供者が運用を気にする必要ができてしまい面倒。あとIPアドレス帯域の制限は結構よくある話なのでサービスの展開に影響する。ユーザの管理者に頼んで許可IPアドレスに入れてもらうくらいならVisualforceで作っても似たようなもん。

「PhoneGapでやったらいいじゃない」

⇒ PhoneGapだと結局iPhoneアプリ作って配布することになる。AppStore?エンタープライズ配布?それもいいが、そうじゃない選択肢としてHTML5はあるのだ。

「Visualforceで(以下同文)」

⇒ 管理者にセットアップ(インストール)してもらうようなことを想定したアプリ・サービスならそれでもよいが、そうでない、バイラル的・ボトムアップ的に利用者が広がるようなものについては提供が不可能。

「管理者が認めたアプリからしかアクセスさせない、のが普通では?」

⇒ もし本当に管理外のアプリからのアクセスを禁じたいなら、現状それは「アクセス可能なIPアドレス帯の制限」「ネットワーク内PC/スマートフォンへのインストールアプリの制限」などと同時に実施されなければ意味がない。前者はSalesforceの設定の話だが、後者はそれ以上の全社的な運用ポリシーの話になるので、徹底してやってるところは稀。あるいは「APIアクセスの禁止」も選択肢となりえるが、それは同時に認可したい他のアプリ/サービスからのアクセスも禁ずることになる。
さらに、先ほど述べたとおり、現状Flash Webアプリが直接APIコールできるため、単なるアプリケーションのインストール規制というのはそれほど意味をもたない。
なお、管理者による利用アプリを制限を真っ向から否定するほどラジカルな意見を持っているわけではない。ただ、それを実現するために、低レベルなレイヤーで境界を設けるのは時代遅れだと言いたい。実際 Chuck もそういう意見でしょ(参考:Cloud Identity Summit でのcmortのプレゼン資料

SalesforceのOAuth Implicit Grant (User-Agent flow) はAndroid/iOSアプリのためにあるのでは」

⇒ そうだとするなら、なぜOAuthクライアントの登録時に https:// ではじまるリダイレクトURIの登録を許しているのか。