Hatena::ブログ(Diary)

なんとなくな Developer のメモ RSSフィード

2013-04-25

[][][][][][][][][] 記号文字の URL エンコード - Java, .NET, JavaScript, Ruby, Python, PHP

下記のような文字をいくつかのプログラム言語の標準的な APIURLURIエンコードしてみたらどうなるか試してみました。

; / ? : @ = & % $ - _ . + ! * ' " ( ) , { } | \ ^ ~ [ ]

使用した言語は下記の通りです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20130425/

Java の場合

Java では下記メソッドが使えます。

今回は Groovy で実装してみました。

url_encode.groovy
str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]'

println URLEncoder.encode(str)
実行結果
> groovy url_encode.groovy
%3B%2F%3F%3A%40%3D%26%25+%24-_.%2B%21*%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D

.NET の場合

.NET では下記メソッドが使えます。

今回は C# で実装してみました。

url_encode.cs
using System;
using System.Web;

class UrlEncode
{
    static void Main(string[] args)
    {
        var str = ";/?:@=&% $-_.+!*'\"(),{}|\\^~[]";

        // (1)
        Console.WriteLine(Uri.EscapeUriString(str));
        // (2)
        Console.WriteLine(HttpUtility.UrlEncode(str));
    }
}
実行結果
> csc url_encode.cs
・・・
> url_encode.exe
;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~%5B%5D
%3b%2f%3f%3a%40%3d%26%25+%24-_.%2b!*%27%22()%2c%7b%7d%7c%5c%5e%7e%5b%5d
  • (1) Uri.EscapeUriString()
    • 半角スペースが %20 になる
    • ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ はエンコードされない
  • (2) HttpUtility.UrlEncode()
    • 半角スペースが + になる
    • - _ . ! * ( ) はエンコードされない
    • アルファベットが小文字

JavaScript の場合

JavaScript では下記 API が使えます。

  • escape()
  • encodeURI()
  • encodeURIComponent()

node.js で実行してみました。

url_encode.js
var str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]';

// (1)
console.log(escape(str));
// (2)
console.log(encodeURI(str));
// (3)
console.log(encodeURIComponent(str));
実行結果
> node url_encode.js
%3B/%3F%3A@%3D%26%25%20%24-_.+%21*%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~%5B%5D
%3B%2F%3F%3A%40%3D%26%25%20%24-_.%2B!*'%22()%2C%7B%7D%7C%5C%5E~%5B%5D
  • (1) escape()
    • 半角スペースが %20 になる
    • / @ - _ . + * がエンコードされない
  • (2) encodeURI()
    • 半角スペースが %20 になる
    • ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ がエンコードされない
  • (3) encodeURIComponent()
    • 半角スペースが %20 になる
    • - _ . ! * ' ( ) ~ がエンコードされない

Ruby の場合

Ruby では下記 API が使えます。

  • URI.escape(), encode()
  • CGI.escape()
url_encode.rb
require 'uri'
require 'cgi'

str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]'

# (1)
puts URI.escape(str)
puts URI.encode(str)
# (2)
puts CGI.escape(str)
実行結果
> ruby url_encode.rb
;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~[]
;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~[]
%3B%2F%3F%3A%40%3D%26%25+%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
  • (1) URI.escape(), encode()
    • 半角スペースが %20 になる
    • ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ [ ] がエンコードされない
  • (2) CGI.escape()

Python の場合

Python では下記 API が使えます。

  • urllib.quote()
url_encode.py
import urllib

str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]'

print urllib.quote(str)
実行結果
> python url_encode.py
%3B/%3F%3A%40%3D%26%25%20%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
  • 半角スペースが %20 になる
  • / - _ . がエンコードされない

PHP の場合

PHP では下記 API が使えます。

  • urlencode()
  • rawurlencode()
url_encode.php
<?php
$str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]';

// (1)
echo urlencode($str), "\n";
// (2)
echo rawurlencode($str);
?>
実行結果
> php url_encode.php
%3B%2F%3F%3A%40%3D%26%25+%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
%3B%2F%3F%3A%40%3D%26%25%20%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E~%5B%5D
  • (1) urlencode()
  • (2) rawurlencode()
    • 半角スペースが %20 になる
    • - _ . ~ がエンコードされない

