HTTP_WebDAV_Clientを勝手に修正

哀れWebDAV足切りクライアント - 南旺理工

 ライセンスとか詳しくないんで、怒られたら「てぃひっ」と言って消すつもり。


HTTP_WebDAV_Client
 PEARのHTTP_WebDAV_Client 1.0.0内のStream.phpを勝手にオレオレ修正してみた。だってバグってるんだもの…。
 修正した関数は2個。修正箇所は計3ヶ所。

オレオレ修正後のstream_open関数

    /**
     * Stream wrapper interface open() method
     *
     * @access public
     * @var    string resource URL
     * @var    string mode flags
     * @var    array  not used here
     * @var    string return real path here if suitable
     * @return bool   true on success
     */
    function stream_open($path, $mode, $options, &$opened_path) 
    {
        // rewrite the request URL
        if (!$this->_parse_url($path)) return false;

        // query server for WebDAV options
        if (!$this->_check_options())  return false;

        // now get the file metadata
        // we only need type, size, creation and modification date
        $req = &new HTTP_Request($this->url);
        $req->setMethod(HTTP_REQUEST_METHOD_PROPFIND);
        if (is_string($this->user)) {
            $req->setBasicAuth($this->user, @$this->pass);          
        }
        $req->addHeader("Depth", "0");
        $req->addHeader("Content-Type", "text/xml");
        $req->addRawPostData('<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
 <prop>
  <resourcetype/>
  <getcontentlength/>
  <getlastmodified />
  <creationdate/>
 </prop>
</propfind>
');
        $req->sendRequest();

        // check the response code, anything but 207 indicates a problem
        switch ($req->getResponseCode()) {
        case 207: // OK
            // now we have to parse the result to get the status info items
            $propinfo = &new HTTP_WebDAV_Client_parse_propfind_response($req->getResponseBody());
            $this->stat = $propinfo->stat();
            unset($propinfo);
            break;

        case 404: // not found is ok in write modes
            if (preg_match('|[aw\+]|', $mode)) {
                break; // write
            } 
            $this->eof = true;
            // else fallthru
        default: 
//////オレオレ削除 BEGIN ... (1)
            //error_log("file not found: ".$req->getResponseCode());
//////オレオレ削除 END
            return false;
        }
        
        // 'w' -> open for writing, truncate existing files
        if (strpos($mode, "w") !== false) {
            $req = &new HTTP_Request($this->url);
            $req->setMethod(HTTP_REQUEST_METHOD_PUT);
            if (is_string($this->user)) {
                $req->setBasicAuth($this->user, @$this->pass);          
            }
            $req->sendRequest();
//////オレオレ追加 BEGIN ... (2)
            $this->position = 0;
//////オレオレ追加 END
        }

        // 'a' -> open for appending
        if (strpos($mode, "a") !== false) {
            $this->eof = true;
        }

        // we are done :)
        return true;
    }


(1)の修正理由:
 WebDAVの謎な特性として、lsすると先頭に自ディレクトリ名が表示されるというものがある。
 いちおう、元のコードでもこれを除外しようとはしているっぽいのだが、実際dir_readdir()するとなぜか、自ディレクトリ名が採れたり採れなかったりする。何に依存するのかは謎。
 採れてしまった場合は、実在しないhoge/hoge/ディレクトリがあたかも存在しているかのように見える。
 しかし、もしかすると本当にhoge/hoge/ディレクトリがあるのかもしれない。
 どちらなのかを知るには、hoge/hoge/をif(dir_opendir())してみるしかない。ifが通ればhoge/hoge/は実在する。
 しかしそもそもそれ以前にまず、dir_readdir()が返したhogeが、ディレクトリなのかファイルなのかをまず弁別しなければならない。そのためには、url_stat関数でfalseが返ればディレクトリだと考える。
 ところが実は、url_stat関数の中の人は横着して(大したコストじゃないだろ、とコメントに書いている)、stream_open関数を呼んでおり、hoge/hoge/が実在しなかったときにこのerror_logがウザいので、眠ってもらった。


(2)の修正理由:
 修正前のこのクラスの1個のインスタンスを使いまわして複数のファイルをアップロードすると、妙にデカいファイルサイズになる。よく見ると、ファイルサイズがどんどん足し算になっている。
 たとえば100KB, 200KB, 300KBのファイルをこの順にアップロードすると、100KB, 300KB, 600KBのファイルができあがる、なんじゃこりゃwwww
 ダウンロード後にアップロードでも加算されている。200KBのファイルをダウンロードしたあとに300KBのファイルをアップロードすると500KBになっているw
 原因は、元のコードだとopen時にpositionをリセットしていないからだ。
 openとcloseがあるんだから、当然そのへんは面倒見てると人は思うはずだ。というか、面倒見てるだろうかという疑問すら抱かないと思う。
 いちいちnewするのはエレガントでない。そこでつい、クラスの中に手を突っ込んでしまった。
 しかし今気づいたんですが、
i-revo お客様サポート 重要なお知らせ

  • fopen で 'w' で開いたときに truncate されない問題の対応

と書かれてるのはもしかしてこの問題を指している? だとしたら、車輪を再発明してしまったかもしれん俺…。

オレオレ修正後の_parse_url関数

    /**
     * Helper function for URL analysis
     *
     * @access private
     * @param  string  original request URL
     * @return bool    true on success else false
     */
    function _parse_url($path) 
    {
        // rewrite the WebDAV url as a plain HTTP url
        $url = parse_url($path);

        // detect whether plain or SSL-encrypted transfer is requested
        switch ($url['scheme']) {
        case "webdav":
            $url['scheme'] = "http";
            break;
        case "webdavs":
            $url['scheme'] = "https";
            break;
        default:
            error_log("only 'webdav:' and 'webdavs:' are supported, not '$url[scheme]:'");
            return false;
        }

        // if a TCP port is specified we have to add it after the host
        if (isset($url['port'])) {
            $url['host'] .= ":$url[port]";
        }

        // store the plain path for possible later use
        $this->path = $url["path"];

        // now we can put together the new URL 
//////オレオレ変更 BEGIN ... (3)
        //$this->url = "$url[scheme]://$url[host]$url[path]";
        $sDirectorys = explode("/", $url[path]);
        $sPath = "";
        $bDirectoryFirst = true;
        foreach ($sDirectorys as $sDirectory)
        {
	        if ($bDirectoryFirst)
	        {
	        	$bDirectoryFirst = false;
	        }
	        else
	        {
	        	$sPath .= "/" . rawurlencode(rawurldecode($sDirectory));
	        }
        }
        $this->url = "$url[scheme]://$url[host]$sPath";
//////オレオレ変更 END
        
        // extract authentication information
        if (isset($url['user'])) {
            $this->user = urldecode($url['user']);
        }
        if (isset($url['pass'])) {
            $this->pass = urldecode($url['pass']);
        }
        
        return true;
    }


(3)の修正理由:
 修正前のコードだと、パスに1バイトスペースが含まれているとき、URLの解釈が失敗する。
 いろいろ試してみて、このように変更したらなぜか動作するようになった。
 なぜなのかはほんとに謎なのがヘボプログラマーの悲哀。泥臭いコードでお恥ずかしいかぎりです…。

2009.11.22追記

 (2)については、上述のworkaroundでもまだ足りない。
 'r'でstream_openしたとき、同じインスタンスを使いまわすと、eofがいきなりtrueになるため、何も読み出せない問題が残っているからだ。
 だから(2)はやめて、いっそ下記のように、stream_openされるたびに全メンバ変数を初期化してしまったほうがすっきりする。僕はそうした。

    /**
     * Stream wrapper interface open() method
     *
     * @access public
     * @var    string resource URL
     * @var    string mode flags
     * @var    array  not used here
     * @var    string return real path here if suitable
     * @return bool   true on success
     */
    function stream_open($path, $mode, $options, &$opened_path) 
    {
/////091122 オレオレ追加 BEGIN
		//初期化
		$this->url = false;
		$this->path = false;
		$this->position = 0;
		$this->stat = array();
		$this->user = false;
		$this->pass = false;
		$this->dav_level = array();
		$this->dav_allow = array();
		$this->dirfiles = false;
		$this->dirpos = 0;
		$this->eof = false;
		$this->locktoken = false;
/////091122 オレオレ追加 END

        // rewrite the request URL
        if (!$this->_parse_url($path)) return false;

...以下は同じ