Author Archives: neobit

EC-CUBE 2系でポート番号付で管理画面を動かす

Apacheの処理分散させるために、ロードバランサーを介してラウンドロビンを掛けた複数台で同じEC-CUBEを動かす必要が出てきました。
Apacheを動作させるフロントエンドをコピーした複数台でPHP処理を分担、MySQLはローカル接続されているバックエンドの専用サーバーで運用という形です。

EC-CUBEはそもそもPHPセッションをDB保存しているので、DBをバックエンドのサーバーで共通化しておけば複数台でのPHPセッションの共有化を配慮する必要はなく、複数台運用への切り替えは簡単です。

ただし、管理画面からアップロードした画像等はApacheを動かしているサーバーに物理的に保存されるため、ラウンドロビンが掛かった状態ではどのサーバーにファイル保存されるか分からなくバラバラになってしまうため、どれか1台固定のサーバーを正としてそこへ保存し、rsync等で各サーバーへコピーしなくてはなりません。

サブドメインではっきり分けれれば良いのですが、ドメイン追加をできない場合は1443などの独自のポート番号でポートフォワードで振り分けて管理画面を動かすサーバーを固定したいところ。

ただし、普通にポート番号付きでアクセスしてしまうと、管理画面でログインした直後にリダイレクトエラーが出ます。
これは/data/config/config.php で設定している HTTP_URL や HTTPS_URL と異なるURLへジャンプすることを防ぐセキュリティ対策の影響です。
では、そもそもHTTP_URL に:1443付きで定義したらどうかとなりますが、そうすると今後はポート番号無しでアクセスしているカート画面などでリダイレクトエラーが出ます。訪問者画面も管理画面も同じEC-CUBEだと設定も共通なので。

そこで、ポート番号付きでアクセスしたときだけこのURLを書き換えてしまうと良いです。
具体的には /data/config/config.php をこんな風に書きます。常時SSLのご時勢ですからHTTP_URLは振り分けなくても良いかな?

define('HTTP_URL', 'http://www.exsample.co.jp/');
if ($_SERVER['SERVER_PORT'] == '1443') {
  define('HTTPS_URL', 'https://www.exsample.co.jp:1443/');
} else {
  define('HTTPS_URL', 'https://www.exsample.co.jp/');
}

PHP5.6でEUC-JPやShift_JISのサイトを動かす

今さら何でPHP5.6なのかと悲しくなりますが、サーバー変更するときにmysql_connect等のPHP7で廃止された関数を大量に使用しているサイトだったので、修正予算が無い関係でどうしてもPHP5系を選ばないといけませんでした。

しかもMySQLデータもPHPソースもHTMLも、全てEUC-JPで書かれているというサイトです。

せめてものあがきで最新である5.6を選択するわけですが、PHP5.6はデフォルトの言語がUTF-8になっているため、UTF-8で構築する分には何も考えなくて良いのですが、EUC-JPやShift_JISで書かれたサイトを設置すると文字化けにハマります。
今回、試行錯誤で時間が掛かったので、その解決策をメモしておきます。

通常はPHPソースもHTMLもUTF-8で掛かれている前提でphp.iniは、こう設定されていると思います。

; mbstring.language = Japanese
; mbstring.detect_order = 
; mbstring.encoding_translation = Off
; mbstring.func_overload = 0
; mbstring.http_input = 
; mbstring.http_output = 
; mbstring.internal_encoding = 
; mbstring.strict_detection = Off
; mbstring.substitute_character = 
default_language = UTF-8

今回、他のバーチャルドメインも乗せているレンタルサーバーでphp.iniは変更できない状況だったので、php.iniはUTF-8の設定のままでプログラム側で対処します。

共通設定処理など、サイト全体の最初に処理されるPHPソースに以下の文を書き足します。

ini_set('input_encording', 'eucjp');
ini_set('internal_encoding', 'eucjp');
header('Content-Type: text/html; charset=EUC-JP');

mb_~関数では無いのが重要。
mb_http_inputやmb_internal_encodingでEUC-JPを指定した場合、表示はうまく出来てもフォームのPOST値がコード変換がされず文字化けしたり$_POSTで受け取れなかったりしました。
PHP5.6ではmb_internal_encoding等が非推奨になっているのはこの辺りの問題もあるからなのかも?

そこでPHP5.6で新設された input_encoding や internal_encoding で指定します。

また default_language = UTF-8 がデフォルトになっていて Content-type: text/html; charset=UTF-8 というヘッダーが吐かれてしまうので、header関数でEUC-JPに上書きします。

MySQLデータベースは5系以降は内部コードがUTF-8で処理されますので、EUC-JPでdumpしてきたデータはエディタ等でUTF-8にコード変換して、UTF-8でデータベースに流し込み通信段階でクライアント側の文字コードに変換させるのが吉。