まとめ

まとめると以下のようになりました。

こうしてみると微妙な違いが結構ありますね。

言語環境API半角スペースのエンコードエンコードされない文字
JavaURLEncoder.encode()+- _ . *
.NETUri.EscapeUriString()%20; / ? : @ = & $ - _ . + ! * ' ( ) , ~
.NETHttpUtility.UrlEncode()+- _ . ! * ( )
JavaScriptescape()%20/ @ - _ . + *
JavaScriptencodeURI()%20; / ? : @ = & $ - _ . + ! * ' ( ) , ~
JavaScriptencodeURIComponent()%20- _ . ! * ' ( ) ~
RubyURI.escape(), encode()%20; / ? : @ = & $ - _ . + ! * ' ( ) , ~ [ ]
RubyCGI.escape()+- _ .
Pythonurllib.quote()%20/ - _ .
PHPurlencode()+- _ .
PHPrawurlencode()%20- _ . ~

2012-08-09

[][][][][][][] Markdown の HTML 変換 - Ruby, PHP, Groovy, Scala, Node.js

Markdown 形式の文字列HTML 変換する処理を複数のプログラム言語で試してみました。

処理としては、標準入力から UTF-8 の Markdown 形式の文字列を取得し HTML 変換した結果を標準出力UTF-8 で出力しています。

ちなみに、Markdown 文字列は LOGGiX プロジェクトの日本語版サンプル markdown-sample.text を使用しました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20120809/

Ruby の場合

Pure Ruby な kramdown モジュールJRuby で実行してみました。

tohtml.rb
require 'kramdown'

Encoding.default_external = 'utf-8'

puts Kramdown::Document.new($stdin.read).to_html
実行例
 jruby tohtml.rb < markdown-sample.text > ruby_result.txt
結果

特に問題なく変換されました。

更に kramdown は以下のような表組みにも対応していました。

プログラム言語 | モジュール名
---------------|-------------
PHP            |   Markdown
Ruby           |   kramdown
Groovy         |   MarkdownJ
Scala          |   knockoff
Node.js        |   markdown

HTML 変換結果は以下の通りです。

<table>
  <thead>
    <tr>
      <th>プログラム言語</th>
      <th>モジュール名</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PHP</td>
      <td>Markdown</td>
    </tr>
    ・・・
  </tbody>
</table>

PHP の場合

Markdown Extra をダウンロードし、markdown.php をカレントディレクトリに配置して、実行しました。

tohtml.php
<?php
include_once "markdown.php";

$mkStr = stream_get_contents(STDIN);

echo Markdown($mkStr);
?>
実行例
 php tohtml.php < markdown-sample.text > php_result.txt
結果

kramdown と同様、特に問題なく変換され表組みにも対応していました。

Extra の付いていない Markdown 1.0.1o の方は表組みに対応していないのでご注意ください。


Groovy の場合

Java 用のモジュール MarkdownJ を Groovy で使いました。

tohtml.groovy
@Grab('com.madgag:markdownj-core:0.4.1')
import com.petebevin.markdown.*

def mk = new MarkdownProcessor()
def enc = 'UTF-8'

System.out.withWriter enc, {w ->
    w.print mk.markdown(System.in.getText(enc))
}
実行例
 groovy tohtml.groovy < markdown-sample.text > groovy_result.txt
結果

以下のような問題が発生しました。

・・・
<p>
「リファレンス」スタイル: !
<a title="Loggix" href="../../../theme/images/loggix-logo.png">Loggix</a>
</p>

Scala の場合

Scala は 2種類のモジュールを試してみました。Scala 2.9.2 用のモジュールが見当たらなかったため、2.9.1 用のモジュールを 2.9.2 でスクリプト実行しました。

tohtml.scala (Knockoff 版)
import scala.io._
import com.tristanhunt.knockoff.DefaultDiscounter._

val mkStr = new BufferedSource(System.in)(Codec.UTF8).mkString

