プログラミング

FuelPHPのEmailパッケージをS/MIMEに対応させる

2015/9/28

FuelPHPのEmailパッケージをS/MIMEに対応させるお話です。(今回は電子署名のみ)

Emailパッケージを拡張するのには、以下のような方法で行いました。

#! /bin/blog: [FuelPHP]ネームスペースを上書きしてパッケージのクラスを書き換える

変更が必要なのは Email\Email_Driver だけなので、Emailパッケージから必要なファイルをコピーして以下のような構成を作ります。

fuel
└ packages
    └myemail
        ├─ bootstrap.php
        |─ classes
        |    └─ email
        |         └─ driver.php
        └─ config
             └─ email.php

bootstrap.php は以下のように指定して、コピーした方のファイルでEmail_Driverを上書きします。
また、fuel/app/config 以下の config.php のalways_load に myemail を追加するのも忘れずに。

<?php
\Autoloader::add_core_namespace('Email');

\Autoloader::add_classes(array(
	/**
	 * Email classes.
	 */
	'Email\\Email_Driver'                      => __DIR__.'/classes/email/driver.php',

));

これで下準備は完了です。それでは、コピーしたファイルに変更を加えていきましょう。
driver.php
は build_message メソッドを以下のように変更します。

	protected function build_message($no_bcc = false)
	{
		$newline = $this->config['newline'];
		$charset = $this->config['charset'];
		$encoding = $this->config['encoding'];

		$headers = '';
		$parts = array('Date', 'Return-Path', 'From', 'To', 'Cc', 'Bcc', 'Reply-To', 'Subject', 'Message-ID', 'X-Priority', 'X-Mailer', 'MIME-Version', 'Content-Type');
		$no_bcc and array_splice($parts, 5, 1);
		$this->config['smime']['enabled'] and array_splice($parts, -2, 2);
		
		foreach ($parts as $part)
		{
			$headers .= $this->get_header($part);
		}

		foreach ($this->extra_headers as $header => $value)
		{
			$headers .= $header.': '.$value.$newline;
		}

		$headers .= $newline;

		$body = '';

		if ($this->type === 'plain' or $this->type === 'html')
		{
			if ($this->config['smime']['enabled'])
			{
				$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
				$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
			}
			$body .= $this->body;
		}
		else
		{
			if ($this->config['smime']['enabled'])
			{
				$body .= $this->get_header('Content-Type').$newline;
			}
			
			switch ($this->type)
			{
				case 'html_alt':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'plain_attach':
				case 'html_attach':
				case 'html_inline':
					$body .= '--'.$this->boundaries[0].$newline;
					$text_type = (stripos($this->type, 'html') !== false) ? 'html' : 'plain';
					$body .= 'Content-Type: text/'.$text_type.'; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$attach_type = (stripos($this->type, 'attach') !== false) ? 'attachment' : 'inline';
					$body .= $this->get_attachment_headers($attach_type, $this->boundaries[0]);
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'html_alt_inline':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: text/plain'.'; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: multipart/related;'.$newline."\tboundary=\"{$this->boundaries[1]}\"".$newline.$newline;
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$body .= $this->get_attachment_headers('inline', $this->boundaries[1]);
					$body .= '--'.$this->boundaries[1].'--'.$newline.$newline;
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'html_alt_attach':
				case 'html_inline_attach':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: multipart/alternative;'.$newline."\t boundary=\"{$this->boundaries[1]}\"".$newline.$newline;
					if (stripos($this->type, 'alt') !== false)
					{
						$body .= '--'.$this->boundaries[1].$newline;
						$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
						$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
						$body .= $this->alt_body.$newline.$newline;
					}
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					if (stripos($this->type, 'inline') !== false)
					{
						$body .= $this->get_attachment_headers('inline', $this->boundaries[1]);
						$body .= $this->alt_body.$newline.$newline;
					}
					$body .= '--'.$this->boundaries[1].'--'.$newline.$newline;
					$body .= $this->get_attachment_headers('attachment', $this->boundaries[0]);
					$body .= '--'.$this->boundaries[0].'--';
					break;
				case 'html_alt_inline_attach':
					$body .= '--'.$this->boundaries[0].$newline;
					$body .= 'Content-Type: multipart/alternative;'.$newline."\t boundary=\"{$this->boundaries[1]}\"".$newline.$newline;
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: text/plain; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[1].$newline;
					$body .= 'Content-Type: multipart/related;'.$newline."\t boundary=\"{$this->boundaries[2]}\"".$newline.$newline;
					$body .= '--'.$this->boundaries[2].$newline;
					$body .= 'Content-Type: text/html; charset="'.$charset.'"'.$newline;
					$body .= 'Content-Transfer-Encoding: '.$encoding.$newline.$newline;
					$body .= $this->body.$newline.$newline;
					$body .= $this->get_attachment_headers('inline', $this->boundaries[2]);
					$body .= $this->alt_body.$newline.$newline;
					$body .= '--'.$this->boundaries[2].'--'.$newline.$newline;
					$body .= '--'.$this->boundaries[1].'--'.$newline.$newline;
					$body .= $this->get_attachment_headers('attachment', $this->boundaries[0]);
					$body .= '--'.$this->boundaries[0].'--';
					break;
			}

		}
		
		if ($this->config['smime']['enabled'])
		{
			$tmp_dir = $this->config['smime']['tmp_dir'];
			$infilename = 'email_'.md5($body.microtime());
			$outfilename = $infilename.'_out';
			$signcert = 'file://'.$this->config['smime']['signcert'];
			$privkey = 'file://'.$this->config['smime']['privkey'];
			$headers = substr($headers, 0, -strlen($newline)*2);
			
			if ( ! is_writable($tmp_dir))
			{
				throw new \FuelException('Cannot write into directory "'.$tmp_dir.'"');
			}
			if ( ! is_readable($signcert) || ! is_readable($privkey))
			{
				throw new \FuelException('Cannot read certificate or key.');
			}
			
			if ($this->config['smime']['keypass'])
			{
				$privkey = array($privkey, $this->config['smime']['keypass']);
			}
			
			\File::create($tmp_dir, $infilename, $body);

			openssl_pkcs7_sign($tmp_dir.$infilename, $tmp_dir.$outfilename, $signcert, $privkey, array($headers));

			$body = \File::read($tmp_dir.$outfilename, true);

			\File::delete($tmp_dir.$infilename);
			\File::delete($tmp_dir.$outfilename);
			
			$headers = '';
			$body = strtr($body, array_fill_keys(array("\r\n", "\r", "\n"), $newline));
		}

		return array(
			'header' => $headers,
			'body' => $body,
		);
	}

