PHPIndex

This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).

kodWebDav.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/kodWebDav.class.php'
View Content
<?php

/**
 * webdav 文件管理处理;
 */
class kodWebDav extends webdavServer {
	public function __construct($DAV_PRE) {
		$this->plugin = Action('webdavPlugin');
		$this->checkUser();
		$this->initPath($DAV_PRE);
	}

	public function run(){
		$method = 'http'.HttpHeader::method();
		if(!method_exists($this,$method)){
			return HttpAuth::error();
		}
		$result = $this->$method();
		if(!$result) return;//文件下载;
		self::response($result);
    }

	/**
	 * 用户登录校验;权限判断;
	 * 性能优化: 通过cookie处理为已登录; (避免ad域用户或用户集成每次进行登录验证;)
	 * 
	 */
	public function checkUser(){
		$userInfo = Session::get("kodUser");
	    if(!$userInfo || !is_array($userInfo)){
    	    $user = HttpAuth::get();
			
			// 兼容webdav挂载不支持中文用户名; 中文名用户名编解码处理;
			if(substr($user['user'],0,2) == '$$'){
				$user['user'] = rawurldecode(substr($user['user'],2));
			}
    		$find = ActionCall('user.index.userInfo', $user['user'],$user['pass']);
    		if ( !is_array($find) || !isset($find['userID']) ){
    			$this->plugin->log(array($user,$find,$_SERVER['HTTP_AUTHORIZATION']));
    			return HttpAuth::error();
    		}
    		ActionCall('user.index.loginSuccess',$find);
			// 登录日志;
			if(HttpHeader::method() == 'OPTIONS'){
				Model('User')->userEdit($find['userID'],array("lastLogin"=>time()));
				ActionCall('admin.log.loginLog');
			}
	    }
		if(!$this->plugin->authCheck()){
			self::response(array('code'=>404,'body'=>"<error>您没有此权限!</error>"));exit;
		}
	}
	public function parsePath($path){
		$options = $this->plugin->getConfig();
		$rootPath = '{block:files}/';
		if($options['pathAllow'] == 'self'){ //个人网盘
			$rootPath = MY_HOME;
		}
		if(!$path || $path == '/') return $rootPath;

		$pathArr = explode('/',KodIO::clear(trim($path,'/')));
		$rootList = Action('explorer.list')->path($rootPath);
		return $this->pathInfoDeep($rootList,$pathArr);
	}
	/**
	 * 向下回溯路径;
	 */
	private function pathInfoDeep($parent,$pathArr){
		$list = $this->pathListMerge($parent);
		$itemArr = array_to_keyvalue($list,'name');
		$item = $itemArr[$pathArr[0]];
		if(!$item) return false;
		if(count($pathArr) == 1) return $item['path'];
		
		$pathAppend = implode('/',array_slice($pathArr,1));
		$newPath = KodIO::clear($item['path'].'/'.$pathAppend);
		$info = IO::infoFull($newPath);

		// 已存在回收站中处理;
		if($info && $info['isDelete'] == '1'){
			$resetName = $info['name'] .date('(H:i:s)');
			if($info['type'] == 'file'){
				$ext = '.'.get_path_ext($info['name']);
				$theName   = substr($info['name'],0,strlen($info['name']) - strlen($ext));
				$resetName = $theName.date('(H:i:s)').$ext;
			}
			IO::rename($info['path'],$resetName);
			$info = IO::infoFull($newPath);
		}
		// pr($newPath,$item,$pathArr,$info,count($parent['folderList']));
		if($info) return $info['path'];

		$parent = Action('explorer.list')->path($item['path']);
		$result  = $this->pathInfoDeep($parent,array_slice($pathArr,1));
		if(!$result){
			$result = $newPath;
			//虚拟目录追; 没找到字内容;则认为不存在;
			if(Action('explorer.auth')->pathOnlyShow($item['path']) ){
				$result = false;
			}
		}
		return $result;
	}
	
	public function can($path,$action){
		$result = Action('explorer.auth')->fileCan($path,$action);
		// 编辑;则检测当前存储空间使用情况;
		if($result && $action == 'edit'){
			$result = Action('explorer.auth')->spaceAllow($path);
		}
		return $result;
	}
	public function pathExists($path){
		$info = IO::infoFull($path);
		if(!$info) return false;
		if($info['isDelete'] == '1') return false;
		return true;
	}
	
	/**
	 * 文档属性及列表;
	 * 不存在:404;存在207;  文件--该文件属性item; 文件夹--该文件属性item + 多个子内容属性
	 */
	public function pathList($path){
		if(!$path) return false;
		$info  = IO::infoFull($path);
		if(!$info && !Action('explorer.auth')->pathOnlyShow($path) ){
			return false;
		}

		if(!$this->can($path,'show')) return false;
		if($info && $info['isDelete'] == '1') return false;//回收站中;
		if($info && $info['type'] == 'file'){ //单个文件;
			return array('fileList'=>array($info),'current'=>$info);
		}
		
		$pathParse = KodIO::parse($path);
		// 分页大小处理--不分页; 搜索结果除外;
		if($pathParse['type'] != KodIO::KOD_SEARCH){
			$GLOBALS['in']['pageNum'] = -1;
		}
		// write_log([$path,$pathParse,$GLOBALS['in']],'test');		
		return Action('explorer.list')->path($path);
	}
	
	public function pathMkdir($path){
		$path = $this->pathCreateParent($path);
		if(!$this->can($path,'edit')) return false;
		return IO::mkdir($path);
	}
	public function pathOut($path){
		if(!$this->pathExists($path) || !$this->can($path,'view')){
			self::response(array('code' => 404));exit;
		}
		if(IO::size($path)<=0) return;//空文件处理;
		//部分webdav客户端不支持301跳转;
		if($this->notSupportHeader()){
			IO::fileOutServer($path); 
		}else{
			IO::fileOut($path); 
		}
	}
	// GET 下载文件;是否支持301跳转;对象存储下载走直连;
	private function notSupportHeader(){
		$software = array(
			'ReaddleDAV Documents',	//ios Documents 不支持;
		);
		$ua = $_SERVER['HTTP_USER_AGENT'];
		foreach ($software as $type){
			if(stristr($ua,$type)) return true;
		}
		return false;
	}
	
	// 收藏夹下文件夹处理;(新建,上传)
	private function pathCreateParent($path){
		if($path) return $path;
		$inPath  = $this->pathGet();
		return rtrim($this->parsePath(IO::pathFather($inPath)),'/').'/'.IO::pathThis($inPath);
	}
	
	public function pathPut($path,$localFile=''){
		$pathBefore = $path;
		$path = $this->pathCreateParent($path);
		$info = IO::infoFull($path);
		if($info){	// 文件已存在; 则使用文件父目录追加文件名;
			$name 		= IO::pathThis($this->pathGet());
			$uploadPath = rtrim(IO::pathFather($info['path']),'/').'/'.$name; //构建上层目录追加文件名;
		}else{// 首次请求创建,文件不存在; 则使用{source:xx}/newfile.txt;
			$uploadPath = $path;
		}
		if(!$this->can($path,'edit')) return false;

		// 传入了文件; wscp等直接一次上传处理的情况;  windows/mac等会调用锁定,解锁,判断是否存在等之后再上传;
		// 文件夹下已存在,或在回收站中处理;
		// 删除临时文件; mac系统生成两次 ._file.txt;
		$size = 0;
		if($localFile){
			$size = filesize($localFile);
			$result = IO::upload($uploadPath,$localFile,true,REPEAT_REPLACE);
			$this->pathPutRemoveTemp($uploadPath);
		}else{
			if(!$info){ // 不存在,创建;
				$result = IO::mkfile($uploadPath,'',REPEAT_REPLACE);
			}
			$result = true;	
		}
		$this->plugin->log("upload=$uploadPath;path=$path,$pathBefore;res=$result;local=$localFile;size=".$size);
		return $result;
	}
	private function pathPutRemoveTemp($path){
		$pathArr = explode('/',$path);
		$pathArr[count($pathArr) - 1] = '._'.$pathArr[count($pathArr) - 1];
		$tempPath = implode('/',$pathArr);
		
		$tempInfo = IO::infoFull($tempPath);
		if($tempInfo && $tempInfo['type'] == 'file'){
			IO::remove($tempInfo['path'],false);
		}
	}
	
	public function pathRemove($path){
		if(!$this->can($path,'remove')) return false;
		$tempInfo = IO::infoFull($path);
		if(!$tempInfo) return true;
		
		$toRecycle = Model('UserOption')->get('recycleOpen');
		return IO::remove($tempInfo['path'], $toRecycle);
	}
	public function pathMove($path,$dest){
		$pathUrl = $this->pathGet();
		$destURL = $this->pathGet(true);		
		$path 	= $this->parsePath($pathUrl);
		$dest   = $this->parsePath(IO::pathFather($destURL)); //多出一层-来源文件(夹)名
		$this->plugin->log("from=$path;to=$dest;$pathUrl;$destURL");

		// 目录不变,重命名,(编辑文件)
		$io = IO::init('/');
		if($io->pathFather($pathUrl) == $io->pathFather($destURL)){
			if(!$this->can($path,'edit')) return false;
			$destFile = rtrim($dest,'/').'/'.$io->pathThis($destURL);
			$this->plugin->log("edit=$destFile;exists=".intval($this->pathExists($destFile)));
			$fromExt = get_path_ext($pathUrl);
			$toExt   = get_path_ext($destURL);
			$officeExt = array('doc','docx','xls','xlsx','ppt','pptx');
			/**
			 * office 编辑保存最后落地时处理(导致历史记录丢失); 
			 * 0. 上传~tmp1601041332501525796.TMP //锁定,上传,解锁;
			 * 1. 移动 test.docx => test~388C66.tmp 				// 改造,识别到之后不进行移动重命名;
			 * 2. 移动 ~tmp1601041332501525796.TMP => test.docx; 	// 改造;目标文件已存在则更新文件;删除原文件;
			 * 3. 删除 test~388C66.tmp  
			 */
			if( $this->isWindows() && $toExt == 'tmp' && in_array($fromExt,$officeExt) ){
				$result =  IO::mkfile($destFile);
			    $this->plugin->log("move mkfile=$path;$pathUrl;$destURL;result=".$result);
			    return $result;
			}
			// 都存在则覆盖;
			if( $this->pathExists($path) && $this->pathExists($destFile) ){
				$destFileInfo = IO::infoFull($destFile);

				// $content = IO::getContent($path);
				// IO::setContent($destFileInfo['path'],$content);
				// IO::remove($path);$result = $destFileInfo['path'];
				$result  = IO::saveFile($path,$destFileInfo['path']);//覆盖保存;
				$this->plugin->log("move saveFile; to=$path;toFile=".$destFileInfo['path'].';result='.$result);
				return $result;
			}
			return IO::rename($path,$io->pathThis($destURL));
		}
		
		if(!$this->can($path,'remove')) return false;
		if(!$this->can($dest,'edit')) return false;
		
		// 名称不同先重命名;
		if( $io->pathThis($destURL) != $io->pathThis($pathUrl) ){
			$path = IO::rename($path,$io->pathThis($destURL));
		}
		return IO::move($path,$dest);
	}
	public function pathCopy($path,$dest){
		$pathUrl = $this->pathGet();
		$destURL = $this->pathGet(true);		
		$path 	= $this->parsePath($pathUrl);
		$dest   = $this->parsePath(IO::pathFather($destURL)); //多出一层-来源文件(夹)名
		$this->plugin->log("from=$path;to=$dest;$pathUrl;$destURL");

		if(!$this->can($path,'download')) return false;
		if(!$this->can($dest,'edit')) return false;
		return IO::copy($path,$dest);
	}
	private function isWindows(){
	    return stristr($_SERVER['HTTP_USER_AGENT'],'Microsoft-WebDAV-MiniRedir');
	}
}
pathDriverNFS.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/pathDriverNFS.class.php'
View Content
<?php

