Displaying posts filed under

プログラミング

mod_rewriteを使ってドキュメントルートを分割する

(特にCakePHPに限定される話題ではありませんが、CakePHPの.htaccessを例に説明します)

通常、Webアプリケーションを構成するファイルは全てソースコード管理システムの管理下に置き、何らかのデプロイツールを用いて本番環境に転送します。本番環境でファイルを直接編集することはしません。

しかし時には一部の画像ファイルやCSSファイルを管理から除外して、自由に編集できるようにしたい場合があります。そういう場合は mod_rewrite を活用して、本来のドキュメントルートの他にもう一つ別のディレクトリを実質的なドキュメントルートとして割り当てることで、高い柔軟性を持たせることができます。

前提

CakePHPで構築したアプリケーションが /var/www/cake にあり、ドキュメントルートは /var/www/cake/app/webroot である。CakePHPデフォルトの .htaccess は次の通り。

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]

やりたいこと

/home/files/public に置いたファイルに対して、あたかもドキュメントルートに置いたかのようにアクセスさせたい。
例) /home/files/public/css/main.css に対して http://example.com/css/main.css でアクセスできる。

シンボリックリンクの作成とmod_rewriteの設定

まずドキュメントルート内に /home/files/public に対するシンボリックリンクを適当な名前で作成します。当該ディレクトリに対して Options FollowSymlinks の権限が必要です。

ln -s /home/files/public /var/www/cake/app/webroot/__files

次に mod_rewrite の設定を追加します。

RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}/__files/%{REQUEST_URI} -d [OR]
RewriteCond %{DOCUMENT_ROOT}/__files/%{REQUEST_URI} -f
RewriteRule ^(.+)$  __files/$1 [QSA,L]
 
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]

この方法ではシンボリックリンクを使っていますが、mod_rewrite の設定を httpd.conf に書く場合は Alias を使って実現することもできます。

いずれにせよまず /home/files/public の検索が行われ、次に /var/www/cake/app/webroot の検索が行われます。パスの衝突には十分な注意が必要です。あらかじめ管理外とするパスを固定しておけるのであれば、それがベストなのは言うまでもありません。

第5回CakePHP勉強会に参加してきました

「CakePHP TOKYO 2010.5.29」ロゴ入りのマシュマロ

会場で配られたマシュマロ

5月29日の『第5回CakePHP勉強会』に参加してきました。

京都から東京まで移動するため、今回初めて夜行バスを使ったのですが……失敗でした。想像以上に揺れがひどくて椅子が堅くて、ほとんど眠れなかったのです。おまけに東京はあまり天気がよくなくて、汗だくになったかと思うと寒くて震えるというような気温の変化が連続して、勉強会会場にたどり着いたときには既に満身創痍という有様でした。

そんなこんなで、プレゼンのたび電気が消されるたびに意識が飛びそうになるのを懸命に堪えつつ、きっちり懇親会まで参加してきました。その感想です。

技術ネタ

全部は書ききれないので、私がコメントできそうなものを選んで感想を書いています。プログラムの全体はEvent Entry::第5回CakePHP勉強会@TokyoおよびMASA-Pさんのブログ記事をご覧ください。

CakePHP1.3の概要 市川さん(@cakephper)

betaのころから1.3を使っていたので、発表中いくつかの機能については「そういえばこれは1.3の新機能だったな」と妙な感慨を持って聞いていました。それだけ1.3の新機能は利便性が高く、一度使えば当たり前になるということです。

一方で、それら新機能の実践的なノウハウについてはまだまだこれから発見・共有されていく段階だな、とも感じました。例えば当日 #cakephpstudy のTLでは「libsとverndorsの使い分けはどうするの?」という疑問が流れていましたし、スライド中routeClassの例として『Using custom Route classes in CakePHP』から転載されたコードは、直接Modelをnewしているためにテスタビリティに難があります。そうしたノウハウはいずれこのブログなどでもまとめていければと思います。

Ktai Library on CakePHP1.3 滝下さん(@ecworks_masap)