PHPプログラムからは、connectしたあとにeucでデータを読み書きできるように文字コード設定しておけばOK

$con = mysql_connect($host, $user, $pass); 
mysql_set_charset('eucjpms', $con);

もう試行錯誤しないで済みますように。

PHPの array_column array_combine は超便利

PHPでデータベースを読み書きして操作していると、連想配列が配列になった以下のような配列からデータを取り出して操作する事が多いと思います。

$rec = array(
	array(
		'id' => '1001',
		'name' => 'りんご',
		'area' => '青森'
	),
	array(
		'id' => '1002',
		'name' => '梨',
		'area' => '鳥取'
	),
	array(
		'id' => '1003',
		'name' => 'ぶどう',
		'area' => '山梨'
	)
);

たとえば、areaの配列を取り出したいとき

foreachを使うと以下のようになりますが。

$ary = array();
foreach ($rec as $d) {
	$ary[] = $d['area'];
}

これを array_column を使うとこう書けます。

$ary = array_column($rec, 'area');

結果はこう

$ary = array(
	'青森',
	'鳥取',
	'山梨'
);

また、idをキーにしたnameの配列を取り出したいときは array_column の第2引数にキーにしたい項目のキー名を指定します

foreachだと以下のように書きますが

$ary = array();
foreach ($rec as $d) {
	$ary[$d['id']] = $d['name'];
}

これを array_column を使うとこう書けます。

$ary = array_column($rec, 'name', 'id');

結果はこう

$ary = array(
	'1001' => 'りんご',
	'1002' => '梨',
	'1003' => 'ぶどう'
);

また、idをキーにして$recの各項目のArrayをそのまま保持した配列を取り出したいときは、array_combine という第1引数の配列をキーにして、第2引数の配列を値として1つの配列を組み立てる関数を array_column と組み合わせて使うと簡単です。

foreachだと以下のように書きますが

$ary = array();
foreach ($rec as $d) {
	$ary[$d['id']] = $d;
}

これを array_column と array_combine を使ってこう書けます。

$ary = array_combine(array_column($rec, 'id'), $rec);

結果はこう

$ary = array(
	'1001' => array(
		'id' => '1001',
		'name' => 'りんご',
		'area' => '青森'
	),
	'1002' => array(
		'id' => '1002',
		'name' => '梨',
		'area' => '鳥取'
	),
	'1003' => array(
		'id' => '1003',
		'name' => 'ぶどう',
		'area' => '山梨'
	)
);

コーディングがシンプルに、かつ見通しが良くなるので積極的に使っていきたいお勧め手法です。

EC-CUBE 2系で注文を受けた商品の規格を後で削除すると、管理画面で受注内容を編集できなくなる

注文された規格を削除して、dtb_products_classテーブルから該当する規格データが無くなると、受注内容のたとえば発送先住所などを編集しようとしても数量の上限チェックに引っかかりエラーが出て受注内容の変更ができなくなってしまいます。

※ この現象は、規格のチェックBOXを外して更新 → チェックBOXを再度立てて更新 でも発生します。見た目は同じ規格が存在しているように見えますが、内部のproduct_class_idが変わってしまうのが原因です

product_class_idも引用してこれていないので画面処理もうまくいきません。
対応策として、もし規格データが削除されていた場合は在庫数チェックを行わないようにする事で受注内容の編集をできるようにしてみます。

まずdtb_products_classから規格データが無くなっていてもproduct_class_idやproduct_idを引用できるように商品詳細dtb_order_detailの方から参照するようにします
SC_Helper_Purchase.php の
function getOrderDetail のSQL文を書き換えます

-            T3.product_id,
-            T3.product_class_id as product_class_id,
+            T2.product_id,
+            T2.product_class_id as product_class_id,

LC_Page_Admin_Order_Edit.php の
function lfCheckError に処理をスルーさせるIF文を追加

            // 在庫数のチェック
            $arrProduct = $objProduct->getDetailAndProductsClass($arrValues['product_class_id'][$i]);

+            // 規格が削除されていたら在庫数チェックしない
+            if ($arrProduct['product_class_id'] == '') {
+                continue;
+            }

商品種類 product_type_id も引用してこれていなくて、画面にはエラー表示されませんが必須チェックに引っかかり先に進めなくなるのでこれもスルーさせます。
LC_Page_Admin_Order_Edit.php の
function lfInitParam を修正

        // 受注詳細情報
-        $objFormParam->addParam('商品種別ID', 'product_type_id', INT_LEN, 'n', array('EXIST_CHECK', 'MAX_LENGTH_CHECK', 'NUM_CHECK'), '0');
+        $objFormParam->addParam('商品種別ID', 'product_type_id', INT_LEN, 'n', array('MAX_LENGTH_CHECK', 'NUM_CHECK'), '0');