class PathDriverNFS extends PathDriverLocal{
	public function __construct($config) {
		parent::__construct();
		$pluginOption = Model("Plugin")->getConfig('webdav');
	}
}
pathDriverSamba.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/pathDriverSamba.class.php'
View Content
<?php

class PathDriverSamba extends PathDriverLocal{
	public function __construct($config) {
		parent::__construct();
		$pluginOption = Model("Plugin")->getConfig('webdav');
	}
}
pathDriverWebdav.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/pathDriverWebdav.class.php'
View Content
<?php

/**
 * webdav client IO;
 * 
 * 支持kodserver支持协议特性(文件属性扩展extendFileInfo,文件列表扩展extendFileList)
 * 
 * //kodserver: 1, 2, 3, sabredav-partialupdate, extended-mkcol, extended-kodbox
 * //sabre    : 1, 3, extended-mkcol, 2, sabredav-partialupdate
 * //patch文件更新: https://sabre.io/dav/http-patch/  curl请求传递contentType会丢失,暂不支持该情况
 */
class PathDriverWebdav extends PathDriverBase {
	public function __construct($config) {
		parent::__construct();
		
		// 挂载插件开关处理;
		$pluginOption = Model("Plugin")->getConfig('webdav');
		if(!$pluginOption || $pluginOption['mountWebdav'] == '0'){$config = array();}

		$this->config = $config;
		$this->dav = new webdavClient($config);// host/user/password/basePath
		
		$this->ioFileOutServer 	= $config['ioFileOutServer'] != '0'; // 下载是否中转
		$this->ioUploadServer 	= $config['ioUploadServer']  != '0'; // 上传是否中转
		$this->davServerKod 	= false;
		$this->uploadChunkSize  = 1024*1024*5; 	// patch分片上传时; 分片大小;
		
		if(!is_string($config['dav'])){return;}
		$davSupport = $config['dav'] ? $config['dav']:'';
		$davSupport = explode(',',$davSupport);
		foreach($davSupport as $key => $type){$davSupport[$key] = trim($type);}
		if(in_array('extended-kodbox',$davSupport)){$this->davServerKod = true;}
	}
	public function mkdir($dir,$repeat=REPEAT_SKIP){
		$parent = $dir;$add = array();// 循环创建文件夹;
		while($parent && $parent != '/' && !$this->exist($parent)){
			$name   = get_path_this($parent);
			$parent = get_path_father($parent);
			// 避免 'test/'类型路径parent为test的情况;
			$add[] = array($parent,$name);
			if(count(explode('/',trim($parent,'/'))) == 1){break;}
		}
		$add = array_reverse($add);
		for($i=0; $i < count($add); $i++) {
			$path = rtrim($add[$i][0],'/').'/'.$add[$i][1];
			if(!$this->dav->mkdir($path)){break;}
		}
		if(count($add) == 0 || $i == count($add) ){
			return $this->getPathOuter($dir);
		}
		return false;
	}

	private function _copyMove($action,$from,$to,$repeat=REPEAT_REPLACE,$newName=''){
		if(!$this->exist($from)) return false;
		$this->mkdir($this->pathFather($to));
		if(!$newName && $newName != '0'){
			$newName = get_path_this($from);
			$newName = $this->fileNameAuto($to,$newName,$repeat,false);
		}
		
		$destPath = rtrim($to,'/').'/'.$newName;
		if($action == 'copy'){$result = $this->dav->copy($from,$destPath);}
		if($action == 'move'){$result = $this->dav->move($from,$destPath);}
		return $this->exist($destPath) ? $this->getPathOuter($destPath):false;
	}
	public function moveSameAllow(){}
	public function move($from,$to,$repeat=REPEAT_REPLACE) {
		return $this->_copyMove('move',$from,$to,$repeat);
	}
	public function copy($from,$to,$repeat=REPEAT_REPLACE) {
		return $this->_copyMove('copy',$from,$to,$repeat);
	}
	
	public function copyFile($from,$to){return $this->copy($from,$to);}
	public function moveFile($from,$to){return $this->move($from,$to);}
	public function remove($path,$toRecycle=true){return $this->dav->delete($path);}
	public function rename($from,$newName){
		if(!$newName && $newName != '0'){return $this->getPathOuter($from);}
		$to = get_path_father($from);		
		return $this->_copyMove('move',$from,$to,REPEAT_SKIP,$newName);
	}
	public function has($path,$count=false,$isFolder=false){
		$info = $this->listPath($path);
		if(!$info) return false;
		$children = array(
			'hasFolder' => count($info['folderList']),
			'hasFile'   => count($info['folderList']),
		);
		if($count){return $children;}
		return $isFolder ? $children['hasFolder'] : $children['hasFile'];
	}
	public function listAll($path){
		// 优先处理按kod进行尝试请求;
		$data = $this->dav->propfind($path,'0','X-DAV-ACTION: kodListAll');
		$current = $this->_pathInfoParse(_get($data,'data.response',false));
		if($current && isset($current['listAllChildren'])){
			foreach ($current['listAllChildren'] as &$item){
				$arr = explode('/',trim($item['path'],'/'));
				$pathShow = implode('/',array_slice($arr,1)).($item['folder'] ? '/':'');
				$item["path"] = rtrim($current['path'],'/').'/'.$pathShow; // 都使用包含上层的方式,兼容1.45前版本;
				unset($item["filePath"]);
			};unset($item);
			return $current['listAllChildren'];
		}
		
		$result = array();
		$this->listAllMake($path,$result);
		return $result;
	}
	
	public function canRead($path) {return $this->exist($path);}
	public function canWrite($path) {return $this->exist($path);}
	public function getContent($file){return $this->fileSubstr($file, 0, -1);}
	public function fileSubstr($file, $start, $length){
		$start 		= $start ? $start : 0;
		$end   		= $start + $length - 1;
		$range 		= $length > 0 ? 'bytes='.$start.'-'.$end: '';

		$tempFile 	= $this->tempFile();
		$result 	= $this->dav->get($file,$tempFile,$range);
		$content    = $result ? file_get_contents($tempFile):false;	
		$this->tempFileRemve($tempFile);
		return $content;
	}
	
	public function mkfile($path,$content='',$repeat = REPEAT_RENAME){
		$tempFile = $this->tempFile('',$content);
		$result   = $this->upload($path,$tempFile,false,$repeat);
		$this->tempFileRemve($tempFile);
		
		// io 添加时检测; 根目录新建空文件则放过
		if(trim($path,'/') == 'index.html' && !$content){return $this->getPathOuter('/index.html');}
		return $result;
	}
	public function setContent($file, $content = ''){
		return $this->mkfile($file,$content,REPEAT_REPLACE) ? true : false;
	}	
	public function upload($destPath,$localPath,$moveFile=false,$repeat=REPEAT_REPLACE){
		if($this->davServerKod && filesize($localPath) >= $this->uploadChunkSize ){
			return $this->uploadChunk($destPath,$localPath,$moveFile,$repeat);
		}
		$savePath = $this->pathFather($destPath);$this->mkdir($savePath);
		$saveName = $this->fileNameAuto($savePath,$this->pathThis($destPath),$repeat,false);
		$destPath = rtrim($savePath,'/').'/'.$saveName;
		$result   = $this->dav->put($destPath,$localPath);
		// write_log([$destPath,$localPath,$result],'dav');
		return $result['status'] ? $this->getPathOuter($destPath) : false;
	}
	
	// 如若是kod,前端直传;
	public function uploadLink($destPath,$size=0){
		$pose   = strpos($this->config['host'],'/index.php/plugin/webdav/');
		$server = $pose ? substr($pose,0,$pose+1):'';
		if($this->ioUploadServer) return;
		if(!$this->davServerKod || !$server || $size <= 1024*10) return;
		
		$in   = $GLOBALS['in'];
		$args = array(
			'size'				=> $size,
			'uploadWeb'			=>'1',
			'fullPath'			=> _get($in,'fullPath',''),
			'checkType'   		=> _get($in,'checkType',''),// 必须是'checkHash',
			'checkHashSimple'	=> _get($in,'checkHashSimple',''),
		);
		if($args['checkType'] != 'checkHash') return;
		
		$result = $this->uploadSend($destPath,'',$args);
		if(!$result['status'] || !is_array($result['data']['info'])){
			show_json(IO::getLastError(),false);
		}
		$uploadInfo = $result['data']['info'];
		$uploadInfo['webdavUploadTo'] = $uploadInfo['addUploadParam'];
		unset($uploadInfo['addUploadParam']);
		// write_log($uploadInfo,'upload');
		return $uploadInfo;
	}
	
	private function uploadChunk($destPath,$localPath,$moveFile=false,$repeat=REPEAT_REPLACE){
		$savePath = $this->pathFather($destPath);$this->mkdir($savePath);
		$saveName = $this->fileNameAuto($savePath,$this->pathThis($destPath),$repeat,false);
		$destPath = rtrim($savePath,'/').'/'.$saveName;
		
		// 秒传检测处理;
		$checkResult = $this->uploadHashCheck($destPath,$localPath);
		if($checkResult == false){
			$checkResult = $this->uploadWithChunk($destPath,$localPath);
		}
		return $checkResult['code'] ? $this->getPathOuter($destPath) : false;
	}
	
