ファイル添付でHTTP POST(PHP)

curlモジュールを使うことも検討したけど

  • データの先頭が@だったらファイル名指定として扱う

などという馬鹿げた仕様が怖くて、でも、

  • read/write callbackを使うやり方の例が見つけられず

結局、標準的な書き方で。同様の処理をしてくれるライブラリは既存だと思われる。

以前は、stream_context_createtとfopenを使っていたけれど、
この方法だとリクエストをメモリにいったん保持する必要があって、
大きなファイルを扱いづらいので、今回の方法に変更。

エラーチェック、SSL対応などは、若干省略気味。

<?php
	$url = "http://hostname/path/uploader";
	$param = array();
	$param['hoge'] = 'funi';
	$param['piyo'] = array('/path/file.jpg', 'image/jpeg', 'file.jpg');
	
	# URLを分解する(HTTPのみサポート)
	if(preg_match('{^([^:]+)://([^:/]+)(:(\d+))?(/.*)?$}', $url, $m)) {
		list($schema, $host, $port, $path) = array($m[1], $m[2], $m[4], $m[5]);
		if(!$port) {
			$port = 80;
		}
	} else {
		# wrong url
		user_error('Wrong url.', E_USER_ERROR);
	}

	# 適当に決める
	$boundary = "X".date('YmdHis')."X".date('YmdHis')."X".date('YmdHis')."X";
	
	$data = array();
	foreach($param as $name => $value) {
		$data[] = "--" . $boundary;
		if(is_array($value)) {
			if(!file_exists($value[0])) {
				user_error('File is not found.', E_USER_WARNING);
				continue;
			}
			$filename = $value[2]!='' ? $value[2] : basename($value[0]);
			$data[] = sprintf('Content-Disposition: form-data; name="%s"; filename="%s"', $name, $filename);
			$data[] = sprintf('Content-Type: %s', $value[1]);
			$data[] = "";
			$data[] = array($value[0], filesize($value[0]));
		} else {
			$data[] = sprintf('Content-Disposition: form-data; name="%s"', $name);
			$data[] = "";
			$data[] = $value;
		}
		if(strpos($data[count($data)-1], $boundary) !== FALSE) {
			# bad boundary !!
			user_error('Wrong boundary!', E_USER_ERROR);
			return FALSE;
		}
	}
	$data[] = "--" . $boundary . "--";
	
	# 長さを計算する
	$length = 0;
	foreach($data as $d) {
		$length += is_array($d) ? $d[1]+2 : strlen($d)+2;
	}
	
	# ヘッダを準備する
	if($header == false) {
		$header = array();
	}
	$header[] = "POST $path HTTP/1.0";
	$header[] = "Host: $host";
	$header[] = "Content-Type: multipart/form-data; boundary=$boundary";
	$header[] = "Content-Length: ".$length;
	
	# 接続する
	$sock = fsockopen($host, $port, $errno, $errstr, 30);
	if(!$sock) {
		# 接続失敗
		user_error('cannot connect.', E_USER_ERROR);
		return;
	}
	
	# リクエストを送信する
	foreach($header as $h) {
		fputs($sock, $h . "\r\n");
	}
	fputs($sock, "\r\n");
	foreach($data as $d) {
		if(is_array($d)) {
			# ファイル
			$fp = fopen($d[0], 'rb');
			while(!feof($fp)) {
				fputs($sock, fread($fp, 8192));
			}
			fclose($fp);
			fputs($sock, "\r\n");
		} else {
			# 文字列
			fputs($sock, $d . "\r\n");
		}
	}
	
	$header = array();	# ヘッダ配列
	$res = '';		# 本体
	while(!feof($sock)) {
		$line = fgets($sock);
		$line = rtrim($line, "\r\n");
		if(0 < strlen($line)) {
			$header[] = $line;
		} else {
			break;
		}
	}
	while(!feof($sock)) {
		$res .= fread($sock, 8192);
	}
	fclose($sock);
?>