It's raining cats and dogs.

無駄なことなんてないはず

携帯(ガラケー)サイトで、PHP5.2以下から5.3以上へのバージョンアップ時にmb_output_handlerを使った文字コード変換でハマった時のメモ

前回のエントリでも触れたけど、とある携帯(ガラケー)サイトをPHP5.2から5.4にバージョンアップ対応していた時にまたハマった。※文字多めのエントリ

ガラケーといえば、3キャリア対応するためにShift_JISでページを作るのは鉄板。やり方はいろいろあるけど、このサイトはmb_output_handler文字コード変換していた。mb_output_handlerはざっくり言うと、ob_startでバッファリングした出力をmbstring.internal_encodingで指定した文字コードから、mbstring.http_outputで指定した文字コードによろしく変換してくれるというすぐれものだ。

ただ、このmb_output_handlerには罠があって、

http://php.net/manual/ja/function.mb-output-handler.php

にも書かれている通り、

デフォルトの MIME 型が text/ で始まる

という条件で発動するらしい。

で、今回何にハマったのかというと、

このサイトではapache側でphp_valueを使って、mbstring.internal_encodingEUC-JP、mbstring.http_outputShift_JISで設定されていた。でもってhtmlのテンプレートは基本EUC-JPで書かれていて、それをtext/htmlで出力していたので、mb_output_handlerが効いていたために、特に問題なくShift_JISに変換されて表示されていたのだけど、何故か一部のページが文字化けしてしまっていた。PHP5.2の環境だと問題なく表示されるのに。

よくよく調べてみると、文字化けしていたページはなぜかはわからないけどapplication/xhtml+xmlが使われていてShift_JISで書かれていた。(たぶん何かの装飾をしたかったのだろう) ただ、mb_output_handlerの仕様上、mimeタイプがtext/でなければ、文字コード変換は行われず、普通にShift_JISとして表示されるはずだし、実際PHP5.2の環境では問題なく表示できていた。

ではなぜ文字化けが起きたのか。

PHP5.3からはmbstring.http_output_conv_mimetypesという設定が追加されている。

http://php.net/manual/ja/migration53.ini.php

ここでも書かれている通り、

このディレクティブは mb_output_handler() 関数が呼び出された場合に使う Content-Type の正規表現パターンを指定します。

要はmbstring.http_output_conv_mimetypesmb_output_handlerで変換対象にしたいContent-Typeの正規表現を書くことで、text/以外でも変換できるとのこと。

で、mbstring.http_output_conv_mimetypesのデフォルト値が^(text/|application/xhtml\+xml)だったために、今まで変換されないはずであったapplication/xhtml+xmlで出力していたShift_JISのページがmb_output_handlerで変換されてしまい文字化けてしまっていたのだった。(Shift_JISのページをEUC-JPとして解釈してShift_JISに変換していた)

対応としては、application/xhtml+xmlのページをEUC-JPにするなどを考えたけど、影響範囲を調査するのが手間だったので、auto_prepend_fileで起動されるbootstrap的なphpmbstring.http_output_conv_mimetypes正規表現text/でのみ文字コード変換されるように変更することにした。

<?php
ini_set('mbstring.http_output_conv_mimetypes', 'text/*');

相変わらず設定値にはハマりますね。

ちなみに

http://php.net/manual/ja/migration53.ini.php

ではmbstring.http_output_conv_mimetypeと書かれているけど、実際にはmbstring.http_output_conv_mimetypesのようにsを付けないと動かない。(もしかしたら5.3では動いたのかもしれない)

PHP5.2以下から5.3以上へのバージョンアップ時にcookie以外の手段でセッションIDを保持する場合のメモ

とある携帯(ガラケー)サイトのPHPを5.2から5.4にアップデートしようとしてハマった。

携帯サイトではcookieを使えない場合もあるので、(セキュリティ的に良いか悪いかは別にして)URLにセッションIDを付ける場合はわりと多いのではないだろうか?

で、試しに5.4で動かしてみたらセッションがうまく引き継がれず、ログイン状態を維持できない場面に遭遇した。PHPのバージョンアップでセッション周りの動きが変わったのかと思い、殆ど触ったことのないプログラムをデバッグしまくって、原因箇所を特定しようと躍起になってみたものの全くわからない。コードを追うのを諦めてphpマニュアルを見ているとそれらしきものを発見。(というかビンゴ)

http://www.php.net/manual/ja/session.configuration.php#ini.session.use-only-cookies