config/email.php には、以下の設定を追加します。

	'defaults' => array(
		
		<省略>
		
		/**
		 * S/MIME
		 */
		'smime' => array(
			'enabled' => false,
			'tmp_dir' => APPPATH.'tmp/',
			'signcert' => '',
			'privkey' => '',
			'keypass' => '',
		),
	),

実際使うときは、もちろんこれを fuel/app/config以下にコピーして書き換えてください。
tmp_dir は、openssl_pkcs7_sign の入出力に必要なファイルが読み書きされます。
適切なパーミッションが与えられたディレクトリを指定する必要があります。

証明書と秘密鍵はsigncert, privkeyでファイルパスを指定します。PEM形式じゃないとダメっぽいです。
詳しことは「PHP: OpenSSL キー/証明書パラメータ」をご覧ください。

これで設定が正しければS/MIME電子署名メールが送れるはずです。
注意としては、Emailのドライバがmail, smtp, sendmailでないとうまくいかないはずです。
mailgunだとWeb API経由で送信しており、こちらで組み立てたメール本文がそのまま配送されないためです。
mailgunでもSMTP経由で送信することも可能ですので、smtpドライバを用いてmailgunのSMTPサーバーで設定を行いましょう。

多次元配列の要素を特定の値をキーとしてマージする

2015/8/25

FuelPHPのArrクラスでは、非常に便利な配列操作のヘルパー関数が用意されているのですが、ありそうでなかったので…

それから、記事タイトルが何言ってるのか正直自分もよく分からないので例を。

array(
    array(
        'group' => 'CV1',
        'name' => 'Miku'
    ),
    array(
        'group' => 'CV2',
        'name' => 'Rin'
    ),
    array(
        'group' => 'CV2',
        'name' => 'Len'
    )
);

のような入力を与えた時に、

array(
    'CV1' => array(
        array(
            'group' => 'CV1',
            'name' => 'Miku'
        )
    ),
    'CV2' => array(
        array(
            'group' => 'CV2',
            'name' => 'Rin'
        ),
        array(
            'group' => 'CV2',
            'name' => 'Len'
        )
    )
);

あるいは、

array(
    'CV1' => array(
        'Miku'
    ),
    'CV2' => array(
        'Rin',
        'Len'
    )
);

のような出力を得たい、ということです。あるキーの同じ値同士でまとめるような感覚でしょうか。

とりあえず以下のように書いてみました。