val ps = new java.io.PrintStream(System.out, false, "UTF-8")

ps.println(toXHTML(knockoff(mkStr)))
実行例1 (Knockoff 版)
 scala -nc -cp knockoff_2.9.1-0.8.0-16.jar tohtml.scala < markdown-sample.text > scala_result.txt

ここで -nc オプションを指定している点にご注意ください。-nc オプションを付けないとコンパイラプロセスリダイレクト先のファイルを開いたままになり不都合が生じます。

結果

以下のような問題が発生しました。

  • '=' と '-' の見出しが正しく変換されなかった *2
  • 画像の「リファレンス」スタイル箇所が a タグになった
<p>Markdown Sample
=======================
</p><p>サンプルドキュメント日本語版
-----------------------
</p>
tohtml2.scala (Actuarius 版)
import scala.io._
import eu.henkelmann.actuarius.ActuariusApp

val mkStr = new BufferedSource(System.in)(Codec.UTF8).mkString

val ps = new java.io.PrintStream(System.out, false, "UTF-8")

ps.println(ActuariusApp(mkStr))
実行例2 (Actuarius 版)
 scala -nc -cp actuarius_2.9.1-0.2.3.jar tohtml2.scala < markdown-sample.text > scala_result2.txt
結果2 (Actuarius 版)

Knockoff とは別の問題が発生しました。

  • '+' と '-' のリストが正しく変換されなかった
<p>(プラス記号で記述)</p>
<p>+   モツァレラチーズ
+   パスタ
+   ワイン</p>
<p>(ハイフン(マイナス記号)で記述)</p>
<p>-   モツァレラチーズ
-   パスタ
-   ワイン</p>

なお、Scala IO を使うと標準入出力まわりの処理が以下のようになります。

ToHtml.scala
package fits.sample

import scalax.io.JavaConverters._
import com.tristanhunt.knockoff.DefaultDiscounter._

object ToHtml extends App {
    val mkStr = System.in.asUnmanagedInput.slurpString

    val html = toXHTML(knockoff(mkStr)).mkString

    System.out.asUnmanagedOutput.write(html)
}

Node.js (CoffeeScript) の場合

Markdown-js を npm install markdown でインストールし、CoffeeScript で実行しました。

tohtml.coffee
mk = require 'markdown'

process.stdin.resume()

process.stdin.on 'data', (data) ->
    console.log mk.markdown.toHTML data.toString()
実行例
 coffee tohtml.coffee < markdown-sample.text > coffee_result.txt
結果

以下のような問題が発生しました。

  • '=' と '-' の見出しが正しく変換されなかった
  • '<' '>' '&' などの不要なエスケープが実施されてしまった
<pre><code>&lt;blockquote&gt;</code></pre>

<p>
        &lt;p&gt;For example.&lt;/p&gt;
    &lt;/blockquote&gt;
</p>

<h3>HTMLとの共存</h3>
・・・
<p>例:April 1&lt;sup&gt;st&lt;/sup&gt;

まとめ

今回の結果をまとめると以下のようになります。

言語モジュール見出し = -リファレンススタイル(画像)リスト + -エスケープ表組み
Rubykramdown 0.13.7
PHPMarkdown Extra 1.2.5
GroovyMarkdownJ Core 0.4.1××
ScalaKnockoff 0.8.0×××
ScalaActuarius 0.2.3××
Node.js (CoffeeScript)Markdown-js 0.4.0×××

*1:img タグになるのが正しい

*2:それぞれ h1h2 タグに変換されるのが正しい

2011-10-02

[][][][][] 信頼されない証明書を使ったHTTPSサーバーにBasic認証でPOST - Ruby, PHP, C#, Java, Groovy

信頼されないSSL証明書(自己証明書)を使ったサイトに対して、Basic認証を行い POST するサンプルを Ruby, PHP, C#, Java, Groovy で実装してみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20111002/


サンプルは、第1引数に URL、第2引数と第3引数に Basic 認証のユーザー名とパスワード、第4引数に POST するデータを指定するようにしています。

なお、POST データを & で区切ると Windows のコマンドプロンプトなどで実行するには不都合があるので ; で区切っている点に注意。