どうやらPHP5.3からsession.user_only_cookiesのデフォルト値がオンになったみたい。すなわちcookie以外でのセッションIDの保持がデフォルトではできなくなってしまったようだ。このせいでURLにくっつけたセッションIDは使われなくなり、アプリケーション側はログオフ状態になってしまっていたようだ。

対策としては php.ini

session.use_only_cookies = 0

とするか .htaccess

php_value session.use_only_cookies Off

とするか php

<?php
ini_set('session.use_only_cookies', 0);

とすれば良い。

PHPのバージョンアップは、言語の動きというよりも、設定のデフォルト値が変わってしまうことでハマることが多い気がする。

[ssh] ssh経由で他のサーバのスクリプトを実行するときに.bash_profileを読ませる方法

書きなぐりメモ。

ssh経由で何かのスクリプトを実行したい時は

$ ssh <hostname> "/usr/bin/perl /path/to/hoge.pl"

という感じで実行できるけど、~/.bash_profileとかに書かれた環境変数を使って処理をするスクリプトだと、sshの非対話モードでは~/.bash_profileを読んでくれないので、困ってしまう。

そんなときは

$ ssh <hostname> "source ~/.bash_profile; /usr/bin/perl /path/to/hoge.pl"

のように明示的に読ませれば良いらしい。

ちなみに

sshdの設定でPermitUserEnvironment=yesを設定して、~/.ssh/environmentというファイルに環境変数を設定するやり方もあるようだけど、必ずしもssh経由で実行するものではなく、対象のサーバで直接実行したい場合もあるので、~/.bash_profile~/.ssh/environmentの両方に設定するのが冗長なので今回は上記のやり方にした。

composerでSmarty2をインストールする

composerでsmartyを入れる時は

{
    "require": {
        "smarty/smarty": "*"
    }
}

とやるとインストールできるのだけど、これだとSmarty3が入ってしまう。でも今の環境ではSmarty2が使われているので2を入れたい(バージョンアップしたらいいがなという話は置いといて)。いろいろ調べてみたけどどうやらSmarty2はcomposerに対応していないようなので、packageを使って個別に入れることにする。

{
    "repositories":[
    {
        "type": "package",
        "package": {
            "name": "smarty/smarty2",
            "version": "2.6.28",
            "dist": {
                "url": "http://www.smarty.net/files/Smarty-2.6.28.tar.gz",
                "type": "tar"
            },
            "include-path": [
                "libs/"
            ]
        }
    }
    ],
    "require": {
        "smarty/smarty2": "*"
    }
}

nameは適当。あとは

$ php composer.phar install

でインストールし、

<?php
require 'vendor/autoload.php';
require 'Smarty.class.php';

$smarty = new Smarty();

という感じで使える。 composer.jsonの中でinclude-pathという項目を設定している。これが何かというと、composerではvendor/autoload.phpをrequireするとcomposerで使うためのinclude_pathを設定してくれる。これはvendor/composer/include_paths.phpの内容が設定されるようだ。composerに対応しているライブラリであれば、このinclude_paths.phpをよろしく編集してくれるので、特にこまらないのだけど、今回のSmarty2のようにpackageを使って入れたりするとinclude_pathを設定してくれないので、そのための設定。ちなみに上記のようにinclude-pathを設定すると

<?php

// include_paths.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    $vendorDir . '/smarty/smarty2/libs',
);

という感じでinclude_paths.phpを更新してくれる。
Smarty.class.phpvendor/smarty/smarty2/libs/Smarty.class.phpにいるので、"include-path": ["libs/"]を書いておくとvendor/smarty/smarty2/libs/までinclude_pathが通るので、

<?php
require 'Smarty.class.php';

で使えるわけだ。

ちなみに、require自体したくない場合は

{
    "repositories":[
    {
        "type": "package",
        "package": {
            "name": "smarty/smarty2",
            "version": "2.6.28",
            "dist": {
                "url": "http://www.smarty.net/files/Smarty-2.6.28.tar.gz",
                "type": "tar"
            },
            "autoload": {
                "files": ["libs/Smarty.class.php"]
            },
            "include-path": [
                "libs/"
            ]
        }
    }
    ],
    "require": {
        "smarty/smarty2": "*"
    }
}

という感じでautoload要素を追加しておけば良い。

composerでライブラリのインストールディレクトリを変える

composerphpのライブラリ管理ツールだ。改めて説明する必要はないくらいだけど、rubyのbundlerとかperlのcartonみたいなもんだ。

