document.writeの遅延実行

html中にアクセス解析用だったり広告表示用だったりで他ホスト上のjavascript外部ファイルを読み込む必要がある場合がありますが、そのjavascriptの読み込みや実行時間に引っぱられて自ページの表示が遅れるのはいやだって事はよくあります。

例えば以下のような構成の場合

|.
|-- index.html
`-- js
    `-- other-hosts-js1.js

index.html

<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>as-lazy-writter</title>
</head>
<body style="background-color:#f0f0f0;">
	<p>start</p>
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<hr />
	<script type="text/javascript" src="js/other-hosts-js1.js"></script>
	<hr />
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<p>end</p>
</body>
</html>

othre-hosts-js1.js

for (var i = 0; i < 1000000000; i++) {
}
document.write('<p>other-hosts-js1.jsから出力</p>');

index.htmlの表示結果は以下のようになります。

startを描画してからendを描画するまでに5秒ほどの待ち時間が出ました。

ここでは「other-hosts-js1.js」を同ホスト上に置いてますが、実際は他ホスト上にありこちらからは編集出来ないケースを想定してますので、何とか意図するページの描画を優先させる為には「other-hosts-js1.js」の読み込み位置を下にずらす事にします。
けど単純に下に移動させただけではjs内で出力されている「other-hosts-js1.jsから出力」という文字列の描画位置まで変わってしまいます。
ので、「document.write」をオーバーライドして、どこで実行しても任意の箇所に描画可能なように変更してやろうかと。

変更後のファイル構成はこんなんで

.
|-- index.html
`-- js
    |-- as-lazy-writter.js
    `-- other-hosts-js1.js

index.html

<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<title>as-lazy-writter</title>
	<script type="text/javascript" src="js/as-lazy-writter.js"></script>
</head>
<body style="background-color:#f0f0f0;">
	<p>start</p>
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<hr />
	<div id="output1"></div>
	<hr />
	<p><script type="text/javascript">document.write(new Date());</script></p>
	<p>end</p>
	<script type="text/javascript">asLazyWriteId = "output1";</script>
	<script type="text/javascript" src="js/other-hosts-js1.js"></script>
</body>
</html>

as-lazy-writter.js

var asLazyWriteId = "";

var asLazyWrite = function() {
	var str = "";
	for (var i = 0; i < asLazyWrite.arguments.length; i++) {
		str += asLazyWrite.arguments[i];
	}
	var elm = document.getElementById(asLazyWriteId);
	if (!elm) {
		elm = document.body;
	}
	elm.innerHTML += str;
};

window.addEventListener("load", function(event) {
	document.write = document._write;
	document.writeln = document._writeln;
}, false);

document._write = document.write;
document._writeln = document.writeln;
document.write = asLazyWrite;
document.writeln = asLazyWrite;

とかってしてやると

って風にメインのコンテンツが表示された後に

document.writeを通した内容が描画される。

けどこれ手抜きしてinnerHTMLとかで文字列を挿入したけど、やっぱり問題ありでした。
実行する際にはもうDOMの構築が完了してるから、構築に汲んで欲しい文字列をdocument.write中に記述してても全く解釈してくれない。。

構成を

.
|-- index.html
`-- js
    |-- as-lazy-writter.js
    |-- other-hosts-js1.js
    `-- other-hosts-js2.js

に変更して例えばother-hosts-js1.jsを

for (var i = 0; i < 1000000000; i++) {
}
document.write('<p>other-hosts-js1.jsから出力</p>');
document.write('<script type="text/javascript" src="js/other-hosts-js2.js"></script>');

ってして、other-hosts-js2.jsを

document.write('<p>other-hosts-js2.jsから出力</p>');

って内容で追加しても

other-hosts-js2.js中でwriteってる内容が描画されない。
むしろブラウザはother-hosts-js2.jsを取得にすら行ってない。

innerHTMLじゃダメでした。

なので、簡易にでもHTMLを解析してappendChildしてやる事に。

as-lazy-writter.jsを以下に変更。

var asLazyWriteId = "";
var asLazyWriteIndex = 0;
var asLazyWriteStack = new Array();
var asLazyFunctions = new Array();

var asLazyWrite = function() {
	var str = "";
	for (var i = 0; i < asLazyWrite.arguments.length; i++) {
		str += asLazyWrite.arguments[i];
	}
	var elm = document.getElementById(asLazyWriteId);
	if (!elm) {
		elm = document.body;
	}

	asLazyAppend(elm, str);
};