視覚的にも、規格が削除されていて在庫数が連動しない商品だと分かったほうが良いので、数量欄のあるTDの背景色を赤色で表示してみます。
/data/Smarty/templates/admin/order/edit.tpl を修正

-                    <td class="center">
-                        <!--{assign var=key value="quantity"}-->
+                    <td class="center"<!--{if $arrForm.product_type_id.value[$product_index] == ''}--> style="background-color:#fdd"<!--{/if}-->>
+                        <!--{assign var=key value="quantity"}-->

そもそも関連データが存在している規格データを容易にdeleteしてしまう実装ってどうなの?っていう話なんですが。。

EC-CUBE 2系でShift_JISに存在しない文字が含まれた受注データがあると、その受注内容が受注CSVからまるごと欠落する

受注データの備考などにお客様が入力した文章にShift_JISで表現できない文字や記号がまざっていると、受注CSVをダウンロードしたときにその受注データがそっくり欠落してしまいます。

これはCSVデータをShift_JISに変換するときのiconvのパラメータがデフォルトの「不正な文字があったらデータを捨てる」指定になっているからです。
受注データが抜け落ちてしまっては、発見が遅れて実務上のトラブルになりかねないので、変換できない文字があっても近似文字に置き換えて出力するTRANSLIT指定をつけた方が良いと思います。

SC_Helper_CSV.php の
function fopen_for_output_csv のiconv指定を変更

-        stream_filter_append($fp, 'convert.iconv.utf-8/cp932');
+        stream_filter_append($fp, 'convert.iconv.utf-8/cp932//TRANSLIT');

EC-CUBE 2系で商品情報をCSVで更新するとき関連商品情報が削除される

CSVアップロードして商品データを更新する事ができるのですが、そのCSVに「関連商品」の列が無いと、CSVをアップロードしたときにその商品に今まで登録されていた関連商品の内容が全て削除されてしまいます。

これは、CSVアップロード処理が「関連商品」に関する列がCSVに存在している事を前提に処理しているからで、関連商品がCSV列名に存在しているかどうかチェックして、列が無かったら関連商品の処理を行わないようにすることで回避できます。

LC_Page_Admin_Products_UploadCSV.php の
function lfRegistReccomendProducts の冒頭に以下のコードを挿入

        $cnt = 0;
        for ($i = 1; $i <= RECOMMEND_PRODUCT_MAX; $i++) {
            $keyname = 'recommend_product_id' . $i;
            $comment_key = 'recommend_comment' . $i;
            if (array_key_exists($keyname, $arrList) == true) $cnt++;
            if (array_key_exists($comment_key, $arrList) == true) $cnt++;
        }
        if ($cnt == 0) return;

※ 自分でコミュニティへ参加してパッチを作成するマンパワーが無いのでその予定はありません

EC-CUBE 2系で商品を沢山購入すると住所情報が欠落する

以外と有名な問題なのですが、カートに沢山の商品を入れて注文すると、受注メールは送信されてエラーも出ないのに「送料がゼロ円になる」とか「管理画面に受注データが表示されない」という障害が起きます。

これは、セッション情報を保存するDBテーブルが text型で取られているため、65,535バイト(MySQLの場合)以上のセッション情報を保存しようとすると後ろの方のデータが欠落するためです。

CREATE TABLE dtb_session (
    sess_id text NOT NULL,
    sess_data text,
    create_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_date timestamp NOT NULL,
    PRIMARY KEY (sess_id(255))
);

このセッションデータには、カートに入れた商品の情報+購入者情報+配送先情報が入るため、欠落するときは真っ先に配送先の情報が欠落してしまい、結果として都道府県が分からなくなるので送料が計算できずにゼロ円になってしまいます。また、受注データとJOINする配送先データが生成されないので一覧に受注データが表示されないという症状に繋がります。

対策としては、text → logtext に型を変更するだけで65Kバイト→4Gバイトまで保持できるようになるので情報欠落は無くなります。sessionデータはdtb_order_tempテーブルにも持っているのでそちらも修正が必要です。

alter table dtb_session modify sess_data longtext;
alter table dtb_order_temp modify session longtext;

※ かなり以前から報告されている障害ですがバージョン2.13.5でも対策されていないので公式に対応する予定は無いようです
※ 自分でコミュニティへ参加してパッチを作成するマンパワーが無いのでその予定はありません

Smartyのtruncateをマルチバイト対応で

Smartyのtruncate修飾子はマルチバイト対応されていないので文字化けしちゃう。
独自にtruncateの拡張をするまでもなく、PHP自身が持っているmb_strimwidthを使えばOK