幸か不幸かまだ本格的な携帯サイトを作った事はないのですが、将来作るとなったらほぼ間違いなくお世話になるであろうライブラリなので、興味深く聞くことができました。最新版ではktai-devという形でライブラリ統合済のCakeコアも配布しているとのことで、もはや一ライブラリというよりも「CakePHPをベースとした用途特化型フレームワーク」に近づきつつあると言えそうです。それすなわち、CakePHP 1.xの成熟度が一定のレベルに達した証左だと言えるかもしれません。

ただそうして「ライブラリ」がカバーする範囲が大きくなればなるほど、ソフトウェアとしての Responsibility も大きくなります。その点については懇親会で直接お話しさせていただく機会があったのですが、やはり既存のライブラリ・サービスとの兼ね合いや、本来キャリアが提供すべき情報をどこまでサポートすべきかといった点について苦労されているようでした。

コアライブラリのエレガントなハック 清水紘己さん(@hiromi2424)

「Cakeコアの構成要素をいかにエレガントに置き換えるか」という、なかなか聞く人を選ぶ話題でした。個人的には大好物です。特にコンポーネント初期化処理をハックする苦労を味わった同士として、懇親会では固い握手を交わさせていただきました。

CakePHP2.0の概要を見たときに「エイリアス」というのがイマイチ何なのかピンと来なかったのですが、この発表を聞いていてよく理解できました。静的型付言語でいうと置き換えられる「名前」がインターフェイスを規定し、置き換えるクラスが実装クラスという事になります。依存関係が生ずる対象がクラスではなく「名前」なので、コア内部の依存関係であっても後から置き換える事が可能になります。

「CakePHPでjQueryを使ってみた(仮)」 – @nano_eightさん

CakePHPでjQueryを使うなら下手にHelperなど使うよりも直接JavaScriptを書いた方が早い……と考えていたので、flashとelementの組み合わせ+SCRIPTタグべた書きという手法は目から鱗でした。Cake/PHP, JavaScriptそれぞれを分けて考えるのではなく、トータルで最も効率の良い方法をこそ考えていくべきなんだな、と認識を改めました。

「WordPressの管理画面のプラグインにCakePHPを使う」- 原さん(@kara_d)

「あのCMS eZ publishをCakePHPのModelにしちゃう」- @leebennyさん

他のPHP製ソフトウェアとCakePHPを組み合わせて使用する、というアイデアは多くの人が検討したことがあるだろうと思います(私もあります)。@kara_dさんのWordPressプラグインではDispatcherを無効にしたindex.phpを読み込むという大胆な手法で、@leebennyさんのeZ publishではDataSourceを実装するという正攻法で、これを実現されていました。いずれも他のソフトウェアに応用可能な方法だろうと思います。またCakeとは直接関係ありませんが、eZ publishの概略について学べたのも良かったです。

会場・コミュニティの雰囲気など

発表の様子は常時Ustreamで配信され、全国各地のサテライト会場に中継がつながっており、海外からの発表がSkype経由で行われ、会場では記念ロゴ入りのマシュマロが配られ……「コミュニティーベースの勉強会でここまでのことができるようになったのか!」と感嘆せずにはいられませんでした。

この種の勉強会に参加するのが初めてということもあり、正直なところ、ちょっと距離感を掴むのに戸惑ったというのも事実です。ただ懇親会で近くで飲んでいた@nano_eightさんのところに「プレゼン良かったですよ!」と多くの人が集まるのを見て「うらやましいなー」と思ったのもまた事実で、次の機会にはぜひ発表者として参加しよう!と思いました(実は今回もLTのお誘いは受けていたのですが、忙しさと不慣れを理由に断っていたのです)。


……

東京散策ではiPhoneが超役に立ちました。慣れない都市で迷わずに動き回るコツは「自分の勘を一切信用しないこと」なのです。午前中は国立新美術館で開催中の「オルセー美術館展」を堪能しました。モネの『ロンドン国会議事堂、霧の中に差す陽光』とゴッホの『星降る夜』には魂が震えました。もう一回見に行きたいくらいです。久しぶりに歩き回った東京は広々としていました。京都と比べればほとんどどんな土地も広々と感じるのですが、東京も例に漏れず、ずいぶんと空間に余裕があるなあ、と思いました。

find(‘list’) で集約関数を使用する方法

単に field パラメータに指定するだけではうまく動作しないので、一旦 find(‘all’) で結果を取得してから Set::combine を使って同等の構造を得る。

