function get_file_url()について

function get_file_url()について

- Tatsuya Shirai の投稿
返信数: 9

 先々週辺りのMoodle1.8.4+(5+),Moodle1.9+から,lib/filelib.phpの冒頭にfunction get_file_url()関数が追加されました.

 この関数は,file.phpなどへの引数として渡されるコースファイルのフォルダ名・ファイル名にマルチバイト文字列を用いた場合の対策(日本語の場合はWebブラウザがIEの場合のみ正常に処理できない)と"スラッシュを使用する"のOn/Offに対応した処理を一つにまとめられています.

 いまはまだ「課題」のアップロードに関してのみ利用されはじめたところですが,今後は他の(file.phpを利用した)ファイルのダウンロードに関する箇所に適用が広がると予想されます.

 ただ,なんとなく私の目からみると少し中途半端に見えなくもない箇所があります.

  • explode()関数はマルチバイトに対応している?
  • urlencode()ではなくrawurlencode()の方が良くないか?
  • ファイルの削除(delete.php)にはまだ利用していない.今後はそれらにも対応するために拡張される可能性がある.

日本語対応に関して非常に重要な意味合いをもつ関数ですので,別に議論を分けてみました.

Tatsuya Shirai への返信

explode(), trim()を使って大丈夫?

- Tatsuya Shirai の投稿

現状では以下のような内容です.

function get_file_url($path, $options=null, $type='coursefile') {
    global $CFG;

    $path = trim($path, '/'); // no leading and trailing slashes

    // type of file
    switch ($type) {
        case 'coursefile':
        default:
            $url = "$CFG->wwwroot/file.php";
    }

    if ($CFG->slasharguments) {
        $parts = explode('/', $path);
        $parts = array_map('urlencode', $parts);
        $path  = implode('/', $parts);
        $ffurl = "$CFG->wwwroot/file.php/$path";
        $separator = '?';
    } else {
        $path = urlencode("/$path");
        $ffurl = "$CFG->wwwroot/file.php?file=$path";
        $separator = '&';
    }

    if ($options) {
        foreach ($options as $name=>$value) {
            $ffurl = $ffurl.$separator.$name.'='.$value;
            $separator = '&';
        }
    }

    return $ffurl;
}

 