	/**
	 * 上传处理
	 * 
	 * 1. 上传请求 simpleHash; [checkType=checkHash, path,name,checkHashSimple]
	 * 2. 如果simpleHash匹配到simpleHash则秒传请求; [checkType=matchMd5, path,name,checkHashMd5]
	 * 
	 * 正常分片上传 [path,name,size,chunkSize,chunks,chunk]; 
	 * data.info字段是否为非空字符串(目标文件路径),来确认文件秒传是否成功
	 */
	private function uploadHashCheck($destPath,$localPath){
		// 检测hashSimple
		$hashSimple = IO::hashSimple($localPath);
		$args = array('checkType'=>'checkHash','checkHashSimple'=>$hashSimple);
		$result = $this->uploadSend($destPath,'',$args);

		if(!is_array($result['data']) || !array_key_exists('code',$result['data'])) return false;
		if(!$result['data']['code']) return $result['data'];//结束;
		$infoData = _get($result,'data.info',array());
		$hashInfo = _get($infoData,'checkFileHash');
		if(!is_array($hashInfo) || !$hashInfo['hashMd5']) return false;
		if($infoData['uploadChunkSize']){$this->uploadChunkSize = $infoData['uploadChunkSize'];}
		$fileMd5 = IO::hashMd5($localPath);
		if($hashInfo['hashMd5'] != $fileMd5) return false;
		
		// 秒传检测
		$args = array('checkType'=>'matchMd5','checkHashMd5'=>$fileMd5,'checkHashSimple'=>$hashSimple);
		$result = $this->uploadSend($destPath,'',$args);
		if(!is_array($result['data'])) return array('code'=>false,'data'=>$result['error']);
		return $result['data'];
	}
	private function uploadWithChunk($destPath,$localPath){
		$totalSize 	= @filesize($localPath);
		$chunkSize 	= $this->uploadChunkSize;
		$chunkCount	= ceil($totalSize / $chunkSize);$chunkIndex = 0;
		while($chunkIndex < $chunkCount){
			$content  = IO::fileSubstr($localPath,$chunkIndex * $chunkSize,$chunkSize);
			$tempFile = $this->tempFile('',$content);

			$args = array('size'=>$totalSize,'chunkSize'=>$chunkSize,'chunks'=>$chunkCount,'chunk'=>$chunkIndex);
			$result = $this->uploadSend($destPath,$tempFile,$args);$chunkIndex++;
			if(!is_array($result['data'])) return array('code'=>false,'data'=>$result['error']);
			if(!$result['data']['code']) return $result['data'];//有失败情况,则直接结束;
		}
		return $result;
	}
	private function uploadSend($destPath,$tempFile,$args){
		$this->dav->setHeader('X-DAV-UPLOAD','kodbox');
		$this->dav->setHeader('X-DAV-ARGS',base64_encode(json_encode($args)));
		return $this->dav->put($destPath,$tempFile);
	}

	
	public function download($file, $destFile){
		$result = $this->dav->get($file,$destFile);
		return $result ? $destFile : false;
	}
	public function listPath($path,$simple=false){
		$data = $this->dav->propfind($path);
		if(!$data['status'] || !$data['data'] || !$data['data']['response']) return false;

		$list = $data['data']['response'];
		$current = isset($list[0]) ? $list[0]:$list;
		$current = $this->_pathInfoParse($current);
		$result  = array('fileList'=>array(),'folderList'=>array(),'current'=>$current);
		if(!isset($list[0])) return $result;
		
		foreach($list as $index => $val){
			if($index == 0 ) continue;
			$item = $this->_pathInfoParse($val);
			$type = $item['type'] == 'file' ? 'fileList':'folderList';
			$result[$type][] = $item;
			
			$key = trim($item['path'],'/');
			$this->infoCache[$key] = $item;
		}
		// 追加信息处理;extendFileList
		$extendData = _get($data,'data.extendFileList');
		$extendData = $extendData ? $extendData : _get($data,'header.X-extendFileList');
		$extendData = $extendData ? $extendData : _get($data,'header.X-EXTENDFILELIST');
		if($extendData){
			$arr  = json_decode(base64_decode($extendData),true);
			$result = array_merge($result,$arr ? $arr:array());
		}
		if(is_array($result['pageInfo']) && $result['pageInfo']['pageNum'] < 100){
			unset($result['pageInfo']);
		}
		// pr($data,$result);exit;
		return $result;
	}
	

	public function fileOut($path, $download = false, $downFilename = false, $etag=''){
		if($this->fileOutKod($path)) return;
		$this->fileOutServer($path, $download, $downFilename, $etag);
	}
	public function fileOutServer($path, $download = false, $downFilename = false, $etag=''){
		parent::fileOut($path, $download, $downFilename, $etag);
	}
	public function fileOutImage($path,$width=250){
		if($this->fileOutKod($path)) return;
		parent::fileOutImage($path,$width);
	}
	public function fileOutImageServer($path,$width=250){
		parent::fileOutImage($path,$width);
	}
	private function fileOutKod($path){
		if(!$this->davServerKod) return false;
		if($this->isFileOutServer()) return false;
		$data = $this->info($path);
		if(!$data || !$data['fileOutLink']) return false;
		
		$link  = $data['fileOutLink'];
		$param = parse_url_query(this_url());
		$disableKey = array('accessToken','path');
		foreach($param as $key=>$val){
			if(!$val || in_array($key,$disableKey)) continue;
			$link .= '&'.$key.'='.$val;
		}
		$this->fileOutLink($link);
	}
	
	// 缓存处理;
	public static $infoCache = array();
	public static $listCache = array();
	private function _pathInfo($path,$cacheInfo=false){
		if(!$this->pathCheck($path)) return false;
		$key = trim($path,'/');
		if($cacheInfo){self::$infoCache[$key] = $cacheInfo;return;}
		if(isset(self::$infoCache[$key])) return self::$infoCache[$key];
		
		$data = $this->listPath($path);
		$pathInfo = $data ? $data['current']:false;
		if($pathInfo){self::$infoCache[$key] = $pathInfo;}
		return $pathInfo;
	}
	// 文件属性; name/path/type/size/createTime/modifyTime/
	private function _pathInfoParse($item){
		if(!$item || !isset($item['href'])){return array();}
		$path 	= $this->dav->uriToPath($item['href']);
		$info   = array('name'=>get_path_this($path),'path'=>$path,'type'=>'folder');
		$prop   = _get($item,'propstat.prop',array());
		if(is_array($item['propstat']) && is_array($item['propstat'][0])){
			$prop  = $item['propstat'][0]['prop'];
		}
		if(isset($prop['getlastmodified'])){
			$info['modifyTime'] = strtotime($prop['getlastmodified']);
		}
		if(isset($prop['creationdate'])){
			$info['createTime'] = strtotime($prop['creationdate']);
		}
		if(isset($prop['getcontentlength'])){
			$info['size'] = $prop['getcontentlength'];
		}
		$info['type'] = $prop['resourcetype'] == '' ? 'file':'folder';
		$info['name'] = ($info['name'] || $info['name'] == '0') ? $info['name']:'/';
		$info['path'] = $this->getPathOuter($info['path']);
		$mimeType = $prop['getcontenttype'];
		if($mimeType){$info['type'] = ($mimeType == 'httpd/unix-directory') ? 'folder':'file';}
		
		$info['_infoSimple'] = true;
		if($info['type'] == 'file'){
			$info['ext'] = get_path_ext($info['name']);
		}
		if(isset($prop['extendFileInfo'])){
			$arr  = json_decode(base64_decode($prop['extendFileInfo']),true);
			$info = array_merge($info,$arr ? $arr:array());
		}
		return $info;
	}

	public function fileInfo($path,$simple=false,$fileInfo = array()) {
		return $this->_pathInfo($path);
	}
	public function folderInfo($path,$simple=false,$itemInfo=array()) {
		return $this->_pathInfo($path);
	}
	public function size($path){
		$info = $this->_pathInfo($path);
		return $info ? $info['size'] : 0;
	}
	public function info($path){
		return $this->_pathInfo($path);
	}
	public function infoWithChildren($path){
		$this->dav->setHeader('X-DAV-ACTION','infoChildren');
		$info = $this->_pathInfo($path);
		if($this->davServerKod) return $info;
		if(is_array($info) && is_array($info['children'])) return $info;
				
		return parent::infoWithChildren($path);
	}
	
	public function exist($path){
		$info = $this->_pathInfo($path);
		return $info ? true : false;
	}
	public function isFile($path){
		$info = $this->_pathInfo($path);
		return ($info && $info['type'] == 'file') ? true : false;
	}
	public function isFolder($path){
		$info = $this->_pathInfo($path);
		return ($info && $info['type'] == 'folder') ? true : false;
	}
	private function pathCheck($path){
		$PATH_LENGTH_MAX = 4096;//路径最长限制;
		return strlen($path) >= $PATH_LENGTH_MAX ? false:true;
	}
}
webdavClient.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/webdavClient.class.php'
View Content
<?php

/**
 * webdav 客户端
 */
class webdavClient {
	public function __construct($options = array()) {
		$this->options 	= $options;
		$this->header  	= array();
		
		$this->basePath = isset($options['basePath']) ? '/'.trim($options['basePath'],'/').'/' : '/';
		$this->basePath = '/'.ltrim(KodIO::clear($this->basePath),'/');
		$this->baseUrl  = rtrim($options['host'],'/').'/';
		$urlInfo = parse_url($this->baseUrl);
		$this->baseUrlPath = '/'.ltrim(KodIO::clear($urlInfo['path']),'/');
		
		$this->basicAuth= "Basic ".base64_encode($options['user'].":".$options['password']);
		$this->plugin 	= Action('webdavPlugin');
		$GLOBALS['requestFrom'] = 'webdav';
	}
	
	public function check(){
		$data = $this->propfind('/');
		if(is_array($data['data'])){
			$data['status'] = array_key_exists('response',$data['data']);
		}else{
			$data['status'] = false;
		}
		
		// 不同CURLOPT_HTTPAUTH 账号密码加解密处理; CURLAUTH_DIGEST
		$authHeader = _get($data,'header.WWW-Authenticate');$authCheck  = 'Digest';
		// TODO header.WWW-Authenticate可能是个数组,有多种认证方式(Basic、Digest、Negotiate、NTLM等)
		if (is_array($authHeader)) $authHeader = $authHeader[0];
		if(!$data['status'] && substr($authHeader,0,strlen($authCheck)) == $authCheck){
			$this->lastRequest = false;
			$this->options['authType'] = 'Digest';
			$data = $this->propfind('/');
			
			if(is_array($data['data'])){
				$data['status'] = array_key_exists('response',$data['data']);
			}else{
				$data['status'] = false;
			}
			$GLOBALS['in']['config'] = json_encode($this->options,true);
		}

		// trace_log(array($data,$this->options('/'),$this->options));
		$this->patchCheck();		
		return $data;
	}
	// 存储options返回头DAV字段;(标记支持项)
	private function patchCheck(){
		$data = $this->options('/');
		if(!$data['header'] || !$data['header']['DAV']) return;
		$this->options['dav'] = $data['header']['DAV'];
		$GLOBALS['in']['config'] = json_encode($this->options,true);// 修改配置;
	}

	/**
	 * Overwrite已存在处理; 
	 * F=skip跳过,已存在时不执行copy或move; 
	 * T=覆盖;已存在则继续执行; 默认为T
	 */
	public function mkdir($path){
		$this->setHeader('Overwrite','F');
		$data = $this->send('MKCOL',$this->makeUrl($path));
		return $data['status'];
	}
	public function move($from,$to,$lockToken=''){
		$this->setHeader('Destination',$this->makeUrl($to));
		$this->setHeader('Overwrite','T');
		if($lockToken){
			$this->setHeader('IF','<'.$lockToken .'>');
		}
		return $this->send('MOVE',$this->makeUrl($from));
	}
	public function copy($from,$to){
		$this->setHeader('Destination',$this->makeUrl($to));
		$this->setHeader('Overwrite','T');
		return $this->send('COPY',$this->makeUrl($from));
	}
	
