Wikiのエクスポートがかなり変

Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
返信数: 10

当方の問題だけでなければ良いのですが...

あまり使われていない機能なのでしょうか.
Wikiの”Wikiリンクの選択"プルダウンの一番下にある,”ページのエクスポート”の挙動がどうも気になります.WikiのデータをHTMLのWebページの形式でzip化してクライアント側に送信できる機能です.

動作の実体は,mod/wiki/ewiki/plugins/moodle/moodle_wikidump.phpにあります.
中心的な関数は,function ewiki_page_wiki_dump_send()です.

ダウンロードしたzipファイル(バイナリファイルですが)を強制的にエディタで開いてみてみると,最後尾にHTMLのソースがくっついています.

$archivename=$wname.".zip";
zip_files($filestozip, "$exportbasedir/$archivename");

#-- Headers
Header("Content-type: application/zip");
Header("Content-disposition: attachment; filename=\"$archivename\"");
Header("Cache-control: private");
Header("Original-Filename: $archivename");
Header("X-Content-Type: application/zip");
Header("Content-Location: $archivename");
if(!@readfile("$exportbasedir/$archivename")) {
error("Cannot read $exportbasedir/$archivename");
}
get_string("exportsuccessful","wiki")."<br />";
if(!deldir($exportbasedir)) {
error("Cannot delete $exportbasedir");
}
#exit();
return false;

readfile()でzipファイルの中身を標準出力にダーッと出力します.なにをもってしてファイルの終わりであるとクライアント側に判断させるのか,そのメカニズムを知らないのですが,終わったことに気付かずに,エラーメッセージ(上の青文字のerror())までzipファイルのお尻に追加されています.

ソースのインデントも2文字だったり,行末に空白があったり,中カッコの付け方に統一性が無かったり,あまりメンテナンスされていない関数のようです.

自分で作ったWikiをWebページとして公開することができる,かなり便利な機能ですので,もう少しメンテナンスされても良いと思うのですが.

少なくともzipファイルのダウンロードに関しては,もっと良いコードをどこかで見たような気がします.

他にも,リンクファイル名をurlencode()した後に+を""にリプレースして取り除いたり(そもそもrawurlencode()で済ませて欲しい)など,ファイル名の扱いにも色々と”仕様上の悩み”が見て取れます.

Wikiのエクスポートの機能を使っている方,使い勝手は如何です? 以上の問題は当方の環境だけの問題でしょうか?

#近頃,moodleといえばWikiというくらい,仕事の道具としてWikiに依存し始めています.

headerを出力したあとにreadfile()を使うのは普通に行なわれることのようですね.
http://www.microsoft.com/japan/msdn/asp.net/tips/download/
なにをもってしてファイル終端であることを判断するのだろう???
http://dns5.m-craft.com/mc_kadai/hamamura/pukiwiki/index.php?%A5%D5%A5%A1%A5%A4%A5%EB%A4%CE%A5%C0%A5%A6%A5%F3%A5%ED%A1%BC%A5%C9


(上の2つ目のページを参考に)
// ダウンロードファイルを出力
$fname = $path . $fname;
header("Content-type: " . $mime);
header("Content-Disposition: attachment; filename=" . $fname);
header("Content-length: " . filesize( $fname));
readfile( $fname);

青い文字で表したContent-lengthが指定されていませんね.それでセッションの終わりまでに出力された文字列まで全てzipファイルのお尻につけてしまうのじゃないのかな?

Tatsuya Shirai への返信

Re: Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
Header()とボディの関係が絡んでいるらしいということで,
php.iniのoutput_bufferingをOffからOnにしたら,余計に酷いことになってしまいました...
ZIpファイルのお尻だけではなく,頭にまで文字が入ってしまう.

HeaderにContent-Length:を付けても無視されているようですし,
Content-type:をapplication/octet-streamに変更したり,と,
色々試みたのですが...

lib/filelib.phpのsend_file()を利用することも考えたのですが,この関数はこの関数で,転送が終わったところでdie()するような気がしますので,ファイルダウンロードが終わったら元のファイルや作業フォルダを消すという後処理ができないような気がします...どうすれば良いのでしょう.途方にくれています.本当にデバッグしないといけない部分に取り掛かれないです.

Tatsuya Shirai への返信