サンプルの実行例
> jruby basic_post_novalidate_certificate.rb https://localhost:8443/ user1 pass1 mode=test;id=123
hello

HTTPS サーバープログラム作成

まずは、クライアントからの処理を受け付けるサーバープログラムを作成し、起動しておきます。

サーバープログラムは HTTPS・Basic認証・POST を処理する必要がありますので、今回は Sinatra + WEBrick で実現してみました。

実装が容易で証明書などの準備も不要なのでテスト用途にはお勧めな方法です。(証明書が未指定なら実行時に自動生成してくれる)


下記では ポート 8443 で実行、クライアントの検証を実施せず(:SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE)、ユーザー名が user1 ならBasic認証を通るように実装しています。

https_server.rb (Sinatra を使った HTTPS サーバープログラム
require "rubygems"
require "sinatra/base"
require "webrick/https"
require "openssl"

class SampleApp < Sinatra::Base
    #Basic認証
    use Rack::Auth::Basic do |user, pass|
        user == 'user1'
    end

    #POST の処理
    post '/' do
        p params
        'hello'
    end
end

#WEBrick で SSL を使用するための処理
#(実行時に自己証明書が自動生成される)
Rack::Handler::WEBrick.run SampleApp, {
    :Port => 8443, 
    :SSLEnable => true, 
    #クライアントを検証しないための設定
    :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, 
    :SSLCertName => [
        ["CN", WEBrick::Utils::getservername]
    ]
}
サーバープログラム実行例
> jruby https_server.rb

今回はサーバープログラムJRuby 1.6.4 (JavaSE 7) や Ruby 1.8.7 で実行しています。

Ruby 1.9.2(RubyInstaller for Windows)で実行した場合では、Java 系のクライアントからの接続に失敗するようなので注意が必要です。


Ruby による Web クライアント

Ruby では、use_ssl を true にした Net::HTTP オブジェクトの verify_mode に OpenSSL::SSL::VERIFY_NONE を設定すれば自己証明書を処理できるようになります。

basic_post_novalidate_certificate.rb
require 'net/https'
require 'uri'

url = URI.parse(ARGV[0])
user = ARGV[1]
pass = ARGV[2]
postData = ARGV[3]

https = Net::HTTP.new(url.host, url.port)
#SSLの有効化
https.use_ssl = true
#SSL証明書を検証しないための設定
https.verify_mode = OpenSSL::SSL::VERIFY_NONE

res = https.start do
    req = Net::HTTP::Post.new(url.path)
    #Basic認証
    req.basic_auth user, pass

    #POSTデータの設定
    req.body = postData

    #POST
    https.request(req)
end

#結果の出力
print res.body

PHP による Web クライアント

PHP で file_get_contents を使えば HTTPS を特に意識しなくてもよいので簡単に実装できます。(自己証明書を意識する必要も無いみたいです)

ただし、php_openssl の extension を有効化する等、file_get_contents で HTTPS を処理するための環境設定が必要かもしれません。(今回は php.ini で php_openssl の extension を有効化しました)

Basic認証に関しては URL でユーザー名とパスワードを指定する方法もありますが、今回は Authorization ヘッダーで指定する方法をとっています。

basic_post_novalidate_certificate.php
<?php
$url = $argv[1];
$user = $argv[2];
$pass = $argv[3];
$postData = $argv[4];

$options = array('http' => array(
    'method' => 'POST',
    'header' => 
        "Authorization: Basic " . base64_encode("$user:$pass") . "\r\n" . 
        "Content-Type: application/x-www-form-urlencoded\r\n",
    'content' => $postData
));

//POST
$res = file_get_contents($url, false, stream_context_create($options));

//結果の出力
echo $res;
?>

C# による Web クライアント

C# では ServicePointManager.ServerCertificateValidationCallback に true を返すコールバックを設定するだけで自己証明書を処理できるようになります。

Basic認証も POST も WebClient クラスを使えば簡単に実装できます。

BasicPostNovalidateCertificate.cs
using System;
using System.Net;

class BasicPostNovalidateCertificate
{
    public static void Main(string[] args)
    {
        var url = args[0];
        var user = args[1];
        var pass = args[2];
        var postData = args[3];

        //SSL 証明書を検証しないようにする設定
        //(証明書を何でも受け入れるようにする)
        ServicePointManager.ServerCertificateValidationCallback = 
            (sender, cert, chain, errors) => true;

        using (WebClient wc = new WebClient())
        {
            //Basic認証
            wc.Credentials = new NetworkCredential(user, pass);

            //POST
            var res = wc.UploadString(url, "POST", postData);

            //結果の出力
            Console.Write(res);
        }
    }
}

なお、上記を実行すると結果は一応返って来ますがサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。(解決策は不明。サーバーの実行環境が JRubyRuby の違いに関わらず発生する)


Java による Web クライアント

Java では SSLContext を何もしない X509TrustManager で初期化し、どんなホスト名でも受け入れる HostnameVerifier を設定します。

  • JavaSE 7
BasicPostNovalidateCertificate.java
import java.io.*;
import java.net.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.*;

public class BasicPostNovalidateCertificate {
    public static void main(String[] args) throws Exception {

        URL url = new URL(args[0]);
        final String user = args[1];
        final String pass = args[2];
        String postData = args[3];

        //Basic認証
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, pass.toCharArray());
            }
        });

        // ホスト名を検証しないようにする設定
        //(openConnection する前に設定しておく必要あり)
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            public boolean verify(String host, SSLSession ses) {
                return true;
            }
        });

        HttpsURLConnection con = (HttpsURLConnection)url.openConnection();
        con.setDoOutput(true);
        con.setRequestMethod("POST");

        //SSL 証明書を検証しないための設定
        SSLContext sslctx = SSLContext.getInstance("SSL");
        sslctx.init(null, new X509TrustManager[] {
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
                }
                public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
                }
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            }
        }, new SecureRandom());

        con.setSSLSocketFactory(sslctx.getSocketFactory());

        //POSTデータの出力
        OutputStream os = con.getOutputStream();
        PrintStream ps = new PrintStream(os);
        ps.print(postData);
        ps.close();

        //結果の出力
        InputStream is = con.getInputStream();
        BufferedInputStream bis = new BufferedInputStream(is);

        int len = 0;
        byte[] buf = new byte[1024];

        while ((len = bis.read(buf, 0, buf.length)) > -1) {
            System.out.write(buf, 0, len);
        }

        bis.close();
    }
}