	public function delete($path,$lockToken=''){
		if($lockToken){
			$this->setHeader('IF','<'.$lockToken .'>');
		}
		$data = $this->send('DELETE',$this->makeUrl($path));
		return $data['status'];
	}
	public function propfind($path,$depth='1',$header=''){	
		$this->setHeader('Depth',$depth);//遍历深度
		$this->setHeader('Content-type','text/xml; charset=UTF-8');
		if($header){$this->setHeader($header);}
		$body = '<D:propfind xmlns:D="DAV:"><D:allprop /></D:propfind>';
		return $this->send('PROPFIND',$this->makeUrl($path),$body);
	}
	public function options($path){
		return $this->send('OPTIONS',$this->makeUrl($path));
	}

	public function get($path,$localFile,$range=''){
		$getFileInfo = array('path'=>$localFile,'range'=>$range);
		$data = $this->send('GET',$this->makeUrl($path),false,false,$getFileInfo);
		return $data['status'];
	}
	public function put($path,$localFile,$lockToken=''){
		if($lockToken){
			$this->setHeader('IF','<'.$lockToken .'>');
		}
		$this->setHeader('Overwrite','T');
		$putFileInfo = array('name'=>get_path_this($path),'path'=>$localFile);
		return $this->send('PUT',$this->makeUrl($path),false,$putFileInfo);
	}
	
	public function uriToPath($uri){
		if(substr($uri,0,strlen($this->baseUrl)) == $this->baseUrl){
			$path = substr($uri,strlen($this->baseUrl));
		}else{
			$path = substr($uri,strlen($this->baseUrlPath));
		}
		return '/'.ltrim(rawurldecode($path),'/');
	}
	public function makeUrl($path){
		$path = KodIO::clear($path);
		$path = $this->encodeUrl($path);
		return $this->baseUrl.ltrim($path,'/');
	}
	public function encodeUrl($path){
		if(!is_string($path) || !$path) return '';
		$arr = explode('/',$path);
		for ($i=0; $i < count($arr); $i++) { 
			$arr[$i] = rawurlencode($arr[$i]);
		}
		$path = implode('/',$arr);
		return $path;
	}
	public function setHeader($key,$value=false){
		if($value === false){
			$this->header[] = $key;
		}else{
			$this->header[] = $key.': '.$value;
		}
	}
	
	// 缓存连续请求内容一致的内容;PROPFIND
	public function send($method,$requestUrl,$body=false,$putFileInfo=false,$getFileInfo=false,$timeout=3600){
		$lastRequest = $method.';'.$requestUrl;
		if($method == 'PROPFIND' && $this->lastRequest == $lastRequest){
			$this->header = array();
			return $this->lastRequestData;
		}

		$this->lastRequest = $lastRequest;
		$result = $this->_send($method,$requestUrl,$body,$putFileInfo,$getFileInfo,$timeout);
		$this->lastRequestData = $result;
		return $result;
	}
	
	private function _send($method,$requestUrl,$body=false,$putFileInfo=false,$getFileInfo=false,$timeout=3600){
		$this->cookieSet();
		if($body){$body = '<?xml version="1.0" encoding="UTF-8" ?>'.$body;}
		if(!request_url_safe($requestUrl)){$this->header = array();return false;}
		$ch = curl_init($requestUrl);
		if($this->options['authType'] == 'Digest'){
			curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST);
			curl_setopt($ch, CURLOPT_USERPWD, $this->options['user'].":".$this->options['password']);
		}else{
			$this->setHeader('Authorization',$this->basicAuth);
			curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
		}
		
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
		curl_setopt($ch, CURLOPT_HEADER,1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
		curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
		curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
		curl_setopt($ch, CURLOPT_NOPROGRESS, false);
		curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,'curl_progress');curl_progress_start($ch);
		curl_setopt($ch, CURLOPT_USERAGENT,'webdav/kodbox 1.0');
		curl_setopt($ch, CURLOPT_REFERER,get_url_link($requestUrl));
		curl_setopt($ch, CURLOPT_TIMEOUT,$timeout);
		
		// location跳转时;referer 处理(默认会带入最开始的url; alist挂载阿里云盘兼容处理)
		if(defined('CURLOPT_HEADERFUNCTION')){
			curl_setopt($ch, CURLOPT_HEADERFUNCTION,function($ch,$header){
				if(stristr($header,'location:') && stristr($header,'.aliyundrive.')){
					curl_setopt($ch, CURLOPT_REFERER,'');
				}
				return strlen($header);
			});
		}
		
		if($getFileInfo){$this->getFile($ch,$getFileInfo);}
		if($putFileInfo){$this->putFile($ch,$putFileInfo);}
		curl_setopt($ch, CURLOPT_HTTPHEADER, $this->header);
		$response = curl_exec($ch);curl_progress_end($ch,$response);
		
		$headerSize 	= curl_getinfo($ch,CURLINFO_HEADER_SIZE);
		$responseInfo 	= curl_getinfo($ch);
		$responseBody 	= substr($response, $headerSize);
		$responseHeader = substr($response, 0, $headerSize);
		$responseHeader = parse_headers($responseHeader);

		$headerSet = $this->header;$this->header = array();
		$code = $responseInfo['http_code'];
		if($code == 0){
			$errorMessage = curl_error($ch);
			$errorMessage = $errorMessage ? "\n".$errorMessage : 'Network error!';
			return $this->parseResult(0,$errorMessage,$responseInfo,$headerSet);
		}

		curl_close($ch);
		$result = $this->parseResult($code,$responseBody,$responseHeader,$headerSet);
		$this->sendLog($result,$method,$requestUrl,$headerSet);
		$this->cookieSave($result);
		// trace_log([$requestUrl,$method,$result]);
		
		return $result;
	}
	
	private function parseResult($code,$body,$header,$headerSet){
		$status = $code >= 200 && $code <= 299;
		$result = array('code'=>$code,'status'=>$status,'header'=>$header,'data'=>$body);
		if($code == 0){$result['error'] = $body;}
		if(!$body) return $result;

		$error = $status ? '':$header['0'];
		$contentType = is_array($header['content-type']) ? $header['content-type'][0]:$header['content-type'];
		if(!$contentType){ // 301跳转情况;
			$contentType = is_array($header['Content-Type']) ? $header['Content-Type'][0]:$header['Content-Type'];
		}
		if(strstr($contentType,'/json')){
			$result['data']  = @json_decode($body,true);
			if( !$status && is_array($result['data']) && 
				array_key_exists('code',$result['data']) &&
				array_key_exists('data',$result['data']) &&
				$result['data']['code'] != true ){
				$error = _get($result['data'],'data','');
			}
		}
		if(!is_array($result['data'])){
			$result['data'] = webdavClient::xmlParse($body);
			if( !$status && is_array($result['data']) && 
				array_key_exists('exception',$result['data']) ){
				$exception = _get($result['data'],'exception','');
				$message   = _get($result['data'],'message','');
				$error = $exception ? $exception.';'.$message : $message;
			}
		}
		if(is_array($result['data'])){$result['_data'] = $body;}
		if($error){$result['error'] = $error;}
		return $result;
	}
	// 请求日志; 
	private function sendLog($result,$method,$requestUrl,$headerSet){
		// $this->plugin->clientLog(array($method,$requestUrl,$headerSet,$result));
		$message = $result['status'] ? $result['header']['0'] : $result['error'];
		$this->plugin->clientLog($method.':'.$requestUrl.';'.$message);
		if(!$result['status'] && $method != 'PROPFIND'){
			IO::errorTips('[webdav error] '.$result['error']);
		}
	}
	
	private function getFile($curl,$fileInfo){
		if(isset($fileInfo['range']) && $fileInfo['range']){
			$this->setHeader('Range',$fileInfo['range']);
		}
		$fp = fopen($fileInfo['path'],'w+');
		if(!$fp){return;}
		curl_setopt($curl, CURLOPT_HTTPGET,1);
		curl_setopt($curl, CURLOPT_HEADER,0);//不输出头
		curl_setopt($curl, CURLOPT_FILE, $fp);
		curl_setopt($curl, CURLOPT_TIMEOUT,3600*10);
	}
	private function putFile($curl,$fileInfo){
		if(!$fileInfo['path']){
			curl_setopt($curl, CURLOPT_PUT,true);
			return;
		}
		$path = $fileInfo['path'];
        /*
        $postData = array();$key = 'UPLOAD_FILE'; // post方式上传;
        $filename = $fileInfo['name'];
        $mime = get_file_mime(get_path_ext($filename));
        if (class_exists('\CURLFile')){
        	$postData[$key] = new CURLFile(realpath($path),$mime,$filename);
        }else{
        	$postData[$key] = "@".realpath($path).";type=".$mime.";filename=".$filename;
        }
        // post方式上传; 默认会加上header: multipart/form-data; boundary=-----xxxx--
        //$this->setHeader('Content-Type','application/octet-stream');//"application/x-www-form-urlencoded"
        //$this->setHeader('Content-Length',@filesize($path));
        curl_setopt($curl, CURLOPT_POSTFIELDS,$postData);
        */
		
		curl_setopt($curl, CURLOPT_PUT,true);
		curl_setopt($curl, CURLOPT_INFILE,@fopen($path,'r'));
		curl_setopt($curl, CURLOPT_INFILESIZE,@filesize($path));
		curl_setopt($curl, CURLOPT_TIMEOUT,3600*10);
		if(class_exists('\CURLFile')){
			curl_setopt($curl, CURLOPT_SAFE_UPLOAD, true);
		}else if(defined('CURLOPT_SAFE_UPLOAD')) {
			curl_setopt($curl, CURLOPT_SAFE_UPLOAD, false);
		}
	}
	
	// 请求前,设置上次保存的cookie;
	private function cookieSet(){
		$key = 'webdav_cookie_'.md5($this->basicAuth);
		$cookie = Cache::get($key);
		if(!$cookie || !is_array($cookie)) return;

		$cookieItem = array();
		foreach($cookie as $key => $val){
			$cookieItem[] = $key.'='.rawurlencode($val);
		}
		$setCookie = implode('; ',$cookieItem);
		if(!$cookieItem || !$setCookie) return;
		$this->setHeader('Cookie',$setCookie);
	}
	
	// 请求完成保存cookie;
	private function cookieSave($data){
		if(!is_array($data['header'])) return;
		if(!is_array($data['header']['set-cookie'])) return;

		$cookie = $data['header']['set-cookie'];
		$data   = array();
		foreach ($cookie as $item) {
			$value = substr($item,0,strpos($item,';'));
			if(!$value || !strpos($value,'=')) continue;
			$keyValue = explode('=',trim($value));
			if(count($keyValue) != 2) continue;
			if(!$keyValue[0] || !$keyValue[1]) continue;
			if($keyValue[1] == 'deleted') continue;
			$data[$keyValue[0]] = rawurldecode($keyValue[1]);
		}
		if(count($data) == 0) return;
		$key = 'webdav_cookie_'.md5($this->basicAuth);
		Cache::set($key,$data);
	}
	
	
	public static function xmlParse($xml){
		$doc = new DOMDocument();
		$doc->loadXML($xml);
		$root = $doc->documentElement;
		$result = self::domNodeToArray($root);
		$result = self::xmlParseKey($result);
		return $result;
	}
	public static function domNodeToArray($node){
		$output = array();
		switch ($node->nodeType) {
			case 4: // XML_CDATA_SECTION_NODE
			case 3: // XML_TEXT_NODE
				$output = trim($node->textContent);
				break;
			case 1: // XML_ELEMENT_NODE
				for ($i = 0, $m = $node->childNodes->length; $i < $m; $i++) {
					$child = $node->childNodes->item($i);
					$v = self::domNodeToArray($child);
					if (isset($child->tagName)) {
						$t = $child->tagName;
						if (!isset($output[$t])) {
							$output[$t] = array();
						}
						if (is_array($v) && empty($v)) {$v = '';}
						$output[$t][] = $v;
					} elseif ($v || $v === '0') {
						$output = is_array($v) ? json_encode($v) : $v;
					}
				}
				if ($node->attributes->length && !is_array($output)) { // has attributes but isn't an array
					$output = array('@content' => $output); // change output into an array.
				}
				if (is_array($output)) {
					if ($node->attributes->length) {
						$a = array();
						foreach ($node->attributes as $attrName => $attrNode) {
							$a[$attrName] = (string) $attrNode->value;
						}
						$output['@attributes'] = $a;
					}
					foreach ($output as $t => $v) {
						if ($t !== '@attributes' && is_array($v) && count($v) === 1) {
							$output[$t] = $v[0];
						}
					}
				}
				break;
		}
		return $output;
	}
	private static function xmlParseKey($arr){
		$result = array();
		foreach ($arr as $key => $value) {
			if(is_string($key) && strstr($key,':')){
				$keyArr = explode(':',$key);
				$key = $keyArr[count($keyArr) - 1];
			}
			if(is_array($value)){$value = self::xmlParseKey($value);}
			$result[$key] = $value;
		}
		return $result;
	}
	
	

	public static function xmlParse2($xml){
		$parse = simplexml_load_string($xml);
		if($parse === false) return array();
		$namespace = $parse->getNamespaces(true);
		if(!$namespace){$namespace = array();}
		$namespace[''] = '';
		$result = array();
		foreach($namespace as $key=>$v){
			// $children = $parse->children($key,true); // 多个命名空间会丢失部分的情况
			$children = $parse->children($v);
			self::objectToArr($children,$result);
		}
		//$parse->children($v) 第一项多一层情况处理;
		if(isset($result['response'][0][0])){ 
			unset($result['response'][0]);
			$result['response'] = array_values($result['response']);
		}
		return $result;
	}
	private static function objectToArr($obj,&$arr){
		$arrCount = 0; // 相同key,重复出现则归并为数组;
		foreach($obj as $key => $val){
			if(count($val) == 0){$arr[$key] = (string)$val;continue;}
			if(!isset($arr[$key])){$arr[$key] = array();}
			
			if(!$arr[$key]){self::objectToArr($val,$arr[$key]);continue;}
			if(!$arrCount){$arr[$key] = array($arr[$key]);$arrCount++;}
			if(!isset($arr[$key][$arrCount])){$arr[$key][$arrCount] = array();}
			self::objectToArr($val,$arr[$key][$arrCount]);$arrCount++;
		}
	}
}
webdavServer.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/webdavServer.class.php'
View Content
<?php