Re: Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
昨日,都内へ出張ついでに東京駅近くの丸善でPHPやJavascriptなどの本を漁ったのですが,自動的にファイルをダウンロードする手順は皆同じで,

<?php
$file = "test.txt";
header("Content-type: text/plain");
header("Content-Disposition: attachment; filename=$file");
readfile($file);
?>
のように,ダウンロードさせたら,それで終わり,という例ばかりでした.

http://jamz.jp/tech/2006/11/how-to-download-after-display-thanks-page.html
こちらのページの方が多少,請ったことにチャレンジしているようですが,具体例が示されていないので私には分かりません.

Wikiのエクスポートでは,

  1. Wikiのデータをデータベースから読み出してHTML文書に変換
  2. それをテンポラリフォルダにコピーし,
  3. zip化する.
  4. zip化されたWebページを自動ダウンロード(header+readfile())させ,
  5. テンポラリフォルダを丸ごと削除.
  6. 完了した後のページを表示
という手順を取っています.これを一つのリクエストに対して行ないますので,zipファイルに6のページのデータまでzipファイルに含まれています.それどころか,テンポラリフォルダの削除にエラーが発生した場合には,そのエラー画面まで追加されます(エラー画面が表示されないので不審に思ってzipファイルを強制的にエディタで開いて発見しました).

ローカルファイルですので,file.phpを介すのも変ですし,大げさすぎます.ポップアップウィンドウを開いて,そこで4のダウンロードを行なうか,というのが今考えている手です.ただ,zipファイルのダウンロード(送信)が完了してから5のテンポラリフォルダの削除を行なう必要があるので,モーダルなウィンドウを開く必要があるのではないか(クライアントにダウンロードの完了を確認して貰う),と思ってJavascriptの本も探した次第です.

「それがHTTP手順というものだよ」
そう言われてしまえばそうなのですが,Webサーバからのデータの自動ダウンロードは,かなり危険な状況にあるのですね.(ハイパーリンクでダウンロードさせるのに比べると).zipファイルの場合はお尻に余計なデータが付いていても無視してくれているようですので,問題が表面化しにくいのですが,readfile()でファイルを出力した後に,phpソースに改行等の文字があると,(content-lengthが指定されていて,しかも有効に機能している場合を除くと),ファイルの後ろに意図せずにゴミが付着している場合が結構ありそうです.

少々大きい画像ですが,今回発生している問題を画像にしたものを添付します.よろしかったら,お手元のWikiをエクスポートして,そのダウンロードしたzipファイルのお尻の方をエディタで開いて見て頂けないでしょうか? もし当方の環境だけの問題ならば私の問題(zip_files()関数にも手を入れているのが原因?)かも知れません.そうでないならば,moodleTrackerに報告します.


添付 Wiki_export_zip_download.jpg
Tatsuya Shirai への返信

Re: Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
自動的にファイルをダウンロードするのは諦めました.
その代わりに,コースファイルのフォルダのbackupフォルダに完成したzipファイルをコピーするという方法にします.(fs_moodle)

ふー,一進一退です.
ではbackupフォルダにコピーされたHTML文書化されたWikiデータをダウンロードしよう,と思った矢先に,今度はファイル名の半角空白文字の悪影響で,正しくダウンロードできず...
http://moodle.org/mod/forum/discuss.php?d=72567

グチはともかくとして,エクスポートはこれ以外にもファイル名が全てurlencode()されたり,といった問題があって,そのままでは使いにくい感じですねぇ.

Tatsuya Shirai への返信

Re: Wikiのエクスポートがかなり変

- Haruhiko Okumura の投稿
確認しました。いやー,とんでもないバグですね。
Wiki使っていないものでわかりませんでした。
暇があったら直してみます。
Haruhiko Okumura への返信

Re: Wikiのエクスポートがかなり変

- Haruhiko Okumura の投稿
Headerを出しているところの次に
#exit();
とコメントアウトしてありますが,これを生かせばいいのじゃないでしょうか。
Haruhiko Okumura への返信

Re: Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
本当ですね.
exit()を有効にするとzipファイルのお尻に余計なゴミが付かなくなりました.
簡単な解決策としてはexit()を有効にすることでOKですね.

