nas_docker_compose/kodbox/site/plugins/webdav/php/webdavServerKod.class.php
2024-08-31 01:03:37 +08:00

524 lines
20 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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){
$info = $this->fileLockCheck($path);if(!$info) return;
$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);
// 锁定(userid):存缓存,超时时间为 锁定超时-当前 ——没有必要,每次保存时都会先执行加锁,超时会被删除(缓存)
// $time = (int) _get($info, 'metaInfo.systemLockTime', 0);
// $time = $time - time();
// if ($time <= 0) return;
// Cache::set($key, 1, $time);
Cache::set($key, 1);
}
}