/**
 * webdav 服务端
 * 
 * 简易文档: https://tech.yandex.com/disk/doc/dg/reference/put-docpage/
 * 文档:     http://www.webdav.org/specs/rfc2518.html
 */
class webdavServer {
	public $lastError = '';
	public $method = '';
	public function __construct($root,$DAV_PRE_PATH) {
		$this->root = $root;
		$this->initPath($DAV_PRE_PATH);
		$this->start();
	}
	public function initPath($DAV_PRE_PATH){
		$GLOBALS['requestFrom'] = 'webdav';
		$this->method = 'http'.HttpHeader::method();
		$uri  = rtrim($_SERVER['REQUEST_URI'],'/').'/'; //带有后缀的从domain之后部分;
		if(!$this->pathCheck($uri)){//路径长度限制
			$this->lastError = LNG('common.lengthLimit');
			$this->response(array("code"=>404));exit;
		}
		$this->urlBase  = substr($uri,0,strpos($uri,$DAV_PRE_PATH)+1); //$find之前;
		$this->urlBase  = rtrim($this->urlBase,'/').$DAV_PRE_PATH;
		$this->uri   	= $this->pathGet();
		$this->path 	= $this->parsePath($this->uri);
		if(strpos($uri,$DAV_PRE_PATH) === false){
			$this->lastError = LNG('common.noPermission');
			$this->response(array("code"=>404));exit;
		}		
	}
	public function checkUser(){
		$user = HttpAuth::get();
		if($user['user'] == 'admin' && $user['pass'] == '123'){
			return true;
		}
		HttpAuth::error();
	}
	private function pathCheck($path){
		$PATH_LENGTH_MAX = 4096;//路径最长限制;
		return strlen($path) >= $PATH_LENGTH_MAX ? false:true;
	}
	
	public function start(){
		$method = 'http'.HttpHeader::method();
		if(!method_exists($this,$method)){
			return HttpAuth::error();
		}
		if($method == 'httpOPTIONS'){
			return self::response($this->httpOPTIONS());
		}
		$this->checkUser();
		$notCheck = array('httpMKCOL','httpPUT');
		if( !in_array($method,$notCheck) && 
			!$this->pathExists($this->path,true) ){
			$result = array('code' => 404);
		}else{
			$result = $this->$method();
		}
		if(!$result) return;//文件下载;
		self::response($result);
	}
	public function pathGet($dest=false){
		$path = $dest ? $_SERVER['HTTP_DESTINATION'] : $_SERVER['REQUEST_URI'];
		$path = KodIO::clear(rawurldecode($path));
    	if(!strstr($path,KodIO::clear($this->urlBase))) return false;
		return substr($path,strpos($path,$this->urlBase)+ strlen($this->urlBase) );
	}
	
	public function pathExists($path,$allowInRecycle=false){
		return file_exists($path);
	}
	public function pathMkdir($path){
		return mkdir($path,DEFAULT_PERRMISSIONS,true);
	}
	public function pathInfo($path){
		return path_info($path);
	}
	public function pathList($path){
		return path_list($path);
	}
	// range支持;
	public function pathOut($path){
		echo file_get_contents($path);
	}
	public function pathPut($path,$tempFile=''){
		if(!$tempFile){
			return file_put_contents($path,'');
		}
		return move_path($tempFile,$path);
	}
	public function pathRemove($path){
		if(is_file($path)){
	        return @unlink($this->path);
	    }else{
	        return del_dir($this->path);
	    }
	}
	public function pathMove($path,$dest){
		return move_path($path,$dest);
	}
	public function pathCopy($path,$dest){
		return copy_dir($path,$dest);
	}
    public function parsePath($path){
    	return $path;
	}
	public function parseItem($item,$isInfo){
		$pathAdd = $this->pathGet().'/'.$item['name'];
		$pathAdd = '/'.str_replace('%2F','/',rawurlencode($pathAdd));
		if($isInfo){
			$pathAdd = '/'.str_replace('%2F','/',rawurlencode($this->pathGet()));
		}
		if(!trim($item['modifyTime'])){$item['modifyTime'] = time();}
		if(!trim($item['createTime'])){$item['createTime'] = time();}
		
		$result  = array(
			'href' 			=> KodIO::clear($this->urlBase.$pathAdd),
			'modifyTime' 	=> @gmdate("D, d M Y H:i:s",$item['modifyTime']).' GMT',
			'createTime' 	=> @gmdate("Y-m-d\\TH:i:s\\Z",$item['createTime']),
			'size' 			=> $item['size'] ? $item['size']:0,
		);
		return $result;
	}
	
	public function parseItemXml($itemFile,$isInfo){
		$item = $this->parseItem($itemFile,$isInfo);
		if ($itemFile['type'] == 'folder') {//getetag
			$xmlAdd = "<D:resourcetype><D:collection/></D:resourcetype>";
			$xmlAdd.= "<D:getcontenttype>httpd/unix-directory</D:getcontenttype>";
			$item['href'] = rtrim($item['href'],'/').'/';
			if(isset($_SERVER['HTTP_DATE']) && isset($_SERVER['HTTP_DEPTH'])){
				// goodsync同步处理;HTTP_DATE/HTTP_DEPTH; 首次列表展开失败问题处理;
				$result['modifyTime'] = $_SERVER['HTTP_DATE'];
			}
		}else{
			$ext    = $itemFile['ext'] ? $itemFile['ext']:get_path_ext($itemFile['name']);
			$mime   = get_file_mime($ext);
			$xmlAdd = '<D:resourcetype/>';
			$xmlAdd.= "<D:getcontenttype>{$mime}</D:getcontenttype>";
		}
		
		$infoMore = array();
		$picker = array(
			'hasFile','hasFolder','fileInfo','fileInfoMore','oexeContent','parentID','isTruePath',
			'isReadable','isWriteable','sourceRoot','icon','iconClassName','children',
			'listAllChildren','fileThumb','fileThumbCover','fileShowView',
		);
		foreach ($picker as $key){
			if(array_key_exists($key,$itemFile)){$infoMore[$key] = $itemFile[$key];}
		}
		if($itemFile['type'] == 'file'){
			$param = array('path'=>$itemFile['path']);
			$infoMore['fileOutLink'] = Action('user.index')->apiSignMake('explorer/index/fileOut',$param);
		}
		if($infoMore){
			$xmlAdd.= "<D:extendFileInfo>".base64_encode(json_encode($infoMore))."</D:extendFileInfo>";
		}
		
		return "
		<D:response>
			<D:href>{$item['href']}</D:href>
			<D:propstat>
				<D:prop>
					<D:getlastmodified>{$item['modifyTime']}</D:getlastmodified>
					<D:creationdate>{$item['createTime']}</D:creationdate>
					<D:getcontentlength>{$item['size']}</D:getcontentlength>
					{$xmlAdd}
				</D:prop>
				<D:status>HTTP/1.1 200 OK</D:status>
			</D:propstat>
		</D:response>";
	}
	public function pathListMerge($listData){
		if(!$listData) return $listData;
		$keyList = array('fileList','folderList','groupList');
		$list    = array();
		foreach ($listData as $key=>$typeList){
			if(!in_array($key,$keyList) || !is_array($typeList)) continue;
			$list = array_merge($list,$typeList);
		}
		//去除名称中的/分隔; 兼容存储挂载
		foreach ($list as &$item) {
			$item['name'] = str_replace('/','@',$item['name']);
		}
		return $list;
	}