class Arr extends \Fuel\Core\Arr
{
	/**
	 * @param array  $assoc     変換する配列
	 * @param string $key_field keyとなる連想配列の添字
	 * @param string $val_field valueとなる連想配列の添字
	 * @return array
	 */
	public static function merge_assoc_key($assoc, $key_field, $val_field = null)
	{
		$output = array_fill_keys(array_column($assoc, $key_field, $key_field), array());

		foreach ($assoc as $elm)
		{
			array_push($output[$elm[$key_field]], $val_field ? $elm[$val_field] : $elm);
		}

		return $output;
	}
}

パフォーマンスがどうか全く分からんですけど、

Arr::merge_assoc_key($input, 'group');
Arr::merge_assoc_key($input, 'group', 'name');

みたいな感じで、期待した結果が得られます。

データベースから取ってきた配列をこんな感じで整形したいことがそこそこある気がします、よね?

書いてて思ったけど、PHPには豊富な配列操作の標準関数が用意されるけども、マニュアルを眺めてあーこれこれ、こんなんあったね、とかやってるあたりまだ未熟だなぁ…

一つのカラムに貼られた複数のFULLTEXT INDEXを使い分ける

2015/7/26

Mroongaでお手軽全文検索して遊んでいたんですが、前方一致を使う場合は

Qiita: mroongaでgroongaの前方一致検索を使うには

にあるように、パーサーを無効にしないと期待した結果が得られません。

一つのカラムに対して、普通に全文検索する場合と前方一致検索を使い分けたい場合には、上記記事のように2つカラム用意してそれぞれにパーサーを使ったFULLTEXT INDEXと、無効にしたFULLTEXT INDEXを貼らなきゃいけないのでしょうか。
ちょっと無駄な感じがしましてどうにかできないかな~と。

そこで思いついたのが、USE INDEXとかで明示的にインデックスを指定してやったらどうなるのでしょうか。
そんでリファレンスを見てみると・・・

MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.9.3 インデックスヒントの構文

「自然言語モードの検索の場合、インデックスヒントは暗黙のうちに無視されます。」
を読んで、ダメか~と思ったのですが、
「ブールモードの検索の場合、FOR ORDER BY または FOR GROUP BY を含むインデックスヒントは暗黙のうちに無視されます。FOR JOIN を含むインデックスヒント、または FOR 修飾子を含まないインデックスヒントは受け付けられます。」
ということらしいのでIN BOOLEAN MODEであれば、FOR {ORDER|GROUP} BYを除き、使えるっぽいですね。

ということで試してみましょう。

サンプルとして使うスキーマとデータは以下の通り。

CREATE TABLE test (
  id int(11) NOT NULL AUTO_INCREMENT,
  content varchar(255) NOT NULL COLLATE 'utf8_unicode_ci',
  PRIMARY KEY (id),
  FULLTEXT INDEX idx_content (content),
  FULLTEXT INDEX idx_content_off (content) COMMENT 'parser "off"'
) ENGINE=mroonga COMMENT = 'engine "InnoDB"' DEFAULT CHARSET=utf8;
INSERT INTO test (content) VALUES ('とらっく');
INSERT INTO test (content) VALUES ('とらっくばっく');
INSERT INTO test (content) VALUES ('東京都');
INSERT INTO test (content) VALUES ('京都');

まずはインデックスを指定せず前方一致検索をしてみます。

SELECT * FROM `test` WHERE MATCH(content) AGAINST('とらっく*' IN BOOLEAN MODE)

検索結果は空でした。EXPLAINしてみると、使用されたのはidx_contentが使われていたので、正しい結果です。

お次はUSE INDEXでパーサを無効にしたidx_content_offを指定してみます。

SELECT * FROM `test` USE INDEX (idx_content_off) WHERE MATCH(content) AGAINST('とらっく*' IN BOOLEAN MODE)

すると、以下のような結果が得られました。
idx_content_offが使用され、意図した前方一致検索が行われているようです。

|2|とらっくばっく
|1|とらっく

ということで、USE INDEXを使うことで、異なるパーサを用いたFULLTEXT INDEXを使い分けすることができますね。

FuelPHP MigrationでMroongaのラッパーモードを利用する

2015/6/11

FuelPHPのマイグレーションでMroongaのラッパーモードを利用したテーブルを作成する方法です。
備忘録として書いておきます。

Mroongaのラッパーモードを利用するには、テーブルオプションを ENGINE = Mroonga COMMENT = 'engine "InnoDB"' のように指定する必要があります。