var asLazyAppend = function(elm, str) {
	var name, attrs, value, matches;
	var node, attr;
	var wrapper;
	var loop = true;
	while (loop) {
		name = "";
		attrs = "";
		value = "";
		if (matches = str.match(/^<([0-9a-z_\-]+)((?: +[0-9a-z_\-]+(?:=(?:\\?"[^"]*\\?"|\\?'[^']*\\?'|[0-9a-z_\-]+))?)*) *\\?\/?>(?:(.*?)<\\?\/\1>)?/i)) {
			name = matches[1];
			attrs = matches[2];
			if (matches[3]) {
				value = matches[3];
			}
		} else if (matches = str.match(/^[^<]+/i)) {
			value = matches[0];
		}
		if (name) {
			node = document.createElement(name);
			attr = "";
			while (attr = attrs.match(/ *([0-9a-z_\-]+)(?:=(\\?"[^"]*\\?"|\\?'[^']*\\?'|[0-9a-z_\-]+))?/i)) {
				if (attr[2]) {
					attr[2] = attr[2].replace(/^\\?["']?(.*?)\\?["']?$/i, '$1');
					switch (attr[1]) {
					case "class":
						node["className"] = attr[2];
						break;
					case "style":
						node["style"]["cssText"] = attr[2];
						break;
					default:
						node[attr[1]] = attr[2];
						break;
					}
				} else {
					node[attr[1]] = attr[1];
				}
				attrs = attrs.replace(attr[0], '');
			}
			if (name == "script") {
				wrapper = document.createElement("span");
				wrapper["id"] = "as-lazy-write-index-" + asLazyWriteIndex;
				elm.appendChild(wrapper);
				if (!value) {
					node.onload = function(event) {
						asLazyDeploy();
					};
					asLazyWriteStack.push({id: wrapper["id"], elm: node});
				} else {
					wrapper.appendChild(node);
					node.appendChild(document.createTextNode(value));
				}
				asLazyWriteIndex++;
			} else {
				elm.appendChild(node);
				if (value) {
					asLazyAppend(node, value);
				}
			}
		} else if (value) {
			elm.appendChild(document.createTextNode(value));
		}
		if (matches) {
			str = str.replace(matches[0], '');
		} else {
			loop = false;
		}
	}
};

var asLazyDeploy = function() {
	if (asLazyWriteStack.length > 0) {
		var hash = asLazyWriteStack.shift();
		document.getElementById(asLazyWriteId = hash.id).appendChild(hash.elm);
	} else {
		asLazyClose();
	}
};

var asLazyClose = function() {
	document.write = document._write;
	document.writeln = document._writeln;

	for (var i = 0; i < asLazyFunctions.length; i++) {
		asLazyFunctions[i]();
	}
}

var asLazyEventListener = function() {
	var type = asLazyEventListener.arguments[0];
	var listener = asLazyEventListener.arguments[1];
	var useCapture = false;
	if (asLazyEventListener.arguments.length > 2) {
		useCapture = asLazyEventListener.arguments[2];
	}
	if (type == "load") {
		asLazyFunctions.push(listener);
	} else {
		window._addEventListener(type, listener, useCapture);
	}
};

var asLazyAttachEvent = function() {
	var type = asLazyAttachEvent.arguments[0];
	var listener = asLazyAttachEvent.arguments[1];
	if (type == "onload") {
		asLazyFunctions.push(listener);
	} else {
		window._attachEvent(type, listener);
	}
};

if (window.addEventListener) {
	window.addEventListener("load", function(event) {
		asLazyDeploy();
	}, false);

	window._addEventListener = window.addEventListener;
	window.addEventListener = asLazyEventListener;
} else if (window.attachEvent){
	window.attachEvent("onload", function(event) {
		asLazyDeploy();
	});

	window._attachEvent = window.attachEvent;
	window.attachEvent = asLazyAttachEvent;
}

document._write = document.write;
document._writeln = document.writeln;
document.write = asLazyWrite;
document.writeln = asLazyWrite;

ある程度単純なカタチでHTMLタグがネストされててもそういうのが連続して記述されてても描画されるようにはしました。
これで実行した結果はこれ

まとめると

  • as-lazy-writter.jsを読み込んだ後のdocument.writeは、実行時に変数[asLazyWriteId]にて指定済みのelementが存在する場合はそのelement中に描画する
  • 上記にて[asLazyWriteId]が未指定または指定してるがそのelementが存在しない場合は通常のdocument.writeと同様に実行箇所に描画する
  • 外部js中のdocument.writeの中にインラインのjavascriptタグを記述したりしてると意図する箇所に出力されないことがある
  • その他の不具合の可能性?

という適当なものになりました。

単純な使い方しかしてないので今のとこ想定通りには動いてますがjavascriptめったに書かないので自信無しです。
改善点やバグがあったら連絡お待ちしてます。

CakePHP2系でConsoleプログラム実行時のエラー対応

CakePHPの2系でConsoleプログラムを実行しようとすると実行環境によっては

PHP Warning:  preg_replace_callback(): Compilation failed: unrecognized character after (?< at offset 4 in lib/Cake/Console/ConsoleOutput.php on line 186

とかって警告が出る場合があります。
これはCakePHPのConsoleOutput.phpで利用されてる正規表現がPCREの7.0からサポートされてるPerl5.10の互換構文に依存してるんで、実行環境のPCREのバージョンが7.0よりも古ければそんな警告が出てしまいます。
単純にPCREのバージョン上げれば問題ないんですがそうもいかない場合は以下のパッチを当ててやればok。
(Ver2.0.2)

diff --git lib/Cake/Console/ConsoleOutput.php lib/Cake/Console/ConsoleOutput.php
index a6cfd7d..80659fb 100755
--- lib/Cake/Console/ConsoleOutput.php
+++ lib/Cake/Console/ConsoleOutput.php
@@ -182,7 +182,7 @@ class ConsoleOutput {
 			return preg_replace('#</?(?:' . $tags . ')>#', '', $text);
 		}
 		return preg_replace_callback(
-			'/<(?<tag>[a-z0-9-_]+)>(?<text>.*?)<\/(\1)>/ims', array($this, '_replaceTags'), $text
+			'/<(?P<tag>[a-z0-9-_]+)>(?P<text>.*?)<\/(\1)>/ims', array($this, '_replaceTags'), $text
 		);
 	}
 

パターンの記述をPCREの4.0からサポートされてるPython互換の構文にしてやっただけです。

詳細はPCREのchangelogをどうぞ。
http://www.pcre.org/changelog.txt

踏み台サーバを利用してサーバ接続を簡単に

IDCのサーバや、運営してるサイトの管理システムには会社の事務所内からアクセス許可しないというのはよくある。
けどどうしても自宅や社外のアクセスポイントから接続したい場合もある。
そういう時にはssh_configを設定しておくとかなり便利で快適な接続が可能。
OSはMac10.6、ブラウザはFirefox3.6を利用する際のメモ。
ここで

hogehoge.com
接続したい目的のサーバ
fugafuga.com
踏み台サーバ

とする。

踏み台サーバを利用したssh接続

基本的にmacssh_configファイルは「~/.ssh/config」になる。
で、そのssh_configファイルに以下を記述

Host hogehoge.com
  HostName hogehoge.com
  User root
  Port 22
  IdentityFile ~/.ssh/id_rsa
  ProxyCommand ssh fugafuga.com nc %h %p
Host
sshで接続する時に指定するホスト名(エイリアスでも可)
HostName
上のHostでエイリアスを記述した場合はここに実際のホスト名かIPアドレス(省略可)
User
ログインする際のユーザ名(省略可)
Port
サーバ接続する際のポート番号(省略可)
IdentityFile
ログインする際に利用する秘密鍵(省略可)
ProxyCommand
サーバに接続する際に使用するコマンド

踏み台サーバに接続する際も細かい設定が必要ならそっちにも上のようなホスト設定をしとけばok。
設定オプションはもっといろいろ指定出来るけど細かいとこは下記サイト参照。

踏み台サーバを利用したHTTP(Firefox)接続

こちらもまずはssh_configファイルを編集。
上の「ssh接続」用の設定と異なり、こちらは踏み台サーバのホスト設定に記述する点に注意。
追記するのは「DynamicForward」の指定のみ。

Host fugafuga.com
  DynamicForward 1080
DynamicForward
ローカルホスト側の動的転送するポート番号

これでssh_configの設定は完了。
次はプロキシの設定だけど、Macデフォルトの環境設定>ネットワークとかからやってるとイチイチ設定を有効にしたり無効にしたりするのもメンドクサイし、有効にしたら通信時に毎回プロキシを経由するから通信速度も落ちるしネットワーク環境によっては接続出来なくなるホストもある。
ので、プロキシの設定はブラウザの拡張機能、今回はFirefoxのアドオンFOXY PROXYでの方法。

ホスト名
localhost
ポート
1080(ssh_configのDynamicForwardで指定したポート番号)

この設定のSOCKS(v5)プロキシを追加。ここまではMacデフォルトのネットワーク設定でもいっしょ。
で、後は踏み台サーバを経由しないと接続出来ないURLをURL Patternsに設定すれば準備は完了。
※実際に接続する際には、追加したプロキシを有効にするのと、sshで踏み台サーバに接続しておく事を忘れずに。


以上

WORDPRESSの本体・プラグイン・テーマアップグレード方法にsshを利用する

基本的には以下のサイトの「FTPSSH 定数」を参考にする。
wp-config.php の編集 - WordPress Codex 日本語版

ただ、

とあるが、sshを利用する場合はここでは「ssh2」を指定しないと動作しなかった。
ssh2を利用するのでもちろんpecl SSH2 拡張をインストールする必要もある。

最終的な設定は以下のようになる。

define('FS_METHOD', 'ssh2');
define('FTP_BASE', '/var/www/wordpress/');
define('FTP_CONTENT_DIR', '/var/www/wordpress/wp-content/');
define('FTP_PLUGIN_DIR', '/var/www/wordpress/wp-content/plugins/');
define('FTP_PUBKEY', '/home/hogeuser/.ssh/id_rsa.pub');
define('FTP_PRIKEY', '/home/hogeuser/.ssh/id_rsa');
define('FTP_USER', 'hogeuser');
define('FTP_PASS', 'hogehoge');
define('FTP_HOST', 'example.com');

※上記設定は

の場合。

sshで接続時の「Too many authentication failures for username」エラーの対処法

ssh-agent」が動作している場合 の内容を訂正しました

原因は接続先サーバのsshd_configで設定されてるsshの最大試行可能回数を越えたって事。
何でこんなエラーが発生したかってのは、
接続元のsshクライアントがssh_configのPreferredAuthenticationsにて設定されてる順で接続を試していき、失敗しすぎたから。
対応方法は、今回接続しようとしているサーバの認証方法によってちょっと違ってくる。

password認証のサーバに接続したい場合

上述したクライアントのssh_configに以下の設定を追加してやればいい。

PreferredAuthentications password

または、接続コマンドに直接オプション指定してやってもok。

ssh username@IPAddress -o PreferredAuthentications=password

publickey認証のサーバに接続したい場合

この場合はクライアントで「ssh-agent」が動作してるかどうかで対応方法が変わる。

ssh-agent」が動作していない場合

上述したクライアントのssh_configに以下の設定を追加してやればいい。

IdentityFile /root/.ssh/id_rsa

または、接続コマンドに直接オプション指定してやってもok。

ssh username@IPAddress -o IdentityFile /root/.ssh/id_rsa
ssh-agent」が動作している場合

ssh-agentはssh-addにて登録されている鍵ファイルを順に試していき、任意にその順番を指定する事は出来ないので、根本的に鍵ファイルを登録し過ぎているのが問題。
必要の無い鍵ファイルを削除するか、サーバ周りの環境を見直した方がいいのかも。
IdentitiesOnlyを指定してやればokでした。

Host hogehoge.com
  HostName fugafuga.com
  User hogo
  Port 12345
  IdentitiesOnly yes
  IdentityFile ~/.ssh/id_rsa


と、いろいろ書いてはみたけどそんな自信はない。
しかもsshの認証方式はもちろん他にもあるけどそこはノータッチで。

いつかのIE6とCakePHP1.3と文字化け

CakePHPもとうとう1.3になってしばらく期間が経ったので使ってるとHTMLフォームから送信された文字がIE6で文字化けしてた。
CakePHPはプログラムの内部エンコーティングを「/app/config/core.php」の「App.encoding」で指定出来るけど、1.3ではデフォルトでそこで指定した文字コードでHTMLフォーム中の文字列を送信するようにシステムが改善されてた。
具体的には「/app/config/core.php」中にて

	Configure::write('App.encoding', 'UTF-8');

とかって設定してた場合、view中で

<?php e($this->Form->create('[コントローラ名]', array('url'=>'[コントローラ名]/[アクション名]'))); ?>

とかすると出力されるHTMLはデフォルトで

<form method="post" action="[コントローラ名]/[アクション名]" accept-charset="utf-8">
</form>

となり、formタグ中に「accept-charset="utf-8"」のプロパティが付加される。
が、
この「accept-charset="utf-8"」なんてプロパティはIEは認識してくれません。
フロントはShift-JISでシステム内部はUTF-8でなんてやった場合は、FirefoxからはUTF-8で受け取れるのにIE6からはShift-JISで受け取ってしまう・・・という事に。
対応策としてはまずはformタグ中に「accept-charset="utf-8"」のプロパティを付加しないようにする。

<?php e($this->Form->create('[コントローラ名]', array('encoding'=>null, 'url'=>'[コントローラ名/アクション名]'))); ?>

とすると、HTMLフォームからはページの文字コードにてそのままフォーム中のデータが送信されてくるので、あとは好きなサーバ処理中のタイミングで手動でエンコードしてやればいい。


ie