なお、Java でも C# と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。


Groovy による Web クライアント

Groovy は基本的に Java 版の内容をそのまま実装しました。

basic_post_novalidate_certificate.groovy
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.*

def url = new URL(args[0])
def user = args[1]
def pass = args[2]
def postData = args[3]

//Basic認証(getPasswordAuthentication() をオーバーライド)
Authenticator.default = {
    new PasswordAuthentication(user, pass.toCharArray())
} as Authenticator

// ホスト名を検証しないようにする設定
//(注)openConnection する前に設定しておく必要あり
HttpsURLConnection.defaultHostnameVerifier = {
    host, session -> true
} as HostnameVerifier

def con = url.openConnection()
con.doOutput = true
con.requestMethod = "POST"

//SSL 証明書を検証しないための設定
def sslctx = SSLContext.getInstance("SSL")
//X509TrustManager インターフェースの実装
def tmanager = [
    checkClientTrusted: {chain, authType -> },
    checkServerTrusted: {chain, authType -> },
    getAcceptedIssuers: {null}
] as X509TrustManager

sslctx.init(null as KeyManager[], [tmanager] as X509TrustManager[], new SecureRandom())

con.SSLSocketFactory = sslctx.socketFactory

//POSTデータの出力
con.outputStream.withWriter {
    it.print postData
}

//結果の出力
print con.inputStream.text

なお、Java と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。

2010-12-04

[][][] PHP, C# での Excel準拠 CSV ファイルのパース処理

前回 id:fits:20101129 の続きで、Excel の仕様に準拠した CSV ファイル(改行・カンマ・ダブルクォーテーションを要素内に含む)をパースするサンプルの PHPC# 版です。