$cont=ewiki_page_wiki_dump_send($binaries,
$exportformats,
$_REQUEST["withvirtualpages"],
$_REQUEST["exportdestinations"]);
}
if($cont===false) return;
ewiki_page_wiki_dump_send()の後処理があるのかとも思ったのですが,ごらんの通りに(正常時にewiki_page_wiki_dump_send()はfalseを返すので),そのまま抜けていきそうですね.ちなみにexit()しない場合はまっさらな何もないwikiのページに遷移することで処理が終了したぞ,という(控えめな)主張をするのに比べて,exit()ですと画面が全く変化しないので寂しいですね.気付かずに”エクスポート”を連打してデスクトップ上に大量のzipファイルがダウンロードされていそうです満面の笑顔(:しました)


ところがexit()の対策ではエラーメッセージが表示できません...痛し痒しですね!
error("Cannot read $exportbasedir/$archivename");
error("Cannot delete $exportbasedir");
の二つのエラーが発生する(ことは少ないのですが)と,同じような事態に巻き込まれます.特にzipファイルが見つからない,という前者の場合には,何もエラーを表示しない上に,パッと見はファイルがダウンロードされていながら,実際は拡張子が.zipのhtmlファイルです悲しい.このzipファイルの拡張子を.htmlに変更してダブルクリックすると「"Cannot read ...."」と表示されると思うと,ちょっと喜劇的です.

Tatsuya Shirai への返信

Re: Wikiのエクスポートがかなり変

- Haruhiko Okumura の投稿
エラーを表示しようとするとZipファイルに入ってしまう,というわけではないのですね。ヘッダはerrorの中で取り消しているのでしょうか。であれば,エラーが起こったらexit()しないようにすればいいのでしょうか。それは簡単ですね。

乱打対策には,JavaScriptで,一度クリックしたらボタンがグレーアウトする?ような工夫ができそうですね。
Haruhiko Okumura への返信

Re: Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
いえ,exit()を有効にした場合でも,エラー発生時にはzipファイルにエラー画面の表示用HTMLソースが入り込んでしまいます.

header()およびreadfile()の出力をバッファリングしておいて,エラー発生時には出力バッファを破棄してエラーを表示する,もしいずれのエラーも発生しなかったら出力バッファをフラッシュしてexit()する,という手順はOKかも知れません.あぁ,段々と良い手のような気がしてきました笑顔.ただ,いままでob_start()などの関数を使用したことが無いので上手くいくかどうか...

Haruhiko Okumura への返信

Re: Wikiのエクスポートがかなり変

- Tatsuya Shirai の投稿
なんとかうまく行きました.
やはり一人で悩んでいても答えは出ないものですが,アドバイスを頂くと非常にスマートに解決できました.
ありがとうございました > 奥村先生

if(!$exportdestinations) {
$archivename=$wname.".zip";
zip_files($filestozip, "$exportbasedir/$archivename");

ob_start(); // (ADD)
if(!@readfile("$exportbasedir/$archivename")) {
ob_end_clean(); // (ADD)
error("Cannot read $exportbasedir/$archivename");
}
if(!deldir($exportbasedir)) {
ob_end_clean(); // (ADD)
error("Cannot delete $exportbasedir");
}
#-- Headers (Moved here)
Header("Content-type: application/zip");
Header("Content-disposition: attachment; filename=\"$archivename\"");
Header("Cache-control: private");
Header("Original-Filename: $archivename");
Header("X-Content-Type: application/zip");
Header("Content-Location: $archivename");
ob_end_flush(); // (ADD)
exit(); // (Removed #)
// return false;
} else {
return get_string("exportsuccessful","wiki")."<br />";
}
}

出力バッファを用いています.
readfile()でzipファイルの中身を出力バッファへ出力させ,もしエラーが発生したらそれを破棄(ob_end_clean())してエラー画面を表示させています.
ただし,
http://php.mirror.camelnetwork.com/manual/ja/ref.outcontrol.php
こちらのページにも書いてありますように,出力バッファではheader()情報はバッファリングされませんので,ob_end_flush()でzipファイルの中身を送信する直前の位置にHeader()の塊を大きく移動させています.

これでエラーが発生した場合はエラー画面を,エラーが発生しなかった場合はバッファを出力してexit()するようになりました.

Tatsuya Shirai への返信

Re: Wikiのエクスポートがかなり変

- Haruhiko Okumura の投稿
すごい! できたですね! ありがとうございます。
うちのほうにも同じパッチをあてさせていただきます。