	public function httpPROPFIND(){
		$listFile = $this->pathList($this->path);
		$list = $this->pathListMerge($listFile);
		$pathInfo = $listFile['current'];
		if(!is_array($list) || (isset($pathInfo['exists']) && $pathInfo['exists'] === false) ){//不存在;
			return array("code" => 404,"body" => $this->errorBody('ObjectNotFound','not exist'));
		}
		if(isset($listFile['folderList'])){
			$pathInfo['type'] = 'folder';
		}		
		//只显示属性;
		$isInfo = $pathInfo['type'] == 'file' || HttpHeader::get('Depth') == '0';
		// kodbox webdav挂载获取文件夹属性;
		if( $pathInfo['type'] == 'folder' && 
			HttpHeader::get('X_DAV_ACTION') == 'infoChildren'){
			$pathInfo = IO::infoWithChildren($pathInfo['path']);
		}
		
		// kodbox 挂载kod存储; listAll请求优化;
		if( $pathInfo['type'] == 'folder' && 
			isset($_SERVER['HTTP_X_DAV_ACTION']) && $_SERVER['HTTP_X_DAV_ACTION'] == 'kodListAll'){
			$pathInfo['listAllChildren'] = IO::listAllSimple($this->path,true);
		}
		
		if($isInfo){
			$list = array($pathInfo);
		}else{
			$pathInfo['name'] = '';
			$list = array_merge(array($pathInfo),$list);
		}
		$out = '';
		foreach ($list as $itemFile){
			$out .= $this->parseItemXml($itemFile,$isInfo);
		}
		// write_log([$this->pathGet(),$this->path,$pathInfo],'webdav');
		$code = 207;//207 => 200;
		if(strstr($this->uri,'.xbel')){$code = 200;} // 兼容floccus

		// 扩展kod内容;
		$infoMore = array();
		$picker   = array('groupShow','pageInfo','targetSpace','listTypePhoto','listTypeSet','pageSizeArray');
		foreach ($picker as $key){
			if(array_key_exists($key,$listFile)){$infoMore[$key] = $listFile[$key];}
		}
		$infoMoreData = $infoMore ? base64_encode(json_encode($infoMore)):'';
		return array(
			"code" => $code,
			"headers" => array('X-extendFileList: '.$infoMoreData),
			"body" => "<D:multistatus xmlns:D=\"DAV:\">\n{$out}\n</D:multistatus>"
		);
	}
		
	public function httpHEAD() {
		$info = $this->pathInfo($this->path);
        if(!$info || $info['type'] == 'folder'){
        	return array(
                'code' => 200,
                'headers' => array(
                    'Content-Type: text/html; charset=utf8',
					'Last-Modified: '.gmdate("D, d M Y H:i:s ",time())."GMT",
					'ETag: "'.md5(time()).'"', 
                )
            );
        }

		return array(
			'code'=> 200,
			'headers' => array(
				'Vary: Range',
                'Accept-Ranges: bytes',
				'Content-length: '.$info['size'],
                'Content-type: '.get_file_mime($info['ext']),
                'Last-Modified: '.gmdate("D, d M Y H:i:s ", $info['mtime'])."GMT",
                'Cache-Control: max-age=86400,must-revalidate',
                'ETag: "'.md5($info['mtime'].$info['size']).'"', 
            )
		);
	}
	public function httpOPTIONS() {
		return array(
            'code'	  => 200,
            'headers' => array(
                'DAV: 1, 2, 3, extended-kodbox',
                'MS-Author-Via: DAV',
                'Allow: OPTIONS, PROPFIND, PROPPATCH, MKCOL, GET, PUT, DELETE, COPY, MOVE, LOCK, UNLOCK, HEAD',
				'Accept-Ranges: bytes',
                'Content-Length: 0',
            )
        );
	}
	public function httpPROPPATCH(){
		$out = '
		<D:response>
			<D:href>'.$_SERVER['REQUEST_URI'].'</D:href>
			<D:propstat>
				<D:prop>
					<m:Win32LastAccessTime xmlns:m="urn:schemas-microsoft-com:" />
					<m:Win32CreationTime xmlns:m="urn:schemas-microsoft-com:" />
					<m:Win32LastModifiedTime xmlns:m="urn:schemas-microsoft-com:" />
					<m:Win32FileAttributes xmlns:m="urn:schemas-microsoft-com:" />
				</D:prop>
				<D:status>HTTP/1.1 200 OK</D:status>
			</D:propstat>
		</D:response>';
		return array(
			"code" => 207,
			"body" => "<D:multistatus xmlns:D=\"DAV:\">\n{$out}\n</D:multistatus>"
		);
	}
	
	public function httpGET() {
		$this->pathOut($this->path);
	}
	// 分片支持; X-Expected-Entity-Length
	public function httpPUT() {
		$tempFile = $this->uploadFileLocal();
		if($tempFile){
			$code = 204; 
		}else{
		    $tempFile = '';
			$code = 201;
		}
		$result = $this->pathPut($this->path,$tempFile);
		@unlink($tempFile);
		if($result == false){$code = 404;}
		return array("code"=>$code);
	}
	
	public function uploadFileTemp(){
		return TEMP_FILES;
	}
	// 兼容move_uploaded_file 和 流的方式上传
	public function uploadFileLocal(){
		$dest 	= TEMP_FILES.'upload_dav_'.rand_string(32);mk_dir(TEMP_FILES);
		$outFp 	= @fopen($dest, "wb");
		$in  	= @fopen("php://input","rb");
		if(!$in || !$outFp){@unlink($dest);return false;} 	
		while(!feof($in)) {
			fwrite($outFp, fread($in, 1024*200));
		}
		fclose($in);fclose($outFp);
		if(@filesize($dest) > 0) return $dest;
		@unlink($dest);return false;
	}

	
	/**
	 * 新建文件夹
	 */
	public function httpMKCOL() {
		if ($this->pathExists($this->path)) {
            return array('code' => 201); // alist 等程序可能额外调用;
            // return array('code' => 409);
        }
        $res  = $this->pathMkdir($this->path);
        return array('code' => $res?201:403);
	}

	public function httpMOVE() {
		$dest    = $this->parsePath($this->pathGet(true));
		if (isset($_SERVER["HTTP_OVERWRITE"])) {
            $options["overwrite"] = $_SERVER["HTTP_OVERWRITE"] == "T";
        }
		$res 	= $this->pathMove($this->path,$dest);
		return array('code' => $res?201:404);
	}
	public function httpCOPY() {
		$dest   = $this->parsePath($this->pathGet(true));
		$res 	= $this->pathCopy($this->path,$dest);
		return array('code' => $res?201:404);
	}
	public function httpDELETE() {
		$res = $this->pathRemove($this->path);
        return array('code' => $res?200:503);
	}
	public function httpLOCK() {
		$this->fileLock($this->path);
		$token      = $this->makeLockToken();
		$depth      = _get($_SERVER,'HTTP_DEPTH','infinity');
		$timeout    = _get($_SERVER,'HTTP_TIMEOUT','Infinite');
		$lockInfo   = '<d:prop xmlns:d="DAV:">
			<d:lockdiscovery>
				<d:activelock>
				    <d:locktype><d:write/></d:locktype>
				    <d:lockscope><d:exclusive/></d:lockscope>
					<d:depth>'.$depth.'</d:depth>
					<d:lockroot><d:href>'.$_SERVER['REQUEST_URI'].'</d:href></d:lockroot>
					<d:owner>'.trim($this->xmlGet('lockinfo/owner/href')).'</d:owner>
					<d:timeout>'.$timeout.'</d:timeout>
					<d:locktoken><d:href>opaquelocktoken:'.$token.'</d:href></d:locktoken>
				</d:activelock>
			</d:lockdiscovery>
		</d:prop>';
		
		return array(
            'code' => 201,
            'headers' => array(
				'Lock-Token: <opaquelocktoken:'.$token.'>',
				'Connection: keep-alive',
			),
			'body' => $lockInfo,
        );
	}
	
	private function makeLockToken(){
	    return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff)
        );
	}
	
	public function httpUNLOCK() {
		$this->fileUnLock($this->path);
		return array('code' => 204);
	}
	public function fileLock($path){}
	public function fileUnLock($path){}

	public function xmlGet($key){
		static $xml = false;
		if(!$xml){
			// 禁用xml实体,避免xxe攻击; php8以上已废弃
			if(PHP_VERSION_ID < 80000) {libxml_disable_entity_loader(true);}
			$body = file_get_contents('php://input');
			if(!$body) return '';
			$xml = new DOMDocument();
			$xml->loadXML($body);
		}
		
        $tag = array_shift(explode('/', $key));
		$objData = $xml->getElementsByTagNameNS('DAV:', $tag);
		if($objData) return $objData[0]->nodeValue;
		return '';
	}
	
	public function getLastError(){return $this->lastError;}
	public function errorBody($title='',$desc=''){
		if(!$desc){$desc = $this->getLastError();}
		return 
		'<D:error xmlns:D="DAV:" xmlns:S="http://kodcloud.com">
			<S:exception>'.htmlentities($title).'</S:exception>
			<S:message>'.htmlentities($desc).'</S:message>
		</D:error>';
	}
	
	/**
    * 输出应答信息
    * @param array $data [header:array,code:int,body:string]
    */
    public function response($data) {
        $headers   = is_array($data['headers']) ? $data['headers'] :array();
		$headers[] = HttpHeader::code($data['code']);
		$headers[] = 'Pragma: no-cache';
		$headers[] = 'Cache-Control: no-cache';
		$headers[] = 'X-DAV-BY: kodbox';
        foreach ($headers as $header) {
            header($header);
        }
		
		if($data['code'] >= 400 && !$data['body']){
			$data['body'] = $this->errorBody();
		}
        if(is_string($data['body'])) {
        	header('Content-Type: application/xml; charset=utf-8');
        	echo '<?xml version="1.0" encoding="utf-8"?>'."\n".$data['body'];
        }
        
        if($this->method != 'httpPROPFIND'){
            // write_log(array($_SERVER['REQUEST_URI'],$headers,$data),'webdav');
        }
	}
}
webdavServerKod.class.php
wget 'https://sme10.lists2.roe3.org/kodbox/plugins/webdav/php/webdavServerKod.class.php'
View Content
<?php

/**
 * webdav 文件管理处理;
 * 
 * kod自定义扩展支持:
 * 1. 文件属性数组追加 extendFileInfo; 数据:base64_encode(json_encode({}));//hasFile,fileInfoMore,children,fileOutLink...
 * 2. 文件列表数组追加 extendFileList; 数据:base64_encode(json_encode({}));//groupShow,pageInfo,targetSpace
 * 
 * 兼容sabre的文件patch追加协议: https://sabre.io/dav/http-patch/  
 */
class webdavServerKod extends webdavServer {
	public function __construct($DAV_PRE) {
		$this->davPre = $DAV_PRE;
		$this->plugin = Action('webdavPlugin');
		Hook::bind('show_json',array($this,'showErrorCheck'));
	}

	public function run(){
		$method = 'http'.HttpHeader::method();
		if(!method_exists($this,$method)){
			return HttpAuth::error();
		}
		if($method == 'httpOPTIONS'){
			return self::response($this->httpOPTIONS());
		}
		
		$this->checkUser();
		$this->initPath($this->davPre);
		$result = $this->$method();
		if(!$result) return;//文件下载;
		$this->response($result);
    }
	