使用する CSV ファイルや出力結果の例は、前回 id:fits:20101129 を参照。

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20101204/

PHP の場合

PHP では標準で用意されている fgetcsv を使います。

parse_csv.php
<?php
if (($h = fopen($argv[1], "r")) !== FALSE) {
    while (($r = fgetcsv($h)) !== FALSE) {
        echo "$r[0] : $r[2]\n";
    }
    fclose($h);
}

str_getcsv というのもありますが、これは 1行分の文字列を処理するために使います。

以下の環境で実行してみました。

  • PHP 5.3.3 Win32 VC9
実行例
> php parse_csv.php test.csv

C# の場合

C# というか .NET では、Microsoft.VisualBasic.FileIO.TextFieldParser を使えば、今回のような CSV ファイルを処理できます。

デフォルト設定だとファイルの文字コードが UTF-8 で処理されてしまうので、エンコードを指定しています。

parse_csv.cs
using System;
using System.Text;
using Microsoft.VisualBasic.FileIO;

class CSVParse
{
    public static void Main(string[] args)
    {
        using (var reader = new TextFieldParser(args[0], Encoding.Default))
        {
            //区切り文字を設定する必要あり
            reader.SetDelimiters(",");

            while (!reader.EndOfData)
            {
                var r = reader.ReadFields();
                Console.WriteLine("{0} : {1}", r[0], r[2]);
            }
        }
    }
}

以下の環境でビルド実行してみました。

実行例
> csc /r:Microsoft.VisualBasic.dll parse_csv.cs
> parse_csv.exe test.csv

2010-11-12

[][] CakePHP on Java Servlet - quercus 使用

前回 id:fits:20101111 に続き、今度は CakePHP を Java Servlet Engine(jetty)上で実行してみました。

使用した環境は以下の通り。CakePHP 以外は id:fits:20101111 と同じです。

サンプルのソースコード、CakePHP の変更ファイルは http://github.com/fits/try_samples/tree/master/blog/20101112/

Maven プロジェクトの作成・設定や quercus のファイルの配置に関しては id:fits:20101111 を参照。

CakePHP を Java Servlet で実行するための作業

Java Servlet(quercus)で CakePHP を実行するには、以下を実施します。

  • CakePHPのシングルトンの実装を変更
  • CakePHP用の Servlet Filter を用意
  • キャッシュを無効化

なお、事前準備として src/main/webapp に CakePHP のソースファイルを配置して、core.php に必要最小限の設定を行っておきます。

CakePHPのシングルトンの実装を変更

まず、シングルトンの実装を変更します。

これをやらないとシングルトンが有効に機能せず、ページの表示時に Fatal Error が発生し、CakePHP が正常に動作しません。

cake/libs 内のソースファイルを対象に getInstance の実装を以下のように変更します。