mod/assignment/type/upload/assignment.class.phpの350行目あたりにあるfunction print_user_files()関数の中.

        if ($basedir = $this->file_area($userid)) {
            if ($files = get_directory_list($basedir, 'responses')) {
                require_once($CFG->libdir.'/filelib.php');
                foreach ($files as $key => $file) {

                    $icon = mimeinfo('icon', $file);

                    $ffurl   = "$CFG->wwwroot/file.php?file=/$filearea/$file";

この部分が,一瞬だけ,

                  $ffurl   = "$CFG->wwwroot/file.php?file=". urlencode("/$filearea/$file");

このように変更され,それが(多分,'/'もurlencode()されるとダウンロードできなくなる問題が発生したのでしょう)慌てて元に戻された経緯があります.そしていまはこの部分が,

                    $ffurl = get_file_url("$filearea/$file");

このようになりました.


新しく作られた関数,function get_file_url()の重要な部分だけ抜き出します.

    if ($CFG->slasharguments) {
        $parts = explode('/', $path);
        $parts = array_map('urlencode', $parts);
        $path  = implode('/', $parts);
        $ffurl = "$CFG->wwwroot/file.php/$path";
        $separator = '?';
    } else {
        $path = urlencode("/$path");
        $ffurl = "$CFG->wwwroot/file.php?file=$path";
        $separator = '&';
    }

 $CFG->slashargumentsがtrueの場合は$pathをexplode()で分解してurlencode()しているのですが,falseの場合は以前に慌てて取り消したのと同じように,$pathを全てまとめてurlencode()しています.$pathに'/'が含まれていたら同じことではないのでしょうか.

    if ($CFG->slasharguments) {
//      $parts = explode('/', $path);
        $parts = mb_splt('/', $path);
//
      $parts = array_map('urlencode', $parts);
        $parts = array_map('rawurlencode', $parts);
        $path  = implode('/', $parts);
        $ffurl = "$CFG->wwwroot/file.php/$path";
        $separator = '?';
    } else {
//      $path = urlencode("/$path");
        $parts = mb_splt('/', $path);
        $parts = array_map('rawurlencode', $parts);
        $ffurl = "$CFG->wwwroot/file.php?file=$path";
        $separator = '&';
    }

この方が良いのではないでしょうか(未確認).あと,関数の頭で余計な'/'を取り除くために,

    $path = trim($path, '/'); // no leading and trailing slashes

を行なっていますが,これも少し不安があります.$partsをimplode()した後に'/'を取り除くという訳にはいかなでしょうか.さらに欲を言えば,$pathがUTF-8ではない場合(は無いと思いますが)や逆にWebブラウザが日本版IEの場合を考えるのでればShift-JISに変換して送出するということも考えに入れても良いかも知れません.(これは本家に言うことではありませんが)

 UTF-8に'/'に相当するキャラクタコードが含まれることはない,と断言できるのであれば元の通りにexplode()で問題ありませんし,trim()しても良いですが(未調査).

 

Tatsuya Shirai への返信

Re: explode(), trim()を使って大丈夫?

- Tatsuya Shirai の投稿

 いま調べてみた範囲(JIS第一水準)ならば,UTF-8に2Fを含むものは無さそうです.ではexplode()で大丈夫なのでしょうか.

Tatsuya Shirai への返信

Re: explode(), trim()を使って大丈夫?

- Tatsuya Shirai の投稿

 Unicode(UTF-16)からUTF-8への変換に関するここの記述が正しいならば,

1 バイトコード(0+7bit) : ASCII コードをそのまま. '/': 2Fはここ.
2 バイトコード (11+6bit, 10+6bit): C0以上+80以上.
3 バイトコード (1110+4bit, 10+6bit, 10+6bit): E0以上+80以上+80以上.

どうやら2byte,3byteの場合に理論上,2Fを含むことは無さそうです.したがって,explode('/', $path)で良さそうです.fs_moodleは何箇所かでexplode()は危険ではないか?と考えてmb_split()に直していますが,それも不要ですね.お騒がせしました.eplode()とtrim()はOKです.


 でも,$CFG->slashargument が false の場合の問題は対策が必要でしょう.

Tatsuya Shirai への返信

Re: explode(), trim()を使って大丈夫?

- Tatsuya Shirai の投稿

> でも,$CFG->slashargument が false の場合の問題は対策が必要でしょう.

 課題の場合は/を%2Fに変換されてもダウンロードが(IE7では)可能ですね.

こんな感じです.でも少し気持ちが悪いですね...

Tatsuya Shirai への返信

Re: explode(), trim()を使って大丈夫?

- Haruhiko Okumura の投稿
白井先生,すばらしい分析どうもありがとうございます。

もともとUTF-8は文字の一部にASCIIを含むことがないように作られていますので大丈夫です。

なるほど,/で分解してurlencodeして(なんでrawurlencodeでないんだろう)また/で結合しているのですね。私の1行プログラムのほうがかっこいいと思うのですが,まぁいいか。
Haruhiko Okumura への返信

Re: explode(), trim()を使って大丈夫?

- Tatsuya Shirai の投稿

 オリジナルのMoodleではファイル名に使用できる文字にかなり厳しい制限がシングルバイト文字に関してありますよね.'/'を使えないのは仕方ないのですが,半角スペースも使用できない('_'に置換)ですよね.したがってurlencode()でもrawurlencode()でも同じということでしょうね.

 fs_moodleではかなりこの条件を緩めているのでrawurlencode()でないと気持ちが悪いことになります.多分,rawurlencode()でもurlencode()でも処理に要する時間は変わらないと思うのですが,”半角スペースはファイル名に使えないよ”と自己主張したいのかな?程度に受け止めています.

Tatsuya Shirai への返信

Re: explode(), trim()を使って大丈夫?

- Haruhiko Okumura の投稿
もしスペースを許すなら,スペースは+でなく%20にエンコードしないと,ダウンロードでエラーになりますよね。それに原理的にはrawurlencode()のほうがほんの少し速いはずです。urlencode()は本来ファイル名でなくフォームのエンコードの方式なので,学生のプログラムなら注意したいところです。
Haruhiko Okumura への返信

Re: explode(), trim()を使って大丈夫?

- Tatsuya Shirai の投稿

 先ほどアップデートされたMoodle1.9.1+で,

    if ($CFG->slasharguments) {
        $parts = explode('/', $path);
        $parts = array_map('rawurlencode', $parts);
        $path  = implode('/', $parts);
        $ffurl = "$CFG->wwwroot/file.php/$path";
        $separator = '?';
    } else {
        $path = rawurlencode("/$path");
        $ffurl = "$CFG->wwwroot/file.php?file=$path";
        $separator = '&';
    }

rawurlencode()に変更されました.


おや,同じlib/filelib.php中のfunction send_file()も好ましい修正が加わっていますね.

    // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
    if (check_browser_version('MSIE')) {
        $filename = rawurlencode($filename);
    }

ここがurlencode()からrawurlencode()に変更されたのと,その直後の,

    if ($forcedownload) {
        @header('Content-Disposition: attachment; filename="'.$filename.'"');
    } else {
        @header('Content-Disposition: inline; filename="'.$filename.'"');
    }

です.前者は私は別の関数を作成して使っているので関係ないのですが,後者は同じ修正をしているので助かります.ファイル名をダブルクオートで括っています.これで半角スペースを含むファイルもダウンロードできますね.(rawurlencode()されてはいますが,害があるわけではない).

Tatsuya Shirai への返信

file.phpだけではなく,delete.phpにも対応できないか?

- Tatsuya Shirai の投稿

 先ほどのfunction print_user_files()ですが,ファイルをダウンロードするためのURLにはfunction get_file_url()を適用してマルチバイトへの対応を行なおうという試みがなされていますが,ファイルの削除に関してはノーマークのようです.

        if ($basedir = $this->file_area($userid)) {
            if ($files = get_directory_list($basedir, 'responses')) {
                require_once($CFG->libdir.'/filelib.php');
                foreach ($files as $key => $file) {

                    $icon = mimeinfo('icon', $file);
                    $ffurl = get_file_url("$filearea/$file");

                    $output .= '<a href="'.$ffurl.'" ><img src="'.$CFG->pixpath.'/f/'.$icon.'" class="icon" alt="'.$icon.'" />'.$file.'</a>';

                    if ($candelete) {
// (IE_Problem011): IEでアップロード済みファイルを削除できない
//
                      $delurl  = "$CFG->wwwroot/mod/assignment/delete.php?id={$this->cm->id}&amp;file=$file&amp;userid={$submission->userid}&amp;mode=$mode&amp;offset=$offset";
                        $delurl  = "$CFG->wwwroot/mod/assignment/delete.php?id={$this->cm->id}&amp;file=".fs_rawurlencode($file)."&amp;userid={$submission->userid}&amp;mode=$mode&amp;offset=$offset";
// (IE_Problem011): ここまで

                        $output .= '<a href="'.$delurl.'">&nbsp;'
                                  .'<img title="'.$strdelete.'" src="'.$CFG->pixpath.'/t/delete.gif" class="iconsmall" alt="" /></a> ';
                    }

                    $output .= '<br />';
                }
            }
        }

WebブラウザがIEの場合は日本語ファイル名のファイルをダウンロードできなかった(file.phpに正しくファイル名を渡せない)のと同じように,ファイルの削除もできません(delete.phpにファイル名を渡せない).この場合は"スラッシュの使用"のメリットが無いのかどうか分かりませんが,'?'をセパレータとしたURLを作成しますが,$fileをurlencode()していません.function get_file_url()の引数として$optionが(省略可能な)オプションとして用意されていますが,それとは別に必須の引数として"file.php","delete.pp"などを渡すようにするか,それともそもそもfile.phpのような文字列はget_file_url()ではreturnしないで自分で付ける,とすれば,ファイルの削除にも対応できますよね.

function get_file_url($path, $options=null, $type='coursefile') {
    global $CFG;

    $path = trim($path, '/'); // no leading and trailing slashes

    // type of file
    switch ($type) {
        case 'coursefile':
        default:
            $url = "$CFG->wwwroot/file.php";
    }

function get_file_url()の冒頭部です.よく見てみたら,3つ目の引数$typeがありましたね.この$typeは冒頭のswich文でしか使われていません.この引数の用途に迷いが感じられますね.たとば'assignment/delete'を引数として与え,

      case 'assignment/delete': $url = "$CFG->wwwroot/mod/assignment/delete.php"; break;

のようにすることも考えられますね.その場合は2つ目の引数である$optionsを明示的にnull,いや,"&amp;userid={$submission->userid}&amp;mode=$mode&amp;offset=$offset"を与える必要があるのか? いやぁ面倒ですね.欲張らずにfunction get_file_url()とは別にexplod()してurlendocd()してimplode()する別の関数を用意した方が良いでしょうか.

(といった感じに悩んでいる最中?)