	// head时一直返回200; 登录失败或无权限则直接返回; 登录检测等成功同理多了文件信息;
	// 兼容 win10下office打开异常情况;
	private function checkErrorHead(){		
		if(HttpHeader::method() != 'HEAD') return;
		self::response(array(
			'code' => 200,
			'headers' => array(
				'Content-Type: text/html; charset=utf8',
			)
		));exit;
	}
	
	// 错误处理;(空间不足,无权限等)
	public function showErrorCheck($json){
		if(!is_array($json)) return $json;
		if($json['code'] == true || $json['code'] == 1) return $json;
		
		$this->checkErrorHead();
		$this->lastError = is_string($json['data']) ?$json['data']:'';
		$this->response(array('code'=>404));exit;
	}
	public function getLastError(){
		$error = $this->lastError;
		if(!$error){$error = Action('explorer.auth')->getLastError();}
		if(!$error){$error = IO::getLastError();}
		return $error;
	}

	/**
	 * 用户登录校验;权限判断;
	 * 性能优化: 通过cookie处理为已登录; (避免ad域用户或用户集成每次进行登录验证;)
	 * 
	 */
	public function checkUser(){
		$userInfo = Session::get("kodUser");
	    if(!$userInfo || !is_array($userInfo)){
    	    $user = HttpAuth::get();
			// 兼容webdav挂载不支持中文用户名; 中文名用户名编解码处理;
			if(substr($user['user'],0,2) == '$$'){
				$user['user'] = rawurldecode(substr($user['user'],2));
			}
			// Windows下wps打开文件需要再次输入用户名密码情况; 用户名带入了电脑名称兼容(eg:'DESKTOP-E12RTST\admin:123')
			$startPose = strrpos($user['user'],"\\"); 
			if($startPose){$user['user'] = substr($user['user'],$startPose + 1);}
			
    		$find = ActionCall('user.index.userInfo', $user['user'],$user['pass']);
    		if ( !is_array($find) || !isset($find['userID']) ){
    			// $this->plugin->log(array($user,$find,$_SERVER['HTTP_AUTHORIZATION'],$GLOBALS['_SERVER']));
				$this->checkErrorHead();
    			return HttpAuth::error();
    		}
    		ActionCall('user.index.loginSuccess',$find);
			
			// 登录日志;
			$needLog = time() - intval($find['lastLogin']) >= 60; // 超过1分钟才记录
			if($needLog && HttpHeader::method() == 'OPTIONS'){
				Model('User')->userEdit($find['userID'],array("lastLogin"=>time()));
				ActionCall('admin.log.loginLog');
			}
	    }
		if(!$this->plugin->authCheck()){
			$this->checkErrorHead();
			$this->lastError = LNG('common.noPermission');
			$this->response(array('code'=>404));exit;
		}
	}
	public function parsePath($path){
		$options   = $this->plugin->getConfig();
		$rootBlock = '{block:files}/';
		$rootPath  = $options['pathAllow'] == 'self' ? MY_HOME:$rootBlock;
		if(!$path || $path == '/') return $rootPath;

		$pathArr = explode('/',KodIO::clear(trim($path,'/')));
		if($rootPath == $rootBlock){
			$rootList = $this->pathBlockRoot();
			$this->rootPathAutoLang($rootList,$pathArr);
		}else{
			$rootList = Action('explorer.list')->path($rootPath);
		}
		return $this->pathInfoDeep($rootList,$pathArr);
	}

	//获取{block:files}/下面的子文件夹;(从pathList直接获取较耗时(70ms),性能优化)  
	private function pathBlockRoot(){
		$list = array(
			array("path"=> KodIO::KOD_USER_FAV,'name'=>LNG('explorer.toolbar.fav')),
			array("path"=> KodIO::make(Session::get('kodUser.sourceInfo.sourceID')),'name'=>LNG('explorer.toolbar.rootPath')),
			array("path"=> KodIO::KOD_GROUP_ROOT_SELF,'name'=>LNG('explorer.toolbar.myGroup')),
			array("path"=> KodIO::KOD_USER_SHARE_TO_ME,'name'=> LNG('explorer.toolbar.shareToMe')),
		);
		// 企业网盘;
		$groupArray = Action('filter.userGroup')->userGroupRoot();
	    if (is_array($groupArray) && $groupArray[0]){
			$groupInfo = Model('Group')->getInfo($groupArray[0]);
			$list[] = array("path"=> KodIO::make($groupInfo['sourceInfo']['sourceID']),'name'=>$groupInfo['name']);
		}
		return array('folderList'=>$list,'fileList'=>array());
	}
	
	// 如果挂载全部路径; 第一层路径自适应多语言处理;
	private function rootPathAutoLang($rootList,&$pathArr){
		$rootPathName = array_to_keyvalue($rootList['folderList'],'','name');
		if(in_array($pathArr[0],$rootPathName)) return;

		$langKeys = $this->loadLangKeys();
		foreach($langKeys as $key=>$langValues){
			if(in_array($pathArr[0],$langValues)){
				$pathArr[0] = LNG($key);break;
			}
		}
	}
	
	// 获取key对应多个语言的值; [收藏夹,个人空间,我所在的部门,与我协作]; //企业网盘为部门名
	private function loadLangKeys(){
		$langKeys = Cache::get('webdav_lang_path_root');
		if(is_array($langKeys)) return $langKeys;
		
		$langKeys = array(
			'explorer.toolbar.fav'			=> array(),	// 收藏夹
			'explorer.toolbar.rootPath'		=> array(),	// 个人空间
			'explorer.toolbar.myGroup'		=> array(),	// 我所在的部门
			'explorer.toolbar.shareToMe'	=> array(),	// 与我协作
		);
		$languageList = $GLOBALS['config']['settingAll']['language'];
		foreach($languageList as $lang=>$info){
			$langFile = LANGUAGE_PATH.$lang.'/index.php';
			$langArr  = include($langFile);
			if(!is_array($langArr)) continue;
			foreach ($langKeys as $key=>$val){
				if(!$langArr[$key]) continue;
				$langKeys[$key][] = $langArr[$key];
			}
		}
		$langKeys['explorer.toolbar.rootPath'][] = 'my'; // 增加;
		Cache::set('webdav_lang_path_root',$langKeys,3600);
		return $langKeys;
	}
	
	/**
	 * 向下回溯路径;
	 */
	private function pathInfoDeep($parent,$pathArr){
		$list = $this->pathListMerge($parent);
		$itemArr = array_to_keyvalue($list,'name');
		$item = $itemArr[$pathArr[0]];
		if(!$item) return false;
		if(count($pathArr) == 1) return $item['path'];
		
		$pathAppend = implode('/',array_slice($pathArr,1));
		$newPath = KodIO::clear($item['path'].'/'.$pathAppend);
		$info = IO::infoFull($newPath);

		// 已存在回收站中处理;
		if($info && $info['isDelete'] == '1'){
			$resetName = $info['name'] .date('(H-i-s)');
			if($info['type'] == 'file'){
				$ext = '.'.get_path_ext($info['name']);
				$theName   = substr($info['name'],0,strlen($info['name']) - strlen($ext));
				$resetName = $theName.date('(H-i-s)').$ext;
			}
			IO::rename($info['path'],$resetName);
			$info = IO::infoFull($newPath);
		}
		// pr($newPath,$item,$pathArr,$info,count($parent['folderList']));
		if($info) return $info['path'];

		$parent = Action('explorer.list')->path($item['path']);
		$result  = $this->pathInfoDeep($parent,array_slice($pathArr,1));
		if(!$result){
			$result = $newPath;
			//虚拟目录追; 没找到字内容;则认为不存在;
			if(Action('explorer.auth')->pathOnlyShow($item['path']) ){
				$result = false;
			}
		}
		return $result;
	}
	
	public function pathInfo($path){
		return IO::info($path);
	}
	
	public function can($path,$action){
		$result = Action('explorer.auth')->fileCan($path,$action);
		// 编辑;则检测当前存储空间使用情况;
		if($result && $action == 'edit'){
			$result = Action('explorer.auth')->spaceAllow($path);
		}
		return $result;
	}
	public function pathExists($path,$allowInRecycle = false){
		$info = IO::infoFull($path);
		if(!$info) return false;
		if(!$allowInRecycle && $info['isDelete'] == '1') return false;
		return true;
	}
	
	/**
	 * 文档属性及列表;
	 * 不存在:404;存在207;  文件--该文件属性item; 文件夹--该文件属性item + 多个子内容属性
	 */
	public function pathList($path){
		if(!$path) return false;
		$info  = IO::infoFull($path);
		if(!$info && !Action('explorer.auth')->pathOnlyShow($path) ){
			return false;
		}
		
		// if($info && $info['isDelete'] == '1') return false;//回收站中; 允许复制下载等操作;
		if(!$this->can($path,'show')) return false;
		if($info && $info['type'] == 'file'){ //单个文件;
			return array('fileList'=>array($info),'current'=>$info);
		}
		
		$pathParse = KodIO::parse($path);
		// 分页大小处理--不分页; 搜索结果除外;
		if($pathParse['type'] != KodIO::KOD_SEARCH){
			$GLOBALS['in']['pageNum'] = -1;
		}
		// write_log([$path,$pathParse,$GLOBALS['in']],'test');		
		return Action('explorer.list')->path($path);
	}
	
	public function pathMkdir($pathBefore){
		$path = $this->pathCreateParent($pathBefore);
		if(!$path || !$this->can($path,'edit')) return false;
		return IO::mkdir($path);
	}
	public function pathOut($path){
		if(!$this->pathExists($path) || !$this->can($path,'view')){
			$this->response(array('code' => 404));exit;
		}
		if(IO::size($path)<=0) return;//空文件处理;
		//部分webdav客户端不支持301跳转;
		if($this->notSupportHeader()){
			IO::fileOutServer($path); 
		}else{
			// $GLOBALS['config']['settings']['ioFileOutServer'] = 1;
			IO::fileOut($path); 
		}
	}
	// GET 下载文件;是否支持301跳转;对象存储下载走直连;
	private function notSupportHeader(){
		$software = array(
			'ReaddleDAV Documents',	// ios Documents 不支持;
			'GstpClient',			// goodsync 同步到对象存储问题
		);
		$ua = $_SERVER['HTTP_USER_AGENT'];
		foreach ($software as $type){
			if(stristr($ua,$type)) return true;
		}
		return false;
	}
	
	// 收藏夹下文件夹处理;(新建,上传)
	private function pathCreateParent($path){
		if($path) return $path;
		$inPath  = $this->pathGet();
		if(IO::pathFather($inPath) == '.recycle') return false;
		$pathFather = rtrim($this->parsePath(IO::pathFather($inPath)),'/');
		return $pathFather.'/'.IO::pathThis($inPath);
	}
	