変更後(src/main/webapp/cake/libs/configure.php
function &getInstance() {
    if (is_null(self::$instance)) {
        self::$instance =& new App();
        self::$instance->__map = (array)Cache::read('file_map', '_cake_core_');
    }
    return self::$instance;
}
//$instance を外出し
static $instance = null;

ちなみに、元の実装は以下のようになっています。

変更前(src/main/webapp/cake/libs/configure.php
function &getInstance() {
    static $instance = array();
    if (!$instance) {
        $instance[0] =& new App();
        $instance[0]->__map = (array)Cache::read('file_map', '_cake_core_');
    }
    return $instance[0];
}

なお、変更対象ファイルは以下のようなものです。

シングルトンの実装変更対象ファイル
  • src/main/webapp/cake/libs

また、変数未定義の Notice が発生する箇所も必要に応じて修正します。(例えば、debugger.php の compact に渡す helpID の変数定義など)

CakePHP用の Servlet Filter を用意

.htaccess の代わりにアクセス先を変更する Servlet Filter を用意します。

必要最低限の処理はファイルの有無に応じて app/webroot もしくは app/webroot/index.php?url=[URL] にフォワードする事です。今回も自作してみました。

src/main/java/fits/sample/CakeRoutingFilter.java
package fits.sample;
・・・
public class CakeRoutingFilter implements Filter {
    ・・・
    public void init(FilterConfig config) {
        this.ctx = config.getServletContext();
    }
    //フィルター処理
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req = (HttpServletRequest)request;
        String contextPath = req.getContextPath();
        String requestPath = req.getRequestURI();
        String path = requestPath.replace(contextPath + "/", "").trim();

        if (!path.startsWith("app/webroot")) {
            String vPath = "app/webroot/" + path;
            String rPath = this.ctx.getRealPath(vPath);

            if (new File(rPath).exists()) {
                this.dispatchForward("/" + vPath, request, response);
                return;
            }
            else {
                this.dispatchForward("/app/webroot/index.php?url=" + path, request, response);
                return;
            }
        }
        chain.doFilter(request, response);
    }
    ・・・
    private void dispatchForward(String path, ServletRequest request, ServletResponse response) throws ServletException, IOException {
        request.getRequestDispatcher(path).forward(request, response);
    }
}

キャッシュを無効化

キャッシュが有効な状態だと 2度目以降のアクセスで例外が発生するようになるため、app/config/core.php で Cache.disable を true に設定しキャッシュを無効化します。

src/main/webapp/app/config/core.php
    Configure::write('Cache.disable', true);

動作確認

DB に MySQL を使って、簡単なモデル・ビュー・コントローラーを実行してみます。

DB・テーブルを作成(sql/todo_db.sql 参照)し、app/config/database.php を作成します。

なお、src/main/webapp/WEB-INF/lib に MySQL の JDBC ドライバー(mysql-connector-java-5.1.13-bin.jar)を配置しておきます。

src/main/webapp/app/config/database.php
<?php
class DATABASE_CONFIG {
    var $default = array(
        'driver' => 'mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'root',
        'password' => '',
        'database' => 'todo',
        'prefix' => '',
        'encoding' => 'utf8'
    );
}

モデル・コントローラーを作成します。

src/main/webapp/app/models/datasources/tasks.php
<?php
class Task extends AppModel {
    var $name = "Task";
}
src/main/webapp/app/controllers/tasks_controller.php
<?php
class TasksController extends AppController {
    var $name = "Tasks";

    function index() {
        $tasks = $this->Task->find('all');
        $this->set('tasks', $tasks);
    }
    //追加
    function add() {
        $this->Task->save($this->data);
        $this->redirect('.');
    }
}

ビューを作成します。(レイアウトは src/main/webapp/app/views/layouts/default.ctp 参照)

Java Servlet として実行する場合、find の結果がテーブル名 tasks の連想配列の配列になるようなので注意。(通常はモデル名 Task の連想配列の配列になる)

src/main/webapp/app/views/tasks/index.ctp
<div>
<?php
  echo $form->create('Task', array('type' => 'POST', 'action' => 'add'));
  echo $form->label(null, 'タイトル');
  echo $form->text('Task.title');
  echo $form->submit('追加');
  echo $form->end();
?>
</div>
<div>
  <table>
    <tr>
      <th>ID</th>
      <th>タイトル</th>
      <th>登録日</th>
      <th>更新日</th>
    </tr>
    <?php
      foreach ($tasks as $task) {
        //Java で実行する場合はテーブル名 tasks の連想配列になっている
        //XAMPP で普通に実行する場合はモデル名 Task の連想配列になる
        $t = $task['tasks'];
    ?>
    <tr>
      <td><?php echo $t['task_id']; ?></td>
      <td><?php echo $t['title']; ?></td>
      <td><?php echo $t['created']; ?></td>
      <td><?php echo $t['modified']; ?></td>
    </tr>
    <?php
      }
    ?>
  </table>
</div>

jetty を起動して http://localhost:8080/cakephp-sample/tasks/ にアクセスすれば、一応動作している事が確認できると思います。

jetty 起動
> mvn jetty:run

f:id:fits:20101113004151p:image

ただし、原因は未調査ですが。

タイトルに日本語を入力して「追加」ボタンを押すと文字化けするようなので注意。(同一コードを XAMPP で実行しても文字化けは発生しない)