DBUtil::create_table メソッドの第5引数 $engineでストレージエンジンの指定は可能なのですが、コメントの指定をどうしようかと悩んでいました。
create_tableメソッドのソースを読んでみると、$engineはエスケープなども行われずにそのまま文字列結合しているようなので、COMMENTも一緒にぶち込むことにしました。
見た目は良くないですが、Coreを拡張するのも面倒なので…

class Create_test
{
	public function up()
	{
		\DBUtil::create_table('test', array(
			'id' => array('constraint' => 11, 'type' => 'int', 'auto_increment' => true, 'unsigned' => true),
			'content' => array('constraint' => 255, 'type' => 'varchar'),
		), array('id'), false, "Mroonga COMMENT  = 'engine \"InnoDB\"'", 'utf8mb4_general_ci');

		\DBUtil::create_index('test', 'content', 'idx_content', 'fulltext');

	}

	public function down()
	{
		\DBUtil::drop_table('test');
	}
}

他に良い方法があれば是非教えてくださいー

Open Web Board のBLE Notification のバグ修正

2015/3/21

KDDIが発表したFirefox OSを搭載した開発ボード「Open Web Board
イメージファイルやソースコードも公開されました。

au Firefox OS 関連技術情報: http://opensource.kddi.com/owb/#download

以前、とあるイベントでOpen Web BoardとBLEデバイスを使ったアプリ開発をしていたのですが、その際にOpen Web BoardのGeckoに独自実装されているBLE GATTのNotificationの値が取れないというバグを発見しました。
現時点(2015.1.23公開)のソースコードでも未修正のようなので、まとめておきます。


ソースコードの取得についてはKDDIのサイトに書いてある通りです。

イメージファイル作成方法: http://opensource.kddi.com/owb/owbsource.html

ビルド環境についてはMDNを参照のこと。私が以前書いた記事も参考になるかもです。
(=>ZTE OPEN C FirefoxOS(B2G)をビルドしてアップデートする2

また、ビルドすると以下のようなエラーで止まってしまうので、Makefileの修正が必要です。

device/rockchip/rksdk/BoardConfig.mk:57: *** 空の変数名. 中止.

コロンとイコールの間にスペースが入ってしまっているだけです。

--- device/rockchip/rksdk/BoardConfig.mk.orig   2015-01-07 14:47:22.000000000 +0900
+++ device/rockchip/rksdk/BoardConfig.mk        2015-03-21 01:20:09.295321600 +0900
@@ -54,7 +54,7 @@
 include device/rockchip/rksdk/wifi_bt_common.mk

 #Bluetooth
-BOARD_HAVE_BLUETOOTH : = true
+BOARD_HAVE_BLUETOOTH := true
 BOARD_HAVE_BLUETOOTH_BCM := true

 #xuxy add

本題のBLEの修正は以下のようになります。(Gist)

--- ./gecko/dom/bluetooth/bluedroid/BluetoothGatt.cpp.orig      2015-03-19 02:05:19.361468298 +0900
+++ ./gecko/dom/bluetooth/bluedroid/BluetoothGatt.cpp   2015-03-19 02:50:24.378295237 +0900
@@ -2922,7 +2922,9 @@
     data_conn_id.AppendInt(mNotifyConnCommPara.connId);

     nsString data_value;
-//    data_value.APpendInt(mNotifyParaData);
+    char strValue[MAX_HEX_VAL_STR_LEN];
+    array2str(mNotifyParaData.value, mNotifyParaData.len, strValue, sizeof(strValue));
+    data_value = NS_ConvertUTF8toUTF16(strValue);

     nsString data_bdAddr;
     BdAddressTypeToString(&mNotifyParaData.bda, data_bdAddr);

以下のAPI仕様によると、Notification のデータは void onnotify(BluetoothGattEvent gattEvent) のパラメータgattEvent.value で DOMString として取得できます。
ソースコードを見ても分かるように、データのバイト配列を16進数文字列に変換しています。
アプリ側でデータを使うにしても、割と泥臭い処理を書く必要がありますね…

Open Web Board / BLEマニュアル API仕様: http://opensource.kddi.com/owb/owbble.html#api

ちなみに、早い段階でOpen Web Boardを入手された方への注意点があります。
BLE APIの名前が navigator.mozBle から navigator.owbBle へ変更されているので、以前のバージョンのサンプルプログラム等を参照されている場合は変更が必要です。
まだ独自実装の段階なので、ベンダープレフィックスがmozからowbへ変更されたのでしょう。

そういえば、以下との兼ね合いはどういう感じなんでしょうかね?

mozilla wiki: B2G/Bluetooth/WebBluetooth-v2