// user_id => 件数
$tmp = $Post->find('all',
                   array('fields' => array('user_id', 'COUNT(*) AS posts_count'),
                         'group'  => 'user_id'));
return Set::combine($tmp, '{n}.Post.user_id', '{n}.0.posts_count');

Set::combine は恐ろしく多機能なので、find(‘list’) をそのまま使うよりずっと多くの事ができる。例えば次のように、複数カラムで GROUP BY した結果をフラットな配列にまとめることができる。

// "user_id:category_id" => 件数
$tmp = $Post->find('all',
                   array('fields' => array('user_id', 'category_id', 'COUNT(*) AS posts_count'),
                         'group'  => array('user_id', 'category_id')));
return Set::combine($tmp,
                    array('{0}:{1}', '{n}.Post.user_id', '{n}.Post.category_id'),
                    '{n}.0.posts_count');

値を連想配列にする事もできる。

// user_id => array("created_max" => 最大値, "created_min" => 最小値)
$tmp = $Post->find('all',
                   array('fields' => array('user_id',
                                           'MAX(created) AS created_max',
                                           'MIN(created) AS created_min'),
                         'group'  => 'user_id'));
return Set::combine($tmp, '{n}.Post.user_id', '{n}.0');

jQuery History is now on Github

I created a fork of jQuery History plugin on Github.

Though this is just a fork for now, Mikage Sawatari, the original author of this plugin, and I agreed that it could be the mainstream in the near future.