でも、すぐに忘れちゃうのでメモ
{$contents|mb_strimwidth:0:80:"..."}

変数にHTMLタグが入ってるなら、それも除去
{$contents|strip_tags|mb_strimwidth:0:80:"..."}

PEAR Mail_mimeDecodeでヘッダー文字化け

PHPで送信されてきたメールを解析して処理を行うため、ネット情報を参考に以下のようにやってみた。

PEAR::Mail_mimeDecodeをインストールして以下のように処理したのだが、$mail_body->bodyの処理は良かったとして、ヘッダーの文字列が内部エンコードに変換されず元のままのエンコードで入ってきている。UTF-8だったりJISだったり。。困った。

ネット情報を参考に書いた元ソースがこれ

require_once 'Mail/mimeDecode.php';

$rawdata = file_get_contents("php://stdin"); // 標準入力から

// メールParse
$params = array();
$params['include_bodies'] = true;
$params['decode_bodies']  = true;
$params['decode_headers'] = true';
$params['crlf'] = "\r\n";
$Mail = new Mail_mimeDecode($rawdata);
$mail_data = $Mail->decode($params);

$form = array();
$form['from'] = preg_replace('/^.*<([^<>]+?)>.*$/', '$1', $mail_data->headers['from']);
$form['to'] = preg_replace('/^.*<([^<>]+?)>.*$/', '$1', $mail_data->headers['to']);
$form['subject'] = $mail_data->headers['subject'];

// 本文の取得
$encode = "ISO-2022-JP";
if ($mail_data->ctype_primary == 'multipart') {
	foreach ($mail_data->parts as $parts) {
		if ($parts->ctype_primary == 'text' and $parts->ctype_secondary == 'plain') {
			$MailBody = $parts->body;
			if ($parts->ctype_parameters['charset']) {
				$encode = $parts->ctype_parameters['charset'];
			}
			break;
		}
	}
} else {
	if ($mail_data->ctype_parameters['charset']) {
		$encode = $mail_data->ctype_parameters['charset'];
	}
	$MailBody = $mail_data->body;
}
$form['body'] = mb_convert_encoding($MailBody, "UTF-8", $encode);

print_r($form);

$mail_dataを見てもヘッダーの素のエンコードが何だったかパラメータに入ってこないから、後処理しようがないじゃん!

とmimeDecodeのソースを覗いてみたらこう書かれてるじゃない。

     * @param array An array of various parameters that determine
*              various things:
*              include_bodies - Whether to include the body in the returned
*                               object.
*              decode_bodies  - Whether to decode the bodies
*                               of the parts. (Transfer encoding)
*              decode_headers - Whether to decode headers,
*                             - use "UTF8//IGNORE" to convert charset.
*
*              input          - If called statically, this will be treated
*                               as the input
* @return object Decoded results
* @access public
*/
function decode($params = null)

つまり、decode_bodiesはtrue/falseで良いけど、decode_headresはtrue/falseじゃなくて期待する変換後エンコードを文字で指定するのが正解。

ただ、これも間違ってる。iconvで処理してるから、UTF8じゃなくてUTF-8ね。

それにIGNOREだと不正文字をカットされちゃって不正文字が混ざってた事が分からなくなるので、不正文字マークにしてくれるTRANSLITの方が良いと思われ。

$params['decode_headers'] = 'UTF-8//TRANSLIT';

これで解決!ネット情報もソースのコメントさえもあやふや。

疑って掛かる事をしばらく忘れていましたが、何もかも確認は必要ですね。

Category: PHP

gmoserverでKCFinderが変なパスを返す不具合

CKEditorと組み合わせてKCFinderを画像アップローダーとして使う事が良くあるのですが、gmoserverのSDプランのサイトに設置したときに、画像はKCFinder上でちゃんとアップロードできているのに、選択してCKEditorに戻ると画像のパスがおかしくなっていて、画像が壊れて表示されるという現象にあいました。

正常に動作する他のサーバーと色々と比較してみたところ、$_SERVER[‘DOCUMENT_ROOT’] に返されるパスが、自サイトのサイトTOPと全く異なるパスを返していることが判明。

そのため、KCFinderが相対パス→絶対パスに変換しようとDOCUMENT_ROOTと比較したところで失敗していた。(つまりuploadURLやuploadDirに相対パスを指定したときだけ発生する問題)

gmoserverでは実際のサイトROOTパスは、$_SERVER[‘HOME’] にセットされているようなので、KCFinderのプログラムファイルを全検索して$_SERVER[‘DOCUMENT_ROOT’] → $_SERVER[‘HOME’] に書き換えてOK

時間の無いところで、思わぬワナに嵌ってしまった。

他のプログラムでもDOCUMENT_ROOTに依存しているコーディングは要注意ですよ。