	public function pathPut($path,$localFile=''){
		$pathBefore = $path;
		$path = $this->pathCreateParent($path);
		if(!$path || !$this->can($path,'edit')) return false;
		$name = IO::pathThis($this->pathGet());
		$info = IO::infoFull($path);
		if($info){	// 文件已存在; 则使用文件父目录追加文件名;
			$uploadPath = rtrim(IO::pathFather($info['path']),'/').'/'.$name; //构建上层目录追加文件名;
		}else{
			// 首次请求创建,文件不存在; 则使用{source:xx}/newfile.txt; 自动创建文件夹: /src/aa/s.txt => / [文件夹不存在时]
			$pathFatherStr = get_path_father($path);
			$pathFather    = IO::mkdir($pathFatherStr); 
			$uploadPath    = rtrim($pathFather,'/').'/'.$name;
			$this->plugin->log("pathPut-mkdir:pathFatherStr=$pathFatherStr;pathFather=$pathFather;uploadPath=$uploadPath");
			//$uploadPath = $path;
		}
		$this->pathPutCheckKod($uploadPath);

		// 传入了文件; wscp等直接一次上传处理的情况;  windows/mac等会调用锁定,解锁,判断是否存在等之后再上传;
		// 文件夹下已存在,或在回收站中处理;
		// 删除临时文件; mac系统生成两次 ._file.txt;
		$size = 0;
		if($localFile){
			$size = filesize($localFile);
			$result = IO::upload($uploadPath,$localFile,true,REPEAT_REPLACE);
			// $result = IO::move($localFile,$uploadPath,REPEAT_REPLACE);
			$this->pathPutRemoveTemp($uploadPath);
		}else{
			if(!$info){ // 不存在,创建;
				$result = IO::mkfile($uploadPath,'',REPEAT_REPLACE);
			}
			$result = true;	
		}
		$this->plugin->log("upload=$uploadPath;path=$path,$pathBefore;res=$result;local=$localFile;size=".$size);
		return $result;
	}
	private function pathPutRemoveTemp($path){
		$pathArr = explode('/',$path);
		$pathArr[count($pathArr) - 1] = '._'.$pathArr[count($pathArr) - 1];
		$tempPath = implode('/',$pathArr);
		
		$tempInfo = IO::infoFull($tempPath);
		if($tempInfo && $tempInfo['type'] == 'file'){
			IO::remove($tempInfo['path'],false);
		}
	}
	
	// kodbox 挂载链接
	private function pathPutCheckKod($uploadFile){
		if($_SERVER['HTTP_X_DAV_UPLOAD'] != 'kodbox') return;
		if(!$_SERVER['HTTP_X_DAV_ARGS']) return;
		
		$args = json_decode(base64_decode($_SERVER['HTTP_X_DAV_ARGS']),true);
		if(!is_array($args)) return false;
		$io   = IO::init('/');
		$info = array(
			'name' => $io->pathThis($uploadFile),
			'path' => $io->pathFather($uploadFile)
		);
		if($args['uploadWeb'] && $args['checkType'] == 'checkHash'){
			// 前端上传文件夹,层级处理; eg: /self/a1/a2/a3.txt ; fullPath: /a1/a2/a3.txt ===> /self/
			$fullPath = $args['fullPath'] ? $args['fullPath']:'';
			$fullArr  = explode('/', trim($fullPath,'/'));
			if(count($fullArr) > 1){
				$uriArr = explode('/', trim($this->pathGet(),'/'));
				$uriArr = array_slice($uriArr,0,count($uriArr) - count($fullArr));
				$info['path'] = $this->parsePath('/'.implode('/',$uriArr).'/');
			}
			
			$argsCheck = array('path'=>$info['path']);//,'size'=>$args['size']
			$link = Action('user.index')->apiSignMake('explorer/upload/fileUpload',$argsCheck,false,false,true);
			$info['addUploadParam'] = $link;
		}
		$GLOBALS['in'] = array_merge($GLOBALS['in'],$args,$info);
		Action('explorer.upload')->fileUpload();exit;
	}
	
	public function pathRemove($path){
		if(!$this->can($path,'remove')) return false;
		$tempInfo = IO::infoFull($path);
		if(!$tempInfo) return true;
		
		$toRecycle = Model('UserOption')->get('recycleOpen');
		if($tempInfo['isDelete'] == '1'){$toRecycle = false;}
		return IO::remove($tempInfo['path'], $toRecycle);
	}
	public function pathMove($path,$dest){
		$pathUrl = $this->pathGet();
		$destURL = $this->pathGet(true);		
		$path 	= $this->parsePath($pathUrl);
		$dest   = $this->parsePath(IO::pathFather($destURL)); //多出一层-来源文件(夹)名
		$this->plugin->log("from=$path;to=$dest;$pathUrl;$destURL");

		// 目录不变,重命名,(编辑文件)
		$io = IO::init('/');
		if($io->pathFather($pathUrl) == $io->pathFather($destURL)){
			if(!$this->can($path,'edit')) return false;
			$destFile = rtrim($dest,'/').'/'.$io->pathThis($destURL);
			$this->plugin->log("edit=$destFile;exists=".intval($this->pathExists($destFile)));

			/**
			 * office 编辑保存最后落地时处理(导致历史记录丢失)
			 * window下文件保存处理(office文件保存时 file=>file.tmp 不做该操作,避免历史版本丢失)
			 * 
			 * 0. 上传~tmp1601041332501525796.TMP //锁定,上传,解锁;
			 * 1. 移动 test.docx => test~388C66.tmp 				// 改造,识别到之后不进行移动重命名;
			 * 2. 移动 ~tmp1601041332501525796.TMP => test.docx; 	// 改造;目标文件已存在则更新文件;删除原文件;
			 * 3. 删除 test~388C66.tmp  
			 * 
			 * window + raidrive + wps编辑
			 *      delete ~$file.docx
             *      put    ~$file.docx
             *      put    ~tmpxxx.TMP
             *      delete ~$file.docx
             *      move   file.docx   file~xxx.tmp
             *      move   ~tmpxxx.TMP file.docx
             *      delete file~xxx.tmp
			 */
			$fromFile 	= $io->pathThis($pathUrl);
			$toFile 	= $io->pathThis($destURL);
			$fromExt 	= get_path_ext($pathUrl);
			$toExt   	= get_path_ext($destURL);// 误判情况: 将xx/aa.docx 移动到xx/aa~xxx.tmp会失败;
			$officeExt 	= array('doc','docx','xls','xlsx','ppt','pptx');
			if( $toExt == 'tmp' && in_array($fromExt,$officeExt) && strstr($toFile,'~')){
				$result =  IO::mkfile($destFile);
			    $this->plugin->log("move mkfile=$path;$pathUrl;$destURL;result=".$result);
			    return $result;
			}
			// 都存在则覆盖;
			if( $this->pathExists($path,true) && $this->pathExists($destFile) ){
				$destFileInfo = IO::infoFull($destFile);

				// $content = IO::getContent($path);
				// IO::setContent($destFileInfo['path'],$content);
				// IO::remove($path);$result = $destFileInfo['path'];
				$result  = IO::saveFile($path,$destFileInfo['path']);//覆盖保存;
				$this->plugin->log("move saveFile; to=$path;toFile=".$destFileInfo['path'].';result='.$result);
				return $result;
			}
			return IO::rename($path,$io->pathThis($destURL));
		}
		
		if(!$this->can($path,'remove')) return false;
		if(!$this->can($dest,'edit')) return false;
		
		// 名称不同先重命名;
		if( $io->pathThis($destURL) != $io->pathThis($pathUrl) ){
			$path = IO::rename($path,$io->pathThis($destURL));
		}
		return IO::move($path,$dest);
	}
	public function pathCopy($path,$dest){
		$pathUrl = $this->pathGet();
		$destURL = $this->pathGet(true);		
		$path 	= $this->parsePath($pathUrl);
		$dest   = $this->parsePath(IO::pathFather($destURL)); //多出一层-来源文件(夹)名
		$this->plugin->log("from=$path;to=$dest;$pathUrl;$destURL");

		if(!$this->can($path,'download')) return false;
		if(!$this->can($dest,'edit')) return false;
		
		$fromName = get_path_this($pathUrl); 
		$destName = get_path_this($destURL);
		$destName = $fromName != $destName ? $destName : '';
		return IO::copy($path,$dest,false,$destName);
	}
	
	// 上传临时目录; 优化: 默认存储io为本地时,临时目录切换到对应目录的temp/下;(减少从头temp读取->写入到存储i)
	public function uploadFileTemp(){
		$tempPath = TEMP_FILES;
		$path = $this->pathCreateParent();// 上传到目录转换; /dav/test/1.txt=> {source:23}/1.txt;
		$driverInfo = KodIO::pathDriverType($path);
		if($driverInfo && $driverInfo['type'] == 'local'){
			$truePath = rtrim($driverInfo['path'],'/').'/';
			$isSame = KodIO::isSameDisk($truePath,TEMP_FILES);
			if(!$isSame && file_exists($truePath)){$tempPath = $truePath;}
		}
		
		if(!file_exists($tempPath)){
			@mk_dir($tempPath);
			touch($tempPath.'index.html');
		}
		return $tempPath;
	}
	
	// 文件编辑锁添加或移除;(office/wps: 打开编辑时会添加; 保存时会添加/解除; 关闭文件时会解锁)
	public function fileLock($path){
		if(!IO::infoFull($path)){
		    $path = IO::mkfile($path);// 不存在时,自动创建文件;
		}
		
		$info = $this->fileLockCheck($path);		
		$lock = $this->fileLockAllow($path, $info);
		if(!$lock) return;
		
		$this->fileLockCache($path, $lock, $info);
		Model("Source")->metaSet($info['sourceID'],'systemLock',USER_ID);
		Model("Source")->metaSet($info['sourceID'],'systemLockTime',time());		
	}
	public function fileUnLock($path){
		$info = $this->fileLockCheck($path);if(!$info) return;
		if($this->fileLockCache($path)) return;
		if(!$this->fileLockAllow($path, $info)) return;
		Model("Source")->metaSet($info['sourceID'],'systemLock',null);
		Model("Source")->metaSet($info['sourceID'],'systemLockTime',null);
	}
	private function fileLockCheck($path){
		$info = IO::infoFull($path);
		if(!$info || !$info['sourceID'] || !USER_ID) return;
		if(!$this->can($path,'edit')) return;
		return $info;
	}
	// 判断文件是否已加锁:未加锁=>true;已加锁:自己=>userID;他人=>false
	private function fileLockAllow($path, $info) {
		$isLock = _get($info, 'metaInfo.systemLock');
		if(!$isLock) return true;	// 未被锁定
		return $isLock == USER_ID ? $isLock : false;	// 被自己、别人锁定
	}
	private function fileLockCache($path, $lock=false, $info=false) {
		$key = md5('before_webdav_locked_'.USER_ID.'_'.$path);
		// 获取缓存:是否为自己手动锁定
		if(!$lock){return Cache::get($key);}
		// 未锁定(true):删除可能的缓存
		if($lock === true){return Cache::remove($key);}
		Cache::set($key, 1);
	}
}