なぜCSSXSSに抜本的に対策をとることが難しいか
CSSXSSの説明について、その脅威を過剰に表現している部分がありました。その部分について加筆訂正しています。 @ 2006/4/3
tociyukiさんによる「[web]MSIE の CSSXSS 脆弱性とは何か」および「[web]開発者サイドでの CSSXSS 脆弱性対策」には、より正確なCSSXSS脆弱性の内容およびそれに対するサーバーサイド開発者で可能な対策について紹介されていますので、是非そちらもご覧ください。 @ 2006/4/4
今までも何度かこの辺の話はあまり具体的ではなく書いてきたけど、そろそろCSSXSSを悪用したい人には十分情報が行き渡っただろうし、具体的な話を書いてもこれ以上危険が増すということはないだろうから、ちょっと具体的に書いてみる。
ちなみに私自身は、CSSXSSの攻撃コードなどを実際に試したりといった調査は行っておらず、信頼できそうな他者の公開情報を組み合わせて理解しているだけなので、事実誤認等が混ざっているかもしれない。指摘があれば適宜修正する。
まずCSSXSSというのは、現在のIE(Windowsのみ?)が持つセキュリティホールである(とはっきり書いておこう。その他のブラウザで同様の問題が起きるという話は聞かない)。具体的な内容としては、
<link rel=stylesheet href="[URL]" type="text/css" />
などの方法で、(X)HTMLドキュメントから[URL]で指定された外部スタイルシートを参照すると、そのURLから返された内容がスタイルシートではなく、通常の(X)HTMLドキュメントであったとしても、JavaScript(などのIEが持つ拡張機能)を使ってその内容を取得することができる。
ただし、あらゆるドキュメントが取得できるわけではなく、その内容がある程度スタイルシート定義テキストに似ている(文字要素としてスタイルシート定義で使われる記号が含まれる、など)必要があるようだ。しかし、その細かい条件については定かではない。
ただし、単純にドキュメントの内容すべてを取得できるという訳ではなく、ドキュメント中のスタイルシート定義に似た一部分のみが参照できるようだ。tociyuki氏のご指摘を元に、私が試してみた範囲では、通常はHTML中に含まれるCSS定義部分のみが参照でき、CSS定義以外のドキュメントは参照できない。ただし、ドキュメント中にIEからはCSS定義のように見える部分を作り出すことによって、CSS定義以外のドキュメント要素も参照可能にすることができるようだ。そのあたりの具体的な条件については、実験によってある程度は解析できるが、確実な条件は設定できない(=プログラムロジックで判別できない)。
また、上記linkで指定されたURLの内容をブラウザが取得しに行く際には、その時点でそのURL(ドメイン)に対してブラウザ(=閲覧者)が持つ権限が利用される。
たとえば、そのURLにアクセスするためには本来ログインが必要な場合でも、そのブラウザがそのURLで提供されるサービスにおいて、ログイン済みの状態になっているのならば、たとえ他サイト(サービス)上の本来認証しないとみられないはずの情報にも、アクセスすることができる。
具体的には、もしアクセス者がmixiやはてなにログイン済みだった場合、http://free.example.com/cssxss.htmlなどに設置された攻撃コードから、http://mixi.jp/home.plやhttp://www.hatena.ne.jp/などにアクセスすることで、各サービスにそのアクセス者が認証状態のときに表示されるページを構成するHTMLが取得でき、そこに表示された情報(たとえばアカウント名やプロフィール情報など)を読み取ることができる(本当に上記URLにCSSXSSアクセス可能かどうかは未確認通常は単にアクセスしただけでHTMLドキュメント中の情報が盗まれることはないだろう。ただし前記のような条件が整う(攻撃者によって整えられる)ことによって、CSS経由でHTMLドキュメント中の情報を外部から参照することが可能になる)。
これだけでも大問題なのだが、さらに問題なのがCSRF攻撃と組み合わせた場合だ。
CSRF(Web上のサービスで提供されている機能を、他人の権限で操作する攻撃)に対する一般的な対策は、ある程度重要な機能を実行する前の認証チェックは、Cookieのようにアクセスしたら自動的にブラウザから送信される情報だけで行わず、フォームのhiddenフィールドなどを使って「攻撃者が知りえない」&「アクセスしただけで自動送信されない」認証情報を付与し、二重に認証チェックすることによって、攻撃を防ぐ方法だ。
ほかにも対策はあるが、この方法は利用者のユーザビリティをほとんど下げることなく、CSRF対策が行える。
しかしCSSXSSをつかえば、CSRF対策のためにフォームのhiddenフィールドなどにセットしたもう一つの認証情報を、攻撃者が盗むことが可能になる。そして、それを利用して外部サイトからCSRF攻撃を成功することが可能になる。
というように非常に重大な問題でありながらも、もう数カ月も修正されずにマイクロソフトによって放置されているCSSXSSだが、ブラウザ側の修正を待たずにサーバー側で対策を行うことは可能だろうか。
その答えとしては、ある特定された状況における問題を回避するように修正することは可能であろう。しかし、CSSXSSという問題に対して、抜本的な対策をサーバーサイドのみで行うことは、現在のWebアプリケーションの設計手法を根本から変えない限りは、不可能だ。
まず、CSSXSSの根本的な問題は、他サイトの認証済みページに表示される情報を盗むことが可能である、という点だ。これを回避する方法としては、上記までの(私が知りうる限りの)情報から考えるに、
- 認証済みページに表示する内容は、CSSXSSでは読み取れない内容(文字構成)にする
- 外部スタイルシート(として指定されたURLのドキュメント)の取得は必ずGETアクセスで行われるので、認証済みページの重要な情報はすべてPOSTアクセスでないと表示されないようにする
となる。それぞれについて検討してみよう。
最初の条件については、そこに表示される内容が静的なドキュメントならば、対策は可能だろう。実際にIEでCSSXSSアクセスを試みて取得できなければ、それはCSSXSSでは読み取れない内容であるということになる。ただし、そのドキュメントを作成する際の制限およびチェックコストはとても大きい。
一方、動的なドキュメントを生成するようなWebアプリケーションでは、その内容をCSSXSSでは読み取れないようにすることは可能だろうか? 動的なドキュメントを生成するWebアプリケーションでは、そのコンテンツが変化していくものだ。コンテンツが変化するたびに、実際にCSSXSSで読み取られることがないかをチェックすることは可能だろうか? 現実的は不可能だろう。
続いて、認証済みページはPOSTアクセスでのみ情報を表示するようにする、という条件について考えてみる。つまり、認証済みページ内でのページ遷移は単なるリンク(GETアクセス)は許されず、必ずform method="post"を使ったsubmitにする必要がある。
こちらは最初からそういう設計で構築する分には、実現は不可能ではない。しかし、従来の(通常の)Webアプリケーションに関する常識のもとに設計されているサイトを、すべてそのように変更するのは非常に大変だ。また、利用者から見ても、認証済みページのページ遷移がすべてPOSTアクセス(form submit)となると、ユーザビリティは大幅に低下することだろう(たとえばはてなダイアリーで、ログイン後はすべてのリンクがsubmitボタンになると思いねぇ)。
ちなみに、内部的にはすべてPOSTアクセスが行われるが、一般ユーザーレベルの使い勝手はリンク(a href)をクリックするのと変わらないような仕組みを、JavaScriptを駆使して実現することは可能だ。たとえばマイクロソフトのASP.NET(+VisualStudio)でWebフォーム+サーバーコントロールを使えば、単なるリンクも内部的にはPOSTアクセスとして実行される(ここで、実はマイクロソフトがASP.NETを広めるためにCSSXSS対策を行わないのだ、という陰謀論を10行割愛)。
また、認証済みページにおける情報は、すべてAjax(XmlHttpRequest+POST)を使って取得&表示するようにしても、CSSXSSで盗まれることはないだろう(外部スタイルシートを読み取る際に、そのドキュメント上のJavaScriptが実行されたりはしない)。
どちらにしろ、これらの対策は従来のWebアプリケーション設計手法とは異なる方法でWebサイト(アプリケーション)を再構築する必要があり、またブラウザ互換性も損ねてしまう。あるいはブラウザ互換性の代わりにユーザビリティを大幅に下げることになる。
もちろん現時点で、非常に重要な機能・情報をCSSXSSから保護するために、上記のような対策を取ることは必要だろう。しかし、CSSXSSがないことを前提として設計したサイトを、抜本的にCSSXSSフリーにするためには、単に部分的な修正ではなく、サイト全体の再構築を伴うこととなる。
ちなみにCSSXSSを利用したCSRFに対してのみ対策を行うとなると、ある程度は実現可能になってくる。CSRF対策に関係するページは数が限られるので、それらをすべてPOSTアクセスのみに変更することは、現実的に対応可能だろう。
ただし、従来の(CSSXSS対策を考えない)設計とは使い勝手が変わってくるし、場合によってユーザビリティが低下する(従来は不要だったPOSTアクセスが1段階必要になることがある)。つまり、CSSXSSのためのセキュリティ的な改善=ユーザビリティ的な改悪を伴う可能性が高い。そうなると、IE側でCSSXSS対策が行われたあとには、(従来のユーザビリティに戻すために)もう一度全体のデザインを調整する必要が出てくるだろう。
最終的にサーバー側でどう対応するかは、危険性・実害・対応コストあたりの兼ね合いで決まってくるだろうが、その対応コストの見積もりには、単に修正する際の見積もりだけでなく、IE側の対応が終わった後に再修正が必要となる分も含めておくべきだろう。
join機能の独自拡張
「Zend_Db_Tableにjoinがあれば、現時点でも使う気になるのになー」ということで、試しに独自にjoin機能を実装してみることにした。
Zend_Db_Table、Zend_Db_Table_Row、Zend_Db_Table_Rowsetを書き換えているけど、独自拡張した、
Zend_Db_Table::join(Zend_Db_Table $joinTable, $joinType = 'inner')
というメソッドを使わない限りは、従来の各クラスとの互換性があるはず。
上記リンク先にサンプルコードとか制限とかいろいろ載せているけど、要は、
create table product( id integer primary key auto increment, name varchar(100), ); create table stock( id integer primary key auto increment, product_id integer unique, amount integer, );
みたいなテーブルがあった場合、
$productTable = new Product();
$productTable->join(new Stock());
$products = $productTable->fetchAll('stock.amount < 5'); // 在庫が5より少ない商品を取得
foreach ($products as $product) {
$stock = $product->stock;
$stock->amount += 5;
$stock->save();
echo '商品' . $product->name . 'の在庫を' . $stock->amount . 'に増やしました。';
}
みたいな書き方ができるようになる感じね。
ただ実際にjoin機能を作ってみて分かったけど、こういうものを汎用的に使えるようにする設計は難しいね。
今回は割り切って、規約通りのテーブル定義以外では動かないようにしたから実装がシンプルになったけど、どのくらいイレギュラー(規約外)な場合を許容しつつ、その場合の対応をどれだけスマートに記述できるようにするか、ってのを考えはじめると、いつまで経っても設計が終わらない。っつーか、最初ちょっとくらいイレギュラーな場合にも対応できるようにしようと思ってたんだけど、中途半端にしかならないんであきらめた。
ごついjoin定義情報を背後に持っておいて、それを使ってjoinに関するイレギュラーなパターンを解決するというのが、真っ先に思いつく方法ではあるけれども、それって結局は単なる力業だし、そういう実装方法をそのまま表にまで出しちゃうと、利用者側レベルでの使い勝手もjoin定義情報と同じだけ複雑になりうることになっちゃって、あんまりスマートじゃないよなー。かといって、ごついjoin定義情報を利用者レベルでスマートに設定できるインターフェースってのも、なかなか設計が難しい。
俺としては、利用者レベルの使い勝手は今回の俺の実装程度のシンプルさで済みつつも、これよりももっと融通を利かせられるような本家実装を望むところ。
Zend Frameworkをどう使うか その19
フレームワークとしての基本機能を一通りチェックしたんで、実際に何か(ある程度実用的な)アプリケーションを作ろうかと思って試してみたんだけど、現時点ではまだサンプルアプリ以上のものを作るのはきついな。
もちろん動くものを作るだけならなんとかなるんだけど、Zend Frameworkベースのアプリケーションとして、ある程度の将来互換性も確保できるように作ろう、とか思うと現時点の完成度ではまだ無理だ*1。
一応Controller周りは結構完成度が高そうなんで、そこだけならばいけそうではある。かなりフィックスした仕様に見えるし、たとえデフォルトでの挙動が変更されたとしても、現状の拡張性の枠組みの中でカバーできるであろう柔軟性を備えている。プラグインの機構も応用範囲が広そうだ。
Viewに関しては、現状が最終仕様とはとても思えないんで、おそらくまだまだ互換性のない仕様変更があるだろう。っつーか、現在のZend_Viewを使うくらいだったら、もっとこなれた他のテンプレートエンジン(Smartyとか)を使った方が、現時点ではずっとましだろう。
Zend_Logは、ログの保存がstaticメソッドに集約されたという設計はいいんだけど、デフォルトでCompositeじゃないのが使いにくい。自前のComposite Adapterとかを使ってカバーするという手もあるけど、その辺には仕様変更もありそうだし、現時点ではまだPEAR Logを使っておいた方がましかもしれない。
Zend_Db周りは非常にビミョー。Zend_Db_Adapterはあれで十分なんで、そのままいけるかなーと思いつつも、O/Rマッパー系は現状では実用レベルじゃないよなー。1テーブル単位のselectしかサポートしないO/Rマッパーで完結できるような設計なんて現実的じゃないし、ちょっとでもjoinしたければとたんにO/Rマッパーはつかえなくなっちゃうし。
Zend_Db_Tableにjoin系の機能が追加されるのはいつなんだろう? 一応Zend_Db_Inflector::plural(複数形化)とか用意されているし、そっち方向の拡張を行う気はあるんだよね。
ただまあ、すぐに実装されるってことはなさそうだよなー。Zend_Db_Adapterには文句はないから、ひとまずそれ+SQL文でがりがり書くのが現時点では一番無難か? それとも他にいいO/Rマッパーはないかなー。でもZend_Db_Adapterと連動するとなると、無理だよなー。
*1 もちろんPreview Releaseなんだから、そのくらいの完成度であることに文句はない
date.timezone
PHP 5では、php.iniにdate.timezoneという項目が追加されていて、環境変数TZがセットされていないようなシステムで、date.timezoneも設定されていない場合はwarningが出るようになったのね。
PHP 5.1.2-win+MySQL 3.23.58-win
の組み合わせでMySQLにアクセスすると、mysql関数を使ってもPDOを使っても、selectをした後にApache(mod_php)が落ちる(insertとかは問題ないいや、insertだけでも落ちてたっぽい)。原因を究明するのが面倒なんで、MySQLを4.1.15に換えたら問題なくなった。まあそろそろMySQL 3系を使い続けるのもなんだなーと思いつつあったんで、ローカルテスト環境も4.1系でいいか。
ピュアPHPなアップロードステータスバー挫折
PHPでは、ファイルアップロード(サーバーでの受信処理)が完了するまで、サーバー上のどのファイルがどのリクエストに対してアップロードされているファイルなのかを、PHPコードから知る方法がない(アップロード用テンポラリファイル名はPHP言語が自動的に生成する)ため、アップロード経過をAjaxで知らせるような実装は、PHP単体ではできない。PHPで実装されたその手のデモも、よく見るとファイル受信処理自体はPHP以外の言語を使っているはず。
という壁を破るいい方法を思いついた。アップロードフォームを生成する時点で、リクエスト固有のテンポラリディレクトリを作成して、upload_tmp_dirとしてセットしておき、セッションを使ってその情報を引き継ぐようにすれば、そのディレクトリ内に生成されたファイル=アップロードされているファイルになるじゃないか。
と思ったんだけど、upload_tmp_dirってPHPコードからは上書きできないのね。っつーか、実行順序としては、ファイルアップロードの受信処理が終わった(=upload_tmp_dir設定が使われた)後に、PHPで書かれたコードが実行されるんだもんな。くっそー、こうなったら.htaccessとmod_envとphp_valueを組み合わせて、強引にセッション固有のupload_tmp_dirを作成したりすれば、何とかなるかなー。って、それはもう全然PHPネイティブな方法じゃないよ。
クスリだけもらってきた
そろそろ花粉症のクスリが切れる頃なので、処方箋だけもらって3週間分クスリを追加。それにしても、この2、3日は花粉がひどいことになっているな。ちょっとクスリを飲む間隔が空くと、目鼻がひどいことになる。今週末は会社の花見なんだけど、そんなのにいっている場合なんだろうか?
そういや渋谷の桜はもうほとんど満開だね。通りすがりに、桜ヶ丘の坂の写真を撮ってきたけど、なんかひどい写りだ。これじゃ全然桜がきれいに見えないね。木の下から上に向かって撮れば、もうちょっときれいに取れただろうに。携帯の画面だと逆光かどうかの判断がいまいちわからないんだよな。
Zend Frameworkをどう使うか その18
Zend_InputFilterは、Zend_Filterの機能を使って特定の入力(=連想配列)に対するフィルタリングを行うためのクラス。コンストラクタで入力値を渡し、以降はその入力値のキー名に対して、Zend_Filterと同名のメソッドをコールすると、フィルタリングやバリデーションを行うことができる。
具体的には、
$filteredPost = new Zend_InputFilter($_POST);
if (!$filteredPost->isAlpha('name')) {
// 不正な入力値によるエラー処理
} else {
$name = $filteredPost->getAlpha('name'); // if文でalphaであることは確定しているんで、実際にはgetRawでいいけど
}
なんて感じになるんだけど、内部的にZend_Filterを使っているんで、そっちの紹介の追記で解説したように、それらをまとめて、
$filteredPost = new Zend_InputFilter($_POST);
if (!$name = $filteredPost->isAlpha('name')) {
// 不正な入力値によるエラー処理
} else {
// $nameを使った処理
}
といった形で書くこともできる。慣れるまでは可読性が落ちる(is〜で実体が返るのかよ)んで、俺はあんまり好きじゃないけど。
ちなみにZend_InputFilterのコンストラクタの引数には、通常$_POSTみたいなスーパーグローバル変数にセットされた入力値を渡すわけだけど、恐ろしいことにこの引数に対して、デフォルトでは破壊的な操作が行われる。
$filteredPost = new Zend_InputFilter($_POST);
とやったら、ここで$_POSTはNULLにセットされてしまい、以降$_POST相当のデータには$filteredPostを通してしかアクセスできないようになる。
俺は、これってものすごく大きなお世話って気がするんだけど、どうなんだろう? ちなみに第2引数に、
$filteredPost = new Zend_InputFilter($_POST, false);
なんて感じでFALSEを指定してやると、元のスーパーグローバル変数(には限らないけど、第1引数として渡された変数)のリセットは行われない。デフォルトはこっちにしておいた方が良くないか?
ちなみに現状ではこのZend_InputFilterって、フレームワークの一部としてフレームワークの他のコードから利用されたりはしていないんだけど、Zend_Controller_Routerには、$_SERVERから直接REQUEST_URIを取得しているところに、
@todo Replace with Zend_Request object
なんてことが書かれている。ということは、もしかしたら、
class Zend_Request
{
var $_get = null;
/* snip */
public function __construct()
{
$this->_get = new Zend_InputFilter($_GET);
/* snip */
}
function GET()
{
return $this->_get;
}
/* snip */
}
なんてクラスが登場して、Controller配下からそのインスタンスにアクセスできるようになるんじゃなかろうか? ちなみに使い方としては、
$req = new Zend_Request();
$name = $req->GET()->getAlpha('name'); // $_GET['name']のアルファベット要素のみを取得
みたいなイメージね。
ただこうやっちゃうと、
- デフォルトですべての入力値系スーパーグローバル変数をリセットしていいのか
- 上記のようなオブジェクトは、さまざまな場所で利用することになるけど、そのアクセスインターフェースはどうする?(シングルトン?)
あたりが微妙なんで、まだ公開されていないとか?
Zend Frameworkをどう使うか その17
そろそろ本格的に、いわゆるフレームワークとは関係ない、ただのライブラリしか残っていないなー。あとはせいぜいフィルター周りが、ちょっとはフレームワークの一部っぽいか?
入力フィルターの機能を受け持っているのは、汎用フィルター関数群のZend_Filterと、それを使って実際の入力値をフィルタリングするZend_InputFilterの二つ。
Zend_Filterはクラスとして宣言されているけど、public static functionしかないんで、実質は名前空間の代わりだね。で、持っている機能としては、
- Zend_Filter::getAlpha($value) - アルファベット以外の文字を除去して返す。01ab!#23cd → abcd。
- Zend_Filter::getAlnum($value) - アルファベットと数字以外の文字を除去して返す。01ab!#23cd → 01ab23cd。
- Zend_Filter::getDigit($value) - 数字以外の文字を除去して返す。01ab!#23cd → 0123。
みたいな感じで、元となる文字列から、指定した文字要素以外を除去した文字列を取得するパターンのものがいくつか。
- Zend_Filter::getDir($value) - パスからディレクトリ部分のみを抽出する。っつーか単にbasedir関数。
- Zend_Filter::getInt($value) - intにキャストして返す。
- Zend_Filter::getLength($value, $length = NULL) - 最初のn文字を返す。
- Zend_Filter::getPath($value) - 相対パスを絶対パスに変換する。っつーか単にrealpath関数。
- Zend_Filter::noTags($value) - HTMLタグ除去。striptags。
- Zend_Filter::noPath($value) - パスからディレクトリ部分を除去。basename。
みたいな文字列変換系のもの。
- Zend_Filter::isAlpha($value) - アルファベットのみで構成されているかどうかを返す。
みたいなある表現形式に合致しているかどうかを返すもの(このパターンはたくさんあるんでいちいち列挙しない)がある。
うーん、このクラス(ライブラリ)はなんかビミョーな出来だなー。
AlphaやAlphaNum、Digitあたりに関する関数は、まあ汎用性があるからこういう形で持っていてもいいだろう。でもパス操作なんて、別にPHPの関数そのまま使えばいいじゃん。Zend_Filterって殻にかぶせた方が使いやすくなっているとは思えない。
ましてや、isDateとかisPhoneとかisZipとか中途半端に特定の(地域の)パターンに対応した関数をフレームワークの標準機能の一つとして持っていられても、いまいち使えない気がする。たとえばisPhoneなんて、アメリカの地域コード一覧とか直値で持っていたりして、その辺のチェックまで行っているけど、これってどうよ?
一応アメリカ以外の国のデータも、持とうと思えばもてるようになっているから、将来的には少なくとも数カ国分のパターンは入れるつもりなんだろうけど、でもこういうアプローチじゃ完全な国際化対応はできないよなー。こういう微妙な機能はもっと根本的にプラガブルに設計するか、じゃなかったら標準では取り込まない方がいい気がするなー*1。
と、長くなってきたのでここでいったん終了。Zend_InputFilterについては後で。
訂正
Zend_Filter::isAlpha($value) - アルファベットのみで構成されているかどうかを返す。
みたいなある表現形式に合致しているかどうかを返すもの(このパターンはたくさんあるんでいちいち列挙しない)がある。
と書いたけど、この手のis〜系の関数はbool値を返す一般的なValidate関数ではなかった。Validateに失敗した場合はfalseを返すけど、Validateに成功した場合は、引数として渡された$valueの内容をそのまま返す。
つまり、
if ($value = Zend_Filter::isAlpha($value)) {
// $valueには元の$valueが入っているので
// そのまま$valueを使った処理を書ける
} else {
// $valueにはFALSEが返されている
}
なんて書き方ができるっつーのが、このZend_Filter(およびZend_InputFilter)の工夫らしい。
Zend_Filterだとこの工夫にはあまり意味がないけど、Zend_InputFilterでこの表現を使うとコードが1行省略できる(Validate行とFilterされた値を取得する行を2行に分ける必要がない)んで便利でしょ、ってことらしい。
でも俺的には、この手の基本的なロジックは、変に工夫して行数を削減するよりも、読んでわかりやすい方がいい気がするんだけどね。
*1 ってのは、HTML_QuickFormのDate周りとかを見たときにも思ったし、あれを汎用的に日本語対応するのは結構苦労した
Zend_Log_Adapter_Compositeを作ってみた
Zend_Log_Adapter_Compositeを作ってみたら、それで十分って感じだったんで、Zend_Log本体の実装は現状のままで特に問題はないか。
<?php
/**
* @license LGPL
* @author ishinao <ishinao@ishinao.net>
*/
require_once 'Zend/Log/Adapter/Interface.php';
require_once 'Zend/Log/Adapter/Exception.php';
/**
*
*/
class Zend_Log_Adapter_Composite implements Zend_Log_Adapter_Interface
{
private $_options = array('logNames' => array());
/**
* new Zend_Log_Adapter($logName1, $logName2, ....);
*
*/
public function __construct()
{
$logNames = func_get_args();
foreach ((array)$logNames as $logName) {
$this->add($logName);
}
}
/**
*
* @params string $logName
*/
public function add($logName)
{
if (in_array($logName, array_keys($this->_options['logNames']))) {
throw new Zend_Log_Exception('logName already exists: ' . $logName);
}
if (!strlen($logName) || !is_string($logName)) {
throw new Zend_Log_Exception('invalid logName: ' . $logName);
}
$this->_options['logNames'][$logName] = true;
return true;
}
/**
*
* @params string $logName
*/
public function remove($logName)
{
if (!in_array($logName, $this->_options['logNames'])) {
throw new Zend_Log_Exception('logName not exists: ' . $logName);
}
unset($this->_options['logNames'][$logName]);
return true;
}
public function setOption($optionKey, $optionValue)
{
$this->_options[$optionKey] = $optionValue;
return true;
}
public function open()
{
return true;
}
public function close()
{
return true;
}
public function write($fields)
{
$message = $fields['message'];
unset($fields['message']);
$level = $fields['level'];
unset($fields['level']);
foreach (array_keys($this->_options['logNames']) as $logName) {
Zend_Log::log($message, $level, $fields, $logName);
}
}
}
?>
使い方は、
Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/app.log'), 'file);
Zend_Log::registerLogger(new Zend_Log_Adapter_Db($db, 'logtable'), 'db');
Zend_Log::registerLogger(new Zend_Log_Adapter_Composite('file', 'db'));
なんてしておくと、ふつうに、
Zend_Log::log('message');
としただけで、ファイルログにもDBログにも同じログが出力される。ってだけだとあんまり使い道がないけど、たとえば、
Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/app.log'), 'app);
Zend_Log::registerLogger(new Zend_Log_Adapter_File('/path/to/error.log'), 'error');
Zend_Log::setLevel(Zend_Log::LEVEL_ERROR, 'error');
Zend_Log::registerLogger(new Zend_Log_Adapter_Composite('app', 'error'));
なんてやると、/path/to/app.logにはすべてのログが出力されつつ、LEVEL_ERRORなログだけは/path/to/error.logにも出力される。みたいな感じでログ出力側のコードは変えずに、必要に応じてログ出力先を切り替えるイメージね。