composerのざっくりとしたディレクトリ構成は

current_dir
 ├ composer.phar
 ├ composer.json
 ├ composer.lock
 └ vendor
    ├ autoload.php
    ├ composer
    ├ ・・・
    ├ <ライブラリA>
    └ <ライブラリB>

こんな感じになっていて

[current_dir]$ php composer.phar install<or update>

などでライブラリをインストールするとvenderディレクトリにインストールされる。

で、このvendorディレクトリを別の名前にしたかったので、いろいろ調べてみたがそれらしい情報に出会えなかったので、composerのソースをみて試してみた。

結論としてcomposer.jsonに以下の設定をしたらインストール先のディレクトリ名を変えれた。

{
    "config": {
      "vendor-dir": "my_library"
    },
...
}

このようにconfigにvendor-dirに任意のディレクトリ名を設定できるようだ。

他にも

https://github.com/composer/composer/blob/master/src/Composer/Config.php

この辺りのソースを見るとなにやらいろいろ設定できそう。

追記

よくよくcomposerのドキュメントをみるときちんと書いてあって残念な気分になりました。。。ドキュメントはきちんと読みましょう。

http://getcomposer.org/doc/04-schema.md#config

perlがスクリプトをどのように解釈しているのかを知るためのB::Deparseなるものを知った

僕のperlの先生monmonさんに今日もまた教えてもらった。

perlがどのようにスクリプトを解釈しているのかを事前に知ることのできるB::Deparseというものがあるらしい。

↓のサイトを参考にさせていただいた。
http://www.g-ishihara.com/perl/compiler01.html

B::Deparseってのは、perlスクリプトをコンパイルして、そこから更にソースコードを生成(デコンパイル?)するらしい。なのでperlがどのようにそのスクリプトを解釈しているのかってのがわかるとか。

ちなみにBモジュールってのはperlコンパイラ系のモジュールで、Oモジュールってのはそのモジュールを操作するためのインターフェースみたいなものとのこと。

ではやってみる。

$ perl -MO=Deparse -e 'use constant HOGE => 1;print HOGE;'
use constant ('HOGE', 1);
print 1;
-e syntax OK

HOGEが展開されてprint 1となっている。

↓はmonmonさんに教えてもらった内容。ループの中で何かの計算をして$xに値を入れている。ループのたびに計算して効率の悪そうな感じなのだけど、Deparseすると、コンパイル時点で既に計算されているので、そんなに効率は悪くないことがわかる。

$ perl -MO=Deparse -e 'for my $i (0..100){my $x = 60*60*24;}'
foreach my $i (0 .. 100) {
    my $x = 86400;
}
-e syntax OK

ちなみにこのforはリストコンテキストなので、foreachになっていることもわかった。

あと面白いのが↓

$ perl -MO=Deparse -e 'use constant DEBUG => 1;if(DEBUG){print "debug";} print "done."'
use constant ('DEBUG', 1);
do {
    print 'debug'
};
print 'done.';
-e syntax OK

よくあるデバッグフラグみたいなものを定数に入れていて、if文で判定してデバッグモードだったらほげほげみたいな処理。
これはif文の判定すらなくなっている。

こんな感じでコンパイル具合がわかるので、これからはちょいちょい使ってみたい。

ORA-12154: TNS: 指定された接続識別子を解決できませんでした

sqlplusなどでoracle clientを使ってoracleにアクセスするときによく出るアレ。これがでるとなんだか憂鬱。 たぶんネットに溢れかえっているであろう内容だけど、備忘録。

$ sqlplus <ユーザーID>/<パスワード>@<SID>

SQL*Plus: Release 11.2 Production on 月 9月 30 20:58:17 2013

Copyright (c) 1982, 2010, Oracle.  All rights reserved.

ERROR:
ORA-12154: TNS: 指定された接続識別子を解決できませんでした


ユーザー名を入力してください:

今回は、tnsnames.oraを読み込めていないのが原因だった。oracleはtnsnames.oraはどのパスで見つけに行くのか。

  1. 環境変数 TNS_ADMIN で設定したディレクトリ配下
  2. $ORACLE_HOME/network/admin 配下

とのこと。 今回はtnsnames.oraが

$ORACLE_HOME/lib/network/admin/tnsnames.ora

となっていたのとTNS_ADMINが設定されていなかったのが原因。なぜこのパスになってしまったのかはわからないけど、
とりあえずこのエラーが出た時に少し萎えなくなれた気がする。