// +---------------------------------------------------------------------- namespace app\services\system\log; use app\dao\system\log\SystemFileDao; use app\services\BaseServices; use app\services\system\admin\SystemAdminServices; use crmeb\exceptions\AdminException; use crmeb\exceptions\AuthException; use crmeb\services\CacheService; use crmeb\services\FileService as FileClass; use crmeb\services\FormBuilder as Form; use crmeb\utils\JwtAuth; use Firebase\JWT\ExpiredException; use think\facade\Log; use think\facade\Route as Url; /** * 文件校验 * Class SystemFileServices * @package app\services\system\log */ class SystemFileServices extends BaseServices { /** * 构造方法 * SystemFileServices constructor. * @param SystemFileDao $dao */ public function __construct(SystemFileDao $dao) { $this->dao = $dao; } /** * @param array $admin * @param string $password * @param string $type * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * * @date 2022/09/07 * @author yyw */ public function Login(string $password, string $type) { if (config('filesystem.password') !== $password) { throw new AdminException(400140); } $md5Password = md5($password); /** @var JwtAuth $jwtAuth */ $jwtAuth = app()->make(JwtAuth::class); $tokenInfo = $jwtAuth->createToken($md5Password, $type, ['pwd' => $md5Password]); CacheService::set(md5($tokenInfo['token']), $tokenInfo['token'], 3600); return [ 'token' => md5($tokenInfo['token']), 'expires_time' => $tokenInfo['params']['exp'], ]; } /** * 获取Admin授权信息 * @param string $token * @return bool * @throws \Psr\SimpleCache\InvalidArgumentException */ public function parseToken(string $token): bool { /** @var CacheService $cacheService */ $cacheService = app()->make(CacheService::class); if (!$token || $token === 'undefined') { throw new AuthException(110008); } /** @var JwtAuth $jwtAuth */ $jwtAuth = app()->make(JwtAuth::class); //设置解析token [$id, $type, $pwd] = $jwtAuth->parseToken($token); //检测token是否过期 $md5Token = md5($token); if (!$cacheService->has($md5Token) || !($cacheService->get($md5Token))) { throw new AuthException(110008); } //验证token try { $jwtAuth->verifyToken(); } catch (\Throwable $e) { if (!request()->isCli()) { $cacheService->delete($md5Token); } throw new AuthException(110008); } if ($id !== md5(config('filesystem.password'))) { throw new AuthException(110008); } if ($pwd !== md5(config('filesystem.password'))) { throw new AuthException(110008); } return true; } /** * 获取文件校验列表 * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ public function getFileList() { $rootPath = app()->getRootPath(); $key = 'system_file_app_crmeb_public'; $arr = CacheService::get(md5($key)); if (!$arr) { $app = $this->getDir($rootPath . 'app'); $extend = $this->getDir($rootPath . 'crmeb'); $arr = array_merge($app, $extend); CacheService::set(md5($key), $arr, 3600 * 24); } $fileAll = [];//本地文件 $cha = [];//不同的文件 $len = strlen($rootPath); $file = $this->dao->getAll();//数据库中的文件 if (empty($file)) { foreach ($arr as $k => $v) { $update_time = stat($v); $fileAll[$k]['cthash'] = md5_file($v); $fileAll[$k]['filename'] = substr($v, $len); $fileAll[$k]['atime'] = $update_time['atime']; $fileAll[$k]['mtime'] = $update_time['mtime']; $fileAll[$k]['ctime'] = $update_time['ctime']; } $data_num = array_chunk($fileAll, 100); $res = true; $res = $this->transaction(function () use ($data_num, $res) { foreach ($data_num as $k => $v) { $res = $res && $this->dao->saveAll($v); } return $res; }); if ($res) { $cha = [];//不同的文件 } else { $cha = $fileAll; } } else { $file = array_combine(array_column($file, 'filename'), $file); foreach ($arr as $ko => $vo) { $update_time = stat($vo); $cthash = md5_file($vo); $cha[] = [ 'filename' => str_replace($rootPath, '', $vo), 'cthash' => $cthash, 'atime' => date('Y-m-d H:i:s', $update_time['atime']), 'mtime' => date('Y-m-d H:i:s', $update_time['mtime']), 'ctime' => date('Y-m-d H:i:s', $update_time['ctime']), 'type' => '新增的', ]; if (isset($file[$vo]) && $file[$vo] != $cthash) { $cha[] = [ 'type' => '已修改', ]; unset($file[$vo]); } } foreach ($file as $k => $v) { $cha[] = [ 'filename' => $v['filename'], 'cthash' => $v['cthash'], 'atime' => date('Y-m-d H:i:s', $v['atime']), 'mtime' => date('Y-m-d H:i:s', $v['mtime']), 'ctime' => date('Y-m-d H:i:s', $v['ctime']), 'type' => '已删除', ]; } } $ctime = array_column($cha, 'ctime'); array_multisort($ctime, SORT_DESC, $cha); return $cha; } /** * 获取文件夹中的文件 包括子文件 * @param $dir * @return array */ public function getDir($dir) { $data = []; $this->searchDir($dir, $data); return $data; } /** * 获取文件夹中的文件 包括子文件 不能直接用 直接使用 $this->getDir()方法 P156 * @param $path * @param $data */ public function searchDir($path, &$data) { if (is_dir($path) && !strpos($path, 'uploads')) { $files = scandir($path); foreach ($files as $file) { if ($file != '.' && $file != '..') { $this->searchDir($path . '/' . $file, $data); } } } if (is_file($path)) { $data[] = $path; } } //打开目录 public function opendir($dir, $fileDir, $superior) { $markList = app()->make(SystemFileInfoServices::class)->getColumn([], 'mark', 'full_path'); $fileAll = array('dir' => [], 'file' => []); //根目录 $rootDir = $this->formatPath(app()->getRootPath()); //防止查看站点以外的目录 if (strpos($dir, $rootDir) === false || $dir == '') { $dir = $rootDir; } //判断是否是返回上级 if ($superior) { if (strpos(dirname($dir), $rootDir) !== false) { $dir = dirname($dir); } else { $dir = $rootDir; } } else { $dir = $dir . '/' . $fileDir; } $list = scandir($dir); foreach ($list as $key => $v) { if ($v != '.' && $v != '..') { if (is_dir($dir . DS . $v)) { $fileAll['dir'][] = FileClass::listInfo($dir . DS . $v); } if (is_file($dir . DS . $v)) { $fileAll['file'][] = FileClass::listInfo($dir . DS . $v); } } } //兼容windows $uname = php_uname('s'); if (strstr($uname, 'Windows') !== false) { $dir = ltrim($dir, '\\'); $rootDir = str_replace('\\', '\\\\', $rootDir); } $list = array_merge($fileAll['dir'], $fileAll['file']); $navList = []; foreach ($list as $key => $value) { $list[$key]['real_path'] = str_replace($rootDir, '', $value['pathname']); $list[$key]['mtime'] = date('Y-m-d H:i:s', $value['mtime']); $navList[$key]['title'] = $value['filename']; if ($value['isDir']) $navList[$key]['loading'] = false; $navList[$key]['children'] = []; $navList[$key]['path'] = $value['path']; $navList[$key]['isDir'] = $value['isDir']; $navList[$key]['isLeaf'] = !$value['isDir']; $navList[$key]['pathname'] = $value['pathname']; $navList[$key]['contextmenu'] = true; $list[$key]['mark'] = $markList[str_replace(root_path(), '/', $value['pathname'])] ?? ''; $count = app()->make(SystemFileInfoServices::class)->count(['full_path' => $list[$key]['real_path']]); if (!$count) app()->make(SystemFileInfoServices::class)->save([ 'name' => $value['filename'], 'path' => str_replace('/' . $value['filename'], '', $list[$key]['real_path']), 'full_path' => $list[$key]['real_path'], 'type' => $value['type'], 'create_time' => date('Y-m-d H:i:s', $value['ctime']), 'update_time' => date('Y-m-d H:i:s', time()), ]); } $routeList = [['key' => '根目录', 'route' => '']]; $pathArray = explode('/', str_replace($rootDir, '', $dir)); $str = ''; foreach ($pathArray as $item) { if ($item) { $str = $str . '/' . $item; $routeList[] = ['key' => $item, 'route' => $rootDir . $str]; } } return compact('dir', 'list', 'navList', 'routeList'); } //读取文件 public function openfile($filepath) { //根目录 $rootDir = $this->formatPath(app()->getRootPath()); //防止查看站点以外的文件 if (strpos($filepath, $rootDir) === false || $filepath == '') { throw new AdminException('无法打开站点以外的文件'); } $filepath = $this->formatPath($filepath); $content = FileClass::readFile($filepath);//防止页面内嵌textarea标签 $ext = FileClass::getExt($filepath); $encoding = mb_detect_encoding($content, mb_detect_order()); //前端组件支持的语言类型 //['plaintext', 'json', 'abap', 'apex', 'azcli', 'bat', 'cameligo', 'clojure', 'coffeescript', 'c', 'cpp', 'csharp', 'csp', 'css', 'dart', 'dockerfile', 'fsharp', 'go', 'graphql', 'handlebars', 'hcl', 'html', 'ini', 'java', 'javascript', 'julia', 'kotlin', 'less', 'lexon', 'lua', 'markdown', 'mips', 'msdax', 'mysql', 'objective-c', 'pascal', 'pascaligo', 'perl', 'pgsql', 'php', 'postiats', 'powerquery', 'powershell', 'pug', 'python', 'r', 'razor', 'redis', 'redshift', 'restructuredtext', 'ruby', 'rust', 'sb', 'scala', 'scheme', 'scss', 'shell', 'sol', 'aes', 'sql', 'st', 'swift', 'systemverilog', 'verilog', 'tcl', 'twig', 'typescript', 'vb', 'xml', 'yaml'] $extarray = [ 'js' => 'javascript' , 'htm' => 'html' , 'shtml' => 'html' , 'html' => 'html' , 'xml' => 'xml' , 'php' => 'php' , 'sql' => 'mysql' , 'css' => 'css' , 'txt' => 'plaintext' , 'vue' => 'html' , 'json' => 'json' , 'lock' => 'json' , 'md' => 'markdown' , 'bat' => 'bat' , 'ini' => 'ini' ]; $mode = empty($extarray[$ext]) ? 'php' : $extarray[$ext]; return compact('content', 'mode', 'filepath', 'encoding'); } //保存文件 public function savefile($filepath, $comment) { $filepath = $this->formatPath($filepath); if (!FileClass::isWritable($filepath)) { throw new AdminException('请检查目录权限,需要给全部文件777WWW权限'); } return FileClass::writeFile($filepath, $comment); } // 文件重命名 public function rename($newname, $oldname) { if (($newname != $oldname) && is_writable($oldname)) { return rename($oldname, $newname); } return true; } /** * 删除文件或文件夹 * @param string $path * @return bool * * @date 2022/09/20 * @author yyw */ public function delFolder(string $path) { $path = $this->formatPath($path); if (is_file($path)) { return unlink($path); } $dir = opendir($path); while ($fileName = readdir($dir)) { $file = $path . '/' . $fileName; if ($fileName != '.' && $fileName != '..') { if (is_dir($file)) { self::delFolder($file); } else { unlink($file); } } } closedir($dir); return rmdir($path); } /** * 新建文件夹 * @param string $path * @param string $name * @param int $permissions * @return bool * * @date 2022/09/20 * @author yyw */ public function createFolder(string $path, string $name, int $permissions = 0755) { $path = $this->formatPath($path, $name); /** @var FileClass $fileClass */ $fileClass = app()->make(FileClass::class); return $fileClass->createDir($path, $permissions); } /** * 新建文件 * @param string $path * @param string $name * @return bool * * @date 2022/09/20 * @author yyw */ public function createFile(string $path, string $name) { $path = $this->formatPath($path, $name); /** @var FileClass $fileClass */ $fileClass = app()->make(FileClass::class); return $fileClass->createFile($path); } public function copyFolder($surDir, $toDir) { return FileClass::copyDir($surDir, $toDir); } /** * 格式化路径 * @param string $path * @param string $name * @return string * * @date 2022/09/20 * @author yyw */ public function formatPath(string $path = '', string $name = ''): string { if ($path) { $path = rtrim($path, DS); if ($name) $path = $path . DS . $name; $uname = php_uname('s'); if (strstr($uname, 'Windows') !== false) $path = ltrim(str_replace('\\', '\\\\', $path), '.'); } return $path; } /** * 文件备注表单 * @param $path * @param $fileToken * @return array * @throws \FormBuilder\Exception\FormBuilderException * @author 吴汐 * @email 442384644@qq.com * @date 2023/04/10 */ public function markForm($path, $fileToken) { $full_path = str_replace(root_path(), '/', $path); $mark = app()->make(SystemFileInfoServices::class)->value(['full_path' => str_replace(root_path(), '/', $path)], 'mark'); $f = []; $f[] = Form::hidden('full_path', $full_path); $f[] = Form::input('mark', '文件备注', $mark); return create_form('文件备注', $f, Url::buildUrl('/system/file/mark/save?fileToken=' . $fileToken . '&type=mark'), 'POST'); } /** * 保存文件备注 * @param $full_path * @param $mark * @author 吴汐 * @email 442384644@qq.com * @date 2023/04/10 */ public function fileMarkSave($full_path, $mark) { $res = app()->make(SystemFileInfoServices::class)->update(['full_path' => $full_path], ['mark' => $mark]); if (!$res) { throw new AdminException(100006); } } }