I’ve already done some major changes to the script structure:

  • It no longer uses the Webkit-specific hack for updating the location object, because recent versions of Webkit can correctly update the object just like Firefix and IE8.
  • Hashes are percent-encoded with encodeURIComponent before being put to location.hash. So you can use any characters such as `?’ in your hash.
  • history.load does not execute the callback unless hash is changed.
  • Updated API/code format. Thanks to Kevin Dalman.

It works on IE6, IE7, IE8, Firefox3, Safari4, Chrome4, and Chrome5.

If you have any problems or feature requests, please create a new issue in the ITS. And of course, you can create your own fork.


jQuery Historyプラグインは今後Githubで管理されることになりました。

Mikage Sawatariさんによる元々のバージョンに対し、次のような大きな変更が加えられています。

  • Webkitで動かすためのハックは不要になったので取り除かれました。
  • location.hash に代入する前に encodeURIComponent によるパーセントエンコーディングを行うようにしました。これによって ? を含むあらゆる文字を hash として保存できるようになりました。
  • ハッシュが変更されていない場合はコールバックが実行されないようになりました。
  • API/コーディングスタイルを変更しました。この変更はフォーラムのKevin Dalman氏の投稿に依ります。

IE6, IE7, IE8, Firefox3, Safari4, Chrome4, Chrome5で動作します。

Githubのバグトラッキングシステムに登録してください。もちろん自分でforkを作って修正していただいても構いません。

メールの送信をテストする

環境: CakePHP1.3

ComponentTestCaseを使ってメールの送信をテストする方法をまとめておきます。Qdmailerを使うことを前提としていますが、標準のEmailComponentなど他のコンポーネントでも同じようにテストできると思います。

単純なケース

メール送信をテストする上で最も重要なことは、メールの種類ごとにコンポーネントを作るということです。

まずメール送信コンポーネントに共通の振る舞いを定義するため、以下のような Mailer クラスを app/libs に作ります。 実際のインターフェイスや初期化処理などは自分のアプリケーションに合わせて調整してください。

abstract class Mailer extends Object
{   
  var $components = array('Qdmailer.Qdmailer');
  var $_controller;
  var $_settings;
 
  function initialize($controller, $settings=array()) {
    $this->_controller = $controller;
    $this->_settings = $settings;
  }
 
  function create() {
  }
 
  protected function _rawSend() {
    $this->Qdmailer->send();
  }
 
  function send() {
    $this->Qdmailer->resetHeaderBody();
    $args = func_get_args();
    $this->dispatchMethod('create', $args);
    $this->_rawSend();
  }
}

次にメールの種類に合わせたメール送信用コンポーネントを作ります (他のコンポーネントとの混在を防ぐために components/mailers などサブディレクトリに作成することをおすすめします)。

先ほど作った Mailer クラスを継承して create メソッドを実装します。 メールの送信に使用する値は、create の引数で受け渡すよりは、 コントローラの viewVars 経由で受け取る方が良いと思います(メールテンプレートのレンダリングでも必要になるので)。

App::import('Lib', 'Mailer');
 
class RegisterRequestMailerComponent extends Mailer
{
  function create() {
    $email = $this->_controller->viewVars['User']['email'];
 
    $q = $this->Qdmailer;
    $q->resetHeaderBody();
    $q->to($email);
    $q->subject('ユーザー登録が完了しました');
    $q->cakeText('', 'register');
  }
}

単体テストではこの create メソッドをテストします(Mailerクラスに対するテストも書く必要がありますが省略)。 特定の入力(引数、viewVars)に対して宛先、送信元、そして本文などが期待通りに作成できるかどうかをテストします。

class RegisterMailerComponentTest extends ComponentTestCase
{
  var $components = array('RegisterMailer');
 
  function testCreate() {
    //viewVarsの登録
    $data = array('User' =>
                  array('id' => 123,
                        'username' => 'xxxxxx',
                        'email' => 'abc@example.com'),
                  'login_url' => 'http://example.com/register');
    $this->Controller->set($data);
 
    $this->RegisterMailer->create();
    $q = $this->RegisterMailer->Qdmailer;
 
    //本文
    $body = mb_convert_encoding($q->content['TEXT']['CONTENT'],
                                'utf-8', $q->charset_content);
    $this->assertTrue(strpos($body, $data['login_url']) !== false);
 
    //宛先など
    $this->assertEqual(1, count($q->to));
    $this->assertEqual($data['User']['email'], $q->to[0]['mail']);
  }
}

このコンポーネントを使用するコントローラからは、send メソッドを呼ぶだけです。

class UsersController extends AppController
{
  var $components = array('RegisterMailer');
 
  function finish() {
    //...
    if($this->User->save()) {
      $this->RegisterMailer->send();
    }
  }
}

より複雑なケース

次に一度に複数のメールを送る場合について考えてみます。 例えば登録完了メールをユーザと管理者に送るとしましょう。 この場合は「ユーザ宛メール(UserRegisterMailer)」と「管理者宛メール(AdminRegisterMailer)」の他に、 それらをまとめる「RegisterMailer」という3つの Mailer クラスを作成します。

UserRegisterMailer と AdminRegisterMailer は単純な例と同じように作ってテストしてください。 RegisterMailer では $components 配列を使ってそれら2つを読み込み、送信処理を委譲します。

App::import('Lib', 'Mailer');
 
class RegisterMailerComponent extends Mailer
{
  var $components = array('UserRegisterMailer',
                          'AdminRegisterMailer');
 
  function sendUserMail() {
    //viewVarsの加工処理など...
    $this->UserRegisterMailer->send();
  }
 
  function sendUserMail() {
    //viewVarsの加工処理など...
    $this->AdminRegisterMailer->send();
  }
 
  function send() {
    $this->sendUserMail();
    $this->sendAdminMail();
  }
}

このクラスに対するテストはモックを使って行うことができます。 本文や宛先の生成処理は移譲先でテストされているので、 移譲先に対する入力の形式と、適切なメソッドの呼び出しが行われていることをテストすれば十分です。

App::import('Component', 'UserRegisterMailer');
App::import('Component', 'AdminRegisterMailer');
Mock::generate('UserRegisterMailerComponent');
Mock::generate('AdminRegisterMailerComponent');
 
class RegisterMailerComponentTest extends ComponentTestCase
{   
    var $components = array('RegisterMailer');
 
  function startTest($m) {
    parent::startTest($m);
 
    $this->RegisterMailer->UserRegisterMailer
	  = new MockUserRegisterMailerComponent();
 
    $this->RegisterMailer->AdminRegisterMailer
	  = new MockAdminRegisterMailerComponent();
  }
 
  function testSendUserMail() {
    $this->Controller->set(array(/* ... */));
 
    $this->RegisterMailer->UserRegisterMailer->expectOnce('send');
    $this->RegisterMailer->sendUserMail();
    //...$this->Controller->viewVars が適切に加工されていることなどを確認...
  }
 
  function testSend() {
    $this->RegisterMailer->UserRegisterMailer->expectOnce('send');
    $this->RegisterMailer->AdminRegisterMailer->expectOnce('send');
    $this->RegisterMailer->send();
  }
}

このコンポーネントの使い方は「単純な場合」と全く同じで、コントローラは一切変更する必要がありません。

『Webを支える技術』のER図が変な件

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESSプラスシリーズ)

すごく良い本なんですが、どうしても気になってしまったので、この部分だけ先に書きます。297ページ、17.2『関係モデルからの導出』で示されている郵便番号の関係モデルは変だと思います。ER図は次のようになっています:

「郵便番号データのER図」、『Webを支える技術』298ページより

これのどこが変かというと……要するに正規化が不十分なので、いろいろなところが変です。

  • 郵便番号エンティティが都道府県エンティティと市区町村エンティティを別々に参照しているので、「東京都のIDと大阪市のIDをもつ郵便番号」のようなものが作れてしまいます。
  • 市区町村エンティティが都道府県エンティティに依存していないので、「どの都道府県にも属さない市区町村」が作れてしまいます。
  • 町域エンティティが郵便番号エンティティに入り込んでいます(町域名フリガナは町域名に関数従属する)
    • 町域と都道府県-市区町村の関係が保証されていないので、「東京都のIDと大阪市のIDと愛知県の町域名」みたいなものが作れてしまいます。

実装のことはさておき、純粋にデータの数的な関係に着目するなら、モデルは次のようになるのではないかと思います。

郵便番号データのER図・作図例

(モデルの”意味”を明確に示すために、あえて数値のIDではなく都道府県名や郵便番号などをそのままPRIMARY KEYとしています。また町域と郵便番号の関係などは日本郵便のサイトを参考にしています)

『Webを支える技術』300ページでは「階層構造は関係モデルからはわかりにくい」とありますが、それは誤解だと思います。普通は正規化を進めれば進めるほどエンティティの階層は深くなっていくはずです。市区町村->町域という階層も上のモデルからは自明です。むしろいかに階層を(正規化を)崩してリソースを抽出するかのほうが難しいと思います。

私はモデリングの専門家ではありませんので、勘違いしている点があったら教えてください。

複雑なコンポーネントの単体テスト

環境: CakePHP1.3

単純な場合のテスト方法はクックブックに載っていますが、 より複雑な場合(コントローラや他のコンポーネントと連携する場合)は多くの準備作業が必要です。

その作業を省略するための ComponentTestCase クラスを作りました。ソースコードはとりあえずgistに置いておきます。

component_test_case.php

適当な場所 app/tests/libs などに置いてテストファイルの中で読み込んでください。

Fooコンポーネントをテストする場合の例は次のようになります。

require_once (TESTS .'libs/component_test_case.php');
 
class FooTestCase extends ComponentTestCase
{
  //コントローラの $components と全く同じ書式
  var $components = array('Foo' => array('a' => 'b'));
 
  function testAdd() {
    //$this->Controller でコントローラにアクセスできる
    $this->Controller->set(array('var1' => 1, 'var2' => 2));
 
    //$this->{コンポーネント名} でコンポーネントのインスタンスにアクセスできる
    $this->assertEqual(3, $this->Foo->add());
 
    //Fooの中で別のコンポーネントBarを読み込むことも可能
    //$components = array('Bar');
    $this->Foo->Bar->barmethod();
  }
}

モデルを使用することもできます。

class BarTestCase extends ComponentTestCase
{
  var $components = array('Bar');
  var $uses = array('User');
  var $fixtures = array('app.user');
 
  function testSomething() {
    //Barの中で、Controller経由でモデルにアクセスできる
    //$this->Controller->User
    $result = $this->Bar->doSomething();
  }
}

初期化パラメータを変えてテストしたい場合は _createController メソッドを使って直接 Controller を生成します。

class FooTestCase extends ComponentTestCase
{
  function testAdd() {
    $controller = $this->_createController(array('Foo' => array('c' => 'd')));
    $controller->set(array('var1' => 1, 'var2' => 2));
 
    //$this->{コンポーネント名} でアクセスすることはできない
    $this->assertEqual(3, $controller->Foo->add());
  }
}

デフォルトの初期化は startTest メソッドの中で行われるため、オーバーライドすることで柔軟な設定が可能です。 例えばメソッド名に _disableAutoLoad が入っているときだけ自動処理を停止する、といったことができます。

class FooTestCase extends ComponentTestCase
{
  var $defaultConfig = array('Foo' => array('a' => 'b'));
 
  function startTest($method) {
    if(preg_match('/_disableAutoLoad/', $method)) {
      $this->components = array();
    } else {
      $this->components = $this->defaultConfig;
    }
    parent::startTest($method);
  }
 
  function testAdd() {
    $this->Foo;
  }
 
  function testAdd2_disableAutoLoad() {
    $c = $this->_createController(array('Foo' => array('c' => 'd')));
  }
}

テストに使用するコントローラのクラスを切り替えることもできます。

class BarTestCase_TestController extends Controller
{
   //独自のテスト用ロジックを実装する...
}
 
class BarTestCase extends ComponentTestCase
{
  var $controllerClassName = 'BarTestCase_TestController';
}

TestCase内でのコントローラの初期化については Testing CakePHP Controllers the hard way | Mark Story を参考にしました。

Google App Engine習作: Thumbs.db reader

Google App EngineでWindows XPのThumbs.dbを読み出すWebサービスを作りました。

Thumbs.db reader

アップロードされたThumbs.dbを解析して、サムネイル画像(JPEGファイル)を取り出します。結果はdata:スキームを使ってimgタグで一覧表示するか、zipでまとめて取得するか選べます。アップロードされたファイルはサーバ側には保存されません。

Thumbs.dbの解析にはvinettoというプログラムを改造して使っていますが、GAEの制約によってtype 2という形式のものしか解析できません。

コマンドラインから使用する場合は http://thumbsdbreader.appspot.com/zip というURLに対してPOSTリクエストを送信してください。zipファイルが標準出力に出力されます。

curl -X POST --data-binary @Thumbs.db http://thumbsdbreader.appspot.com/zip > files.zip

Google App EngineでMakoを使う

Google App EngineでテンプレートエンジンMakoを使う方法……といっても、特別なことは何も必要ない。アーカイブを展開してできた mako ディレクトリをアプリケーションディレクトリにコピーすれば使えるようになる。

日本語を使う場合の注意点についてはこちらを参照のこと。GAEではRequestもResponseもDatastoreもUnicodeに対応しているので、あまり神経質にならなくても大丈夫。

コード例

  • テンプレートファイルは views/ ディレクトリに置く
  • デフォルトの拡張子は .html
  • エンコーディングは utf-8 固定

次のようなベースクラスを用意しておく。

from mako.template import Template
from mako.lookup import TemplateLookup
 
class RequestHandler(webapp.RequestHandler):
    viewDir = os.path.join(os.path.dirname(__file__), "views")
    lookup = TemplateLookup(directories=[viewDir], input_encoding='utf-8')
 
    def render(self, view, vars={}, type=".html"):
        tmpl = self.lookup.get_template(view + type)
        return tmpl.render_unicode(**vars)
 
    def display(self, *args, **named):
        self.response.out.write(self.render(*args, **named))

サブクラスでは display または render メソッドでレンダリングを行う。

class MainPage(RequestHandler):
    def get(self):
        self.display('index', {"var": "value"})

Flash(ActionScript3)で新しいウィンドウを開く

ActionScript3でポップアップブロックを回避して新しいウィンドウを開くには、次のようにすれば良いらしい。

  • SafariやGoogle ChromeなどKHTML系のブラウザではnavigateToURLを使う
  • IEやFirefoxではExternalInterfaceでJavaScriptのwindow.openを使う

というわけで関数にまとめた。

import flash.external.ExternalInterface;
 
private function openNewWin(url:String):void {
        var testFunc:String = "function(){ return !/KHTML/.test(navigator.userAgent) }";
        var openInJS:Boolean = ExternalInterface.available && ExternalInterface.call(testFunc);
        if(openInJS) {
                ExternalInterface.call("window.open", url, "_blank");
        } else {
                navigateToURL(new URLRequest(url), '_blank');
        }
}

他にwmodeが可否に影響するという情報もあるのだが、私の環境では確かめることができなかった。

環境

  • Mac: Safari4, Firefox3.6, Chrome3
  • WinXP: IE8, Chrome4

参考